From 1e1c38ffc02405e346ff440aeeccd928960558e7 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 27 Mar 2026 18:15:09 +0000 Subject: [PATCH] fix: #10 Fixes/adds: - ability to edit instructions - center indicator on range inputs - birthdays --- src/app/api/mii/[id]/edit/route.ts | 19 +- src/app/api/submit/route.ts | 29 +--- src/app/globals.css | 2 +- src/components/mii/instructions.tsx | 35 +++- src/components/mii/voice-viewer.tsx | 31 ++-- src/components/submit-form/edit-form.tsx | 26 ++- src/components/submit-form/index.tsx | 41 +---- .../submit-form/mii-editor/tabs/eyebrows.tsx | 2 +- .../submit-form/mii-editor/tabs/eyes.tsx | 10 +- .../submit-form/mii-editor/tabs/glasses.tsx | 4 +- .../submit-form/mii-editor/tabs/hair.tsx | 10 +- .../submit-form/mii-editor/tabs/head.tsx | 7 +- .../submit-form/mii-editor/tabs/lips.tsx | 4 +- .../submit-form/mii-editor/tabs/misc.tsx | 163 ++++++++++++++---- .../submit-form/mii-editor/tabs/other.tsx | 13 +- src/components/tutorial/switch-submit.tsx | 2 +- src/lib/schemas.ts | 18 +- src/lib/switch.ts | 75 ++++++++ src/types.d.ts | 6 + 19 files changed, 344 insertions(+), 153 deletions(-) diff --git a/src/app/api/mii/[id]/edit/route.ts b/src/app/api/mii/[id]/edit/route.ts index e52984f..b2ed57b 100644 --- a/src/app/api/mii/[id]/edit/route.ts +++ b/src/app/api/mii/[id]/edit/route.ts @@ -11,9 +11,11 @@ import { profanity } from "@2toad/profanity"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; -import { idSchema, nameSchema, tagsSchema } from "@/lib/schemas"; +import { idSchema, nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@/lib/schemas"; import { generateMetadataImage, validateImage } from "@/lib/images"; import { RateLimit } from "@/lib/rate-limit"; +import { SwitchMiiInstructions } from "@/types"; +import { minifyInstructions } from "@/lib/switch"; const uploadsDirectory = path.join(process.cwd(), "uploads", "mii"); @@ -21,6 +23,7 @@ const editSchema = z.object({ name: nameSchema.optional(), tags: tagsSchema.optional(), description: z.string().trim().max(256).optional(), + instructions: switchMiiInstructionsSchema, image1: z.union([z.instanceof(File), z.any()]).optional(), image2: z.union([z.instanceof(File), z.any()]).optional(), image3: z.union([z.instanceof(File), z.any()]).optional(), @@ -31,7 +34,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); Sentry.setUser({ id: session.user?.id, name: session.user?.name }); - const rateLimit = new RateLimit(request, 1); // no grouped pathname; edit each mii 1 time a minute + const rateLimit = new RateLimit(request, 3); // no grouped pathname; edit each mii 1 time a minute const check = await rateLimit.handle(); if (check) return check; @@ -63,17 +66,22 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return rateLimit.sendResponse({ error: "Invalid JSON in tags" }, 400); } + let minifiedInstructions: Partial | undefined; + if (mii.platform === "SWITCH") + minifiedInstructions = minifyInstructions(JSON.parse((formData.get("instructions") as string) ?? "{}") as SwitchMiiInstructions); + const parsed = editSchema.safeParse({ name: formData.get("name") ?? undefined, tags: rawTags, description: formData.get("description") ?? undefined, + instructions: minifiedInstructions, image1: formData.get("image1"), image2: formData.get("image2"), image3: formData.get("image3"), }); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); - const { name, tags, description, image1, image2, image3 } = parsed.data; + const { name, tags, description, instructions, image1, image2, image3 } = parsed.data; // Validate image files const images: File[] = []; @@ -91,9 +99,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< // Edit Mii in database const updateData: Prisma.MiiUpdateInput = {}; - if (name !== undefined) updateData.name = profanity.censor(name); // Censor potential inappropriate words - if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t)); // Same here + if (name !== undefined) updateData.name = profanity.censor(name); // Censor potentially inappropriate words + if (tags !== undefined) updateData.tags = tags.map((t) => profanity.censor(t)); if (description !== undefined) updateData.description = profanity.censor(description); + if (instructions !== undefined) updateData.instructions = instructions; if (images.length > 0) updateData.imageCount = images.length; if (Object.keys(updateData).length == 0) return rateLimit.sendResponse({ error: "Nothing was changed" }, 400); diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index d35af49..3101420 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -20,6 +20,7 @@ import Mii from "@/lib/mii.js/mii"; import { ThreeDsTomodachiLifeMii } from "@/lib/three-ds-tomodachi-life-mii"; import { SwitchMiiInstructions } from "@/types"; +import { minifyInstructions } from "@/lib/switch"; const uploadsDirectory = path.join(process.cwd(), "uploads", "mii"); @@ -94,32 +95,8 @@ export async function POST(request: NextRequest) { // Minify instructions to save space and improve user experience let minifiedInstructions: Partial | undefined; - if (formData.get("platform") === "SWITCH") { - const DEFAULT_ZERO_FIELDS = new Set(["height", "distance", "rotation", "size", "stretch"]); - - function minify(object: Partial): Partial { - for (const key in object) { - const value = object[key as keyof SwitchMiiInstructions]; - - if (!value || (DEFAULT_ZERO_FIELDS.has(key) && value === 0)) { - delete object[key as keyof SwitchMiiInstructions]; - continue; - } - - if (typeof value === "object" && !Array.isArray(value)) { - minify(value as Partial); - - if (Object.keys(value).length === 0) { - delete object[key as keyof SwitchMiiInstructions]; - } - } - } - - return object; - } - - minifiedInstructions = minify(JSON.parse((formData.get("instructions") as string) ?? "{}") as SwitchMiiInstructions); - } + if (formData.get("platform") === "SWITCH") + minifiedInstructions = minifyInstructions(JSON.parse((formData.get("instructions") as string) ?? "{}") as SwitchMiiInstructions); // Parse and check all submission info const parsed = submitSchema.safeParse({ diff --git a/src/app/globals.css b/src/app/globals.css index add78f3..c2d01c0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -126,7 +126,7 @@ input[type="range"] { /* Track */ input[type="range"]::-webkit-slider-runnable-track { - @apply h-2 bg-orange-200 border-2 border-orange-400 rounded-full; + @apply h-1 bg-orange-200 border-2 border-orange-400 rounded-full; } input[type="range"]::-moz-range-track { diff --git a/src/components/mii/instructions.tsx b/src/components/mii/instructions.tsx index 3b2bf1f..284f0bf 100644 --- a/src/components/mii/instructions.tsx +++ b/src/components/mii/instructions.tsx @@ -113,7 +113,7 @@ function Section({ name, instructions, children, isSubSection }: SectionProps) { export default function MiiInstructions({ instructions }: Props) { if (Object.keys(instructions).length === 0) return null; - const { head, hair, eyebrows, eyes, nose, lips, ears, glasses, other, height, weight, datingPreferences, voice, personality } = instructions; + const { head, hair, eyebrows, eyes, nose, lips, ears, glasses, other, height, weight, birthday, datingPreferences, voice, personality } = instructions; return (
@@ -122,7 +122,15 @@ export default function MiiInstructions({ instructions }: Props) { Instructions - {head &&
} + {head && ( +
+ {head.skinColor && ( + + + + )} +
+ )} {hair && (
{hair.subColor && ( @@ -196,7 +204,10 @@ export default function MiiInstructions({ instructions }: Props) { - +
+ +
+
)} {weight && ( @@ -204,7 +215,23 @@ export default function MiiInstructions({ instructions }: Props) { - +
+ +
+
+ + )} + {birthday && ( +
+

Birthday

+ + + {birthday.day && {birthday.day}} + {birthday.month && {birthday.month}} + {birthday.age && {birthday.age}} + {birthday.dontAge && {birthday.dontAge ? "Yes" : "No"}} + +
)} {datingPreferences && ( diff --git a/src/components/mii/voice-viewer.tsx b/src/components/mii/voice-viewer.tsx index c423fa8..5e103f7 100644 --- a/src/components/mii/voice-viewer.tsx +++ b/src/components/mii/voice-viewer.tsx @@ -15,23 +15,26 @@ export default function VoiceViewer({ data, onClick, onClickTone }: Props) { return (
{VOICE_SETTINGS.map((label) => ( -
+
- { - if (onClick) onClick(e, label); - }} - /> +
+ { + if (onClick) onClick(e, label); + }} + /> +
+
))} diff --git a/src/components/submit-form/edit-form.tsx b/src/components/submit-form/edit-form.tsx index c287a2e..817c12c 100644 --- a/src/components/submit-form/edit-form.tsx +++ b/src/components/submit-form/edit-form.tsx @@ -7,6 +7,8 @@ import { FileWithPath } from "react-dropzone"; import { Mii } from "@prisma/client"; import { nameSchema, tagsSchema } from "@/lib/schemas"; +import { defaultInstructions, minifyInstructions } from "@/lib/switch"; +import { SwitchMiiInstructions } from "@/types"; import TagSelector from "../tag-selector"; import ImageList from "./image-list"; @@ -14,6 +16,8 @@ import LikeButton from "../like-button"; import Carousel from "../carousel"; import SubmitButton from "../submit-button"; import Dropzone from "../dropzone"; +import MiiEditor from "./mii-editor"; +import SwitchSubmitTutorialButton from "../tutorial/switch-submit"; interface Props { mii: Mii; @@ -40,6 +44,8 @@ export default function EditForm({ mii, likes }: Props) { const [description, setDescription] = useState(mii.description); const hasFilesChanged = useRef(false); + const instructions = useRef({ ...defaultInstructions, ...(mii.instructions as object as Partial) }); + const handleSubmit = async () => { // Validate before sending request const nameValidation = nameSchema.safeParse(name); @@ -58,6 +64,10 @@ export default function EditForm({ mii, likes }: Props) { if (name != mii.name) formData.append("name", name); if (tags != mii.tags) formData.append("tags", JSON.stringify(tags)); if (description && description != mii.description) formData.append("description", description); + console.log(minifyInstructions(structuredClone(instructions.current))); + console.log(mii.instructions); + if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object)) + formData.append("instructions", JSON.stringify(instructions.current)); if (hasFilesChanged.current) { files.forEach((file, index) => { @@ -179,8 +189,22 @@ export default function EditForm({ mii, likes }: Props) { />
+ {/* Instructions (Switch only) */} + {mii.platform === "SWITCH" && ( + <> +
+
+ Instructions +
+
+ + + + + )} + {/* Separator */} -
+

Custom images
diff --git a/src/components/submit-form/index.tsx b/src/components/submit-form/index.tsx index 74b9a2d..fed91d5 100644 --- a/src/components/submit-form/index.tsx +++ b/src/components/submit-form/index.tsx @@ -13,6 +13,7 @@ import { nameSchema, tagsSchema } from "@/lib/schemas"; import { convertQrCode } from "@/lib/qr-codes"; import Mii from "@/lib/mii.js/mii"; import { ThreeDsTomodachiLifeMii } from "@/lib/three-ds-tomodachi-life-mii"; +import { defaultInstructions } from "@/lib/switch"; import { SwitchMiiInstructions } from "@/types"; import TagSelector from "../tag-selector"; @@ -51,45 +52,7 @@ export default function SubmitForm() { const [platform, setPlatform] = useState("SWITCH"); const [gender, setGender] = useState("MALE"); - const instructions = useRef({ - head: { skinColor: null }, - hair: { - color: null, - subColor: null, - subColor2: null, - style: null, - isFlipped: false, - }, - eyebrows: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null }, - eyes: { - main: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null }, - eyelashesTop: { height: null, distance: null, rotation: null, size: null, stretch: null }, - eyelashesBottom: { height: null, distance: null, rotation: null, size: null, stretch: null }, - eyelidTop: { height: null, distance: null, rotation: null, size: null, stretch: null }, - eyelidBottom: { height: null, distance: null, rotation: null, size: null, stretch: null }, - eyeliner: { color: null }, - pupil: { height: null, distance: null, rotation: null, size: null, stretch: null }, - }, - nose: { height: null, size: null }, - lips: { color: null, height: null, rotation: null, size: null, stretch: null, hasLipstick: false }, - ears: { height: null, size: null }, - glasses: { ringColor: null, shadesColor: null, height: null, size: null, stretch: null }, - other: { - wrinkles1: { height: null, distance: null, size: null, stretch: null }, - wrinkles2: { height: null, distance: null, size: null, stretch: null }, - beard: { color: null }, - moustache: { color: null, height: null, isFlipped: false, size: null, stretch: null }, - goatee: { color: null }, - mole: { color: null, height: null, distance: null, size: null }, - eyeShadow: { color: null, height: null, distance: null, size: null, stretch: null }, - blush: { color: null, height: null, distance: null, size: null, stretch: null }, - }, - height: null, - weight: null, - datingPreferences: [], - voice: { speed: null, pitch: null, depth: null, delivery: null, tone: null }, - personality: { movement: null, speech: null, energy: null, thinking: null, overall: null }, - }); + const instructions = useRef(defaultInstructions); const [error, setError] = useState(undefined); diff --git a/src/components/submit-form/mii-editor/tabs/eyebrows.tsx b/src/components/submit-form/mii-editor/tabs/eyebrows.tsx index 1f6f30f..36e1670 100644 --- a/src/components/submit-form/mii-editor/tabs/eyebrows.tsx +++ b/src/components/submit-form/mii-editor/tabs/eyebrows.tsx @@ -8,7 +8,7 @@ interface Props { } export default function EyebrowsTab({ instructions }: Props) { - const [color, setColor] = useState(3); + const [color, setColor] = useState(instructions.current.eyebrows.color ?? 3); return ( <> diff --git a/src/components/submit-form/mii-editor/tabs/eyes.tsx b/src/components/submit-form/mii-editor/tabs/eyes.tsx index 0f655b9..9b75031 100644 --- a/src/components/submit-form/mii-editor/tabs/eyes.tsx +++ b/src/components/submit-form/mii-editor/tabs/eyes.tsx @@ -19,9 +19,13 @@ const TABS: { name: keyof SwitchMiiInstructions["eyes"]; length: number; colorsD export default function EyesTab({ instructions }: Props) { const [tab, setTab] = useState(0); - - // One type/color state per tab - const [colors, setColors] = useState(Array(TABS.length).fill(122)); + const [colors, setColors] = useState(() => + TABS.map((t) => { + const entry = instructions.current.eyes[t.name]; + const color = "color" in entry ? entry.color : null; + return color ?? 122; + }), + ); const currentTab = TABS[tab]; diff --git a/src/components/submit-form/mii-editor/tabs/glasses.tsx b/src/components/submit-form/mii-editor/tabs/glasses.tsx index 4c921de..1b693ca 100644 --- a/src/components/submit-form/mii-editor/tabs/glasses.tsx +++ b/src/components/submit-form/mii-editor/tabs/glasses.tsx @@ -8,8 +8,8 @@ interface Props { } export default function GlassesTab({ instructions }: Props) { - const [ringColor, setRingColor] = useState(133); - const [shadesColor, setShadesColor] = useState(133); + const [ringColor, setRingColor] = useState(instructions.current.glasses.ringColor ?? 133); + const [shadesColor, setShadesColor] = useState(instructions.current.glasses.shadesColor ?? 133); return ( <> diff --git a/src/components/submit-form/mii-editor/tabs/hair.tsx b/src/components/submit-form/mii-editor/tabs/hair.tsx index 7e4d07c..b26f3c3 100644 --- a/src/components/submit-form/mii-editor/tabs/hair.tsx +++ b/src/components/submit-form/mii-editor/tabs/hair.tsx @@ -10,11 +10,11 @@ type Tab = "sets" | "bangs" | "back"; export default function HairTab({ instructions }: Props) { const [tab, setTab] = useState("sets"); - const [color, setColor] = useState(3); - const [subColor, setSubColor] = useState(null); - const [subColor2, setSubColor2] = useState(null); - const [style, setStyle] = useState(null); - const [isFlipped, setIsFlipped] = useState(false); + const [color, setColor] = useState(instructions.current.hair.color ?? 3); + const [subColor, setSubColor] = useState(instructions.current.hair.subColor); + const [subColor2, setSubColor2] = useState(instructions.current.hair.subColor2); + const [style, setStyle] = useState(instructions.current.hair.style); + const [isFlipped, setIsFlipped] = useState(instructions.current.hair.isFlipped); return ( <> diff --git a/src/components/submit-form/mii-editor/tabs/head.tsx b/src/components/submit-form/mii-editor/tabs/head.tsx index 6b95d49..b27677d 100644 --- a/src/components/submit-form/mii-editor/tabs/head.tsx +++ b/src/components/submit-form/mii-editor/tabs/head.tsx @@ -9,7 +9,7 @@ interface Props { const COLORS = ["FFD8BA", "FFD5AC", "FEC1A4", "FEC68F", "FEB089", "FEBA6B", "F39866", "E89854", "E37E3F", "B45627", "914220", "59371F", "662D16", "392D1E"]; export default function HeadTab({ instructions }: Props) { - const [color, setColor] = useState(109); + const [color, setColor] = useState(instructions.current.head.skinColor ?? 109); return ( <> @@ -29,7 +29,10 @@ export default function HeadTab({ instructions }: Props) { diff --git a/src/components/submit-form/mii-editor/tabs/lips.tsx b/src/components/submit-form/mii-editor/tabs/lips.tsx index a0aa068..4c95025 100644 --- a/src/components/submit-form/mii-editor/tabs/lips.tsx +++ b/src/components/submit-form/mii-editor/tabs/lips.tsx @@ -8,8 +8,8 @@ interface Props { } export default function LipsTab({ instructions }: Props) { - const [color, setColor] = useState(128); - const [hasLipstick, setHasLipstick] = useState(false); + const [color, setColor] = useState(instructions.current.lips.color ?? 128); + const [hasLipstick, setHasLipstick] = useState(instructions.current.lips.hasLipstick); return ( <> diff --git a/src/components/submit-form/mii-editor/tabs/misc.tsx b/src/components/submit-form/mii-editor/tabs/misc.tsx index e7b8ded..329ee0a 100644 --- a/src/components/submit-form/mii-editor/tabs/misc.tsx +++ b/src/components/submit-form/mii-editor/tabs/misc.tsx @@ -12,22 +12,28 @@ interface Props { } export default function HeadTab({ instructions }: Props) { - const [height, setHeight] = useState(50); - const [weight, setWeight] = useState(50); - const [datingPreferences, setDatingPreferences] = useState([]); + const [height, setHeight] = useState(instructions.current.height ?? 64); + const [weight, setWeight] = useState(instructions.current.weight ?? 64); + const [datingPreferences, setDatingPreferences] = useState(instructions.current.datingPreferences ?? []); const [voice, setVoice] = useState({ - speed: 50, - pitch: 50, - depth: 50, - delivery: 50, - tone: 0, + speed: instructions.current.voice.speed ?? 25, + pitch: instructions.current.voice.pitch ?? 25, + depth: instructions.current.voice.depth ?? 25, + delivery: instructions.current.voice.delivery ?? 25, + tone: instructions.current.voice.tone ?? 0, + }); + const [birthday, setBirthday] = useState({ + day: instructions.current.birthday.day ?? (null as number | null), + month: instructions.current.birthday.month ?? (null as number | null), + age: instructions.current.birthday.age ?? (null as number | null), + dontAge: instructions.current.birthday.dontAge, }); const [personality, setPersonality] = useState({ - movement: -1, - speech: -1, - energy: -1, - thinking: -1, - overall: -1, + movement: instructions.current.personality.movement ?? -1, + speech: instructions.current.personality.speech ?? -1, + energy: instructions.current.personality.energy ?? -1, + thinking: instructions.current.personality.thinking ?? -1, + overall: instructions.current.personality.overall ?? -1, }); return ( @@ -47,36 +53,44 @@ export default function HeadTab({ instructions }: Props) { - { - setHeight(e.target.valueAsNumber); - instructions.current.height = e.target.valueAsNumber; - }} - /> +
+ { + setHeight(e.target.valueAsNumber); + instructions.current.height = e.target.valueAsNumber; + }} + /> +
+
- { - setWeight(e.target.valueAsNumber); - instructions.current.weight = e.target.valueAsNumber; - }} - /> +
+ { + setWeight(e.target.valueAsNumber); + instructions.current.weight = e.target.valueAsNumber; + }} + /> +
+
@@ -117,6 +131,81 @@ export default function HeadTab({ instructions }: Props) { instructions.current.voice.tone = i; }} /> + +
+
+ Birthday +
+
+ +
+
+ + { + setBirthday((p) => ({ ...p, day: e.target.valueAsNumber })); + instructions.current.birthday.day = e.target.valueAsNumber; + }} + /> +
+
+ + { + setBirthday((p) => ({ ...p, month: e.target.valueAsNumber })); + instructions.current.birthday.month = e.target.valueAsNumber; + }} + /> +
+
+ + { + setBirthday((p) => ({ ...p, age: e.target.valueAsNumber })); + instructions.current.birthday.age = e.target.valueAsNumber; + }} + /> +
+
+ { + setBirthday((p) => ({ ...p, dontAge: e.target.checked })); + instructions.current.birthday.dontAge = e.target.checked; + }} + /> + +
+
diff --git a/src/components/submit-form/mii-editor/tabs/other.tsx b/src/components/submit-form/mii-editor/tabs/other.tsx index 89ef445..b1787cf 100644 --- a/src/components/submit-form/mii-editor/tabs/other.tsx +++ b/src/components/submit-form/mii-editor/tabs/other.tsx @@ -7,14 +7,14 @@ interface Props { instructions: React.RefObject; } -const TABS: { name: keyof SwitchMiiInstructions["other"]; length: number }[] = [ +const TABS: { name: keyof SwitchMiiInstructions["other"]; length: number; defaultColor?: number }[] = [ { name: "wrinkles1", length: 9 }, { name: "wrinkles2", length: 15 }, { name: "beard", length: 15 }, { name: "moustache", length: 16 }, { name: "goatee", length: 14 }, { name: "mole", length: 2 }, - { name: "eyeShadow", length: 4 }, + { name: "eyeShadow", length: 4, defaultColor: 139 }, { name: "blush", length: 8 }, ]; @@ -22,8 +22,13 @@ export default function OtherTab({ instructions }: Props) { const [tab, setTab] = useState(0); const [isFlipped, setIsFlipped] = useState(false); - // One type/color state per tab - const [colors, setColors] = useState([0, 0, 0, 0, 0, 0, 139, 0]); + const [colors, setColors] = useState(() => + TABS.map((t) => { + const entry = instructions.current.other[t.name]; + const color = "color" in entry ? entry.color : null; + return color ?? t.defaultColor ?? 0; + }), + ); const currentTab = TABS[tab]; diff --git a/src/components/tutorial/switch-submit.tsx b/src/components/tutorial/switch-submit.tsx index 1e7fbff..838e0be 100644 --- a/src/components/tutorial/switch-submit.tsx +++ b/src/components/tutorial/switch-submit.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { createPortal } from "react-dom"; import Tutorial from "."; -export default function SubmitTutorialButton() { +export default function SwitchSubmitTutorialButton() { const [isOpen, setIsOpen] = useState(false); return ( diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index c65a20c..dd8ea03 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -279,15 +279,21 @@ export const switchMiiInstructionsSchema = z .optional(), }) .optional(), - height: z.number().int().min(0).max(100).optional(), - weight: z.number().int().min(0).max(100).optional(), + height: z.number().int().min(0).max(128).optional(), + weight: z.number().int().min(0).max(128).optional(), datingPreferences: z.array(z.enum(MiiGender)).optional(), + birthday: z.object({ + day: z.number().int().min(1).max(31).optional(), + month: z.number().int().min(1).max(12).optional(), + age: z.number().int().min(1).max(100).optional(), + dontAge: z.boolean().optional(), + }), voice: z .object({ - speed: z.number().int().min(0).max(100).optional(), - pitch: z.number().int().min(0).max(100).optional(), - depth: z.number().int().min(0).max(100).optional(), - delivery: z.number().int().min(0).max(100).optional(), + speed: z.number().int().min(0).max(50).optional(), + pitch: z.number().int().min(0).max(50).optional(), + depth: z.number().int().min(0).max(50).optional(), + delivery: z.number().int().min(0).max(50).optional(), tone: z.number().int().min(1).max(6).optional(), }) .optional(), diff --git a/src/lib/switch.ts b/src/lib/switch.ts index 81210cc..ef32bff 100644 --- a/src/lib/switch.ts +++ b/src/lib/switch.ts @@ -1,3 +1,78 @@ +import { SwitchMiiInstructions } from "@/types"; + +export function minifyInstructions(instructions: Partial) { + const DEFAULT_ZERO_FIELDS = new Set(["height", "distance", "rotation", "size", "stretch"]); + + function minify(object: Partial): Partial { + for (const key in object) { + const value = object[key as keyof SwitchMiiInstructions]; + + if (!value || (DEFAULT_ZERO_FIELDS.has(key) && value === 0)) { + delete object[key as keyof SwitchMiiInstructions]; + continue; + } + + if (typeof value === "object" && !Array.isArray(value)) { + minify(value as Partial); + + if (Object.keys(value).length === 0) { + delete object[key as keyof SwitchMiiInstructions]; + } + } + } + + return object; + } + + return minify(instructions); +} + +export const defaultInstructions: SwitchMiiInstructions = { + head: { skinColor: null }, + hair: { + color: null, + subColor: null, + subColor2: null, + style: null, + isFlipped: false, + }, + eyebrows: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null }, + eyes: { + main: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null }, + eyelashesTop: { height: null, distance: null, rotation: null, size: null, stretch: null }, + eyelashesBottom: { height: null, distance: null, rotation: null, size: null, stretch: null }, + eyelidTop: { height: null, distance: null, rotation: null, size: null, stretch: null }, + eyelidBottom: { height: null, distance: null, rotation: null, size: null, stretch: null }, + eyeliner: { color: null }, + pupil: { height: null, distance: null, rotation: null, size: null, stretch: null }, + }, + nose: { height: null, size: null }, + lips: { color: null, height: null, rotation: null, size: null, stretch: null, hasLipstick: false }, + ears: { height: null, size: null }, + glasses: { ringColor: null, shadesColor: null, height: null, size: null, stretch: null }, + other: { + wrinkles1: { height: null, distance: null, size: null, stretch: null }, + wrinkles2: { height: null, distance: null, size: null, stretch: null }, + beard: { color: null }, + moustache: { color: null, height: null, isFlipped: false, size: null, stretch: null }, + goatee: { color: null }, + mole: { color: null, height: null, distance: null, size: null }, + eyeShadow: { color: null, height: null, distance: null, size: null, stretch: null }, + blush: { color: null, height: null, distance: null, size: null, stretch: null }, + }, + height: null, + weight: null, + datingPreferences: [], + birthday: { + day: null, + month: null, + age: null, + dontAge: false, + }, + voice: { speed: null, pitch: null, depth: null, delivery: null, tone: null }, + personality: { movement: null, speech: null, energy: null, thinking: null, overall: null }, +}; + export const COLORS: string[] = [ // Outside "000000", diff --git a/src/types.d.ts b/src/types.d.ts index 7ed9692..65d5601 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -143,6 +143,12 @@ interface SwitchMiiInstructions { height: number | null; weight: number | null; datingPreferences: MiiGender[]; + birthday: { + day: number | null; + month: number | null; + age: number | null; // TODO: update accordingly with mii creation date + dontAge: boolean; + }; voice: { speed: number | null; pitch: number | null;