Progress on implementation of skin selector

This commit is contained in:
2026-09-25 00:36:08 +02:00
parent 1279c3921e
commit 1543af0ccf
7 changed files with 342 additions and 114 deletions

View File

@@ -1,10 +1,11 @@
import { Surface } from "@heroui/react"; import { Surface } from "@heroui/react";
import type { AgentInfo } from "~/types/skin";
export function Agent({ agent_id }: { agent_id: string }) { export function Agent({ agent }: { agent: AgentInfo }) {
return ( return (
<Surface className="bg-[#1f2937] flex justify-center flex-1 min-h-0 p-4"> <Surface className="bg-[#1f2937] flex justify-center flex-1 min-h-0 p-4">
<img <img
src={`/skins/${agent_id}_big.png`} src={`/skins/${agent.name}_big.png`}
className="pointer-events-none object-contain object-bottom h-full w-full" className="pointer-events-none object-contain object-bottom h-full w-full"
alt="Agent image" alt="Agent image"
/> />

View File

@@ -1,31 +1,60 @@
import { Slider, Label } from "@heroui/react"; import { Slider, Label, Typography, Input } from "@heroui/react";
import { useEffect, useState } from "react"; import {
useEffect,
type ChangeEvent,
type Dispatch,
type SetStateAction,
} from "react";
import { AgentInfo, WeaponInfo } from "~/types/skin";
export function Float({ export function Float({
float, skin,
minFloat = 0, setSkin,
maxFloat = 1,
acceptInput = false, acceptInput = false,
hasLabel = false,
}: { }: {
float: number; skin: WeaponInfo;
setSkin: Dispatch<SetStateAction<WeaponInfo | AgentInfo>>;
minFloat?: number; minFloat?: number;
maxFloat?: number; maxFloat?: number;
acceptInput?: boolean; acceptInput?: boolean;
hasLabel?: boolean;
}) { }) {
const [value, setValue] = useState(float);
useEffect(() => {
setValue(Math.max(minFloat, Math.min(maxFloat, float)));
}, [float, minFloat, maxFloat]);
const handleChange = (newValue: number | number[]) => { const handleChange = (newValue: number | number[]) => {
const val = Array.isArray(newValue) ? newValue[0]: newValue; const val = Array.isArray(newValue) ? newValue[0] : newValue;
setValue(Math.max(minFloat, Math.min(maxFloat, val))); const newSkin = new WeaponInfo(skin);
newSkin.float = Math.max(skin.min_float, Math.min(skin.max_float, val));
setSkin(newSkin);
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const val = event.currentTarget.valueAsNumber;
const newSkin = new WeaponInfo(skin);
newSkin.float = Math.max(skin.min_float, Math.min(skin.max_float, val));
setSkin(newSkin);
}; };
return ( return (
<> <>
<Slider value={value} maxValue={1} step={0.0001} onChange={handleChange}> {hasLabel && (
<div className="flex items-center gap-2">
<Typography.Paragraph>Float</Typography.Paragraph>
<Input
className="flex-1"
id="float"
type="number"
step={0.0001}
value={skin.float}
onChange={handleInputChange}
/>
</div>
)}
<Slider
value={skin.float}
maxValue={1}
step={0.0001}
onChange={handleChange}
>
<Slider.Track <Slider.Track
className="border-0 h-2 rounded-xl" className="border-0 h-2 rounded-xl"
style={{ style={{
@@ -36,7 +65,7 @@ export function Float({
<div <div
className="h-full rounded-xl" className="h-full rounded-xl"
style={{ style={{
background: `linear-gradient(90deg, rgba(0, 0, 0, 0.8) ${minFloat*100}%, transparent ${minFloat*100}%, transparent ${maxFloat*100}%, rgba(0, 0, 0, 0.8) ${maxFloat*100}%)`, background: `linear-gradient(90deg, rgba(0, 0, 0, 0.8) ${skin.min_float * 100}%, transparent ${skin.min_float * 100}%, transparent ${skin.max_float * 100}%, rgba(0, 0, 0, 0.8) ${skin.max_float * 100}%)`,
}} }}
/> />
<Slider.Thumb className="w-1" isDisabled={!acceptInput} /> <Slider.Thumb className="w-1" isDisabled={!acceptInput} />

View File

@@ -137,23 +137,23 @@ export function Inventory({ side }: { side: "T" | "CT" }) {
className="flex flex-col rounded-xl w-1/3 p-4 gap-2 min-h-0" className="flex flex-col rounded-xl w-1/3 p-4 gap-2 min-h-0"
variant="secondary" variant="secondary"
> >
{agent && <Agent agent_id={agent["name"]}></Agent>} {agent && <Agent agent={agent}></Agent>}
<div className="flex gap-2"> <div className="flex gap-2">
{knife && ( {knife && (
<SkinTile <SkinTile
skin={knife} skinInfo={knife}
side={side} side={side}
></SkinTile> ></SkinTile>
)} )}
{gloves && ( {gloves && (
<SkinTile <SkinTile
skin={gloves} skinInfo={gloves}
side={side} side={side}
></SkinTile> ></SkinTile>
)} )}
{agent && ( {agent && (
<SkinTile <SkinTile
skin={agent} skinInfo={agent}
side={side} side={side}
></SkinTile> ></SkinTile>
)} )}
@@ -167,7 +167,7 @@ export function Inventory({ side }: { side: "T" | "CT" }) {
{weapons.map((v, i) => ( {weapons.map((v, i) => (
<SkinTile <SkinTile
key={i} key={i}
skin={v} skinInfo={v}
side={side} side={side}
/> />
))} ))}

View File

@@ -1,34 +1,77 @@
import { Button, Modal, Surface, Typography } from "@heroui/react"; import { Button, Input, Modal, Surface, Typography } from "@heroui/react";
import type { Dispatch, SetStateAction } from "react"; import {
useEffect,
useState,
type ChangeEvent,
type Dispatch,
type SetStateAction,
} from "react";
import { AgentInfo, WeaponInfo } from "~/types/skin"; import { AgentInfo, WeaponInfo } from "~/types/skin";
import { Float } from "./float"; import { Float } from "./float";
import { SkinSelector } from "./skin_selector";
export function SkinDialog({ export function SkinDialog({
isOpen, isOpen,
setIsOpen, setIsOpen,
skin, skin,
side setSkin,
side,
}: { }: {
isOpen: boolean; isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>; setIsOpen: Dispatch<SetStateAction<boolean>>;
skin: WeaponInfo | AgentInfo; skin: WeaponInfo | AgentInfo;
side: "T" | "CT" setSkin: Dispatch<SetStateAction<WeaponInfo | AgentInfo>>;
side: "T" | "CT";
}) { }) {
const acolor = `${skin.color}64`; const acolor = `${skin.color}64`;
const [selectorOpen, setSelectorOpen] = useState(false);
const [canClose, setCanClose] = useState(true);
const [localSkin, setLocalSkin] = useState(skin);
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
if (localSkin instanceof AgentInfo) return;
const newSkin = new WeaponInfo(localSkin);
newSkin.pattern = event.currentTarget.valueAsNumber;
setLocalSkin(newSkin);
};
const applySkin = (side: "SIDE" | "BOTH") => {
setSkin(localSkin);
}
useEffect(() => {
if (!selectorOpen) {
setTimeout(() => {
setCanClose(true);
}, 100);
}
}, [selectorOpen]);
useEffect(() => {
if (isOpen) {
setLocalSkin(skin);
}
}, [isOpen, skin]);
return ( return (
<>
<Modal isOpen={isOpen}> <Modal isOpen={isOpen}>
<Modal.Backdrop <Modal.Backdrop
variant="blur" variant="blur"
shouldCloseOnInteractOutside={(_element) => { shouldCloseOnInteractOutside={(_element) => {
if (canClose) {
setIsOpen(false); setIsOpen(false);
return true; }
return canClose;
}} }}
> >
<Modal.Container size="lg"> <Modal.Container size="lg">
<Modal.Dialog> <Modal.Dialog>
<Modal.CloseTrigger <Modal.CloseTrigger
onPress={() => { onPress={() => {
if (!selectorOpen) {
setIsOpen(false); setIsOpen(false);
}
}} }}
/> />
<Modal.Body> <Modal.Body>
@@ -37,9 +80,15 @@ export function SkinDialog({
level={4} level={4}
className="py-1 text-wrap" className="py-1 text-wrap"
> >
{skin.display} {localSkin.display}
</Typography.Heading> </Typography.Heading>
<Button className="bg-mist-700 flex flex-col h-full p-0 rounded-none gap-0 hover:bg-indigo-800 active:bg-indigo-900"> <Button
onClick={() => {
setCanClose(false);
setSelectorOpen(true);
}}
className="bg-mist-700 flex flex-col h-full p-0 rounded-none gap-0 hover:bg-indigo-800 active:bg-indigo-900"
>
<div className="flex flex-col w-full h-full"> <div className="flex flex-col w-full h-full">
<div <div
className="bg-linear-to-b pointer-events-none w-full h-full relative" className="bg-linear-to-b pointer-events-none w-full h-full relative"
@@ -48,11 +97,14 @@ export function SkinDialog({
}} }}
> >
<img <img
src={`/skins/${skin.name}_${skin instanceof WeaponInfo ? skin.paint_index : 0}.png`} src={`/skins/${localSkin.name}_${localSkin instanceof WeaponInfo ? localSkin.paint_index : 0}.png`}
className="pointer-events-none object-contain object-bottom h-full" className="pointer-events-none object-contain object-bottom h-full"
alt="Weapon image" alt="Weapon image"
/> />
<Surface className="rounded-full w-fit h-fit p-2 absolute bottom-2 right-2" variant="secondary"> <Surface
className="rounded-full w-fit h-fit p-2 absolute bottom-2 right-2"
variant="secondary"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@@ -72,30 +124,49 @@ export function SkinDialog({
<div <div
className={`h-1 w-full`} className={`h-1 w-full`}
style={{ style={{
background: `${skin.color}`, background: `${localSkin.color}`,
}} }}
></div> ></div>
</div> </div>
</Button> </Button>
{skin instanceof WeaponInfo && ( {localSkin instanceof WeaponInfo && (
<> <Float
<Typography.Paragraph> skin={localSkin}
Float: {skin.float} setSkin={setLocalSkin}
</Typography.Paragraph> acceptInput
<Float float={skin.float} minFloat={skin.min_float} maxFloat={skin.max_float} acceptInput/> hasLabel
</> />
)} )}
{skin instanceof WeaponInfo && ( {localSkin instanceof WeaponInfo && (
<div className="flex items-center gap-2">
<Typography.Paragraph> <Typography.Paragraph>
Pattern: {skin.pattern} Pattern:{" "}
</Typography.Paragraph> </Typography.Paragraph>
<Input
className="flex-1"
id="pattern"
type="number"
step={1}
value={localSkin.pattern}
onChange={handleInputChange}
/>
</div>
)} )}
<Typography.Code> <Typography.Code>
Transaction id: {skin.id} Transaction id: {localSkin.id}
</Typography.Code> </Typography.Code>
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
{skin instanceof WeaponInfo && <Button variant="secondary">Apply to Both</Button>} {localSkin instanceof WeaponInfo && (
<Button>Apply{skin instanceof WeaponInfo ? ` to ${side}` : ""}</Button> <Button onClick={() => {applySkin("BOTH")}} variant="secondary">
Apply to Both
</Button>
)}
<Button onClick={() => {applySkin("SIDE")}}>
Apply
{localSkin instanceof WeaponInfo
? ` to ${side}`
: ""}
</Button>
</div> </div>
</div> </div>
</Modal.Body> </Modal.Body>
@@ -103,5 +174,13 @@ export function SkinDialog({
</Modal.Container> </Modal.Container>
</Modal.Backdrop> </Modal.Backdrop>
</Modal> </Modal>
<SkinSelector
isOpen={selectorOpen}
setIsOpen={setSelectorOpen}
skin={localSkin}
setSkin={setLocalSkin}
side={side}
/>
</>
); );
} }

View File

@@ -0,0 +1,115 @@
import { Button, Modal, Typography } from "@heroui/react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useNotification } from "~/hooks/notification";
import { AgentInfo, WeaponInfo } from "~/types/skin";
interface Info {
name: string;
paint_index: number;
display: string;
color: string;
min_float: number;
max_float: number;
}
export function SkinSelector({
isOpen,
setIsOpen,
skin,
setSkin,
side
}: {
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
skin: WeaponInfo | AgentInfo;
setSkin: Dispatch<SetStateAction<WeaponInfo | AgentInfo>>;
side: "T" | "CT" | "BOTH";
}) {
const [skins, setSkins] = useState<Array<Info>>([]);
const { notify } = useNotification();
const setPaint = (v: Info) => {
if (skin instanceof WeaponInfo) {
const newSkin = new WeaponInfo(skin);
newSkin.paint_index = v.paint_index;
newSkin.display = newSkin.display.split("|")[0] + " | " + v.display;
newSkin.min_float = v.min_float;
newSkin.max_float = v.max_float;
setSkin(newSkin);
} else {
const newSkin = new AgentInfo(skin);
newSkin.name = v.name;
setSkin(newSkin);
}
};
useEffect(() => {
const fetchSkins = async () => {
let response = await fetch(
`${import.meta.env.VITE_BASE_API_URL}/index?weapon=${skin.weapon_id}&team=${side}${skin instanceof AgentInfo ? "&agent=true" : ""}`,
{ credentials: "include" },
)
.then((r) => r.json())
.catch((e) => {
notify(
{
type: "danger",
loading: false,
title: "An error occured.",
message: `An error occured: ${e}`,
},
10000,
);
});
setSkins(response);
};
if (skins.length === 0 && isOpen) {
fetchSkins();
}
}, [isOpen]);
return (
<Modal isOpen={isOpen}>
<Modal.Backdrop
variant="blur"
shouldCloseOnInteractOutside={(_element) => {
setIsOpen(false);
return true;
}}
>
<Modal.Container size="lg">
<Modal.Dialog>
<Modal.CloseTrigger
onPress={() => {
setIsOpen(false);
}}
/>
<Modal.Body className="mt-6">
<div className="flex flex-col gap-2">
{skins &&
skins.map((v, i) => (
<Button
onClick={() => {
setPaint(v);
}}
className="flex flex-col w-full h-fit items-center justify-start"
>
<img
key={i}
src={`/skins/${v.name}_${skin instanceof WeaponInfo ? v.paint_index : 0}.png`}
className="pointer-events-none object-contain object-bottom"
alt="Weapon image"
/>
<Typography.Heading level={3}>
{v.display}
</Typography.Heading>
</Button>
))}
</div>
</Modal.Body>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
);
}

View File

@@ -5,20 +5,22 @@ import { SkinDialog } from "./skin_dialog";
import { WeaponInfo, AgentInfo } from "~/types/skin"; import { WeaponInfo, AgentInfo } from "~/types/skin";
export function SkinTile({ export function SkinTile({
skin, skinInfo,
side, side,
}: { }: {
skin: WeaponInfo | AgentInfo; skinInfo: WeaponInfo | AgentInfo;
side: "T" | "CT"; side: "T" | "CT";
}) { }) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const acolor = `${skin.color}64`; const acolor = `${skinInfo.color}64`;
const [skin, setSkin] = useState(skinInfo);
return ( return (
<> <>
<SkinDialog <SkinDialog
isOpen={isDialogOpen} isOpen={isDialogOpen}
setIsOpen={setIsDialogOpen} setIsOpen={setIsDialogOpen}
skin={skin} skin={skin}
setSkin={setSkin}
side={side} side={side}
/> />
<Button <Button
@@ -50,7 +52,7 @@ export function SkinTile({
{skin.display} {skin.display}
</Typography.Heading> </Typography.Heading>
{skin instanceof WeaponInfo && ( {skin instanceof WeaponInfo && (
<Float float={skin.float} minFloat={skin.min_float} maxFloat={skin.max_float}/> <Float skin={skin} setSkin={setSkin} />
)} )}
{skin instanceof WeaponInfo && ( {skin instanceof WeaponInfo && (
<Typography.Paragraph> <Typography.Paragraph>

View File

@@ -1,5 +1,6 @@
export class WeaponInfo { export class WeaponInfo {
id!: string; id!: string;
weapon_id!: string;
name!: string; name!: string;
color!: string; color!: string;
display!: string; display!: string;
@@ -16,6 +17,7 @@ export class WeaponInfo {
export class AgentInfo { export class AgentInfo {
id!: string; id!: string;
weapon_id!: string;
name!: string; name!: string;
color!: string; color!: string;
display!: string; display!: string;