From c79681dc5934b4af579cebb4f44717ac50d45b37 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 5 Sep 2026 15:27:53 +0500 Subject: [PATCH] feat: setup project --- .gitignore | 13 + README.md | 20 + docs/PLAN.md | 39 +- docs/twitch-setup.md | 49 ++ package.json | 15 + packages/overlay/.env.development | 1 + packages/overlay/index.html | 12 + packages/overlay/package.json | 21 + packages/overlay/src/App.svelte | 63 ++ packages/overlay/src/app.css | 9 + packages/overlay/src/main.ts | 5 + packages/overlay/src/net.ts | 61 ++ packages/overlay/src/vite-env.d.ts | 1 + packages/overlay/tsconfig.json | 8 + packages/overlay/vite.config.ts | 7 + packages/render/package.json | 20 + packages/render/src/avatar.ts | 58 ++ packages/render/src/index.ts | 2 + packages/render/src/world.ts | 64 ++ packages/render/tsconfig.json | 7 + packages/server/package.json | 23 + packages/server/smoke.mjs | 38 + packages/server/src/index.ts | 93 +++ packages/server/src/net/ws.ts | 91 +++ packages/server/src/sim/stub.ts | 43 ++ packages/server/tsconfig.json | 8 + packages/shared/package.json | 16 + packages/shared/src/index.ts | 1 + packages/shared/src/protocol.ts | 41 ++ packages/shared/tsconfig.json | 7 + packages/web/.env.development | 1 + packages/web/index.html | 12 + packages/web/package.json | 21 + packages/web/src/App.svelte | 125 ++++ packages/web/src/app.css | 13 + packages/web/src/main.ts | 5 + packages/web/src/net.ts | 61 ++ packages/web/src/vite-env.d.ts | 1 + packages/web/tsconfig.json | 8 + packages/web/vite.config.ts | 7 + pnpm-lock.yaml | 1099 ++++++++++++++++++++++++++++ pnpm-workspace.yaml | 4 + tsconfig.base.json | 15 + 43 files changed, 2194 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/twitch-setup.md create mode 100644 package.json create mode 100644 packages/overlay/.env.development create mode 100644 packages/overlay/index.html create mode 100644 packages/overlay/package.json create mode 100644 packages/overlay/src/App.svelte create mode 100644 packages/overlay/src/app.css create mode 100644 packages/overlay/src/main.ts create mode 100644 packages/overlay/src/net.ts create mode 100644 packages/overlay/src/vite-env.d.ts create mode 100644 packages/overlay/tsconfig.json create mode 100644 packages/overlay/vite.config.ts create mode 100644 packages/render/package.json create mode 100644 packages/render/src/avatar.ts create mode 100644 packages/render/src/index.ts create mode 100644 packages/render/src/world.ts create mode 100644 packages/render/tsconfig.json create mode 100644 packages/server/package.json create mode 100644 packages/server/smoke.mjs create mode 100644 packages/server/src/index.ts create mode 100644 packages/server/src/net/ws.ts create mode 100644 packages/server/src/sim/stub.ts create mode 100644 packages/server/tsconfig.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/protocol.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/web/.env.development create mode 100644 packages/web/index.html create mode 100644 packages/web/package.json create mode 100644 packages/web/src/App.svelte create mode 100644 packages/web/src/app.css create mode 100644 packages/web/src/main.ts create mode 100644 packages/web/src/net.ts create mode 100644 packages/web/src/vite-env.d.ts create mode 100644 packages/web/tsconfig.json create mode 100644 packages/web/vite.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed9c2cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +*.tsbuildinfo + +# секреты и локальные настройки +.env +.env.local +.env.*.local + +# локальные данные (sqlite и пр.) +data/ + +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..b752d93 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Idle-XBOCT + +Idle-игра для Twitch-чата: аватары зрителей живут на экране стрима, команды чата +управляют ими. План и все решения — в [docs/PLAN.md](docs/PLAN.md). + +## Запуск (M0 — каркас) + +```bash +pnpm install +pnpm dev +``` + +- сервер: http://localhost:3000 — WS на `/ws`, здоровье на `/healthz` +- overlay (browser source для OBS): http://localhost:5173 +- web-мир: http://localhost:5174 + +Сборка клиентов (`pnpm build`) — сервер начнёт раздавать их сам: `/overlay/` и `/web/` +на порту 3000 (это режим для OBS, без vite dev-сервера). + +Twitch-подключение — с M1, чек-лист получения токенов: [docs/twitch-setup.md](docs/twitch-setup.md). diff --git a/docs/PLAN.md b/docs/PLAN.md index d378c92..4aa87ba 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -132,9 +132,10 @@ XP-кривая Melvor-подобная, уровни открывают нод - **M0 — каркас.** Монорепа, overlay+web скелеты, WS-эхо. ✅ аватар-заглушка виден в OBS -- **M1 — вертикальный срез.** `!рубить`/`!стоп`, спавн аватара (аватарка из тегов чата), - ходьба к дереву, брёвна + XP + уровень, автосейв, WS-снапшот+дельты, - камера-окно + простейший автотур. +- **M1 — вертикальный срез.** `!рубить`/`!стоп`, спавн аватара (спрайт-человечек, ник + над головой в цвете чата), ходьба к дереву, брёвна + XP + уровень, автосейв, + WS-снапшот+дельты, камера-окно + простейший автотур, FakeTwitchSource — отладка + команд без живого чата. ✅ зритель пишет `!рубить` и видит себя рубящим в лесу - **M2 — руда и кузница.** `!копать` + `!ковать` + тиры топоров/кирок. ✅ скрафтил железный топор → рубишь заметно быстрее @@ -152,29 +153,36 @@ XP-кривая Melvor-подобная, уровни открывают нод системы оборачиваем тонким интерфейсом для дешёвого fallback - **OBS browser source кэширует**: dev через отдельный порт, версионирование протокола в рукопожатии клиента/сервера -- **Аватарки Twitch CDN**: ленивая загрузка + фолбэк-спрайт +- **Free-версии паков (Sprout Lands, Mystic Woods) — только некоммерческое**: + для монетизированного канала — CC0 или платная лицензия - **Rate limits чата**: бот отвечает дозированно - **EventSub**: нужен Twitch-апп (client id/secret) у стримера; EventSub WebSocket работает локально без публичного webhook-URL — ок для локального MVP ## Ассеты -Аватар зрителя = фото профиля Twitch в кольце; спрайтовые человечки — «тела» под ними -и фолбэк. Поэтому берём паки, где тело и голова разделены. +Аватар зрителя = обычный спрайт-человечек целиком — лицо и тело из пака, фото профиля +Twitch не используем. На старте все зрители с одним и тем же спрайтом; личность читается +по нику над головой в цвете ника чата. Разные скины — на будущее (донатеры/сабы), поэтому +пак желателен с запасом: генератор персонажей или палитрные свопы. ### Человечки + скины -- **LimeZu — Modern Interiors / Modern Exteriors** (itch.io, ~$10–15) — основной кандидат: - генератор персонажей с сотнями скинов/одежды, домики, мебель, здания уровня «ратуша», - всё в одном стиле +- **LimeZu — Modern Interiors / Modern Exteriors** (itch.io, name-your-own-price ≈ $5, + лицензия CC-BY: коммерция ок с указанием авторства) — основной кандидат: домики, мебель, + здания уровня «ратуша», предметы — всё в одном стиле; генератор персонажей в платной + версии пригодится, когда дойдёт до скинов донатеров - **Universal LPC Spritesheet Generator** (бесплатно, онлайн) — сборка спрайт-листов с ходьбой/анимациями из тысяч деталей. Лицензия GPL/CC-BY-SA — проверить перед коммерциализацией -- **Kenney.nl** — CC0, человечки простые геометрические — для прототипа M0–M1 +- **Kenney.nl** — CC0 (тайлы, предметы, простые человечки) — **текущая база проекта**; + конкретные паки подбираем при M1 (нужны: персонаж с ходьбой, деревья/камни, вода) ### Домики, костёр, ратуша -- **Sprout Lands** (Cup Nooble, itch.io) — милый фермерский стиль -- **Mystic Woods** (Game Endeavor, itch.io) — есть бесплатная lite-версия: домики, костёр, природа +- **Sprout Lands** (Cup Nooble, itch.io) — милый фермерский стиль; free-версия только + для некоммерческого, premium от ~$4 снимает ограничение +- **Mystic Woods** (Game Endeavor, itch.io) — есть бесплатная lite-версия: домики, костёр, + природа; free-версия тоже только для некоммерческого - **OpenGameArt.org** — много RPG-паков зданий, лицензии смешанные — читать каждую - **CraftPix freebies** — бесплатные наборы зданий/персонажей @@ -184,11 +192,14 @@ XP-кривая Melvor-подобная, уровни открывают нод CC-BY только с готовностью указать авторство (можно в дашборде) 2. Один стиль важнее качества каждого пака — брать линейку у одного автора (LimeZu закрывает людей, дома, ратушу и предметы) -3. Прототип M0–M1 — на CC0/Kenney, красивые паки подключаем с M2+ +3. **Решено: идём полностью на бесплатном.** База — Kenney (CC0); OpenGameArt — с проверкой + лицензии каждого ассета. Платные паки (LimeZu ≈ $5) — опция на потом, если захочется + единого стиля ## Идеи на потом -- FakeTwitchSource: генератор фейковых зрителей/команд для разработки без живого стрима +- Скины для донатеров/сабов: разные внешности аватаров (генератор персонажей LimeZu + или палитрные свопы) - Событие усталости: аватар временно перестаёт работать, зритель реагирует - Совместная работа: несколько аватаров у одной ноды дают небольшой бонус (социальная связка) - Рандомные события для удержания: золотая жила, дух леса и т.п. diff --git a/docs/twitch-setup.md b/docs/twitch-setup.md new file mode 100644 index 0000000..14e2bb9 --- /dev/null +++ b/docs/twitch-setup.md @@ -0,0 +1,49 @@ +# Twitch: чек-лист подключения (нужен к M1) + +Бот-аккаунт уже есть. Осталось зарегистрировать приложение и получить токены — +примерно 10 минут действий, делается один раз. + +## 1. Приложение (один раз) + +1. На аккаунте, который регистрирует приложение (можно сам бот-аккаунт), включите + 2FA: twitch.tv → Настройки → Безопасность и конфиденциальность. Без 2FA + Twitch не даёт создавать приложения. +2. Зайти на https://dev.twitch.tv/console под этим аккаунтом → **Register Your Application**: + - Name: любое уникальное, например `idle-xboct-bot` + - OAuth Redirect URLs: `http://localhost:3000/oauth/callback` + - Category: **Chat Bot** +3. Скопировать **Client ID**, затем Manage → **New Secret** → **Client Secret** + (показывается один раз). + +## 2. Токен бота — чтение и ответы в чате (M1) + +Scope: `chat:read chat:edit`. + +- Быстрый способ: https://twitchtokengenerator.com → «Chat Bot / Quick Token» → + залогиниться **именно бот-аккаунтом** → получить access token и refresh token. +- Надёжный способ (когда дойдём до M1): мини-OAuth-скрипт на redirect из п.1. + +## 3. Токен стримера — Channel Points (нужен только к M5, спотлайт) + +Scope: `channel:read:redemptions`. Получается так же (twitchtokengenerator или свой +OAuth), но залогиниться нужно **аккаунтом стримера**. EventSub-события выкупа наград +принимают только пользовательский токен владельца канала — токена бота тут недостаточно. + +## 4. Куда положить + +Файл `.env` в корне `packages/server/` (в git не попадает — уже в .gitignore): + +```env +TWITCH_CLIENT_ID=... +TWITCH_CLIENT_SECRET=... +TWITCH_CHANNEL=имя_канала + +TWITCH_BOT_TOKEN=... +TWITCH_BOT_REFRESH=... + +TWITCH_STREAMER_TOKEN=... +TWITCH_STREAMER_REFRESH=... +``` + +Refresh-токены обновляет `RefreshingAuthProvider` из twurple автоматически — +перелогиниваться вручную не придётся. diff --git a/package.json b/package.json new file mode 100644 index 0000000..114e3e9 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "idle-xboct", + "private": true, + "version": "0.0.0", + "type": "module", + "packageManager": "pnpm@11.20.0", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "pnpm -r --parallel --stream dev", + "build": "pnpm -r build", + "typecheck": "pnpm -r typecheck" + } +} diff --git a/packages/overlay/.env.development b/packages/overlay/.env.development new file mode 100644 index 0000000..82be29d --- /dev/null +++ b/packages/overlay/.env.development @@ -0,0 +1 @@ +VITE_WS_URL=ws://localhost:3000/ws diff --git a/packages/overlay/index.html b/packages/overlay/index.html new file mode 100644 index 0000000..6d02cb3 --- /dev/null +++ b/packages/overlay/index.html @@ -0,0 +1,12 @@ + + + + + + Idle XBOCT — overlay + + +
+ + + diff --git a/packages/overlay/package.json b/packages/overlay/package.json new file mode 100644 index 0000000..e81151a --- /dev/null +++ b/packages/overlay/package.json @@ -0,0 +1,21 @@ +{ + "name": "@idle/overlay", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@idle/render": "workspace:*", + "@idle/shared": "workspace:*" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "svelte": "^5.0.0", + "typescript": "^5.6.0", + "vite": "^7.0.0" + } +} diff --git a/packages/overlay/src/App.svelte b/packages/overlay/src/App.svelte new file mode 100644 index 0000000..59d1c93 --- /dev/null +++ b/packages/overlay/src/App.svelte @@ -0,0 +1,63 @@ + + +
+
+
{STATUS_TEXT[status]}
+
+ + diff --git a/packages/overlay/src/app.css b/packages/overlay/src/app.css new file mode 100644 index 0000000..dc359f8 --- /dev/null +++ b/packages/overlay/src/app.css @@ -0,0 +1,9 @@ +html, +body, +#app { + margin: 0; + height: 100%; + /* прозрачность обязательна: канвас лежит поверх картинки стрима в OBS */ + background: transparent; + overflow: hidden; +} diff --git a/packages/overlay/src/main.ts b/packages/overlay/src/main.ts new file mode 100644 index 0000000..dd41642 --- /dev/null +++ b/packages/overlay/src/main.ts @@ -0,0 +1,5 @@ +import { mount } from 'svelte'; +import './app.css'; +import App from './App.svelte'; + +export default mount(App, { target: document.getElementById('app')! }); diff --git a/packages/overlay/src/net.ts b/packages/overlay/src/net.ts new file mode 100644 index 0000000..432b187 --- /dev/null +++ b/packages/overlay/src/net.ts @@ -0,0 +1,61 @@ +import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared'; + +export type Snapshot = Extract; +export type NetStatus = 'connecting' | 'online' | 'reconnecting'; + +export function wsUrl(): string { + const fromEnv = import.meta.env.VITE_WS_URL; + if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv; + // сборка раздаётся самим сервером — тот же origin + const scheme = location.protocol === 'https:' ? 'wss' : 'ws'; + return `${scheme}://${location.host}/ws`; +} + +export interface ConnectHandlers { + onStatus: (status: NetStatus) => void; + onSnapshot: (snap: Snapshot) => void; +} + +/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */ +export function connectWorld(handlers: ConnectHandlers): () => void { + let ws: WebSocket | null = null; + let disposed = false; + let attempt = 0; + + const open = (): void => { + if (disposed) return; + handlers.onStatus(attempt === 0 ? 'connecting' : 'reconnecting'); + ws = new WebSocket(wsUrl()); + + ws.onopen = () => { + attempt = 0; + const hello: ClientMessage = { t: 'hello', v: PROTOCOL_VERSION }; + ws?.send(JSON.stringify(hello)); + }; + ws.onmessage = (ev) => { + let msg: ServerMessage; + try { + msg = JSON.parse(String(ev.data)) as ServerMessage; + } catch { + return; + } + if (msg.t === 'welcome') { + handlers.onStatus('online'); + } else if (msg.t === 'snapshot') { + handlers.onSnapshot(msg); + } + }; + ws.onclose = () => { + if (disposed) return; + attempt += 1; + setTimeout(open, Math.min(500 * 2 ** Math.min(attempt, 4), 8000)); + }; + ws.onerror = () => ws?.close(); + }; + + open(); + return () => { + disposed = true; + ws?.close(); + }; +} diff --git a/packages/overlay/src/vite-env.d.ts b/packages/overlay/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/overlay/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/overlay/tsconfig.json b/packages/overlay/tsconfig.json new file mode 100644 index 0000000..474b75a --- /dev/null +++ b/packages/overlay/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/overlay/vite.config.ts b/packages/overlay/vite.config.ts new file mode 100644 index 0000000..c01cd3e --- /dev/null +++ b/packages/overlay/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +export default defineConfig({ + plugins: [svelte()], + server: { port: 5173, strictPort: true }, +}); diff --git a/packages/render/package.json b/packages/render/package.json new file mode 100644 index 0000000..33469e9 --- /dev/null +++ b/packages/render/package.json @@ -0,0 +1,20 @@ +{ + "name": "@idle/render", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@idle/shared": "workspace:*", + "pixi.js": "^8.6.0" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/packages/render/src/avatar.ts b/packages/render/src/avatar.ts new file mode 100644 index 0000000..0e8aa6e --- /dev/null +++ b/packages/render/src/avatar.ts @@ -0,0 +1,58 @@ +import { Container, Graphics, Text } from 'pixi.js'; +import type { AvatarState } from '@idle/shared'; + +/** + * Заглушка M0: человечек из примитивов. С M1 сюда придут спрайты из паков — + * состав (тело + ник над головой) сохранится. + */ +export class AvatarView { + readonly container = new Container(); + private readonly rig = new Container(); + private readonly label: Text; + private targetX: number; + private targetY: number; + + constructor(av: AvatarState) { + const shadow = new Graphics().ellipse(0, 0, 9, 3.5).fill({ color: 0x000000, alpha: 0.3 }); + const body = new Graphics() + .roundRect(-7, -17, 14, 17, 5) + .fill(av.color) + .circle(0, -23, 7.5) + .fill(0xffe3c0); + this.label = new Text({ + text: av.name, + style: { + fontFamily: 'monospace', + fontSize: 13, + fontWeight: 'bold', + fill: av.color, + stroke: { color: 0x10131a, width: 3 }, + }, + }); + this.label.anchor.set(0.5, 1); + this.label.y = -36; + + this.rig.addChild(shadow, body); + this.container.addChild(this.rig, this.label); + + this.targetX = av.x; + this.targetY = av.y; + this.container.position.set(av.x, av.y); + this.apply(av); + } + + apply(av: AvatarState): void { + this.targetX = av.x; + this.targetY = av.y; + this.rig.scale.x = av.face === 'left' ? -1 : 1; + if (this.label.text !== av.name) this.label.text = av.name; + if (this.label.style.fill !== av.color) this.label.style.fill = av.color; + } + + /** Плавное дотягивание до последнего снапшота — сглаживает дискретность тиков. */ + tick(dtMs: number): void { + const k = 1 - Math.exp(-dtMs / 120); + this.container.x += (this.targetX - this.container.x) * k; + this.container.y += (this.targetY - this.container.y) * k; + } +} diff --git a/packages/render/src/index.ts b/packages/render/src/index.ts new file mode 100644 index 0000000..2a21a42 --- /dev/null +++ b/packages/render/src/index.ts @@ -0,0 +1,2 @@ +export * from './avatar'; +export * from './world'; diff --git a/packages/render/src/world.ts b/packages/render/src/world.ts new file mode 100644 index 0000000..dd4cb95 --- /dev/null +++ b/packages/render/src/world.ts @@ -0,0 +1,64 @@ +import { Application, Container } from 'pixi.js'; +import type { AvatarState } from '@idle/shared'; +import { AvatarView } from './avatar'; + +export type WorldStatus = 'connecting' | 'online' | 'reconnecting'; + +export interface MountWorldOptions { + /** 0 — прозрачный фон (overlay для OBS). */ + backgroundAlpha?: number; + background?: number; + onStatus?: (status: WorldStatus) => void; +} + +export interface WorldHandle { + applySnapshot(avatars: AvatarState[]): void; + destroy(): void; +} + +/** Инициализация pixi-канваса в хосте; состояние мира придёт через applySnapshot. */ +export async function mountWorld(host: HTMLElement, opts: MountWorldOptions = {}): Promise { + opts.onStatus?.('connecting'); + + const app = new Application(); + await app.init({ + backgroundAlpha: opts.backgroundAlpha ?? 1, + background: opts.background ?? 0x151a21, + resizeTo: host, + antialias: true, + }); + host.appendChild(app.canvas); + app.canvas.style.display = 'block'; + + const world = new Container(); + app.stage.addChild(world); + + const views = new Map(); + + app.ticker.add((ticker) => { + for (const view of views.values()) view.tick(ticker.deltaMS); + }); + + return { + applySnapshot(avatars) { + for (const av of avatars) { + let view = views.get(av.id); + if (!view) { + view = new AvatarView(av); + views.set(av.id, view); + world.addChild(view.container); + } + view.apply(av); + } + for (const [id, view] of views) { + if (!avatars.some((av) => av.id === id)) { + view.container.destroy(); + views.delete(id); + } + } + }, + destroy() { + void app.destroy(true, { children: true }); + }, + }; +} diff --git a/packages/render/tsconfig.json b/packages/render/tsconfig.json new file mode 100644 index 0000000..f200f34 --- /dev/null +++ b/packages/render/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["src"] +} diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..4485f89 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,23 @@ +{ + "name": "@idle/server", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "tsx src/index.ts", + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@idle/shared": "workspace:*", + "sirv": "^3.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@types/ws": "^8.5.0", + "tsx": "^4.20.0", + "typescript": "^5.6.0" + } +} diff --git a/packages/server/smoke.mjs b/packages/server/smoke.mjs new file mode 100644 index 0000000..62c00fc --- /dev/null +++ b/packages/server/smoke.mjs @@ -0,0 +1,38 @@ +/* Дымовой тест M0: сервер должен быть запущен. `pnpm --filter @idle/server smoke` */ +import WebSocket from 'ws'; + +const seen = []; +await new Promise((resolve, reject) => { + const ws = new WebSocket('ws://localhost:3000/ws'); + ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 1 }))); + ws.on('message', (d) => { + seen.push(JSON.parse(String(d))); + if (seen.length >= 2) { + ws.close(); + resolve(); + } + }); + ws.on('error', reject); + setTimeout(() => reject(new Error('timeout waiting for welcome+snapshot')), 5000); +}); + +const [welcome, snap] = seen; +console.log('welcome:', JSON.stringify(welcome)); +console.log( + 'avatars:', + snap.avatars.map((a) => `${a.name}@${a.x},${a.y} ${a.face}`).join(' | '), +); +if (welcome.t !== 'welcome' || welcome.v !== 1) throw new Error('bad welcome'); +if (snap.t !== 'snapshot' || snap.avatars.length !== 3) throw new Error('bad snapshot'); + +// неверная версия протокола — сервер обязан закрыть сокет с кодом 4001 +const closeCode = await new Promise((resolve) => { + const ws = new WebSocket('ws://localhost:3000/ws'); + ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 999 }))); + ws.on('close', (code) => resolve(code)); + setTimeout(() => resolve('no-close'), 3000); +}); +console.log('close code for wrong version:', closeCode); +if (closeCode !== 4001) throw new Error(`expected close 4001, got ${closeCode}`); + +console.log('WS smoke: OK'); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts new file mode 100644 index 0000000..7300923 --- /dev/null +++ b/packages/server/src/index.ts @@ -0,0 +1,93 @@ +import { createServer } from 'node:http'; +import path from 'node:path'; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import sirv from 'sirv'; +import { PROTOCOL_VERSION, type ServerMessage } from '@idle/shared'; +import { WsGateway } from './net/ws'; +import { stubAvatarsAt } from './sim/stub'; + +const PORT = Number(process.env.PORT ?? 3000); +const TICK_MS = Number(process.env.TICK_MS ?? 1000); +const startedAt = Date.now(); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const currentTick = (): number => Math.floor((Date.now() - startedAt) / TICK_MS); + +const snapshot = (): ServerMessage => ({ + t: 'snapshot', + tick: currentTick(), + avatars: stubAvatarsAt(Date.now()), +}); + +const gateway = new WsGateway({ getSnapshot: snapshot, tickMs: TICK_MS }); + +// Собранные клиенты раздаём сами — OBS обращается к одному порту. +// В dev клиенты живут на своих vite-портах (5173/5174). +const staticMounts = [ + { prefix: '/overlay/', dir: path.resolve(__dirname, '../../overlay/dist') }, + { prefix: '/web/', dir: path.resolve(__dirname, '../../web/dist') }, +].map((m) => ({ ...m, middleware: existsSync(m.dir) ? sirv(m.dir) : null })); + +function json(res: ServerResponse, status: number, body: unknown): void { + res.writeHead(status, { 'content-type': 'application/json' }); + res.end(JSON.stringify(body)); +} + +function handleHttp(req: IncomingMessage, res: ServerResponse): void { + const url = new URL(req.url ?? '/', 'http://localhost'); + const p = url.pathname; + + if (p === '/healthz') { + json(res, 200, { + ok: true, + protocol: PROTOCOL_VERSION, + tickMs: TICK_MS, + uptimeSec: Math.round((Date.now() - startedAt) / 1000), + }); + return; + } + + for (const m of staticMounts) { + if (!m.middleware) continue; + const base = m.prefix.slice(0, -1); + if (p === base) { + res.writeHead(301, { location: `${base}/` }); + res.end(); + return; + } + if (p.startsWith(m.prefix)) { + req.url = p.slice(base.length) + url.search; + m.middleware(req, res, () => json(res, 404, { error: 'not found' })); + return; + } + } + + json(res, 404, { + error: 'not found', + hint: 'dev: overlay http://localhost:5173, web http://localhost:5174; для /overlay/ и /web/ сначала pnpm build', + }); +} + +const server = createServer(handleHttp); + +server.on('upgrade', (req, socket, head) => { + const { pathname } = new URL(req.url ?? '/', 'http://localhost'); + if (pathname === '/ws') { + gateway.handleUpgrade(req, socket, head); + } else { + socket.destroy(); + } +}); + +setInterval(() => gateway.broadcast(snapshot()), TICK_MS); + +server.listen(PORT, () => { + console.log(`[idle-xboct] http://localhost:${PORT} — протокол v${PROTOCOL_VERSION}, тик ${TICK_MS} мс`); + console.log(`[idle-xboct] overlay: http://localhost:5173 (dev) | web: http://localhost:5174 (dev)`); + const built = staticMounts.filter((m) => m.middleware).map((m) => m.prefix); + if (built.length > 0) { + console.log(`[idle-xboct] раздаю сборки: ${built.join(', ')}`); + } +}); diff --git a/packages/server/src/net/ws.ts b/packages/server/src/net/ws.ts new file mode 100644 index 0000000..bea5982 --- /dev/null +++ b/packages/server/src/net/ws.ts @@ -0,0 +1,91 @@ +import { WebSocketServer, WebSocket } from 'ws'; +import type { Duplex } from 'node:stream'; +import type { IncomingMessage } from 'node:http'; +import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared'; + +declare module 'ws' { + interface WebSocket { + isAlive?: boolean; + } +} + +interface GatewayOptions { + getSnapshot: () => ServerMessage; + tickMs: number; +} + +/** + * Шлюз WS: рукопожатие по версии протокола, welcome + снапшот новому клиенту, + * broadcast снапшота каждый тик, heartbeat для мёртвых OBS-сокетов. + */ +export class WsGateway { + private readonly wss = new WebSocketServer({ noServer: true }); + private readonly clients = new Set(); + + constructor(private readonly opts: GatewayOptions) { + this.wss.on('connection', (ws) => this.onConnection(ws)); + setInterval(() => this.sweep(), 30_000).unref(); + } + + handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void { + this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req)); + } + + broadcast(msg: ServerMessage): void { + const payload = JSON.stringify(msg); + for (const ws of this.clients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(payload); + } + } + } + + private onConnection(ws: WebSocket): void { + ws.isAlive = true; + ws.on('pong', () => { + ws.isAlive = true; + }); + + let helloSeen = false; + ws.on('message', (raw) => { + if (helloSeen) return; + helloSeen = true; + + let msg: ClientMessage | null = null; + try { + msg = JSON.parse(String(raw)) as ClientMessage; + } catch { + msg = null; + } + if (!msg || msg.t !== 'hello' || msg.v !== PROTOCOL_VERSION) { + ws.close(4001, 'protocol version mismatch'); + return; + } + + this.clients.add(ws); + this.send(ws, { t: 'welcome', v: PROTOCOL_VERSION, tickMs: this.opts.tickMs }); + this.send(ws, this.opts.getSnapshot()); + }); + + ws.on('close', () => this.clients.delete(ws)); + ws.on('error', () => ws.terminate()); + } + + private send(ws: WebSocket, msg: ServerMessage): void { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(msg)); + } + } + + private sweep(): void { + for (const ws of this.clients) { + if (ws.isAlive === false) { + this.clients.delete(ws); + ws.terminate(); + continue; + } + ws.isAlive = false; + ws.ping(); + } + } +} diff --git a/packages/server/src/sim/stub.ts b/packages/server/src/sim/stub.ts new file mode 100644 index 0000000..120b1dc --- /dev/null +++ b/packages/server/src/sim/stub.ts @@ -0,0 +1,43 @@ +import type { AvatarState } from '@idle/shared'; + +interface Walker { + id: string; + name: string; + color: string; + y: number; + minX: number; + maxX: number; + /** px/сек */ + speed: number; + /** сдвиг фазы 0..1, чтобы ходоки не шли строем */ + offset: number; +} + +const WALKERS: Walker[] = [ + { id: 'stub-1', name: 'XBOCT', color: '#ff8c3b', y: 300, minX: 140, maxX: 820, speed: 90, offset: 0 }, + { id: 'stub-2', name: 'pixel_proger', color: '#5bd7ff', y: 360, minX: 220, maxX: 900, speed: 70, offset: 0.35 }, + { id: 'stub-3', name: 'viewer_42', color: '#b58cff', y: 240, minX: 80, maxX: 700, speed: 110, offset: 0.7 }, +]; + +/** + * Заглушка M0: движение — чистая функция wall-clock. + * Сервер мог «не жить» сколько угодно — позиция восстановится из таймстемпа, + * как и задумано для ретроактивной симуляции. + */ +export function stubAvatarsAt(nowMs: number): AvatarState[] { + return WALKERS.map((w) => { + const length = w.maxX - w.minX; + const period = (2 * length) / w.speed; + const phase = (((nowMs / 1000) / period) + w.offset) % 1; + const leg = phase < 0.5 ? phase * 2 : 2 - phase * 2; + return { + id: w.id, + name: w.name, + color: w.color, + x: Math.round(w.minX + length * leg), + y: w.y, + face: phase < 0.5 ? 'right' : 'left', + moving: true, + } satisfies AvatarState; + }); +} diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..bdb9327 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022"], + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..a906eaa --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,16 @@ +{ + "name": "@idle/shared", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..995ea19 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export * from './protocol'; diff --git a/packages/shared/src/protocol.ts b/packages/shared/src/protocol.ts new file mode 100644 index 0000000..ac223dd --- /dev/null +++ b/packages/shared/src/protocol.ts @@ -0,0 +1,41 @@ +/** + * Протокол WS — контракт между сервером и клиентами (overlay / web). + * Клиенты зависят только от него; язык сервера можно сменить позже. + * Версия проверяется в рукопожатии (hello/welcome), чтобы OBS со старым + * бандлом не висел молча. + */ + +export const PROTOCOL_VERSION = 1; + +export interface AvatarState { + id: string; + name: string; + /** Цвет ника из тегов чата Twitch. */ + color: string; + /** Позиция в мировых координатах, px. */ + x: number; + y: number; + face: 'left' | 'right'; + moving: boolean; +} + +export interface WelcomeMsg { + t: 'welcome'; + v: number; + /** Период тика симуляции, мс — клиенту для отрисовки прогрессов. */ + tickMs: number; +} + +/** M0: полный снапшот каждый тик; дельты появятся с M1. */ +export interface SnapshotMsg { + t: 'snapshot'; + tick: number; + avatars: AvatarState[]; +} + +export type ServerMessage = WelcomeMsg | SnapshotMsg; + +export type ClientMessage = { + t: 'hello'; + v: number; +}; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..48633a9 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022"] + }, + "include": ["src"] +} diff --git a/packages/web/.env.development b/packages/web/.env.development new file mode 100644 index 0000000..82be29d --- /dev/null +++ b/packages/web/.env.development @@ -0,0 +1 @@ +VITE_WS_URL=ws://localhost:3000/ws diff --git a/packages/web/index.html b/packages/web/index.html new file mode 100644 index 0000000..afb5a81 --- /dev/null +++ b/packages/web/index.html @@ -0,0 +1,12 @@ + + + + + + Idle XBOCT — мир + + +
+ + + diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000..bb70844 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,21 @@ +{ + "name": "@idle/web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@idle/render": "workspace:*", + "@idle/shared": "workspace:*" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "svelte": "^5.0.0", + "typescript": "^5.6.0", + "vite": "^7.0.0" + } +} diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte new file mode 100644 index 0000000..7cd4196 --- /dev/null +++ b/packages/web/src/App.svelte @@ -0,0 +1,125 @@ + + +
+
+ Idle XBOCT + {STATUS_TEXT[status]} +
+ +
+
+ + +
+
+ + diff --git a/packages/web/src/app.css b/packages/web/src/app.css new file mode 100644 index 0000000..67b748b --- /dev/null +++ b/packages/web/src/app.css @@ -0,0 +1,13 @@ +:root { + color-scheme: dark; + font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; +} + +html, +body, +#app { + margin: 0; + height: 100%; + background: #151a21; + color: #dfe7f0; +} diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts new file mode 100644 index 0000000..dd41642 --- /dev/null +++ b/packages/web/src/main.ts @@ -0,0 +1,5 @@ +import { mount } from 'svelte'; +import './app.css'; +import App from './App.svelte'; + +export default mount(App, { target: document.getElementById('app')! }); diff --git a/packages/web/src/net.ts b/packages/web/src/net.ts new file mode 100644 index 0000000..432b187 --- /dev/null +++ b/packages/web/src/net.ts @@ -0,0 +1,61 @@ +import { PROTOCOL_VERSION, type ClientMessage, type ServerMessage } from '@idle/shared'; + +export type Snapshot = Extract; +export type NetStatus = 'connecting' | 'online' | 'reconnecting'; + +export function wsUrl(): string { + const fromEnv = import.meta.env.VITE_WS_URL; + if (typeof fromEnv === 'string' && fromEnv.length > 0) return fromEnv; + // сборка раздаётся самим сервером — тот же origin + const scheme = location.protocol === 'https:' ? 'wss' : 'ws'; + return `${scheme}://${location.host}/ws`; +} + +export interface ConnectHandlers { + onStatus: (status: NetStatus) => void; + onSnapshot: (snap: Snapshot) => void; +} + +/** Подключение к миру с автопереподключением; возвращает функцию закрытия. */ +export function connectWorld(handlers: ConnectHandlers): () => void { + let ws: WebSocket | null = null; + let disposed = false; + let attempt = 0; + + const open = (): void => { + if (disposed) return; + handlers.onStatus(attempt === 0 ? 'connecting' : 'reconnecting'); + ws = new WebSocket(wsUrl()); + + ws.onopen = () => { + attempt = 0; + const hello: ClientMessage = { t: 'hello', v: PROTOCOL_VERSION }; + ws?.send(JSON.stringify(hello)); + }; + ws.onmessage = (ev) => { + let msg: ServerMessage; + try { + msg = JSON.parse(String(ev.data)) as ServerMessage; + } catch { + return; + } + if (msg.t === 'welcome') { + handlers.onStatus('online'); + } else if (msg.t === 'snapshot') { + handlers.onSnapshot(msg); + } + }; + ws.onclose = () => { + if (disposed) return; + attempt += 1; + setTimeout(open, Math.min(500 * 2 ** Math.min(attempt, 4), 8000)); + }; + ws.onerror = () => ws?.close(); + }; + + open(); + return () => { + disposed = true; + ws?.close(); + }; +} diff --git a/packages/web/src/vite-env.d.ts b/packages/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 0000000..474b75a --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts new file mode 100644 index 0000000..65f598a --- /dev/null +++ b/packages/web/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +export default defineConfig({ + plugins: [svelte()], + server: { port: 5174, strictPort: true }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d4350b5 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1099 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/overlay: + dependencies: + '@idle/render': + specifier: workspace:* + version: link:../render + '@idle/shared': + specifier: workspace:* + version: link:../shared + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^6.0.0 + version: 6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)) + svelte: + specifier: ^5.0.0 + version: 5.57.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: ^7.0.0 + version: 7.3.6(@types/node@24.13.3)(tsx@4.23.13) + + packages/render: + dependencies: + '@idle/shared': + specifier: workspace:* + version: link:../shared + pixi.js: + specifier: ^8.6.0 + version: 8.20.1 + devDependencies: + typescript: + specifier: ^5.6.0 + version: 5.9.3 + + packages/server: + dependencies: + '@idle/shared': + specifier: workspace:* + version: link:../shared + sirv: + specifier: ^3.0.0 + version: 3.0.2 + ws: + specifier: ^8.18.0 + version: 8.21.3 + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.13.3 + '@types/ws': + specifier: ^8.5.0 + version: 8.18.1 + tsx: + specifier: ^4.20.0 + version: 4.23.13 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + + packages/shared: + devDependencies: + typescript: + specifier: ^5.6.0 + version: 5.9.3 + + packages/web: + dependencies: + '@idle/render': + specifier: workspace:* + version: link:../render + '@idle/shared': + specifier: workspace:* + version: link:../shared + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^6.0.0 + version: 6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)) + svelte: + specifier: ^5.0.0 + version: 5.57.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vite: + specifier: ^7.0.0 + version: 7.3.6(@types/node@24.13.3)(tsx@4.23.13) + +packages: + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pixi/colord@2.9.6': + resolution: {integrity: sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rollup/rollup-android-arm-eabi@4.63.1': + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.1': + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.1': + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.1': + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.1': + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.1': + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.1': + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.1': + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.1': + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.1': + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.1': + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.1': + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.1': + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.1': + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} + cpu: [x64] + os: [win32] + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2': + resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@types/earcut@3.0.0': + resolution: {integrity: sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@webgpu/types@0.1.72': + resolution: {integrity: sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==} + + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} + engines: {node: '>=10.0.0'} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + devalue@5.9.2: + resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} + + earcut@3.2.3: + resolution: {integrity: sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gifuct-js@2.1.2: + resolution: {integrity: sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + ismobilejs@1.1.1: + resolution: {integrity: sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==} + + js-binary-schema-parser@2.0.3: + resolution: {integrity: sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + parse-svg-path@0.2.0: + resolution: {integrity: sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pixi.js@8.20.1: + resolution: {integrity: sha512-akLVBMLvQbaEViqsmVraK0Njer1q4Q4lm4iNKuzvBiFrV8q+6/+HluJN3sWIwO663IBeQtUUS/CaXFJZs6ffXQ==} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.63.1: + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + svelte@5.57.0: + resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} + engines: {node: '>=18'} + + tiny-lru@11.4.7: + resolution: {integrity: sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==} + engines: {node: '>=12'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@pixi/colord@2.9.6': {} + + '@polka/url@1.0.0-next.29': {} + + '@rollup/rollup-android-arm-eabi@4.63.1': + optional: true + + '@rollup/rollup-android-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-x64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.1': + optional: true + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)))(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)) + obug: 2.1.4 + svelte: 5.57.0 + vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13) + + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)))(svelte@5.57.0)(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)) + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.4 + svelte: 5.57.0 + vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13) + vitefu: 1.1.3(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)) + + '@types/earcut@3.0.0': {} + + '@types/estree@1.0.9': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.3 + + '@webgpu/types@0.1.72': {} + + '@xmldom/xmldom@0.8.15': {} + + acorn@8.18.0: {} + + aria-query@5.3.1: {} + + axobject-query@4.1.0: {} + + clsx@2.1.1: {} + + deepmerge@4.3.1: {} + + devalue@5.9.2: {} + + earcut@3.2.3: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + esm-env@1.2.2: {} + + esrap@2.3.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + eventemitter3@5.0.4: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + gifuct-js@2.1.2: + dependencies: + js-binary-schema-parser: 2.0.3 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + ismobilejs@1.1.1: {} + + js-binary-schema-parser@2.0.3: {} + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mrmime@2.0.1: {} + + nanoid@3.3.18: {} + + obug@2.1.4: {} + + parse-svg-path@0.2.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pixi.js@8.20.1: + dependencies: + '@pixi/colord': 2.9.6 + '@types/earcut': 3.0.0 + '@webgpu/types': 0.1.72 + '@xmldom/xmldom': 0.8.15 + earcut: 3.2.3 + eventemitter3: 5.0.4 + gifuct-js: 2.1.2 + ismobilejs: 1.1.1 + parse-svg-path: 0.2.0 + tiny-lru: 11.4.7 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.63.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.1 + '@rollup/rollup-android-arm64': 4.63.1 + '@rollup/rollup-darwin-arm64': 4.63.1 + '@rollup/rollup-darwin-x64': 4.63.1 + '@rollup/rollup-freebsd-arm64': 4.63.1 + '@rollup/rollup-freebsd-x64': 4.63.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 + '@rollup/rollup-linux-arm64-gnu': 4.63.1 + '@rollup/rollup-linux-arm64-musl': 4.63.1 + '@rollup/rollup-linux-loong64-gnu': 4.63.1 + '@rollup/rollup-linux-loong64-musl': 4.63.1 + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 + '@rollup/rollup-linux-ppc64-musl': 4.63.1 + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 + '@rollup/rollup-linux-riscv64-musl': 4.63.1 + '@rollup/rollup-linux-s390x-gnu': 4.63.1 + '@rollup/rollup-linux-x64-gnu': 4.63.1 + '@rollup/rollup-linux-x64-musl': 4.63.1 + '@rollup/rollup-openbsd-x64': 4.63.1 + '@rollup/rollup-openharmony-arm64': 4.63.1 + '@rollup/rollup-win32-arm64-msvc': 4.63.1 + '@rollup/rollup-win32-ia32-msvc': 4.63.1 + '@rollup/rollup-win32-x64-gnu': 4.63.1 + '@rollup/rollup-win32-x64-msvc': 4.63.1 + fsevents: 2.3.3 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + svelte@5.57.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.2 + esm-env: 1.2.2 + esrap: 2.3.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tiny-lru@11.4.7: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + totalist@3.0.1: {} + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.26 + rollup: 4.63.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + tsx: 4.23.13 + + vitefu@1.1.3(vite@7.3.6(@types/node@24.13.3)(tsx@4.23.13)): + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3)(tsx@4.23.13) + + ws@8.21.3: {} + + zimmerframe@1.1.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..8f4884f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - packages/* +allowBuilds: + esbuild: false diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..a5f06e6 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "noEmit": true + } +}