From a3daed168306b98246630f4c76f22bb03dc7af6c Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 11:26:19 -0800 Subject: [PATCH 01/84] feat: initial elysium idle game prototype Sets up the full monorepo with pnpm workspaces. Includes shared types package, Hono API with Discord OAuth/JWT auth, Prisma v6 + MongoDB Atlas, and React + Vite frontend with game loop, five tabs, and Discord-linked save/load. --- .gitea/workflows/ci.yml | 57 + .gitignore | 6 + apps/api/eslint.config.js | 3 + apps/api/package.json | 31 + apps/api/prisma/schema.prisma | 38 + apps/api/prod.env | 5 + apps/api/src/data/adventurers.ts | 104 + apps/api/src/data/bosses.ts | 64 + apps/api/src/data/initialState.ts | 34 + apps/api/src/data/quests.ts | 63 + apps/api/src/data/upgrades.ts | 98 + apps/api/src/db/client.ts | 3 + apps/api/src/index.ts | 35 + apps/api/src/middleware/auth.ts | 23 + apps/api/src/routes/auth.ts | 97 + apps/api/src/routes/boss.ts | 100 + apps/api/src/routes/game.ts | 62 + apps/api/src/routes/prestige.ts | 63 + apps/api/src/routes/profile.ts | 31 + apps/api/src/services/discord.ts | 74 + apps/api/src/services/jwt.ts | 65 + apps/api/src/services/offlineProgress.ts | 45 + apps/api/src/services/prestige.ts | 54 + apps/api/tsconfig.json | 8 + apps/api/vitest.config.ts | 18 + apps/web/eslint.config.js | 3 + apps/web/index.html | 13 + apps/web/package.json | 31 + apps/web/src/App.tsx | 39 + apps/web/src/api/client.ts | 81 + .../src/components/game/AdventurerPanel.tsx | 70 + apps/web/src/components/game/BossPanel.tsx | 80 + apps/web/src/components/game/ClickArea.tsx | 25 + apps/web/src/components/game/GameLayout.tsx | 82 + apps/web/src/components/game/LoginPage.tsx | 87 + apps/web/src/components/game/OfflineModal.tsx | 27 + .../web/src/components/game/PrestigePanel.tsx | 91 + apps/web/src/components/game/QuestPanel.tsx | 78 + apps/web/src/components/game/UpgradePanel.tsx | 73 + apps/web/src/components/ui/ResourceBar.tsx | 49 + apps/web/src/context/GameContext.tsx | 257 + apps/web/src/engine/tick.ts | 96 + apps/web/src/main.tsx | 16 + apps/web/src/styles.css | 631 ++ apps/web/tsconfig.json | 11 + apps/web/vite.config.ts | 18 + apps/web/vitest.config.ts | 19 + package.json | 15 + packages/types/eslint.config.js | 3 + packages/types/package.json | 18 + packages/types/src/index.ts | 28 + packages/types/src/interfaces/Adventurer.ts | 20 + packages/types/src/interfaces/Api.ts | 63 + packages/types/src/interfaces/Boss.ts | 22 + packages/types/src/interfaces/GameState.ts | 21 + packages/types/src/interfaces/Player.ts | 16 + packages/types/src/interfaces/Prestige.ts | 12 + packages/types/src/interfaces/Quest.ts | 24 + packages/types/src/interfaces/Resource.ts | 6 + packages/types/src/interfaces/Upgrade.ts | 21 + packages/types/tsconfig.json | 8 + pnpm-lock.yaml | 5665 +++++++++++++++++ pnpm-workspace.yaml | 3 + tsconfig.json | 8 + 64 files changed, 9011 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 apps/api/eslint.config.js create mode 100644 apps/api/package.json create mode 100644 apps/api/prisma/schema.prisma create mode 100644 apps/api/prod.env create mode 100644 apps/api/src/data/adventurers.ts create mode 100644 apps/api/src/data/bosses.ts create mode 100644 apps/api/src/data/initialState.ts create mode 100644 apps/api/src/data/quests.ts create mode 100644 apps/api/src/data/upgrades.ts create mode 100644 apps/api/src/db/client.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/middleware/auth.ts create mode 100644 apps/api/src/routes/auth.ts create mode 100644 apps/api/src/routes/boss.ts create mode 100644 apps/api/src/routes/game.ts create mode 100644 apps/api/src/routes/prestige.ts create mode 100644 apps/api/src/routes/profile.ts create mode 100644 apps/api/src/services/discord.ts create mode 100644 apps/api/src/services/jwt.ts create mode 100644 apps/api/src/services/offlineProgress.ts create mode 100644 apps/api/src/services/prestige.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/api/vitest.config.ts create mode 100644 apps/web/eslint.config.js create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/api/client.ts create mode 100644 apps/web/src/components/game/AdventurerPanel.tsx create mode 100644 apps/web/src/components/game/BossPanel.tsx create mode 100644 apps/web/src/components/game/ClickArea.tsx create mode 100644 apps/web/src/components/game/GameLayout.tsx create mode 100644 apps/web/src/components/game/LoginPage.tsx create mode 100644 apps/web/src/components/game/OfflineModal.tsx create mode 100644 apps/web/src/components/game/PrestigePanel.tsx create mode 100644 apps/web/src/components/game/QuestPanel.tsx create mode 100644 apps/web/src/components/game/UpgradePanel.tsx create mode 100644 apps/web/src/components/ui/ResourceBar.tsx create mode 100644 apps/web/src/context/GameContext.tsx create mode 100644 apps/web/src/engine/tick.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/styles.css create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 apps/web/vitest.config.ts create mode 100644 package.json create mode 100644 packages/types/eslint.config.js create mode 100644 packages/types/package.json create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/src/interfaces/Adventurer.ts create mode 100644 packages/types/src/interfaces/Api.ts create mode 100644 packages/types/src/interfaces/Boss.ts create mode 100644 packages/types/src/interfaces/GameState.ts create mode 100644 packages/types/src/interfaces/Player.ts create mode 100644 packages/types/src/interfaces/Prestige.ts create mode 100644 packages/types/src/interfaces/Quest.ts create mode 100644 packages/types/src/interfaces/Resource.ts create mode 100644 packages/types/src/interfaces/Upgrade.ts create mode 100644 packages/types/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a46bc51 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + ci: + name: Lint, Build & Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check dependency pins + uses: naomi-lgbt/dependency-pin-check@main + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: "10" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint (types package) + run: pnpm --filter @elysium/types lint + + - name: Lint (API) + run: pnpm --filter @elysium/api lint + + - name: Lint (web) + run: pnpm --filter @elysium/web lint + + - name: Build (types package) + run: pnpm --filter @elysium/types build + + - name: Build (API) + run: pnpm --filter @elysium/api build + + - name: Build (web) + run: pnpm --filter @elysium/web build + + - name: Test (API) + run: pnpm --filter @elysium/api test + + - name: Test (web) + run: pnpm --filter @elysium/web test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf86cff --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +prod/ +dist/ +.env +*.env.local +coverage/ diff --git a/apps/api/eslint.config.js b/apps/api/eslint.config.js new file mode 100644 index 0000000..64afc0f --- /dev/null +++ b/apps/api/eslint.config.js @@ -0,0 +1,3 @@ +import { NaomisConfig } from "@nhcarrigan/eslint-config"; + +export default [...NaomisConfig]; diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..21f5adc --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,31 @@ +{ + "name": "@elysium/api", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "./prod/src/index.js", + "scripts": { + "build": "prisma generate && tsc -p tsconfig.json", + "db:push": "prisma db push", + "dev": "op run --env-file=./prod.env -- tsx watch src/index.ts", + "lint": "eslint --max-warnings 0 src", + "start": "op run --env-file=./prod.env -- node prod/src/index.js", + "test": "vitest run --coverage" + }, + "dependencies": { + "@elysium/types": "workspace:*", + "@hono/node-server": "1.13.7", + "@prisma/client": "6.5.0", + "hono": "4.7.4", + "prisma": "6.5.0" + }, + "devDependencies": { + "@nhcarrigan/eslint-config": "5.2.0", + "@nhcarrigan/typescript-config": "4.0.0", + "@vitest/coverage-v8": "3.0.8", + "eslint": "9.22.0", + "tsx": "4.19.3", + "typescript": "5.8.2", + "vitest": "3.0.8" + } +} diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma new file mode 100644 index 0000000..bf7ba98 --- /dev/null +++ b/apps/api/prisma/schema.prisma @@ -0,0 +1,38 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model Player { + id String @id @default(auto()) @map("_id") @db.ObjectId + discordId String @unique + username String + discriminator String + avatar String? + characterName String @default("") + createdAt Float + lastSavedAt Float + totalGoldEarned Float @default(0) + totalClicks Float @default(0) +} + +model GameState { + id String @id @default(auto()) @map("_id") @db.ObjectId + discordId String @unique + state Json + updatedAt Float +} + +model BossDamageLog { + id String @id @default(auto()) @map("_id") @db.ObjectId + discordId String + bossId String + damage Float + dealtAt Float + + @@index([discordId, bossId, dealtAt]) +} diff --git a/apps/api/prod.env b/apps/api/prod.env new file mode 100644 index 0000000..2361c07 --- /dev/null +++ b/apps/api/prod.env @@ -0,0 +1,5 @@ +DISCORD_CLIENT_ID="op://Environment Variables - Naomi/Elysium/discord client id" +DISCORD_CLIENT_SECRET="op://Environment Variables - Naomi/Elysium/discord client secret" +DISCORD_REDIRECT_URI="op://Environment Variables - Naomi/Elysium/discord redirect uri" +JWT_SECRET="op://Environment Variables - Naomi/Elysium/jwt secret" +DATABASE_URL="op://Environment Variables - Naomi/Elysium/mongo url" diff --git a/apps/api/src/data/adventurers.ts b/apps/api/src/data/adventurers.ts new file mode 100644 index 0000000..526d412 --- /dev/null +++ b/apps/api/src/data/adventurers.ts @@ -0,0 +1,104 @@ +import type { Adventurer } from "@elysium/types"; + +export const DEFAULT_ADVENTURERS: Adventurer[] = [ + { + id: "peasant", + name: "Peasant", + class: "warrior", + level: 1, + goldPerSecond: 0.1, + essencePerSecond: 0, + count: 0, + unlocked: true, + }, + { + id: "militia", + name: "Militia", + class: "warrior", + level: 2, + goldPerSecond: 0.5, + essencePerSecond: 0, + count: 0, + unlocked: false, + }, + { + id: "apprentice", + name: "Apprentice Mage", + class: "mage", + level: 3, + goldPerSecond: 1.5, + essencePerSecond: 0.01, + count: 0, + unlocked: false, + }, + { + id: "scout", + name: "Scout", + class: "rogue", + level: 4, + goldPerSecond: 4, + essencePerSecond: 0.02, + count: 0, + unlocked: false, + }, + { + id: "acolyte", + name: "Acolyte", + class: "cleric", + level: 5, + goldPerSecond: 10, + essencePerSecond: 0.05, + count: 0, + unlocked: false, + }, + { + id: "ranger", + name: "Ranger", + class: "ranger", + level: 6, + goldPerSecond: 25, + essencePerSecond: 0.1, + count: 0, + unlocked: false, + }, + { + id: "knight", + name: "Knight", + class: "warrior", + level: 7, + goldPerSecond: 75, + essencePerSecond: 0.2, + count: 0, + unlocked: false, + }, + { + id: "archmage", + name: "Archmage", + class: "mage", + level: 8, + goldPerSecond: 200, + essencePerSecond: 0.5, + count: 0, + unlocked: false, + }, + { + id: "paladin", + name: "Paladin", + class: "paladin", + level: 9, + goldPerSecond: 600, + essencePerSecond: 1, + count: 0, + unlocked: false, + }, + { + id: "dragon_rider", + name: "Dragon Rider", + class: "ranger", + level: 10, + goldPerSecond: 2000, + essencePerSecond: 3, + count: 0, + unlocked: false, + }, +]; diff --git a/apps/api/src/data/bosses.ts b/apps/api/src/data/bosses.ts new file mode 100644 index 0000000..135f321 --- /dev/null +++ b/apps/api/src/data/bosses.ts @@ -0,0 +1,64 @@ +import type { Boss } from "@elysium/types"; + +export const DEFAULT_BOSSES: Boss[] = [ + { + id: "troll_king", + name: "The Troll King", + description: + "Gruk the Immovable has terrorised the trade roads for decades. Merchants will pay handsomely for his head.", + status: "available", + maxHp: 1_000, + currentHp: 1_000, + damagePerSecond: 5, + goldReward: 10_000, + essenceReward: 25, + crystalReward: 0, + upgradeRewards: ["click_2"], + prestigeRequirement: 0, + }, + { + id: "lich_queen", + name: "The Lich Queen", + description: + "Seraphina the Undying commands legions of undead from her bone throne. Her defeat will echo through history.", + status: "locked", + maxHp: 10_000, + currentHp: 10_000, + damagePerSecond: 20, + goldReward: 100_000, + essenceReward: 200, + crystalReward: 10, + upgradeRewards: ["global_2"], + prestigeRequirement: 0, + }, + { + id: "elder_dragon", + name: "Elder Dragon Vaeltharox", + description: + "The eldest dragon in existence, older than the kingdom itself. Even his breath can level mountains.", + status: "locked", + maxHp: 100_000, + currentHp: 100_000, + damagePerSecond: 75, + goldReward: 1_000_000, + essenceReward: 1_000, + crystalReward: 50, + upgradeRewards: ["click_3"], + prestigeRequirement: 1, + }, + { + id: "void_titan", + name: "The Void Titan", + description: + "A creature from beyond the veil of reality, drawn by the power your guild has accumulated. It must not be allowed to exist.", + status: "locked", + maxHp: 1_000_000, + currentHp: 1_000_000, + damagePerSecond: 250, + goldReward: 10_000_000, + essenceReward: 5_000, + crystalReward: 200, + upgradeRewards: [], + prestigeRequirement: 3, + }, +]; diff --git a/apps/api/src/data/initialState.ts b/apps/api/src/data/initialState.ts new file mode 100644 index 0000000..334f567 --- /dev/null +++ b/apps/api/src/data/initialState.ts @@ -0,0 +1,34 @@ +import type { GameState, Player, PrestigeData } from "@elysium/types"; +import { DEFAULT_ADVENTURERS } from "./adventurers.js"; +import { DEFAULT_BOSSES } from "./bosses.js"; +import { DEFAULT_QUESTS } from "./quests.js"; +import { DEFAULT_UPGRADES } from "./upgrades.js"; + +export const INITIAL_PRESTIGE: PrestigeData = { + count: 0, + runestones: 0, + productionMultiplier: 1, + purchasedUpgradeIds: [], +}; + +export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameState => ({ + player: { + ...player, + characterName, + totalGoldEarned: 0, + totalClicks: 0, + }, + resources: { + gold: 0, + essence: 0, + crystals: 0, + runestones: 0, + }, + adventurers: structuredClone(DEFAULT_ADVENTURERS), + upgrades: structuredClone(DEFAULT_UPGRADES), + quests: structuredClone(DEFAULT_QUESTS), + bosses: structuredClone(DEFAULT_BOSSES), + prestige: INITIAL_PRESTIGE, + baseClickPower: 1, + lastTickAt: Date.now(), +}); diff --git a/apps/api/src/data/quests.ts b/apps/api/src/data/quests.ts new file mode 100644 index 0000000..b4ebbb8 --- /dev/null +++ b/apps/api/src/data/quests.ts @@ -0,0 +1,63 @@ +import type { Quest } from "@elysium/types"; + +export const DEFAULT_QUESTS: Quest[] = [ + { + id: "first_steps", + name: "First Steps", + description: "Every legend begins somewhere. Send your first adventurer into the field.", + status: "available", + durationSeconds: 60, + rewards: [{ type: "gold", amount: 500 }], + prerequisiteIds: [], + }, + { + id: "goblin_camp", + name: "Goblin Camp", + description: "Clear out a troublesome goblin camp to the east.", + status: "locked", + durationSeconds: 5 * 60, + rewards: [ + { type: "gold", amount: 2_000 }, + { type: "essence", amount: 5 }, + ], + prerequisiteIds: ["first_steps"], + }, + { + id: "haunted_mine", + name: "The Haunted Mine", + description: "An abandoned mine is rich with crystal deposits — if you dare brave its ghosts.", + status: "locked", + durationSeconds: 15 * 60, + rewards: [ + { type: "crystals", amount: 10 }, + { type: "upgrade", targetId: "global_1" }, + ], + prerequisiteIds: ["goblin_camp"], + }, + { + id: "ancient_ruins", + name: "Ancient Ruins", + description: "Scholars believe the ruins hold secrets of a forgotten civilisation.", + status: "locked", + durationSeconds: 30 * 60, + rewards: [ + { type: "essence", amount: 50 }, + { type: "upgrade", targetId: "click_2" }, + ], + prerequisiteIds: ["haunted_mine"], + }, + { + id: "dragon_lair", + name: "Dragon's Lair", + description: + "The legendary lair of Pyraxis the Undying. Few who enter return — those who do are rich beyond imagining.", + status: "locked", + durationSeconds: 60 * 60, + rewards: [ + { type: "gold", amount: 500_000 }, + { type: "crystals", amount: 50 }, + { type: "adventurer", targetId: "dragon_rider" }, + ], + prerequisiteIds: ["ancient_ruins"], + }, +]; diff --git a/apps/api/src/data/upgrades.ts b/apps/api/src/data/upgrades.ts new file mode 100644 index 0000000..2841809 --- /dev/null +++ b/apps/api/src/data/upgrades.ts @@ -0,0 +1,98 @@ +import type { Upgrade } from "@elysium/types"; + +export const DEFAULT_UPGRADES: Upgrade[] = [ + // Click upgrades + { + id: "click_1", + name: "Keen Eye", + description: "Your strikes find weak points. Doubles click power.", + target: "click", + multiplier: 2, + costGold: 100, + costEssence: 0, + purchased: false, + unlocked: true, + }, + { + id: "click_2", + name: "Battle Hardened", + description: "Years of combat sharpen your instincts. Doubles click power again.", + target: "click", + multiplier: 2, + costGold: 1000, + costEssence: 0, + purchased: false, + unlocked: false, + }, + { + id: "click_3", + name: "Legendary Weapon", + description: "A weapon of ancient power. Triples click power.", + target: "click", + multiplier: 3, + costGold: 50_000, + costEssence: 10, + purchased: false, + unlocked: false, + }, + // Global upgrades + { + id: "global_1", + name: "Guild Charter", + description: "Formalising the guild structure increases all income by 25%.", + target: "global", + multiplier: 1.25, + costGold: 500, + costEssence: 0, + purchased: false, + unlocked: false, + }, + { + id: "global_2", + name: "Merchant Alliance", + description: "Trade routes boost all income by 50%.", + target: "global", + multiplier: 1.5, + costGold: 10_000, + costEssence: 5, + purchased: false, + unlocked: false, + }, + // Adventurer-specific upgrades + { + id: "peasant_1", + name: "Better Tools", + description: "Peasants work twice as hard with proper equipment.", + target: "adventurer", + adventurerId: "peasant", + multiplier: 2, + costGold: 200, + costEssence: 0, + purchased: false, + unlocked: false, + }, + { + id: "militia_1", + name: "Militia Training", + description: "Formal training doubles militia effectiveness.", + target: "adventurer", + adventurerId: "militia", + multiplier: 2, + costGold: 1_000, + costEssence: 0, + purchased: false, + unlocked: false, + }, + { + id: "mage_1", + name: "Arcane Tomes", + description: "Ancient books of magic double mage output.", + target: "adventurer", + adventurerId: "apprentice", + multiplier: 2, + costGold: 5_000, + costEssence: 2, + purchased: false, + unlocked: false, + }, +]; diff --git a/apps/api/src/db/client.ts b/apps/api/src/db/client.ts new file mode 100644 index 0000000..901f3a0 --- /dev/null +++ b/apps/api/src/db/client.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from "@prisma/client"; + +export const prisma = new PrismaClient(); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..f0d2513 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,35 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; +import { authRouter } from "./routes/auth.js"; +import { bossRouter } from "./routes/boss.js"; +import { gameRouter } from "./routes/game.js"; +import { prestigeRouter } from "./routes/prestige.js"; +import { profileRouter } from "./routes/profile.js"; + +const app = new Hono(); + +app.use("*", logger()); +app.use( + "*", + cors({ + origin: process.env["CORS_ORIGIN"] ?? "http://localhost:5173", + allowHeaders: ["Authorization", "Content-Type"], + allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + }), +); + +app.route("/auth", authRouter); +app.route("/game", gameRouter); +app.route("/boss", bossRouter); +app.route("/prestige", prestigeRouter); +app.route("/profile", profileRouter); + +app.get("/health", (context) => context.json({ status: "ok" })); + +const port = Number(process.env["PORT"] ?? 3001); + +serve({ fetch: app.fetch, port }, () => { + console.log(`Elysium API running on port ${port}`); +}); diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..69c3489 --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,23 @@ +import type { Context, Next } from "hono"; +import { verifyToken } from "../services/jwt.js"; + +export const authMiddleware = async (context: Context, next: Next): Promise => { + const authorization = context.req.header("Authorization"); + + if (!authorization?.startsWith("Bearer ")) { + context.status(401); + context.json({ error: "Missing or invalid Authorization header" }); + return; + } + + const token = authorization.slice(7); + + try { + const payload = verifyToken(token); + context.set("discordId", payload.discordId); + await next(); + } catch { + context.status(401); + context.json({ error: "Invalid or expired token" }); + } +}; diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..bd358a2 --- /dev/null +++ b/apps/api/src/routes/auth.ts @@ -0,0 +1,97 @@ +import type { Player } from "@elysium/types"; +import { Hono } from "hono"; +import { prisma } from "../db/client.js"; +import { INITIAL_GAME_STATE } from "../data/initialState.js"; +import { + buildOAuthUrl, + exchangeCode, + fetchDiscordUser, +} from "../services/discord.js"; +import { signToken } from "../services/jwt.js"; + +export const authRouter = new Hono(); + +authRouter.get("/url", (context) => { + try { + const url = buildOAuthUrl(); + return context.json({ url }); + } catch { + return context.json({ error: "Failed to build OAuth URL" }, 500); + } +}); + +authRouter.get("/callback", async (context) => { + const code = context.req.query("code"); + + if (!code) { + return context.json({ error: "Missing code parameter" }, 400); + } + + try { + const tokenData = await exchangeCode(code); + const discordUser = await fetchDiscordUser(tokenData.access_token); + + const existing = await prisma.player.findUnique({ + where: { discordId: discordUser.id }, + }); + + const now = Date.now(); + + if (!existing) { + const player = await prisma.player.create({ + data: { + discordId: discordUser.id, + username: discordUser.username, + discriminator: discordUser.discriminator, + avatar: discordUser.avatar, + characterName: discordUser.username, + createdAt: now, + lastSavedAt: now, + totalGoldEarned: 0, + totalClicks: 0, + }, + }); + + const playerShape: Player = { + discordId: player.discordId, + username: player.username, + discriminator: player.discriminator, + avatar: player.avatar ?? null, + characterName: player.characterName, + createdAt: player.createdAt, + lastSavedAt: player.lastSavedAt, + totalGoldEarned: player.totalGoldEarned, + totalClicks: player.totalClicks, + }; + + const initialState = INITIAL_GAME_STATE(playerShape, playerShape.characterName); + await prisma.gameState.create({ + data: { + discordId: player.discordId, + state: initialState, + updatedAt: now, + }, + }); + + const jwtToken = signToken(player.discordId); + const clientUrl = process.env["CORS_ORIGIN"] ?? "http://localhost:5173"; + return context.redirect(`${clientUrl}/auth/callback?token=${jwtToken}&isNew=true`); + } + + const updated = await prisma.player.update({ + where: { discordId: discordUser.id }, + data: { + username: discordUser.username, + discriminator: discordUser.discriminator, + avatar: discordUser.avatar, + }, + }); + + const jwtToken = signToken(updated.discordId); + const clientUrl = process.env["CORS_ORIGIN"] ?? "http://localhost:5173"; + return context.redirect(`${clientUrl}/auth/callback?token=${jwtToken}&isNew=false`); + } catch { + const clientUrl = process.env["CORS_ORIGIN"] ?? "http://localhost:5173"; + return context.redirect(`${clientUrl}/auth/callback?error=auth_failed`); + } +}); diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts new file mode 100644 index 0000000..d4e51aa --- /dev/null +++ b/apps/api/src/routes/boss.ts @@ -0,0 +1,100 @@ +import type { BossDamageRequest, GameState } from "@elysium/types"; +import { Hono } from "hono"; +import { prisma } from "../db/client.js"; +import { authMiddleware } from "../middleware/auth.js"; + +const RATE_LIMIT_WINDOW_MS = 1_000; +const MAX_DAMAGE_PER_SECOND = 10_000; + +export const bossRouter = new Hono(); + +bossRouter.use("*", authMiddleware); + +bossRouter.post("/damage", async (context) => { + const discordId = context.get("discordId") as string; + const body = await context.req.json(); + + if (!body.bossId || body.damage == null || body.damage <= 0) { + return context.json({ error: "Invalid request body" }, 400); + } + + // Rate limiting: sum damage dealt to this boss in the last second + const windowStart = Date.now() - RATE_LIMIT_WINDOW_MS; + const aggregate = await prisma.bossDamageLog.aggregate({ + where: { discordId, bossId: body.bossId, dealtAt: { gt: windowStart } }, + _sum: { damage: true }, + }); + + const recentDamage = aggregate._sum.damage ?? 0; + + if (recentDamage + body.damage > MAX_DAMAGE_PER_SECOND) { + return context.json({ error: "Rate limit exceeded" }, 429); + } + + const record = await prisma.gameState.findUnique({ where: { discordId } }); + + if (!record) { + return context.json({ error: "No save found" }, 404); + } + + const state = record.state as unknown as GameState; + const boss = state.bosses.find((b) => b.id === body.bossId); + + if (!boss) { + return context.json({ error: "Boss not found" }, 404); + } + + if (boss.status !== "in_progress" && boss.status !== "available") { + return context.json({ error: "Boss is not currently available" }, 400); + } + + if (boss.prestigeRequirement > state.prestige.count) { + return context.json({ error: "Prestige requirement not met" }, 403); + } + + await prisma.bossDamageLog.create({ + data: { discordId, bossId: body.bossId, damage: body.damage, dealtAt: Date.now() }, + }); + + boss.status = "in_progress"; + boss.currentHp = Math.max(0, boss.currentHp - body.damage); + const defeated = boss.currentHp <= 0; + + let rewards: { gold: number; essence: number; crystals: number; upgradeIds: string[] } | undefined; + + if (defeated) { + boss.status = "defeated"; + state.resources.gold += boss.goldReward; + state.resources.essence += boss.essenceReward; + state.resources.crystals += boss.crystalReward; + state.player.totalGoldEarned += boss.goldReward; + + for (const upgradeId of boss.upgradeRewards) { + const upgrade = state.upgrades.find((u) => u.id === upgradeId); + if (upgrade) { + upgrade.unlocked = true; + } + } + + const bossIndex = state.bosses.findIndex((b) => b.id === body.bossId); + const nextBoss = state.bosses[bossIndex + 1]; + if (nextBoss && nextBoss.prestigeRequirement <= state.prestige.count) { + nextBoss.status = "available"; + } + + rewards = { + gold: boss.goldReward, + essence: boss.essenceReward, + crystals: boss.crystalReward, + upgradeIds: boss.upgradeRewards, + }; + } + + const now = Date.now(); + await prisma.gameState.update({ + where: { discordId }, + data: { state: state as object, updatedAt: now }, + }); + + return context.json({ currentHp: boss.currentHp, defeated, rewards }); +}); diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts new file mode 100644 index 0000000..c6896a5 --- /dev/null +++ b/apps/api/src/routes/game.ts @@ -0,0 +1,62 @@ +import type { GameState, SaveRequest } from "@elysium/types"; +import { Hono } from "hono"; +import { prisma } from "../db/client.js"; +import { authMiddleware } from "../middleware/auth.js"; +import { calculateOfflineGold } from "../services/offlineProgress.js"; + +export const gameRouter = new Hono(); + +gameRouter.use("*", authMiddleware); + +gameRouter.get("/load", async (context) => { + const discordId = context.get("discordId") as string; + + const record = await prisma.gameState.findUnique({ where: { discordId } }); + + if (!record) { + return context.json({ error: "No save found" }, 404); + } + + const state = record.state as unknown as GameState; + const now = Date.now(); + + const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now); + + if (offlineGold > 0) { + state.resources.gold += offlineGold; + state.player.totalGoldEarned += offlineGold; + } + + state.lastTickAt = now; + + return context.json({ state, offlineGold, offlineSeconds }); +}); + +gameRouter.post("/save", async (context) => { + const discordId = context.get("discordId") as string; + const body = await context.req.json(); + + if (!body.state) { + return context.json({ error: "Missing state in request body" }, 400); + } + + const now = Date.now(); + + await prisma.player.update({ + where: { discordId }, + data: { + lastSavedAt: now, + totalGoldEarned: body.state.player.totalGoldEarned, + totalClicks: body.state.player.totalClicks, + characterName: body.state.player.characterName, + }, + }); + + await prisma.gameState.upsert({ + where: { discordId }, + create: { discordId, state: body.state, updatedAt: now }, + update: { state: body.state, updatedAt: now }, + }); + + return context.json({ savedAt: now }); +}); diff --git a/apps/api/src/routes/prestige.ts b/apps/api/src/routes/prestige.ts new file mode 100644 index 0000000..2a0fc41 --- /dev/null +++ b/apps/api/src/routes/prestige.ts @@ -0,0 +1,63 @@ +import type { GameState, PrestigeRequest } from "@elysium/types"; +import { Hono } from "hono"; +import { prisma } from "../db/client.js"; +import { authMiddleware } from "../middleware/auth.js"; +import { + buildPostPrestigeState, + isEligibleForPrestige, +} from "../services/prestige.js"; + +export const prestigeRouter = new Hono(); + +prestigeRouter.use("*", authMiddleware); + +prestigeRouter.post("/", async (context) => { + const discordId = context.get("discordId") as string; + const body = await context.req.json(); + + const characterName = body.characterName?.trim(); + if (!characterName) { + return context.json({ error: "characterName is required" }, 400); + } + + const record = await prisma.gameState.findUnique({ where: { discordId } }); + + if (!record) { + return context.json({ error: "No save found" }, 404); + } + + const state = record.state as unknown as GameState; + + if (!isEligibleForPrestige(state)) { + return context.json( + { error: "Not eligible for prestige — collect 1,000,000 total gold first" }, + 400, + ); + } + + const { newState, newPrestigeData, runestonesEarned } = buildPostPrestigeState( + state, + characterName, + ); + + const now = Date.now(); + await prisma.gameState.update({ + where: { discordId }, + data: { state: newState as object, updatedAt: now }, + }); + + await prisma.player.update({ + where: { discordId }, + data: { + characterName, + totalGoldEarned: 0, + totalClicks: 0, + lastSavedAt: now, + }, + }); + + return context.json({ + runestones: runestonesEarned, + newPrestigeCount: newPrestigeData.count, + }); +}); diff --git a/apps/api/src/routes/profile.ts b/apps/api/src/routes/profile.ts new file mode 100644 index 0000000..0734a6e --- /dev/null +++ b/apps/api/src/routes/profile.ts @@ -0,0 +1,31 @@ +import type { GameState } from "@elysium/types"; +import { Hono } from "hono"; +import { prisma } from "../db/client.js"; + +export const profileRouter = new Hono(); + +profileRouter.get("/:discordId", async (context) => { + const { discordId } = context.req.param(); + + const [player, gameStateRecord] = await Promise.all([ + prisma.player.findUnique({ where: { discordId } }), + prisma.gameState.findUnique({ where: { discordId } }), + ]); + + if (!player) { + return context.json({ error: "Player not found" }, 404); + } + + const state = gameStateRecord?.state as unknown as GameState | undefined; + const prestigeCount = state?.prestige.count ?? 0; + + return context.json({ + characterName: player.characterName, + username: player.username, + avatar: player.avatar ?? null, + prestigeCount, + totalGoldEarned: player.totalGoldEarned, + totalClicks: player.totalClicks, + createdAt: player.createdAt, + }); +}); diff --git a/apps/api/src/services/discord.ts b/apps/api/src/services/discord.ts new file mode 100644 index 0000000..2908a80 --- /dev/null +++ b/apps/api/src/services/discord.ts @@ -0,0 +1,74 @@ +export interface DiscordTokenResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + scope: string; +} + +export interface DiscordUser { + id: string; + username: string; + discriminator: string; + avatar: string | null; +} + +export const exchangeCode = async (code: string): Promise => { + const clientId = process.env["DISCORD_CLIENT_ID"]; + const clientSecret = process.env["DISCORD_CLIENT_SECRET"]; + const redirectUri = process.env["DISCORD_REDIRECT_URI"]; + + if (!clientId || !clientSecret || !redirectUri) { + throw new Error("Discord OAuth environment variables are required"); + } + + const params = new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + grant_type: "authorization_code", + code, + redirect_uri: redirectUri, + }); + + const response = await fetch("https://discord.com/api/v10/oauth2/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params.toString(), + }); + + if (!response.ok) { + throw new Error(`Discord token exchange failed: ${response.statusText}`); + } + + return response.json() as Promise; +}; + +export const fetchDiscordUser = async (accessToken: string): Promise => { + const response = await fetch("https://discord.com/api/v10/users/@me", { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!response.ok) { + throw new Error(`Discord user fetch failed: ${response.statusText}`); + } + + return response.json() as Promise; +}; + +export const buildOAuthUrl = (): string => { + const clientId = process.env["DISCORD_CLIENT_ID"]; + const redirectUri = process.env["DISCORD_REDIRECT_URI"]; + + if (!clientId || !redirectUri) { + throw new Error("Discord OAuth environment variables are required"); + } + + const params = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUri, + response_type: "code", + scope: "identify", + }); + + return `https://discord.com/api/oauth2/authorize?${params.toString()}`; +}; diff --git a/apps/api/src/services/jwt.ts b/apps/api/src/services/jwt.ts new file mode 100644 index 0000000..3478163 --- /dev/null +++ b/apps/api/src/services/jwt.ts @@ -0,0 +1,65 @@ +import { createHmac } from "crypto"; + +interface JwtPayload { + discordId: string; + iat: number; + exp: number; +} + +const base64UrlEncode = (data: string): string => + Buffer.from(data).toString("base64url"); + +const base64UrlDecode = (data: string): string => + Buffer.from(data, "base64url").toString("utf8"); + +export const signToken = (discordId: string): string => { + const secret = process.env["JWT_SECRET"]; + if (!secret) { + throw new Error("JWT_SECRET environment variable is required"); + } + + const header = base64UrlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const payload = base64UrlEncode( + JSON.stringify({ + discordId, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days + }), + ); + + const signature = createHmac("sha256", secret) + .update(`${header}.${payload}`) + .digest("base64url"); + + return `${header}.${payload}.${signature}`; +}; + +export const verifyToken = (token: string): JwtPayload => { + const secret = process.env["JWT_SECRET"]; + if (!secret) { + throw new Error("JWT_SECRET environment variable is required"); + } + + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid token format"); + } + + const [header, payload, signature] = parts as [string, string, string]; + + const expectedSignature = createHmac("sha256", secret) + .update(`${header}.${payload}`) + .digest("base64url"); + + if (signature !== expectedSignature) { + throw new Error("Invalid token signature"); + } + + const decoded = JSON.parse(base64UrlDecode(payload)) as JwtPayload; + + if (decoded.exp < Math.floor(Date.now() / 1000)) { + throw new Error("Token has expired"); + } + + return decoded; +}; diff --git a/apps/api/src/services/offlineProgress.ts b/apps/api/src/services/offlineProgress.ts new file mode 100644 index 0000000..a46c651 --- /dev/null +++ b/apps/api/src/services/offlineProgress.ts @@ -0,0 +1,45 @@ +import type { GameState } from "@elysium/types"; + +const MAX_OFFLINE_SECONDS = 8 * 60 * 60; // 8 hours + +/** + * Calculates the gold earned whilst the player was offline. + * Capped at 8 hours to prevent exploit via system clock manipulation. + */ +export const calculateOfflineGold = ( + state: GameState, + nowMs: number, +): { offlineGold: number; offlineSeconds: number } => { + const elapsedSeconds = Math.min( + (nowMs - state.lastTickAt) / 1000, + MAX_OFFLINE_SECONDS, + ); + + const goldPerSecond = state.adventurers.reduce((total, adventurer) => { + if (!adventurer.unlocked || adventurer.count === 0) { + return total; + } + + const upgradeMultiplier = state.upgrades + .filter( + (u) => + u.purchased && + (u.target === "global" || + (u.target === "adventurer" && u.adventurerId === adventurer.id)), + ) + .reduce((mult, u) => mult * u.multiplier, 1); + + return ( + total + + adventurer.goldPerSecond * + adventurer.count * + upgradeMultiplier * + state.prestige.productionMultiplier + ); + }, 0); + + return { + offlineGold: goldPerSecond * elapsedSeconds, + offlineSeconds: elapsedSeconds, + }; +}; diff --git a/apps/api/src/services/prestige.ts b/apps/api/src/services/prestige.ts new file mode 100644 index 0000000..53ebbd6 --- /dev/null +++ b/apps/api/src/services/prestige.ts @@ -0,0 +1,54 @@ +import type { GameState, PrestigeData } from "@elysium/types"; +import { INITIAL_GAME_STATE } from "../data/initialState.js"; + +const PRESTIGE_GOLD_THRESHOLD = 1_000_000; +const RUNESTONES_PER_PRESTIGE_LEVEL = 10; + +export const isEligibleForPrestige = (state: GameState): boolean => + state.player.totalGoldEarned >= PRESTIGE_GOLD_THRESHOLD; + +/** + * Calculates how many runestones the player earns from a prestige. + * Formula: floor(sqrt(totalGoldEarned / PRESTIGE_GOLD_THRESHOLD)) * RUNESTONES_PER_PRESTIGE_LEVEL + */ +export const calculateRunestones = (totalGoldEarned: number): number => + Math.floor(Math.sqrt(totalGoldEarned / PRESTIGE_GOLD_THRESHOLD)) * + RUNESTONES_PER_PRESTIGE_LEVEL; + +/** + * Calculates the new prestige production multiplier. + * Formula: 1 + (prestigeCount * 0.1) — each prestige adds 10% global production. + */ +export const calculateProductionMultiplier = (prestigeCount: number): number => + 1 + prestigeCount * 0.1; + +/** + * Generates the reset game state after a prestige. + * Carries over prestige data and runestones; resets everything else. + */ +export const buildPostPrestigeState = ( + currentState: GameState, + characterName: string, +): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number } => { + const runestonesEarned = calculateRunestones( + currentState.player.totalGoldEarned, + ); + const newPrestigeCount = currentState.prestige.count + 1; + + const newPrestigeData: PrestigeData = { + count: newPrestigeCount, + runestones: currentState.prestige.runestones + runestonesEarned, + productionMultiplier: calculateProductionMultiplier(newPrestigeCount), + purchasedUpgradeIds: currentState.prestige.purchasedUpgradeIds, + lastPrestigedAt: Date.now(), + }; + + const freshState = INITIAL_GAME_STATE(currentState.player, characterName); + const newState: GameState = { + ...freshState, + prestige: newPrestigeData, + lastTickAt: Date.now(), + }; + + return { newState, newPrestigeData, runestonesEarned }; +}; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..d3b0572 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nhcarrigan/typescript-config", + "compilerOptions": { + "outDir": "./prod", + "rootDir": "." + }, + "exclude": ["test/**/*.ts"] +} diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 0000000..d0b268e --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: ["src/types/**/*.ts"], + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + include: ["test/**/*.spec.ts"], + }, +}); diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 0000000..64afc0f --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,3 @@ +import { NaomisConfig } from "@nhcarrigan/eslint-config"; + +export default [...NaomisConfig]; diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..d154738 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + Elysium — Idle RPG + + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..3f2bb26 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,31 @@ +{ + "name": "@elysium/web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json && vite build", + "dev": "vite", + "lint": "eslint --max-warnings 0 src", + "preview": "vite preview", + "test": "vitest run --coverage" + }, + "dependencies": { + "@elysium/types": "workspace:*", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@nhcarrigan/eslint-config": "5.2.0", + "@nhcarrigan/typescript-config": "4.0.0", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@vitejs/plugin-react": "4.3.4", + "@vitest/coverage-v8": "3.0.8", + "eslint": "9.22.0", + "jsdom": "26.0.0", + "typescript": "5.8.2", + "vite": "6.2.1", + "vitest": "3.0.8" + } +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..a0694f1 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { GameProvider } from "./context/GameContext.js"; +import { GameLayout } from "./components/game/GameLayout.js"; +import { LoginPage } from "./components/game/LoginPage.js"; + +const handleAuthCallback = (): boolean => { + if (window.location.pathname !== "/auth/callback") { + return false; + } + + const params = new URLSearchParams(window.location.search); + const token = params.get("token"); + + if (token) { + localStorage.setItem("elysium_token", token); + } + + window.history.replaceState(null, "", "/"); + return Boolean(token); +}; + +const isAuthenticated = (): boolean => { + const fromCallback = handleAuthCallback(); + return fromCallback || Boolean(localStorage.getItem("elysium_token")); +}; + +export const App = (): React.JSX.Element => { + const [loggedIn, setLoggedIn] = useState(isAuthenticated); + + if (!loggedIn) { + return { setLoggedIn(true); }} />; + } + + return ( + + + + ); +}; diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts new file mode 100644 index 0000000..3b4fd6a --- /dev/null +++ b/apps/web/src/api/client.ts @@ -0,0 +1,81 @@ +import type { + AuthResponse, + BossDamageRequest, + BossDamageResponse, + LoadResponse, + PrestigeRequest, + PrestigeResponse, + PublicProfileResponse, + SaveRequest, + SaveResponse, +} from "@elysium/types"; + +const BASE_URL = "/api"; + +const getToken = (): string | null => localStorage.getItem("elysium_token"); + +const headers = (): Record => { + const token = getToken(); + return { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +}; + +const request = async ( + path: string, + options?: RequestInit, +): Promise => { + const response = await fetch(`${BASE_URL}${path}`, { + ...options, + headers: { ...headers(), ...options?.headers }, + }); + + if (!response.ok) { + const error = (await response.json().catch(() => ({ error: "Unknown error" }))) as { + error: string; + }; + throw new Error(error.error); + } + + return response.json() as Promise; +}; + +export const getAuthUrl = async (): Promise => { + const data = await request<{ url: string }>("/auth/url"); + return data.url; +}; + +export const handleAuthCallback = async (code: string): Promise => { + const data = await request(`/auth/callback?code=${code}`); + localStorage.setItem("elysium_token", data.token); + return data; +}; + +export const loadGame = async (): Promise => + request("/game/load"); + +export const saveGame = async (body: SaveRequest): Promise => + request("/game/save", { + method: "POST", + body: JSON.stringify(body), + }); + +export const dealBossDamage = async ( + body: BossDamageRequest, +): Promise => + request("/boss/damage", { + method: "POST", + body: JSON.stringify(body), + }); + +export const prestige = async (body: PrestigeRequest): Promise => + request("/prestige", { + method: "POST", + body: JSON.stringify(body), + }); + +export const getPublicProfile = async ( + discordId: string, +): Promise => + request(`/profile/${discordId}`); diff --git a/apps/web/src/components/game/AdventurerPanel.tsx b/apps/web/src/components/game/AdventurerPanel.tsx new file mode 100644 index 0000000..dc2b83c --- /dev/null +++ b/apps/web/src/components/game/AdventurerPanel.tsx @@ -0,0 +1,70 @@ +import type { Adventurer } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; + +const CLASS_ICONS: Record = { + warrior: "🗡️", + mage: "🔮", + rogue: "🗝️", + cleric: "✝️", + ranger: "🏹", + paladin: "🛡️", +}; + +const adventurerCost = (adventurer: Adventurer): number => + Math.ceil(10 * Math.pow(1.15, adventurer.count)); + +interface AdventurerCardProps { + adventurer: Adventurer; + currentGold: number; +} + +const AdventurerCard = ({ adventurer, currentGold }: AdventurerCardProps): React.JSX.Element => { + const { buyAdventurer } = useGame(); + const cost = adventurerCost(adventurer); + const canAfford = currentGold >= cost; + + return ( +
+
{CLASS_ICONS[adventurer.class] ?? "⚔️"}
+
+

{adventurer.name}

+

{adventurer.goldPerSecond.toFixed(2)} gold/s each

+ {adventurer.essencePerSecond > 0 && ( +

{adventurer.essencePerSecond.toFixed(3)} essence/s each

+ )} +
+
×{adventurer.count}
+ +
+ ); +}; + +export const AdventurerPanel = (): React.JSX.Element => { + const { state } = useGame(); + + if (!state) return

Loading...

; + + return ( +
+

Adventurers

+
+ {state.adventurers + .filter((a) => a.unlocked) + .map((adventurer) => ( + + ))} +
+
+ ); +}; diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx new file mode 100644 index 0000000..cb743d7 --- /dev/null +++ b/apps/web/src/components/game/BossPanel.tsx @@ -0,0 +1,80 @@ +import type { Boss } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; + +interface BossCardProps { + boss: Boss; + prestigeCount: number; +} + +const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element => { + const { attackBoss } = useGame(); + const hpPercent = (boss.currentHp / boss.maxHp) * 100; + const isLocked = boss.prestigeRequirement > prestigeCount; + + return ( +
+
+

{boss.name}

+

{boss.description}

+ {isLocked && boss.status === "locked" && ( +

🔒 Requires Prestige {boss.prestigeRequirement}

+ )} +
+ + {boss.status !== "locked" && boss.status !== "defeated" && ( +
+
+
+
+ + {boss.currentHp.toLocaleString()} / {boss.maxHp.toLocaleString()} HP + +
+ )} + +
+ 🪙 {boss.goldReward.toLocaleString()} + {boss.essenceReward > 0 && ✨ {boss.essenceReward.toLocaleString()}} + {boss.crystalReward > 0 && 💎 {boss.crystalReward.toLocaleString()}} +
+ + {boss.status === "available" || boss.status === "in_progress" ? ( + + ) : null} + + {boss.status === "defeated" && ( + ☠️ Defeated + )} +
+ ); +}; + +export const BossPanel = (): React.JSX.Element => { + const { state } = useGame(); + + if (!state) return

Loading...

; + + return ( +
+

Boss Encounters

+
+ {state.bosses.map((boss) => ( + + ))} +
+
+ ); +}; diff --git a/apps/web/src/components/game/ClickArea.tsx b/apps/web/src/components/game/ClickArea.tsx new file mode 100644 index 0000000..6ad6308 --- /dev/null +++ b/apps/web/src/components/game/ClickArea.tsx @@ -0,0 +1,25 @@ +import { useGame } from "../../context/GameContext.js"; +import { calculateClickPower } from "../../engine/tick.js"; + +export const ClickArea = (): React.JSX.Element => { + const { state, handleClick } = useGame(); + + if (!state) return
; + + const clickPower = calculateClickPower(state); + + return ( +
+

Guild Hall

+ +

+{clickPower.toFixed(1)} gold per click

+
+ ); +}; diff --git a/apps/web/src/components/game/GameLayout.tsx b/apps/web/src/components/game/GameLayout.tsx new file mode 100644 index 0000000..dd5e0cd --- /dev/null +++ b/apps/web/src/components/game/GameLayout.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { useGame } from "../../context/GameContext.js"; +import { ResourceBar } from "../ui/ResourceBar.js"; +import { AdventurerPanel } from "./AdventurerPanel.js"; +import { BossPanel } from "./BossPanel.js"; +import { ClickArea } from "./ClickArea.js"; +import { OfflineModal } from "./OfflineModal.js"; +import { PrestigePanel } from "./PrestigePanel.js"; +import { QuestPanel } from "./QuestPanel.js"; +import { UpgradePanel } from "./UpgradePanel.js"; + +type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "prestige"; + +const TABS: { id: Tab; label: string }[] = [ + { id: "adventurers", label: "⚔️ Adventurers" }, + { id: "upgrades", label: "🔧 Upgrades" }, + { id: "quests", label: "📜 Quests" }, + { id: "bosses", label: "👹 Bosses" }, + { id: "prestige", label: "⭐ Prestige" }, +]; + +export const GameLayout = (): React.JSX.Element => { + const { state, isLoading, error } = useGame(); + const [activeTab, setActiveTab] = useState("adventurers"); + + if (isLoading) { + return ( +
+

Loading your adventure...

+
+ ); + } + + if (error) { + return ( +
+

Error: {error}

+
+ ); + } + + if (!state) return

Loading...

; + + return ( +
+ + + +
+ + +
+ + +
+ {activeTab === "adventurers" && } + {activeTab === "upgrades" && } + {activeTab === "quests" && } + {activeTab === "bosses" && } + {activeTab === "prestige" && } +
+
+
+
+ ); +}; diff --git a/apps/web/src/components/game/LoginPage.tsx b/apps/web/src/components/game/LoginPage.tsx new file mode 100644 index 0000000..506e190 --- /dev/null +++ b/apps/web/src/components/game/LoginPage.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from "react"; +import { getAuthUrl, handleAuthCallback } from "../../api/client.js"; + +interface LoginPageProps { + onLogin: () => void; +} + +export const LoginPage = ({ onLogin }: LoginPageProps): React.JSX.Element => { + const [authUrl, setAuthUrl] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // Handle OAuth callback + const params = new URLSearchParams(window.location.search); + const code = params.get("code"); + + if (code) { + setIsLoading(true); + handleAuthCallback(code) + .then(() => { + window.history.replaceState({}, "", "/"); + onLogin(); + }) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : "Authentication failed"); + setIsLoading(false); + }); + return; + } + + // Fetch the Discord OAuth URL + getAuthUrl() + .then((url) => { + setAuthUrl(url); + setIsLoading(false); + }) + .catch(() => { + setError("Failed to load authentication URL"); + setIsLoading(false); + }); + }, [onLogin]); + + if (isLoading) { + return ( +
+
+

Loading...

+
+
+ ); + } + + if (error) { + return ( +
+
+

{error}

+ +
+
+ ); + } + + return ( +
+
+

⚔️ Elysium

+

An idle fantasy RPG. Hire adventurers, defeat bosses, and ascend to glory.

+ + Login with Discord + +

+ Your progress is saved to your Discord account and shareable with others! +

+
+
+ ); +}; diff --git a/apps/web/src/components/game/OfflineModal.tsx b/apps/web/src/components/game/OfflineModal.tsx new file mode 100644 index 0000000..af52793 --- /dev/null +++ b/apps/web/src/components/game/OfflineModal.tsx @@ -0,0 +1,27 @@ +import { useGame } from "../../context/GameContext.js"; + +export const OfflineModal = (): React.JSX.Element | null => { + const { offlineGold, dismissOfflineGold } = useGame(); + + if (offlineGold <= 0) return null; + + return ( +
+
+

Welcome back!

+

+ Your adventurers kept working whilst you were away and earned{" "} + 🪙 {offlineGold.toFixed(0)} gold! +

+

Offline progress is calculated up to 8 hours.

+ +
+
+ ); +}; diff --git a/apps/web/src/components/game/PrestigePanel.tsx b/apps/web/src/components/game/PrestigePanel.tsx new file mode 100644 index 0000000..8fa891c --- /dev/null +++ b/apps/web/src/components/game/PrestigePanel.tsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import { prestige } from "../../api/client.js"; +import { useGame } from "../../context/GameContext.js"; + +const PRESTIGE_THRESHOLD = 1_000_000; + +export const PrestigePanel = (): React.JSX.Element => { + const { state, reload } = useGame(); + const [characterName, setCharacterName] = useState(""); + const [isPending, setIsPending] = useState(false); + const [result, setResult] = useState<{ runestones: number; count: number } | null>(null); + const [error, setError] = useState(null); + + if (!state) return

Loading...

; + + const isEligible = state.player.totalGoldEarned >= PRESTIGE_THRESHOLD; + + const handlePrestige = async (): Promise => { + if (!characterName.trim()) return; + setIsPending(true); + setError(null); + try { + const data = await prestige({ characterName: characterName.trim() }); + setResult({ runestones: data.runestones, count: data.newPrestigeCount }); + await reload(); + } catch (err) { + setError(err instanceof Error ? err.message : "Prestige failed"); + } finally { + setIsPending(false); + } + }; + + return ( +
+

⭐ Prestige

+

+ Prestige resets your progress but grants Runestones — permanent + currency used for powerful upgrades. Each prestige also increases your global + production multiplier by 10%. +

+ +
+

+ Total gold earned:{" "} + {state.player.totalGoldEarned.toLocaleString()} +

+

+ Required: {PRESTIGE_THRESHOLD.toLocaleString()} +

+

Current prestige count: {state.prestige.count}

+

+ Production multiplier:{" "} + ×{state.prestige.productionMultiplier.toFixed(1)} +

+
+ + {isEligible ? ( +
+

You are ready to prestige! Choose your new character name:

+ { setCharacterName(e.target.value); }} + placeholder="Character name..." + maxLength={32} + disabled={isPending} + /> + + {error &&

{error}

} + {result && ( +

+ Ascended to Prestige {result.count}! Earned {result.runestones} Runestones. +

+ )} +
+ ) : ( +

+ Earn {(PRESTIGE_THRESHOLD - state.player.totalGoldEarned).toLocaleString()} more + gold to unlock prestige. +

+ )} +
+ ); +}; diff --git a/apps/web/src/components/game/QuestPanel.tsx b/apps/web/src/components/game/QuestPanel.tsx new file mode 100644 index 0000000..e127b03 --- /dev/null +++ b/apps/web/src/components/game/QuestPanel.tsx @@ -0,0 +1,78 @@ +import type { Quest } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; + +const formatDuration = (seconds: number): string => { + if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; + if (seconds >= 60) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; + return `${seconds}s`; +}; + +const questTimeRemaining = (quest: Quest): number => { + if (quest.status !== "active" || quest.startedAt == null) return 0; + const elapsed = (Date.now() - quest.startedAt) / 1000; + return Math.max(0, quest.durationSeconds - elapsed); +}; + +interface QuestCardProps { + quest: Quest; +} + +const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => { + const { startQuest } = useGame(); + + return ( +
+
+

{quest.name}

+

{quest.description}

+
+ {quest.rewards.map((reward, index) => ( + // eslint-disable-next-line react/no-array-index-key -- rewards have no unique id + + {reward.type === "gold" && `🪙 ${reward.amount?.toLocaleString()}`} + {reward.type === "essence" && `✨ ${reward.amount?.toLocaleString()}`} + {reward.type === "crystals" && `💎 ${reward.amount?.toLocaleString()}`} + {reward.type === "upgrade" && "🔓 Upgrade"} + {reward.type === "adventurer" && "👥 New Adventurer"} + + ))} +
+
+
+ {quest.status === "locked" && 🔒 Locked} + {quest.status === "available" && ( + + )} + {quest.status === "active" && ( + + ⏳ {formatDuration(Math.ceil(questTimeRemaining(quest)))} remaining + + )} + {quest.status === "completed" && ✅ Complete} +
+
+ ); +}; + +export const QuestPanel = (): React.JSX.Element => { + const { state } = useGame(); + + if (!state) return

Loading...

; + + return ( +
+

Quests

+
+ {state.quests.map((quest) => ( + + ))} +
+
+ ); +}; diff --git a/apps/web/src/components/game/UpgradePanel.tsx b/apps/web/src/components/game/UpgradePanel.tsx new file mode 100644 index 0000000..4e24ea9 --- /dev/null +++ b/apps/web/src/components/game/UpgradePanel.tsx @@ -0,0 +1,73 @@ +import type { Upgrade } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; + +interface UpgradeCardProps { + upgrade: Upgrade; + currentGold: number; + currentEssence: number; +} + +const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps): React.JSX.Element => { + const { buyUpgrade } = useGame(); + const canAfford = + currentGold >= upgrade.costGold && currentEssence >= upgrade.costEssence; + + if (upgrade.purchased) { + return ( +
+ ✅ {upgrade.name} + {upgrade.description} +
+ ); + } + + return ( +
+
+

{upgrade.name}

+

{upgrade.description}

+

×{upgrade.multiplier} multiplier

+
+
+ {upgrade.costGold > 0 && 🪙 {upgrade.costGold.toLocaleString()}} + {upgrade.costEssence > 0 && ✨ {upgrade.costEssence.toLocaleString()}} +
+ +
+ ); +}; + +export const UpgradePanel = (): React.JSX.Element => { + const { state } = useGame(); + + if (!state) return

Loading...

; + + const availableUpgrades = state.upgrades.filter((u) => u.unlocked); + + return ( +
+

Upgrades

+ {availableUpgrades.length === 0 ? ( +

No upgrades available yet — keep adventuring!

+ ) : ( +
+ {availableUpgrades.map((upgrade) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/apps/web/src/components/ui/ResourceBar.tsx b/apps/web/src/components/ui/ResourceBar.tsx new file mode 100644 index 0000000..f94ae3c --- /dev/null +++ b/apps/web/src/components/ui/ResourceBar.tsx @@ -0,0 +1,49 @@ +import type { Resource } from "@elysium/types"; + +interface ResourceBarProps { + resources: Resource; + prestigeCount: number; +} + +const formatNumber = (value: number): string => { + if (value >= 1_000_000_000) { + return `${(value / 1_000_000_000).toFixed(2)}B`; + } + if (value >= 1_000_000) { + return `${(value / 1_000_000).toFixed(2)}M`; + } + if (value >= 1_000) { + return `${(value / 1_000).toFixed(2)}K`; + } + return value.toFixed(1); +}; + +export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): React.JSX.Element => ( +
+
+ 🪙 + {formatNumber(resources.gold)} + Gold +
+
+ + {formatNumber(resources.essence)} + Essence +
+
+ 💎 + {formatNumber(resources.crystals)} + Crystals +
+
+ 🔮 + {formatNumber(resources.runestones)} + Runestones +
+ {prestigeCount > 0 && ( +
+ ⭐ Prestige {prestigeCount} +
+ )} +
+); diff --git a/apps/web/src/context/GameContext.tsx b/apps/web/src/context/GameContext.tsx new file mode 100644 index 0000000..26428d7 --- /dev/null +++ b/apps/web/src/context/GameContext.tsx @@ -0,0 +1,257 @@ +import type { GameState } from "@elysium/types"; +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { dealBossDamage, loadGame, saveGame } from "../api/client.js"; +import { applyTick, calculateClickPower } from "../engine/tick.js"; + +interface GameContextValue { + state: GameState | null; + isLoading: boolean; + error: string | null; + /** Click the crystal to earn gold */ + handleClick: () => void; + /** Buy an adventurer */ + buyAdventurer: (adventurerId: string) => void; + /** Buy an upgrade */ + buyUpgrade: (upgradeId: string) => void; + /** Start a quest */ + startQuest: (questId: string) => void; + /** Attack the active boss */ + attackBoss: (bossId: string) => void; + /** Reload state from the server */ + reload: () => Promise; + /** Offline gold earned on login */ + offlineGold: number; + /** Dismiss the offline gold notification */ + dismissOfflineGold: () => void; +} + +const GameContext = createContext(null); + +const AUTO_SAVE_INTERVAL_MS = 30_000; + +export const GameProvider = ({ children }: { children: React.ReactNode }): React.JSX.Element => { + const [state, setState] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [offlineGold, setOfflineGold] = useState(0); + const stateRef = useRef(null); + const lastSaveRef = useRef(Date.now()); + const rafRef = useRef(null); + + stateRef.current = state; + + const reload = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const data = await loadGame(); + setState(data.state); + if (data.offlineGold > 0) { + setOfflineGold(data.offlineGold); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load game"); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + void reload(); + }, [reload]); + + // Game loop via requestAnimationFrame + useEffect(() => { + if (!state) return; + + let lastTime = performance.now(); + + const tick = (now: number): void => { + const deltaSeconds = (now - lastTime) / 1000; + lastTime = now; + + setState((prev) => { + if (!prev) return prev; + return applyTick(prev, deltaSeconds); + }); + + // Auto-save every 30 seconds + if (Date.now() - lastSaveRef.current >= AUTO_SAVE_INTERVAL_MS) { + lastSaveRef.current = Date.now(); + if (stateRef.current) { + void saveGame({ state: stateRef.current }); + } + } + + rafRef.current = requestAnimationFrame(tick); + }; + + rafRef.current = requestAnimationFrame(tick); + return () => { + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- only run when state becomes available + }, [state !== null]); + + const handleClick = useCallback(() => { + setState((prev) => { + if (!prev) return prev; + const clickPower = calculateClickPower(prev); + return { + ...prev, + resources: { ...prev.resources, gold: prev.resources.gold + clickPower }, + player: { + ...prev.player, + totalGoldEarned: prev.player.totalGoldEarned + clickPower, + totalClicks: prev.player.totalClicks + 1, + }, + }; + }); + }, []); + + const buyAdventurer = useCallback((adventurerId: string) => { + setState((prev) => { + if (!prev) return prev; + const adventurer = prev.adventurers.find((a) => a.id === adventurerId); + if (!adventurer || !adventurer.unlocked) return prev; + + const cost = 10 * Math.pow(1.15, adventurer.count); + if (prev.resources.gold < cost) return prev; + + return { + ...prev, + resources: { ...prev.resources, gold: prev.resources.gold - cost }, + adventurers: prev.adventurers.map((a) => + a.id === adventurerId ? { ...a, count: a.count + 1 } : a, + ), + }; + }); + }, []); + + const buyUpgrade = useCallback((upgradeId: string) => { + setState((prev) => { + if (!prev) return prev; + const upgrade = prev.upgrades.find((u) => u.id === upgradeId); + if (!upgrade || !upgrade.unlocked || upgrade.purchased) return prev; + if (prev.resources.gold < upgrade.costGold) return prev; + if (prev.resources.essence < upgrade.costEssence) return prev; + + return { + ...prev, + resources: { + ...prev.resources, + gold: prev.resources.gold - upgrade.costGold, + essence: prev.resources.essence - upgrade.costEssence, + }, + upgrades: prev.upgrades.map((u) => + u.id === upgradeId ? { ...u, purchased: true } : u, + ), + }; + }); + }, []); + + const startQuest = useCallback((questId: string) => { + setState((prev) => { + if (!prev) return prev; + const quest = prev.quests.find((q) => q.id === questId); + if (!quest || quest.status !== "available") return prev; + + return { + ...prev, + quests: prev.quests.map((q) => + q.id === questId + ? { ...q, status: "active" as const, startedAt: Date.now() } + : q, + ), + }; + }); + }, []); + + const attackBoss = useCallback(async (bossId: string) => { + if (!stateRef.current) return; + const clickPower = calculateClickPower(stateRef.current); + + try { + const result = await dealBossDamage({ bossId, damage: clickPower }); + + setState((prev) => { + if (!prev) return prev; + return { + ...prev, + bosses: prev.bosses.map((b) => + b.id === bossId + ? { + ...b, + status: result.defeated ? ("defeated" as const) : ("in_progress" as const), + currentHp: result.currentHp, + } + : b, + ), + ...(result.defeated && result.rewards + ? { + resources: { + ...prev.resources, + gold: prev.resources.gold + result.rewards.gold, + essence: prev.resources.essence + result.rewards.essence, + crystals: prev.resources.crystals + result.rewards.crystals, + }, + player: { + ...prev.player, + totalGoldEarned: + prev.player.totalGoldEarned + result.rewards.gold, + }, + upgrades: prev.upgrades.map((u) => + result.rewards!.upgradeIds.includes(u.id) + ? { ...u, unlocked: true } + : u, + ), + } + : {}), + }; + }); + } catch { + // Rate limited or other error — silently ignore + } + }, []); + + const dismissOfflineGold = useCallback(() => { + setOfflineGold(0); + }, []); + + return ( + + {children} + + ); +}; + +export const useGame = (): GameContextValue => { + const context = useContext(GameContext); + if (!context) { + throw new Error("useGame must be used within a GameProvider"); + } + return context; +}; diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts new file mode 100644 index 0000000..732dd07 --- /dev/null +++ b/apps/web/src/engine/tick.ts @@ -0,0 +1,96 @@ +import type { GameState } from "@elysium/types"; + +/** + * Pure function — applies one game tick to the state. + * deltaSeconds: time elapsed since last tick. + * Returns a new GameState (does not mutate the original). + */ +export const applyTick = (state: GameState, deltaSeconds: number): GameState => { + let goldGained = 0; + let essenceGained = 0; + + for (const adventurer of state.adventurers) { + if (!adventurer.unlocked || adventurer.count === 0) { + continue; + } + + const upgradeMultiplier = state.upgrades + .filter( + (u) => + u.purchased && + (u.target === "global" || + (u.target === "adventurer" && u.adventurerId === adventurer.id)), + ) + .reduce((mult, upgrade) => mult * upgrade.multiplier, 1); + + const prestige = state.prestige.productionMultiplier; + + goldGained += + adventurer.goldPerSecond * adventurer.count * upgradeMultiplier * prestige * deltaSeconds; + + essenceGained += + adventurer.essencePerSecond * + adventurer.count * + upgradeMultiplier * + prestige * + deltaSeconds; + } + + // Complete active quests + const now = Date.now(); + let questGold = 0; + let questEssence = 0; + let questCrystals = 0; + + const updatedQuests = state.quests.map((quest) => { + if ( + quest.status !== "active" || + quest.startedAt == null || + now < quest.startedAt + quest.durationSeconds * 1000 + ) { + return quest; + } + + const completed = { ...quest, status: "completed" as const }; + for (const reward of quest.rewards) { + if (reward.type === "gold" && reward.amount != null) { + questGold += reward.amount; + } else if (reward.type === "essence" && reward.amount != null) { + questEssence += reward.amount; + } else if (reward.type === "crystals" && reward.amount != null) { + questCrystals += reward.amount; + } + } + return completed; + }); + + const newGold = state.resources.gold + goldGained + questGold; + const newEssence = state.resources.essence + essenceGained + questEssence; + + return { + ...state, + resources: { + ...state.resources, + gold: newGold, + essence: newEssence, + crystals: state.resources.crystals + questCrystals, + }, + player: { + ...state.player, + totalGoldEarned: state.player.totalGoldEarned + goldGained + questGold, + }, + quests: updatedQuests, + lastTickAt: now, + }; +}; + +/** + * Calculates the effective click power, including upgrades. + */ +export const calculateClickPower = (state: GameState): number => { + const clickMultiplier = state.upgrades + .filter((u) => u.purchased && u.target === "click") + .reduce((mult, upgrade) => mult * upgrade.multiplier, 1); + + return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier; +}; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..06c5c6a --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; +import "./styles.css"; + +const rootElement = document.getElementById("root"); + +if (!rootElement) { + throw new Error("Root element not found"); +} + +createRoot(rootElement).render( + + + , +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..758277e --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,631 @@ +/* ===================== RESET & BASE ===================== */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --colour-bg: #0d0d1a; + --colour-surface: #1a1a2e; + --colour-surface-2: #16213e; + --colour-border: #2a2a4a; + --colour-accent: #7c3aed; + --colour-accent-light: #a855f7; + --colour-gold: #f59e0b; + --colour-essence: #8b5cf6; + --colour-crystal: #06b6d4; + --colour-rune: #ec4899; + --colour-text: #e2e8f0; + --colour-text-muted: #94a3b8; + --colour-success: #10b981; + --colour-error: #ef4444; + --colour-warning: #f59e0b; + --radius: 8px; + --radius-lg: 12px; + --font: "Segoe UI", system-ui, sans-serif; +} + +body { + background-color: var(--colour-bg); + color: var(--colour-text); + font-family: var(--font); + min-height: 100vh; +} + +/* ===================== RESOURCE BAR ===================== */ +.resource-bar { + display: flex; + align-items: center; + gap: 1.5rem; + padding: 0.75rem 1.5rem; + background: var(--colour-surface); + border-bottom: 1px solid var(--colour-border); + position: sticky; + top: 0; + z-index: 10; +} + +.resource { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.resource-value { + font-weight: 700; + font-size: 1.1rem; +} + +.resource-label { + color: var(--colour-text-muted); + font-size: 0.8rem; +} + +.prestige-badge { + margin-left: auto; + background: linear-gradient(135deg, var(--colour-accent), var(--colour-accent-light)); + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.85rem; + font-weight: 600; +} + +/* ===================== GAME LAYOUT ===================== */ +.game-layout { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.game-main { + display: flex; + flex: 1; +} + +.game-sidebar { + width: 220px; + padding: 1rem; + background: var(--colour-surface-2); + border-right: 1px solid var(--colour-border); + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.game-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ===================== TABS ===================== */ +.tab-bar { + display: flex; + gap: 0.25rem; + padding: 0.75rem 1rem; + background: var(--colour-surface); + border-bottom: 1px solid var(--colour-border); + overflow-x: auto; +} + +.tab-button { + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius); + color: var(--colour-text-muted); + cursor: pointer; + font-size: 0.9rem; + padding: 0.4rem 0.9rem; + transition: all 0.15s; + white-space: nowrap; +} + +.tab-button:hover { + background: var(--colour-surface-2); + color: var(--colour-text); +} + +.tab-button.active { + background: var(--colour-accent); + border-color: var(--colour-accent-light); + color: #fff; +} + +.tab-content { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +/* ===================== CLICK AREA ===================== */ +.click-area { + text-align: center; +} + +.click-area h2 { + font-size: 0.95rem; + color: var(--colour-text-muted); + margin-bottom: 0.75rem; +} + +.click-button { + background: linear-gradient(135deg, var(--colour-accent), var(--colour-accent-light)); + border: none; + border-radius: 50%; + cursor: pointer; + font-size: 3rem; + height: 120px; + transition: transform 0.1s, box-shadow 0.1s; + width: 120px; + box-shadow: 0 0 20px rgba(124, 58, 237, 0.4); +} + +.click-button:active { + transform: scale(0.93); + box-shadow: 0 0 10px rgba(124, 58, 237, 0.2); +} + +.click-power { + color: var(--colour-text-muted); + font-size: 0.8rem; + margin-top: 0.5rem; +} + +/* ===================== PANEL ===================== */ +.panel { + max-width: 900px; +} + +.panel h2 { + font-size: 1.3rem; + margin-bottom: 1rem; + color: var(--colour-accent-light); +} + +.empty-state { + color: var(--colour-text-muted); + font-style: italic; +} + +/* ===================== ADVENTURERS ===================== */ +.adventurer-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.adventurer-card { + align-items: center; + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + gap: 0.75rem; + padding: 0.75rem; +} + +.adventurer-card.locked { + opacity: 0.5; +} + +.adventurer-icon { + font-size: 1.75rem; + min-width: 2.5rem; + text-align: center; +} + +.adventurer-info { + flex: 1; +} + +.adventurer-info h3 { + font-size: 1rem; + margin-bottom: 0.15rem; +} + +.adventurer-info p { + color: var(--colour-text-muted); + font-size: 0.8rem; +} + +.adventurer-count { + color: var(--colour-accent-light); + font-size: 1.1rem; + font-weight: 700; + min-width: 3rem; + text-align: center; +} + +/* ===================== BUTTONS ===================== */ +.buy-button, +.start-quest-button, +.attack-button, +.prestige-button { + background: var(--colour-accent); + border: none; + border-radius: var(--radius); + color: #fff; + cursor: pointer; + font-size: 0.85rem; + font-weight: 600; + padding: 0.4rem 0.9rem; + transition: background 0.15s; +} + +.buy-button:hover:not(:disabled), +.start-quest-button:hover, +.attack-button:hover, +.prestige-button:hover:not(:disabled) { + background: var(--colour-accent-light); +} + +.buy-button:disabled, +.prestige-button:disabled { + background: var(--colour-border); + color: var(--colour-text-muted); + cursor: not-allowed; +} + +/* ===================== UPGRADES ===================== */ +.upgrade-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.upgrade-card { + align-items: center; + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + gap: 0.75rem; + padding: 0.75rem; +} + +.upgrade-card.purchased { + border-color: var(--colour-success); + opacity: 0.7; +} + +.upgrade-info { + flex: 1; +} + +.upgrade-info h3 { + font-size: 1rem; + margin-bottom: 0.15rem; +} + +.upgrade-info p { + color: var(--colour-text-muted); + font-size: 0.8rem; +} + +.upgrade-multiplier { + color: var(--colour-gold) !important; + font-weight: 600; +} + +.upgrade-cost { + display: flex; + flex-direction: column; + gap: 0.2rem; + font-size: 0.85rem; +} + +/* ===================== QUESTS ===================== */ +.quest-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.quest-card { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + gap: 1rem; + padding: 1rem; + align-items: flex-start; +} + +.quest-card.quest-completed { + border-color: var(--colour-success); + opacity: 0.7; +} + +.quest-card.quest-active { + border-color: var(--colour-warning); +} + +.quest-info { + flex: 1; +} + +.quest-info h3 { + font-size: 1rem; + margin-bottom: 0.25rem; +} + +.quest-info p { + color: var(--colour-text-muted); + font-size: 0.85rem; + margin-bottom: 0.5rem; +} + +.quest-rewards { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.reward-tag { + background: var(--colour-surface-2); + border: 1px solid var(--colour-border); + border-radius: 999px; + font-size: 0.75rem; + padding: 0.15rem 0.5rem; +} + +.quest-badge { + font-size: 0.85rem; + font-weight: 600; + padding: 0.3rem 0.6rem; + border-radius: var(--radius); + white-space: nowrap; +} + +.quest-badge.locked { color: var(--colour-text-muted); } +.quest-badge.active { color: var(--colour-warning); } +.quest-badge.completed { color: var(--colour-success); } + +/* ===================== BOSSES ===================== */ +.boss-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.boss-card { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius-lg); + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem; +} + +.boss-card.boss-defeated { + opacity: 0.6; + border-color: var(--colour-text-muted); +} + +.boss-card.boss-in_progress { + border-color: var(--colour-error); + box-shadow: 0 0 12px rgba(239, 68, 68, 0.2); +} + +.boss-info h3 { + font-size: 1.1rem; + margin-bottom: 0.25rem; +} + +.boss-info p { + color: var(--colour-text-muted); + font-size: 0.85rem; +} + +.prestige-lock { + color: var(--colour-warning) !important; + margin-top: 0.25rem; +} + +.hp-bar { + background: var(--colour-border); + border-radius: 999px; + height: 10px; + overflow: hidden; +} + +.hp-fill { + background: linear-gradient(90deg, #ef4444, #f97316); + height: 100%; + transition: width 0.3s ease; +} + +.hp-text { + color: var(--colour-text-muted); + font-size: 0.8rem; +} + +.boss-rewards { + display: flex; + gap: 0.75rem; + font-size: 0.85rem; +} + +.boss-badge.defeated { + color: var(--colour-text-muted); + font-size: 0.9rem; +} + +.attack-button { + align-self: flex-start; + background: linear-gradient(135deg, #ef4444, #b91c1c); + font-size: 0.95rem; + padding: 0.5rem 1.2rem; +} + +.attack-button:hover { + background: linear-gradient(135deg, #f87171, #dc2626); +} + +/* ===================== PRESTIGE ===================== */ +.prestige-panel p { + color: var(--colour-text-muted); + font-size: 0.9rem; + line-height: 1.6; + margin-bottom: 0.75rem; +} + +.prestige-status { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + padding: 1rem; + margin-bottom: 1rem; +} + +.prestige-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 400px; +} + +.prestige-form input { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + color: var(--colour-text); + font-size: 0.95rem; + padding: 0.5rem 0.75rem; + width: 100%; +} + +.prestige-button { + background: linear-gradient(135deg, #7c3aed, #a855f7); + font-size: 1rem; + padding: 0.6rem 1.5rem; +} + +.prestige-locked { + color: var(--colour-text-muted) !important; +} + +/* ===================== LOGIN PAGE ===================== */ +.login-page { + align-items: center; + background: radial-gradient(ellipse at center, #1a1a3e 0%, #0d0d1a 100%); + display: flex; + justify-content: center; + min-height: 100vh; +} + +.login-card { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius-lg); + max-width: 480px; + padding: 2.5rem; + text-align: center; + width: 100%; +} + +.login-card h1 { + font-size: 2rem; + margin-bottom: 0.75rem; +} + +.login-card p { + color: var(--colour-text-muted); + margin-bottom: 1.5rem; +} + +.discord-login-button { + background: #5865f2; + border-radius: var(--radius); + color: #fff; + display: inline-block; + font-size: 1rem; + font-weight: 600; + padding: 0.75rem 2rem; + text-decoration: none; + transition: background 0.15s; +} + +.discord-login-button:hover { + background: #4752c4; +} + +.login-note { + font-size: 0.8rem !important; + margin-top: 1rem !important; +} + +/* ===================== MODAL ===================== */ +.modal-overlay { + align-items: center; + background: rgba(0, 0, 0, 0.7); + display: flex; + inset: 0; + justify-content: center; + position: fixed; + z-index: 100; +} + +.modal { + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius-lg); + max-width: 420px; + padding: 2rem; + text-align: center; + width: 90%; +} + +.modal h2 { + font-size: 1.4rem; + margin-bottom: 0.75rem; +} + +.modal p { + color: var(--colour-text-muted); + margin-bottom: 0.75rem; +} + +.modal-note { + font-size: 0.8rem; +} + +.modal-close-button { + background: var(--colour-accent); + border: none; + border-radius: var(--radius); + color: #fff; + cursor: pointer; + font-size: 1rem; + font-weight: 700; + margin-top: 0.5rem; + padding: 0.6rem 2rem; + transition: background 0.15s; +} + +.modal-close-button:hover { + background: var(--colour-accent-light); +} + +/* ===================== UTILITY ===================== */ +.error { + color: var(--colour-error); + font-size: 0.85rem; +} + +.success { + color: var(--colour-success); + font-size: 0.85rem; +} + +.loading-screen, +.error-screen { + align-items: center; + display: flex; + height: 100vh; + justify-content: center; +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..ab23d1e --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@nhcarrigan/typescript-config", + "compilerOptions": { + "outDir": "./prod", + "rootDir": ".", + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "target": "ES2022" + }, + "exclude": ["test/**/*.ts", "test/**/*.tsx"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..92d6218 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,18 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + "/api": { + target: "http://localhost:3001", + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, + build: { + outDir: "dist", + }, +}); diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 0000000..5443714 --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + coverage: { + provider: "v8", + include: ["src/**/*.ts", "src/**/*.tsx"], + exclude: ["src/types/**/*.ts", "src/main.tsx"], + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + include: ["test/**/*.spec.ts", "test/**/*.spec.tsx"], + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..4601456 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "elysium", + "version": "1.0.0", + "private": true, + "scripts": { + "lint": "pnpm -r lint", + "build": "pnpm -r build", + "test": "pnpm -r test", + "dev": "pnpm -r --parallel dev" + }, + "devDependencies": { + "@nhcarrigan/typescript-config": "4.0.0", + "typescript": "5.8.2" + } +} diff --git a/packages/types/eslint.config.js b/packages/types/eslint.config.js new file mode 100644 index 0000000..d82a8eb --- /dev/null +++ b/packages/types/eslint.config.js @@ -0,0 +1,3 @@ +import { nhcarrigan } from "@nhcarrigan/eslint-config"; + +export default [...(await nhcarrigan())]; diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000..abfd749 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,18 @@ +{ + "name": "@elysium/types", + "version": "1.0.0", + "private": true, + "main": "./prod/src/index.js", + "types": "./prod/src/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "lint": "eslint --max-warnings 0 src", + "test": "echo \"No tests for types package\"" + }, + "devDependencies": { + "@nhcarrigan/eslint-config": "5.2.0", + "@nhcarrigan/typescript-config": "4.0.0", + "eslint": "9.22.0", + "typescript": "5.8.2" + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 0000000..1c72e64 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,28 @@ +export type { Adventurer, AdventurerClass } from "./interfaces/Adventurer.js"; +export type { + ApiError, + AuthResponse, + BossDamageRequest, + BossDamageResponse, + LoadResponse, + PrestigeRequest, + PrestigeResponse, + PublicProfileResponse, + SaveRequest, + SaveResponse, +} from "./interfaces/Api.js"; +export type { Boss, BossStatus } from "./interfaces/Boss.js"; +export type { GameState } from "./interfaces/GameState.js"; +export type { Player } from "./interfaces/Player.js"; +export type { PrestigeData } from "./interfaces/Prestige.js"; +export type { + Quest, + QuestReward, + QuestRewardType, + QuestStatus, +} from "./interfaces/Quest.js"; +export type { Resource } from "./interfaces/Resource.js"; +export type { + Upgrade, + UpgradeTarget, +} from "./interfaces/Upgrade.js"; diff --git a/packages/types/src/interfaces/Adventurer.ts b/packages/types/src/interfaces/Adventurer.ts new file mode 100644 index 0000000..4e10401 --- /dev/null +++ b/packages/types/src/interfaces/Adventurer.ts @@ -0,0 +1,20 @@ +export type AdventurerClass = + | "warrior" + | "mage" + | "rogue" + | "cleric" + | "ranger" + | "paladin"; + +export interface Adventurer { + id: string; + name: string; + class: AdventurerClass; + level: number; + /** Base gold generated per second */ + goldPerSecond: number; + /** Base essence generated per second */ + essencePerSecond: number; + count: number; + unlocked: boolean; +} diff --git a/packages/types/src/interfaces/Api.ts b/packages/types/src/interfaces/Api.ts new file mode 100644 index 0000000..e8a100a --- /dev/null +++ b/packages/types/src/interfaces/Api.ts @@ -0,0 +1,63 @@ +import type { GameState } from "./GameState.js"; +import type { Player } from "./Player.js"; + +export interface AuthResponse { + token: string; + player: Player; + isNew: boolean; +} + +export interface SaveRequest { + state: GameState; +} + +export interface SaveResponse { + savedAt: number; +} + +export interface LoadResponse { + state: GameState; + /** Offline gold earned since last save (server-calculated) */ + offlineGold: number; + /** Seconds the player was offline (capped at 8 hours) */ + offlineSeconds: number; +} + +export interface BossDamageRequest { + bossId: string; + damage: number; +} + +export interface BossDamageResponse { + currentHp: number; + defeated: boolean; + rewards?: { + gold: number; + essence: number; + crystals: number; + upgradeIds: string[]; + }; +} + +export interface PrestigeRequest { + characterName: string; +} + +export interface PrestigeResponse { + runestones: number; + newPrestigeCount: number; +} + +export interface PublicProfileResponse { + characterName: string; + username: string; + avatar: string | null; + prestigeCount: number; + totalGoldEarned: number; + totalClicks: number; + createdAt: number; +} + +export interface ApiError { + error: string; +} diff --git a/packages/types/src/interfaces/Boss.ts b/packages/types/src/interfaces/Boss.ts new file mode 100644 index 0000000..0cb375f --- /dev/null +++ b/packages/types/src/interfaces/Boss.ts @@ -0,0 +1,22 @@ +export type BossStatus = "locked" | "available" | "in_progress" | "defeated"; + +export interface Boss { + id: string; + name: string; + description: string; + status: BossStatus; + maxHp: number; + currentHp: number; + /** Damage dealt to adventurers per second whilst the fight is active */ + damagePerSecond: number; + /** Gold reward on defeat */ + goldReward: number; + /** Essence reward on defeat */ + essenceReward: number; + /** Crystal reward on defeat */ + crystalReward: number; + /** IDs of upgrades unlocked on defeat */ + upgradeRewards: string[]; + /** Minimum prestige level required to access this boss */ + prestigeRequirement: number; +} diff --git a/packages/types/src/interfaces/GameState.ts b/packages/types/src/interfaces/GameState.ts new file mode 100644 index 0000000..c93dcac --- /dev/null +++ b/packages/types/src/interfaces/GameState.ts @@ -0,0 +1,21 @@ +import type { Adventurer } from "./Adventurer.js"; +import type { Boss } from "./Boss.js"; +import type { Player } from "./Player.js"; +import type { PrestigeData } from "./Prestige.js"; +import type { Quest } from "./Quest.js"; +import type { Resource } from "./Resource.js"; +import type { Upgrade } from "./Upgrade.js"; + +export interface GameState { + player: Player; + resources: Resource; + adventurers: Adventurer[]; + upgrades: Upgrade[]; + quests: Quest[]; + bosses: Boss[]; + prestige: PrestigeData; + /** Click power (gold per click, before upgrades) */ + baseClickPower: number; + /** Unix timestamp of the last client-side tick */ + lastTickAt: number; +} diff --git a/packages/types/src/interfaces/Player.ts b/packages/types/src/interfaces/Player.ts new file mode 100644 index 0000000..7f72be1 --- /dev/null +++ b/packages/types/src/interfaces/Player.ts @@ -0,0 +1,16 @@ +export interface Player { + discordId: string; + username: string; + discriminator: string; + avatar: string | null; + /** Player's chosen in-game character name */ + characterName: string; + /** Unix timestamp when the account was created */ + createdAt: number; + /** Unix timestamp of the last server-side save */ + lastSavedAt: number; + /** Total lifetime gold ever collected (for prestige eligibility) */ + totalGoldEarned: number; + /** Total lifetime clicks */ + totalClicks: number; +} diff --git a/packages/types/src/interfaces/Prestige.ts b/packages/types/src/interfaces/Prestige.ts new file mode 100644 index 0000000..51c7b3e --- /dev/null +++ b/packages/types/src/interfaces/Prestige.ts @@ -0,0 +1,12 @@ +export interface PrestigeData { + /** Number of times the player has prestiged */ + count: number; + /** Runestones carried over between prestiges */ + runestones: number; + /** Multiplier applied to all production (based on prestige count) */ + productionMultiplier: number; + /** IDs of prestige upgrades purchased with runestones */ + purchasedUpgradeIds: string[]; + /** Unix timestamp of last prestige */ + lastPrestigedAt?: number; +} diff --git a/packages/types/src/interfaces/Quest.ts b/packages/types/src/interfaces/Quest.ts new file mode 100644 index 0000000..efae46a --- /dev/null +++ b/packages/types/src/interfaces/Quest.ts @@ -0,0 +1,24 @@ +export type QuestStatus = "locked" | "available" | "active" | "completed"; + +export type QuestRewardType = "gold" | "essence" | "crystals" | "upgrade" | "adventurer"; + +export interface QuestReward { + type: QuestRewardType; + amount?: number; + /** ID of the upgrade or adventurer to unlock (if applicable) */ + targetId?: string; +} + +export interface Quest { + id: string; + name: string; + description: string; + status: QuestStatus; + /** Unix timestamp when quest was started (if active) */ + startedAt?: number; + /** Duration in seconds */ + durationSeconds: number; + rewards: QuestReward[]; + /** IDs of quests that must be completed before this one unlocks */ + prerequisiteIds: string[]; +} diff --git a/packages/types/src/interfaces/Resource.ts b/packages/types/src/interfaces/Resource.ts new file mode 100644 index 0000000..4d5f015 --- /dev/null +++ b/packages/types/src/interfaces/Resource.ts @@ -0,0 +1,6 @@ +export interface Resource { + gold: number; + essence: number; + crystals: number; + runestones: number; +} diff --git a/packages/types/src/interfaces/Upgrade.ts b/packages/types/src/interfaces/Upgrade.ts new file mode 100644 index 0000000..c647f88 --- /dev/null +++ b/packages/types/src/interfaces/Upgrade.ts @@ -0,0 +1,21 @@ +export type UpgradeTarget = + | "click" + | "adventurer" + | "global" + | "prestige" + | "boss"; + +export interface Upgrade { + id: string; + name: string; + description: string; + target: UpgradeTarget; + /** ID of the adventurer this applies to (if target is "adventurer") */ + adventurerId?: string; + /** Multiplier applied to the target's output */ + multiplier: number; + costGold: number; + costEssence: number; + purchased: boolean; + unlocked: boolean; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000..d3b0572 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nhcarrigan/typescript-config", + "compilerOptions": { + "outDir": "./prod", + "rootDir": "." + }, + "exclude": ["test/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..88bc570 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5665 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@nhcarrigan/typescript-config': + specifier: 4.0.0 + version: 4.0.0(typescript@5.8.2) + typescript: + specifier: 5.8.2 + version: 5.8.2 + + apps/api: + dependencies: + '@elysium/types': + specifier: workspace:* + version: link:../../packages/types + '@hono/node-server': + specifier: 1.13.7 + version: 1.13.7(hono@4.7.4) + '@prisma/client': + specifier: 6.5.0 + version: 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2) + hono: + specifier: 4.7.4 + version: 4.7.4 + prisma: + specifier: 6.5.0 + version: 6.5.0(typescript@5.8.2) + devDependencies: + '@nhcarrigan/eslint-config': + specifier: 5.2.0 + version: 5.2.0(@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(playwright@1.58.2)(react@19.0.0)(typescript@5.8.2)(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3)) + '@nhcarrigan/typescript-config': + specifier: 4.0.0 + version: 4.0.0(typescript@5.8.2) + '@vitest/coverage-v8': + specifier: 3.0.8 + version: 3.0.8(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3)) + eslint: + specifier: 9.22.0 + version: 9.22.0 + tsx: + specifier: 4.19.3 + version: 4.19.3 + typescript: + specifier: 5.8.2 + version: 5.8.2 + vitest: + specifier: 3.0.8 + version: 3.0.8(jsdom@26.0.0)(tsx@4.19.3) + + apps/web: + dependencies: + '@elysium/types': + specifier: workspace:* + version: link:../../packages/types + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + devDependencies: + '@nhcarrigan/eslint-config': + specifier: 5.2.0 + version: 5.2.0(@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(playwright@1.58.2)(react@19.0.0)(typescript@5.8.2)(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3)) + '@nhcarrigan/typescript-config': + specifier: 4.0.0 + version: 4.0.0(typescript@5.8.2) + '@types/react': + specifier: 19.0.10 + version: 19.0.10 + '@types/react-dom': + specifier: 19.0.4 + version: 19.0.4(@types/react@19.0.10) + '@vitejs/plugin-react': + specifier: 4.3.4 + version: 4.3.4(vite@6.2.1(tsx@4.19.3)) + '@vitest/coverage-v8': + specifier: 3.0.8 + version: 3.0.8(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3)) + eslint: + specifier: 9.22.0 + version: 9.22.0 + jsdom: + specifier: 26.0.0 + version: 26.0.0 + typescript: + specifier: 5.8.2 + version: 5.8.2 + vite: + specifier: 6.2.1 + version: 6.2.1(tsx@4.19.3) + vitest: + specifier: 3.0.8 + version: 3.0.8(jsdom@26.0.0)(tsx@4.19.3) + + packages/types: + devDependencies: + '@nhcarrigan/eslint-config': + specifier: 5.2.0 + version: 5.2.0(@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(playwright@1.58.2)(react@19.0.0)(typescript@5.8.2)(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3)) + '@nhcarrigan/typescript-config': + specifier: 4.0.0 + version: 4.0.0(typescript@5.8.2) + eslint: + specifier: 9.22.0 + version: 9.22.0 + typescript: + specifier: 5.8.2 + version: 5.8.2 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@es-joy/jsdoccomment@0.49.0': + resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} + engines: {node: '>=16'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': + resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@1.2.4': + resolution: {integrity: sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.19.2': + resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.1.0': + resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.12.0': + resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.4': + resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.17.0': + resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.22.0': + resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@hono/node-server@1.13.7': + resolution: {integrity: sha512-kTfUMsoloVKtRA2fLiGSd9qBddmru9KadNyhJCwgKBxTiNkaAJEwkVN9KV/rS4HtmmNRtUh6P+YpmjRMl0d9vQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@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.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nhcarrigan/eslint-config@5.2.0': + resolution: {integrity: sha512-YpTTqhviKMlRwKF+RC/GYiA5i2jTCmg8uftuiufldneNV5HMbGpTfBbV7tpa8++5mpYJc4+eZaf40QbDiz84dQ==} + engines: {node: '>=22', pnpm: '>=9'} + peerDependencies: + eslint: '>=9' + playwright: '>=1' + react: '>=18' + typescript: '>=5' + vitest: '>=2' + + '@nhcarrigan/typescript-config@4.0.0': + resolution: {integrity: sha512-969HVha7A/Sg77fuMwOm6p14a+7C5iE6g55OD71srqwKIgksQl+Ex/hAI/pyzTQFDQ/FBJbpnHlR4Ov25QV/rw==} + engines: {node: '20', pnpm: '9'} + peerDependencies: + typescript: '>=5.5.2' + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.1.2': + resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@prisma/client@6.5.0': + resolution: {integrity: sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.5.0': + resolution: {integrity: sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw==} + + '@prisma/debug@6.5.0': + resolution: {integrity: sha512-fc/nusYBlJMzDmDepdUtH9aBsJrda2JNErP9AzuHbgUEQY0/9zQYZdNlXmKoIWENtio+qarPNe/+DQtrX5kMcQ==} + + '@prisma/engines-version@6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60': + resolution: {integrity: sha512-iK3EmiVGFDCmXjSpdsKGNqy9hOdLnvYBrJB61far/oP03hlIxrb04OWmDjNTwtmZ3UZdA5MCvI+f+3k2jPTflQ==} + + '@prisma/engines@6.5.0': + resolution: {integrity: sha512-FVPQYHgOllJklN9DUyujXvh3hFJCY0NX86sDmBErLvoZjy2OXGiZ5FNf3J/C4/RZZmCypZBYpBKEhx7b7rEsdw==} + + '@prisma/fetch-engine@6.5.0': + resolution: {integrity: sha512-3LhYA+FXP6pqY8FLHCjewyE8pGXXJ7BxZw2rhPq+CZAhvflVzq4K8Qly3OrmOkn6wGlz79nyLQdknyCG2HBTuA==} + + '@prisma/get-platform@6.5.0': + resolution: {integrity: sha512-xYcvyJwNMg2eDptBYFqFLUCfgi+wZLcj6HDMsj0Qw0irvauG4IKmkbywnqwok0B+k+W+p+jThM2DKTSmoPCkzw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@stylistic/eslint-plugin@2.12.1': + resolution: {integrity: sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.40.0' + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/react-dom@19.0.4': + resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.0.10': + resolution: {integrity: sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==} + + '@typescript-eslint/eslint-plugin@8.19.0': + resolution: {integrity: sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/parser@8.19.0': + resolution: {integrity: sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/scope-manager@8.19.0': + resolution: {integrity: sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.19.0': + resolution: {integrity: sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/types@8.19.0': + resolution: {integrity: sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.19.0': + resolution: {integrity: sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/utils@8.19.0': + resolution: {integrity: sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/visitor-keys@8.19.0': + resolution: {integrity: sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + '@vitest/coverage-v8@3.0.8': + resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==} + peerDependencies: + '@vitest/browser': 3.0.8 + vitest: 3.0.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/eslint-plugin@1.1.24': + resolution: {integrity: sha512-7IaENe4NNy33g0iuuy5bHY69JYYRjpv4lMx6H5Wp30W7ez2baLHwxsXF5TM4wa8JDYZt8ut99Ytoj7GiDO01hw==} + peerDependencies: + '@typescript-eslint/utils': '>= 8.0' + eslint: '>= 8.57.0' + typescript: '>= 5.0.0' + vitest: '*' + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + + '@vitest/expect@3.0.8': + resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} + + '@vitest/mocker@3.0.8': + resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.0.8': + resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.0.8': + resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} + + '@vitest/snapshot@3.0.8': + resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} + + '@vitest/spy@3.0.8': + resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} + + '@vitest/utils@3.0.8': + resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.48.0: + resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.2: + resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-deprecation@3.0.0: + resolution: {integrity: sha512-JuVLdNg/uf0Adjg2tpTyYoYaMbwQNn/c78P1HcccokvhtRphgnRjZDKmhlxbxYptppex03zO76f97DD/yQHv7A==} + peerDependencies: + eslint: ^8.0.0 + typescript: ^4.2.4 || ^5.0.0 + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsdoc@50.6.1: + resolution: {integrity: sha512-UWyaYi6iURdSfdVVqvfOs2vdCVz0J40O/z/HTsv2sFjdjmdlUI/qlKLOTmwbPQ2tAfQnE5F9vqx+B+poF71DBQ==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-playwright@2.1.0: + resolution: {integrity: sha512-wMbHOehofSB1cBdzz2CLaCYaKNLeTQ0YnOW+7AHa281TJqlpEJUBgTHbRUYOUxiXphfWwOyTPvgr6vvEmArbSA==} + engines: {node: '>=16.6.0'} + peerDependencies: + eslint: '>=8.40.0' + + eslint-plugin-react@7.37.3: + resolution: {integrity: sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-sort-keys-fix@1.1.2: + resolution: {integrity: sha512-DNPHFGCA0/hZIsfODbeLZqaGY/+q3vgtshF85r+YWDNCQ2apd9PNs/zL6ttKm0nD1IFwvxyg3YOTI7FHl4unrw==} + engines: {node: '>=0.10.0'} + + eslint-plugin-unicorn@56.0.1: + resolution: {integrity: sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=8.56.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.22.0: + resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.4: + resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.14.0: + resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.7.4: + resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} + engines: {node: '>=16.9.0'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-imports@2.2.1: + resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==} + engines: {node: '>= 18'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prisma@6.5.0: + resolution: {integrity: sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regjsparser@0.10.0: + resolution: {integrity: sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==} + hasBin: true + + requireindex@1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slashes@3.0.12: + resolution: {integrity: sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.9.3: + resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==} + engines: {node: ^14.18.0 || >=16.0.0} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.3: + resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vite-node@3.0.8: + resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.2.1: + resolution: {integrity: sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + 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 + + vitest@3.0.8: + resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.8 + '@vitest/ui': 3.0.8 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + 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 + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@es-joy/jsdoccomment@0.49.0': + dependencies: + comment-parser: 1.4.1 + esquery: 1.7.0 + jsdoc-type-pratt-parser: 4.1.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.22.0)': + dependencies: + escape-string-regexp: 4.0.0 + eslint: 9.22.0 + ignore: 5.3.2 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.22.0)': + dependencies: + eslint: 9.22.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@1.2.4(eslint@9.22.0)': + optionalDependencies: + eslint: 9.22.0 + + '@eslint/config-array@0.19.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.1.0': {} + + '@eslint/core@0.12.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.4': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.17.0': {} + + '@eslint/js@9.22.0': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.2.8': + dependencies: + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@hono/node-server@1.13.7(hono@4.7.4)': + dependencies: + hono: 4.7.4 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@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.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(playwright@1.58.2)(react@19.0.0)(typescript@5.8.2)(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3))': + dependencies: + '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.22.0) + '@eslint/compat': 1.2.4(eslint@9.22.0) + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.17.0 + '@stylistic/eslint-plugin': 2.12.1(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + '@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3)) + eslint: 9.22.0 + eslint-plugin-deprecation: 3.0.0(eslint@9.22.0)(typescript@5.8.2) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0) + eslint-plugin-jsdoc: 50.6.1(eslint@9.22.0) + eslint-plugin-playwright: 2.1.0(eslint@9.22.0) + eslint-plugin-react: 7.37.3(eslint@9.22.0) + eslint-plugin-sort-keys-fix: 1.1.2 + eslint-plugin-unicorn: 56.0.1(eslint@9.22.0) + globals: 15.14.0 + playwright: 1.58.2 + react: 19.0.0 + typescript: 5.8.2 + vitest: 3.0.8(jsdom@26.0.0)(tsx@4.19.3) + transitivePeerDependencies: + - '@typescript-eslint/utils' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + '@nhcarrigan/typescript-config@4.0.0(typescript@5.8.2)': + dependencies: + typescript: 5.8.2 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.1.2': {} + + '@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)': + optionalDependencies: + prisma: 6.5.0(typescript@5.8.2) + typescript: 5.8.2 + + '@prisma/config@6.5.0': + dependencies: + esbuild: 0.27.3 + esbuild-register: 3.6.0(esbuild@0.27.3) + transitivePeerDependencies: + - supports-color + + '@prisma/debug@6.5.0': {} + + '@prisma/engines-version@6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60': {} + + '@prisma/engines@6.5.0': + dependencies: + '@prisma/debug': 6.5.0 + '@prisma/engines-version': 6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60 + '@prisma/fetch-engine': 6.5.0 + '@prisma/get-platform': 6.5.0 + + '@prisma/fetch-engine@6.5.0': + dependencies: + '@prisma/debug': 6.5.0 + '@prisma/engines-version': 6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60 + '@prisma/get-platform': 6.5.0 + + '@prisma/get-platform@6.5.0': + dependencies: + '@prisma/debug': 6.5.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@stylistic/eslint-plugin@2.12.1(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@typescript-eslint/utils': 8.56.1(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/normalize-package-data@2.4.4': {} + + '@types/react-dom@19.0.4(@types/react@19.0.10)': + dependencies: + '@types/react': 19.0.10 + + '@types/react@19.0.10': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.19.0 + '@typescript-eslint/type-utils': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.19.0 + eslint: 9.22.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.8.2) + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.19.0 + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.19.0 + debug: 4.4.3 + eslint: 9.22.0 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.8.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.8.2) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/scope-manager@8.19.0': + dependencies: + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/visitor-keys': 8.19.0 + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.8.2)': + dependencies: + typescript: 5.8.2 + + '@typescript-eslint/type-utils@8.19.0(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.8.2) + '@typescript-eslint/utils': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + debug: 4.4.3 + eslint: 9.22.0 + ts-api-utils: 1.4.3(typescript@5.8.2) + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/types@8.19.0': {} + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.8.2)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.7.4 + ts-api-utils: 1.4.3(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.19.0(typescript@5.8.2)': + dependencies: + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/visitor-keys': 8.19.0 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.9 + semver: 7.7.4 + ts-api-utils: 1.4.3(typescript@5.8.2) + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.8.2)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.8.2) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.8.2) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.8.2) + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.22.0) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.2) + eslint: 9.22.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.19.0(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.22.0) + '@typescript-eslint/scope-manager': 8.19.0 + '@typescript-eslint/types': 8.19.0 + '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.8.2) + eslint: 9.22.0 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.22.0) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.8.2) + eslint: 9.22.0 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.19.0': + dependencies: + '@typescript-eslint/types': 8.19.0 + eslint-visitor-keys: 4.2.1 + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@4.3.4(vite@6.2.1(tsx@4.19.3))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 6.2.1(tsx@4.19.3) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@3.0.8(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.0.8(jsdom@26.0.0)(tsx@4.19.3) + transitivePeerDependencies: + - supports-color + + '@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.56.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)(vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3))': + dependencies: + '@typescript-eslint/utils': 8.56.1(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 + optionalDependencies: + typescript: 5.8.2 + vitest: 3.0.8(jsdom@26.0.0)(tsx@4.19.3) + + '@vitest/expect@3.0.8': + dependencies: + '@vitest/spy': 3.0.8 + '@vitest/utils': 3.0.8 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.0.8(vite@6.2.1(tsx@4.19.3))': + dependencies: + '@vitest/spy': 3.0.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.2.1(tsx@4.19.3) + + '@vitest/pretty-format@3.0.8': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.0.8': + dependencies: + '@vitest/utils': 3.0.8 + pathe: 2.0.3 + + '@vitest/snapshot@3.0.8': + dependencies: + '@vitest/pretty-format': 3.0.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.0.8': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.0.8': + dependencies: + '@vitest/pretty-format': 3.0.8 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@7.4.1): + dependencies: + acorn: 7.4.1 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@7.4.1: {} + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + are-docs-informative@0.0.2: {} + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + builtin-modules@3.3.0: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001777: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + ci-info@4.4.0: {} + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comment-parser@1.4.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + core-js-compat@3.48.0: + dependencies: + browserslist: 4.28.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.307: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@6.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.2: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.27.3): + dependencies: + debug: 4.4.3 + esbuild: 0.27.3 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-deprecation@3.0.0(eslint@9.22.0)(typescript@5.8.2): + dependencies: + '@typescript-eslint/utils': 7.18.0(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 + ts-api-utils: 1.4.3(typescript@5.8.2) + tslib: 2.8.1 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.22.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.19.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.19.0(eslint@9.22.0)(typescript@5.8.2) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsdoc@50.6.1(eslint@9.22.0): + dependencies: + '@es-joy/jsdoccomment': 0.49.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 9.22.0 + espree: 10.4.0 + esquery: 1.7.0 + parse-imports: 2.2.1 + semver: 7.7.4 + spdx-expression-parse: 4.0.0 + synckit: 0.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-playwright@2.1.0(eslint@9.22.0): + dependencies: + eslint: 9.22.0 + globals: 13.24.0 + + eslint-plugin-react@7.37.3(eslint@9.22.0): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.2 + eslint: 9.22.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.6 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-plugin-sort-keys-fix@1.1.2: + dependencies: + espree: 6.2.1 + esutils: 2.0.3 + natural-compare: 1.4.0 + requireindex: 1.2.0 + + eslint-plugin-unicorn@56.0.1(eslint@9.22.0): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.22.0) + ci-info: 4.4.0 + clean-regexp: 1.0.0 + core-js-compat: 3.48.0 + eslint: 9.22.0 + esquery: 1.7.0 + globals: 15.14.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.1.0 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + regjsparser: 0.10.0 + semver: 7.7.4 + strip-indent: 3.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.22.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.22.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.19.2 + '@eslint/config-helpers': 0.1.0 + '@eslint/core': 0.12.0 + '@eslint/eslintrc': 3.3.4 + '@eslint/js': 9.22.0 + '@eslint/plugin-kit': 0.2.8 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@6.2.1: + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + eslint-visitor-keys: 1.3.0 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.4 + keyv: 4.5.4 + + flatted@3.3.4: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.14.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.7.4: {} + + hosted-git-info@2.8.9: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-escaper@2.0.2: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdoc-type-pratt-parser@4.1.0: {} + + jsdom@26.0.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@0.5.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + min-indent@1.0.1: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.36: {} + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.11 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + nwsapi@2.2.23: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-imports@2.2.1: + dependencies: + es-module-lexer: 1.7.0 + slashes: 3.0.12 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + + pluralize@8.0.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prisma@6.5.0(typescript@5.8.2): + dependencies: + '@prisma/config': 6.5.0 + '@prisma/engines': 6.5.0 + optionalDependencies: + fsevents: 2.3.3 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.0.0(react@19.0.0): + dependencies: + react: 19.0.0 + scheduler: 0.25.0 + + react-is@16.13.1: {} + + react-refresh@0.14.2: {} + + react@19.0.0: {} + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp-tree@0.1.27: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regjsparser@0.10.0: + dependencies: + jsesc: 0.5.0 + + requireindex@1.2.0: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.25.0: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + slashes@3.0.12: {} + + source-map-js@1.2.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + synckit@0.9.3: + dependencies: + '@pkgr/core': 0.1.2 + tslib: 2.8.1 + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.4 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + ts-api-utils@1.4.3(typescript@5.8.2): + dependencies: + typescript: 5.8.2 + + ts-api-utils@2.4.0(typescript@5.8.2): + dependencies: + typescript: 5.8.2 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.19.3: + dependencies: + esbuild: 0.25.12 + get-tsconfig: 4.13.6 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.8.2: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + vite-node@3.0.8(tsx@4.19.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.2.1(tsx@4.19.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.2.1(tsx@4.19.3): + dependencies: + esbuild: 0.25.12 + postcss: 8.5.8 + rollup: 4.59.0 + optionalDependencies: + fsevents: 2.3.3 + tsx: 4.19.3 + + vitest@3.0.8(jsdom@26.0.0)(tsx@4.19.3): + dependencies: + '@vitest/expect': 3.0.8 + '@vitest/mocker': 3.0.8(vite@6.2.1(tsx@4.19.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.0.8 + '@vitest/snapshot': 3.0.8 + '@vitest/spy': 3.0.8 + '@vitest/utils': 3.0.8 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.2.1(tsx@4.19.3) + vite-node: 3.0.8(tsx@4.19.3) + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 26.0.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c9a2c9d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@nhcarrigan/typescript-config", + "compilerOptions": { + "outDir": "./prod", + "rootDir": "." + }, + "exclude": ["test/**/*.ts", "apps/**", "packages/**"] +} -- 2.52.0 From e9e0df31fd9756b82418fd19503e300f99cef501 Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 13:27:48 -0800 Subject: [PATCH 02/84] feat: add equipment, achievements, and visual polish - Equipment system: 12 items across weapon/armour/trinket slots with common/rare/epic/legendary rarities; starter commons auto-equipped, higher tiers drop from boss victories - Achievement system: 15 milestones with typed conditions; checked each tick and crystal rewards applied automatically - Achievement toast: slide-in notification, auto-dismisses after 4s - Floating click text: +X gold floats on each manual click - Expanded quests (9 total) and upgrades (12 total) - Upgrade panel now shows locked upgrades so players can see their progression path - formatNumber utility (K/M/B/T) used consistently across all panels - Backfill logic for existing saves to add new content gracefully - types package now emits .d.ts declarations --- apps/api/prisma/schema.prisma | 9 - apps/api/src/data/achievements.ts | 139 +++++ apps/api/src/data/adventurers.ts | 10 + apps/api/src/data/bosses.ts | 4 + apps/api/src/data/equipment.ts | 127 +++++ apps/api/src/data/initialState.ts | 4 + apps/api/src/data/quests.ts | 55 ++ apps/api/src/data/upgrades.ts | 47 ++ apps/api/src/routes/boss.ts | 168 ++++-- apps/api/src/routes/game.ts | 76 +++ apps/web/src/api/client.ts | 12 +- .../src/components/game/AchievementPanel.tsx | 75 +++ .../src/components/game/AchievementToast.tsx | 48 ++ apps/web/src/components/game/BattleModal.tsx | 171 ++++++ apps/web/src/components/game/BossPanel.tsx | 114 +++- apps/web/src/components/game/ClickArea.tsx | 59 ++- .../src/components/game/EquipmentPanel.tsx | 102 ++++ apps/web/src/components/game/GameLayout.tsx | 16 +- apps/web/src/components/game/UpgradePanel.tsx | 42 +- apps/web/src/components/ui/ResourceBar.tsx | 14 +- apps/web/src/context/GameContext.tsx | 161 ++++-- apps/web/src/engine/tick.ts | 132 ++++- apps/web/src/styles.css | 485 ++++++++++++++++++ apps/web/src/utils/format.ts | 20 + packages/types/src/index.ts | 16 +- packages/types/src/interfaces/Achievement.ts | 28 + packages/types/src/interfaces/Adventurer.ts | 2 + packages/types/src/interfaces/Api.ts | 27 +- packages/types/src/interfaces/Boss.ts | 2 + packages/types/src/interfaces/Equipment.ts | 25 + packages/types/src/interfaces/GameState.ts | 4 + packages/types/src/interfaces/Quest.ts | 2 +- packages/types/tsconfig.json | 3 +- 33 files changed, 2066 insertions(+), 133 deletions(-) create mode 100644 apps/api/src/data/achievements.ts create mode 100644 apps/api/src/data/equipment.ts create mode 100644 apps/web/src/components/game/AchievementPanel.tsx create mode 100644 apps/web/src/components/game/AchievementToast.tsx create mode 100644 apps/web/src/components/game/BattleModal.tsx create mode 100644 apps/web/src/components/game/EquipmentPanel.tsx create mode 100644 apps/web/src/utils/format.ts create mode 100644 packages/types/src/interfaces/Achievement.ts create mode 100644 packages/types/src/interfaces/Equipment.ts diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index bf7ba98..c68de71 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -27,12 +27,3 @@ model GameState { updatedAt Float } -model BossDamageLog { - id String @id @default(auto()) @map("_id") @db.ObjectId - discordId String - bossId String - damage Float - dealtAt Float - - @@index([discordId, bossId, dealtAt]) -} diff --git a/apps/api/src/data/achievements.ts b/apps/api/src/data/achievements.ts new file mode 100644 index 0000000..241cd0b --- /dev/null +++ b/apps/api/src/data/achievements.ts @@ -0,0 +1,139 @@ +import type { Achievement } from "@elysium/types"; + +export const DEFAULT_ACHIEVEMENTS: Achievement[] = [ + { + id: "first_click", + name: "First Strike", + description: "Click the Guild Hall for the first time.", + icon: "👆", + condition: { type: "totalClicks", amount: 1 }, + reward: { crystals: 5 }, + unlockedAt: null, + }, + { + id: "click_enthusiast", + name: "Click Enthusiast", + description: "Click the Guild Hall 100 times.", + icon: "🖱️", + condition: { type: "totalClicks", amount: 100 }, + reward: { crystals: 25 }, + unlockedAt: null, + }, + { + id: "click_master", + name: "Click Master", + description: "Click the Guild Hall 1,000 times.", + icon: "⚡", + condition: { type: "totalClicks", amount: 1_000 }, + reward: { crystals: 100 }, + unlockedAt: null, + }, + { + id: "first_gold", + name: "First Gold", + description: "Earn your first 100 gold.", + icon: "🪙", + condition: { type: "totalGoldEarned", amount: 100 }, + reward: { crystals: 5 }, + unlockedAt: null, + }, + { + id: "wealthy", + name: "Wealthy", + description: "Earn 10,000 gold in total.", + icon: "💰", + condition: { type: "totalGoldEarned", amount: 10_000 }, + reward: { crystals: 25 }, + unlockedAt: null, + }, + { + id: "rich", + name: "Rich", + description: "Earn 1,000,000 gold in total.", + icon: "👑", + condition: { type: "totalGoldEarned", amount: 1_000_000 }, + reward: { crystals: 100 }, + unlockedAt: null, + }, + { + id: "billionaire", + name: "Billionaire", + description: "Earn 1,000,000,000 gold in total.", + icon: "🏦", + condition: { type: "totalGoldEarned", amount: 1_000_000_000 }, + reward: { crystals: 500 }, + unlockedAt: null, + }, + { + id: "first_quest", + name: "Adventurous Spirit", + description: "Complete your first quest.", + icon: "📜", + condition: { type: "questsCompleted", amount: 1 }, + reward: { crystals: 10 }, + unlockedAt: null, + }, + { + id: "quest_veteran", + name: "Quest Veteran", + description: "Complete 5 quests.", + icon: "📚", + condition: { type: "questsCompleted", amount: 5 }, + reward: { crystals: 50 }, + unlockedAt: null, + }, + { + id: "boss_slayer", + name: "Boss Slayer", + description: "Defeat your first boss.", + icon: "⚔️", + condition: { type: "bossesDefeated", amount: 1 }, + reward: { crystals: 25 }, + unlockedAt: null, + }, + { + id: "legendary_hunter", + name: "Legendary Hunter", + description: "Defeat all four bosses.", + icon: "🏆", + condition: { type: "bossesDefeated", amount: 4 }, + reward: { crystals: 200 }, + unlockedAt: null, + }, + { + id: "guild_master", + name: "Guild Master", + description: "Recruit a total of 50 adventurers.", + icon: "🏰", + condition: { type: "adventurerTotal", amount: 50 }, + reward: { crystals: 50 }, + unlockedAt: null, + }, + { + id: "army_commander", + name: "Army Commander", + description: "Recruit a total of 500 adventurers.", + icon: "🛡️", + condition: { type: "adventurerTotal", amount: 500 }, + reward: { crystals: 200 }, + unlockedAt: null, + }, + { + id: "first_prestige", + name: "Born Again", + description: "Prestige for the first time.", + icon: "⭐", + condition: { type: "prestigeCount", amount: 1 }, + reward: { crystals: 100 }, + unlockedAt: null, + }, + { + id: "collector", + name: "Collector", + description: "Acquire your first piece of boss-dropped equipment.", + icon: "🎒", + condition: { type: "equipmentOwned", amount: 4 }, + reward: { crystals: 10 }, + unlockedAt: null, + }, +]; diff --git a/apps/api/src/data/adventurers.ts b/apps/api/src/data/adventurers.ts index 526d412..f90373d 100644 --- a/apps/api/src/data/adventurers.ts +++ b/apps/api/src/data/adventurers.ts @@ -8,6 +8,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 1, goldPerSecond: 0.1, essencePerSecond: 0, + combatPower: 1, count: 0, unlocked: true, }, @@ -18,6 +19,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 2, goldPerSecond: 0.5, essencePerSecond: 0, + combatPower: 3, count: 0, unlocked: false, }, @@ -28,6 +30,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 3, goldPerSecond: 1.5, essencePerSecond: 0.01, + combatPower: 8, count: 0, unlocked: false, }, @@ -38,6 +41,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 4, goldPerSecond: 4, essencePerSecond: 0.02, + combatPower: 20, count: 0, unlocked: false, }, @@ -48,6 +52,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 5, goldPerSecond: 10, essencePerSecond: 0.05, + combatPower: 50, count: 0, unlocked: false, }, @@ -58,6 +63,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 6, goldPerSecond: 25, essencePerSecond: 0.1, + combatPower: 120, count: 0, unlocked: false, }, @@ -68,6 +74,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 7, goldPerSecond: 75, essencePerSecond: 0.2, + combatPower: 300, count: 0, unlocked: false, }, @@ -78,6 +85,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 8, goldPerSecond: 200, essencePerSecond: 0.5, + combatPower: 800, count: 0, unlocked: false, }, @@ -88,6 +96,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 9, goldPerSecond: 600, essencePerSecond: 1, + combatPower: 2000, count: 0, unlocked: false, }, @@ -98,6 +107,7 @@ export const DEFAULT_ADVENTURERS: Adventurer[] = [ level: 10, goldPerSecond: 2000, essencePerSecond: 3, + combatPower: 6000, count: 0, unlocked: false, }, diff --git a/apps/api/src/data/bosses.ts b/apps/api/src/data/bosses.ts index 135f321..21144eb 100644 --- a/apps/api/src/data/bosses.ts +++ b/apps/api/src/data/bosses.ts @@ -14,6 +14,7 @@ export const DEFAULT_BOSSES: Boss[] = [ essenceReward: 25, crystalReward: 0, upgradeRewards: ["click_2"], + equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, }, { @@ -29,6 +30,7 @@ export const DEFAULT_BOSSES: Boss[] = [ essenceReward: 200, crystalReward: 10, upgradeRewards: ["global_2"], + equipmentRewards: ["enchanted_blade", "plate_armour", "arcane_orb"], prestigeRequirement: 0, }, { @@ -44,6 +46,7 @@ export const DEFAULT_BOSSES: Boss[] = [ essenceReward: 1_000, crystalReward: 50, upgradeRewards: ["click_3"], + equipmentRewards: ["vorpal_sword", "dragon_scale"], prestigeRequirement: 1, }, { @@ -59,6 +62,7 @@ export const DEFAULT_BOSSES: Boss[] = [ essenceReward: 5_000, crystalReward: 200, upgradeRewards: [], + equipmentRewards: ["philosophers_stone"], prestigeRequirement: 3, }, ]; diff --git a/apps/api/src/data/equipment.ts b/apps/api/src/data/equipment.ts new file mode 100644 index 0000000..cc99ae3 --- /dev/null +++ b/apps/api/src/data/equipment.ts @@ -0,0 +1,127 @@ +import type { Equipment } from "@elysium/types"; + +export const DEFAULT_EQUIPMENT: Equipment[] = [ + // Weapons — drop from bosses; common starts owned + { + id: "rusty_sword", + name: "Rusty Sword", + description: "A battered blade, but still sharp enough to draw blood.", + type: "weapon", + rarity: "common", + bonus: { combatMultiplier: 1.1 }, + owned: true, + equipped: true, + }, + { + id: "iron_sword", + name: "Iron Sword", + description: "A sturdy weapon issued to veterans of the guild.", + type: "weapon", + rarity: "rare", + bonus: { combatMultiplier: 1.25 }, + owned: false, + equipped: false, + }, + { + id: "enchanted_blade", + name: "Enchanted Blade", + description: "A sword imbued with ancient magic that makes every strike count.", + type: "weapon", + rarity: "epic", + bonus: { combatMultiplier: 1.5 }, + owned: false, + equipped: false, + }, + { + id: "vorpal_sword", + name: "Vorpal Sword", + description: "A legendary blade that severs even the strongest bonds.", + type: "weapon", + rarity: "legendary", + bonus: { combatMultiplier: 2.0 }, + owned: false, + equipped: false, + }, + // Armour — drop from bosses; common starts owned + { + id: "leather_armour", + name: "Leather Armour", + description: "Simple protection that keeps your adventurers moving efficiently.", + type: "armour", + rarity: "common", + bonus: { goldMultiplier: 1.1 }, + owned: true, + equipped: true, + }, + { + id: "chainmail", + name: "Chainmail", + description: "Interlocked rings that guard against most mundane threats.", + type: "armour", + rarity: "rare", + bonus: { goldMultiplier: 1.25 }, + owned: false, + equipped: false, + }, + { + id: "plate_armour", + name: "Plate Armour", + description: "Full plate protection that inspires confidence — and gold.", + type: "armour", + rarity: "epic", + bonus: { goldMultiplier: 1.5 }, + owned: false, + equipped: false, + }, + { + id: "dragon_scale", + name: "Dragon Scale Armour", + description: "Armour forged from the scales of a defeated elder dragon.", + type: "armour", + rarity: "legendary", + bonus: { goldMultiplier: 2.0 }, + owned: false, + equipped: false, + }, + // Trinkets — drop from bosses; common starts owned + { + id: "lucky_coin", + name: "Lucky Coin", + description: "A coin that always lands on the side you need.", + type: "trinket", + rarity: "common", + bonus: { clickMultiplier: 1.1 }, + owned: true, + equipped: true, + }, + { + id: "mages_focus", + name: "Mage's Focus", + description: "A crystal lens that sharpens magical precision.", + type: "trinket", + rarity: "rare", + bonus: { clickMultiplier: 1.25 }, + owned: false, + equipped: false, + }, + { + id: "arcane_orb", + name: "Arcane Orb", + description: "An orb humming with concentrated arcane energy.", + type: "trinket", + rarity: "epic", + bonus: { clickMultiplier: 1.5 }, + owned: false, + equipped: false, + }, + { + id: "philosophers_stone", + name: "Philosopher's Stone", + description: "The legendary stone that grants mastery over gold and combat alike.", + type: "trinket", + rarity: "legendary", + bonus: { clickMultiplier: 2.0, goldMultiplier: 1.25 }, + owned: false, + equipped: false, + }, +]; diff --git a/apps/api/src/data/initialState.ts b/apps/api/src/data/initialState.ts index 334f567..bc7a35a 100644 --- a/apps/api/src/data/initialState.ts +++ b/apps/api/src/data/initialState.ts @@ -1,6 +1,8 @@ import type { GameState, Player, PrestigeData } from "@elysium/types"; +import { DEFAULT_ACHIEVEMENTS } from "./achievements.js"; import { DEFAULT_ADVENTURERS } from "./adventurers.js"; import { DEFAULT_BOSSES } from "./bosses.js"; +import { DEFAULT_EQUIPMENT } from "./equipment.js"; import { DEFAULT_QUESTS } from "./quests.js"; import { DEFAULT_UPGRADES } from "./upgrades.js"; @@ -28,6 +30,8 @@ export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameS upgrades: structuredClone(DEFAULT_UPGRADES), quests: structuredClone(DEFAULT_QUESTS), bosses: structuredClone(DEFAULT_BOSSES), + equipment: structuredClone(DEFAULT_EQUIPMENT), + achievements: structuredClone(DEFAULT_ACHIEVEMENTS), prestige: INITIAL_PRESTIGE, baseClickPower: 1, lastTickAt: Date.now(), diff --git a/apps/api/src/data/quests.ts b/apps/api/src/data/quests.ts index b4ebbb8..d3a31c3 100644 --- a/apps/api/src/data/quests.ts +++ b/apps/api/src/data/quests.ts @@ -34,6 +34,20 @@ export const DEFAULT_QUESTS: Quest[] = [ ], prerequisiteIds: ["goblin_camp"], }, + { + id: "necromancer_tower", + name: "Necromancer's Tower", + description: + "A rogue necromancer has raised an army of skeletons near the city. Silence him before the dead overrun us.", + status: "locked", + durationSeconds: 25 * 60, + rewards: [ + { type: "gold", amount: 15_000 }, + { type: "essence", amount: 20 }, + { type: "upgrade", targetId: "cleric_1" }, + ], + prerequisiteIds: ["haunted_mine"], + }, { id: "ancient_ruins", name: "Ancient Ruins", @@ -46,6 +60,19 @@ export const DEFAULT_QUESTS: Quest[] = [ ], prerequisiteIds: ["haunted_mine"], }, + { + id: "shadow_mere", + name: "The Shadow Mere", + description: + "A cursed lake shrouded in permanent twilight. Strange energies pulse beneath its surface.", + status: "locked", + durationSeconds: 45 * 60, + rewards: [ + { type: "essence", amount: 150 }, + { type: "upgrade", targetId: "scout_1" }, + ], + prerequisiteIds: ["ancient_ruins"], + }, { id: "dragon_lair", name: "Dragon's Lair", @@ -60,4 +87,32 @@ export const DEFAULT_QUESTS: Quest[] = [ ], prerequisiteIds: ["ancient_ruins"], }, + { + id: "frozen_wastes", + name: "The Frozen Wastes", + description: + "A tundra at the edge of the world, home to creatures that have never seen the sun. Rumours speak of artefacts buried in the permafrost.", + status: "locked", + durationSeconds: 2 * 60 * 60, + rewards: [ + { type: "gold", amount: 2_000_000 }, + { type: "crystals", amount: 150 }, + { type: "upgrade", targetId: "global_3" }, + ], + prerequisiteIds: ["dragon_lair"], + }, + { + id: "void_rift", + name: "Void Rift", + description: + "A tear in reality itself. What lies beyond defies description — but the power within is unlike anything of this world.", + status: "locked", + durationSeconds: 4 * 60 * 60, + rewards: [ + { type: "crystals", amount: 500 }, + { type: "essence", amount: 5_000 }, + { type: "upgrade", targetId: "knight_1" }, + ], + prerequisiteIds: ["frozen_wastes"], + }, ]; diff --git a/apps/api/src/data/upgrades.ts b/apps/api/src/data/upgrades.ts index 2841809..b977d50 100644 --- a/apps/api/src/data/upgrades.ts +++ b/apps/api/src/data/upgrades.ts @@ -58,6 +58,17 @@ export const DEFAULT_UPGRADES: Upgrade[] = [ purchased: false, unlocked: false, }, + { + id: "global_3", + name: "Royal Patronage", + description: "The king himself backs your guild. All income doubled.", + target: "global", + multiplier: 2, + costGold: 1_000_000, + costEssence: 100, + purchased: false, + unlocked: false, + }, // Adventurer-specific upgrades { id: "peasant_1", @@ -95,4 +106,40 @@ export const DEFAULT_UPGRADES: Upgrade[] = [ purchased: false, unlocked: false, }, + { + id: "cleric_1", + name: "Holy Rites", + description: "Sacred ceremonies double the output of your clerics.", + target: "adventurer", + adventurerId: "acolyte", + multiplier: 2, + costGold: 8_000, + costEssence: 3, + purchased: false, + unlocked: false, + }, + { + id: "scout_1", + name: "Stealth Training", + description: "Advanced scouting techniques double ranger effectiveness.", + target: "adventurer", + adventurerId: "ranger", + multiplier: 2, + costGold: 15_000, + costEssence: 5, + purchased: false, + unlocked: false, + }, + { + id: "knight_1", + name: "Tempered Steel", + description: "Superior forging techniques double the output of your knights.", + target: "adventurer", + adventurerId: "knight", + multiplier: 2, + costGold: 50_000, + costEssence: 10, + purchased: false, + unlocked: false, + }, ]; diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index d4e51aa..683f4e8 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -1,34 +1,67 @@ -import type { BossDamageRequest, GameState } from "@elysium/types"; +import type { BossChallengeResponse, GameState } from "@elysium/types"; import { Hono } from "hono"; import { prisma } from "../db/client.js"; import { authMiddleware } from "../middleware/auth.js"; -const RATE_LIMIT_WINDOW_MS = 1_000; -const MAX_DAMAGE_PER_SECOND = 10_000; - export const bossRouter = new Hono(); bossRouter.use("*", authMiddleware); -bossRouter.post("/damage", async (context) => { - const discordId = context.get("discordId") as string; - const body = await context.req.json(); - - if (!body.bossId || body.damage == null || body.damage <= 0) { - return context.json({ error: "Invalid request body" }, 400); +const calculatePartyStats = ( + state: GameState, +): { partyDPS: number; partyMaxHp: number } => { + let globalMultiplier = 1; + for (const upgrade of state.upgrades) { + if (upgrade.purchased && upgrade.target === "global") { + globalMultiplier *= upgrade.multiplier; + } } - // Rate limiting: sum damage dealt to this boss in the last second - const windowStart = Date.now() - RATE_LIMIT_WINDOW_MS; - const aggregate = await prisma.bossDamageLog.aggregate({ - where: { discordId, bossId: body.bossId, dealtAt: { gt: windowStart } }, - _sum: { damage: true }, - }); + const prestigeMultiplier = 1 + state.prestige.count * 0.1; - const recentDamage = aggregate._sum.damage ?? 0; + // Apply equipped weapon's combat bonus + const equipmentCombatMultiplier = (state.equipment ?? []) + .filter((e) => e.equipped && e.bonus.combatMultiplier != null) + .reduce((mult, e) => mult * (e.bonus.combatMultiplier ?? 1), 1); - if (recentDamage + body.damage > MAX_DAMAGE_PER_SECOND) { - return context.json({ error: "Rate limit exceeded" }, 429); + let partyDPS = 0; + let partyMaxHp = 0; + + for (const adventurer of state.adventurers) { + if (adventurer.count === 0) continue; + + let adventurerMultiplier = 1; + for (const upgrade of state.upgrades) { + if ( + upgrade.purchased && + upgrade.target === "adventurer" && + upgrade.adventurerId === adventurer.id + ) { + adventurerMultiplier *= upgrade.multiplier; + } + } + + partyDPS += + adventurer.combatPower * + adventurer.count * + adventurerMultiplier * + globalMultiplier * + prestigeMultiplier; + + partyMaxHp += adventurer.level * 50 * adventurer.count; + } + + partyDPS *= equipmentCombatMultiplier; + + return { partyDPS, partyMaxHp }; +}; + +bossRouter.post("/challenge", async (context) => { + const discordId = context.get("discordId") as string; + const body = await context.req.json<{ bossId: string }>(); + + if (!body.bossId) { + return context.json({ error: "Invalid request body" }, 400); } const record = await prisma.gameState.findUnique({ where: { discordId } }); @@ -44,7 +77,7 @@ bossRouter.post("/damage", async (context) => { return context.json({ error: "Boss not found" }, 404); } - if (boss.status !== "in_progress" && boss.status !== "available") { + if (boss.status !== "available" && boss.status !== "in_progress") { return context.json({ error: "Boss is not currently available" }, 400); } @@ -52,18 +85,40 @@ bossRouter.post("/damage", async (context) => { return context.json({ error: "Prestige requirement not met" }, 403); } - await prisma.bossDamageLog.create({ - data: { discordId, bossId: body.bossId, damage: body.damage, dealtAt: Date.now() }, - }); + const { partyDPS, partyMaxHp } = calculatePartyStats(state); - boss.status = "in_progress"; - boss.currentHp = Math.max(0, boss.currentHp - body.damage); - const defeated = boss.currentHp <= 0; + if (partyDPS === 0 || partyMaxHp === 0 || !isFinite(partyDPS) || !isFinite(partyMaxHp)) { + return context.json( + { error: "Your party has no adventurers ready to fight" }, + 400, + ); + } - let rewards: { gold: number; essence: number; crystals: number; upgradeIds: string[] } | undefined; + const bossHpBefore = boss.currentHp; + const bossDPS = boss.damagePerSecond; + + const timeToKillBoss = bossHpBefore / partyDPS; + const timeToKillParty = partyMaxHp / bossDPS; + + const won = timeToKillBoss <= timeToKillParty; + + let partyHpRemaining: number; + let bossHpAtBattleEnd: number; + let bossNewHp: number; + let rewards: BossChallengeResponse["rewards"]; + let casualties: BossChallengeResponse["casualties"]; + + if (won) { + bossHpAtBattleEnd = 0; + bossNewHp = 0; + partyHpRemaining = Math.max( + 0, + partyMaxHp - bossDPS * timeToKillBoss, + ); - if (defeated) { boss.status = "defeated"; + boss.currentHp = 0; + state.resources.gold += boss.goldReward; state.resources.essence += boss.essenceReward; state.resources.crystals += boss.crystalReward; @@ -76,6 +131,21 @@ bossRouter.post("/damage", async (context) => { } } + // Grant equipment rewards — auto-equip if the slot is currently empty + const equipmentRewards = boss.equipmentRewards ?? []; + for (const equipmentId of equipmentRewards) { + const equipment = (state.equipment ?? []).find((e) => e.id === equipmentId); + if (equipment) { + equipment.owned = true; + const slotAlreadyEquipped = (state.equipment ?? []).some( + (e) => e.type === equipment.type && e.equipped, + ); + if (!slotAlreadyEquipped) { + equipment.equipped = true; + } + } + } + const bossIndex = state.bosses.findIndex((b) => b.id === body.bossId); const nextBoss = state.bosses[bossIndex + 1]; if (nextBoss && nextBoss.prestigeRequirement <= state.prestige.count) { @@ -87,7 +157,33 @@ bossRouter.post("/damage", async (context) => { essence: boss.essenceReward, crystals: boss.crystalReward, upgradeIds: boss.upgradeRewards, + equipmentIds: equipmentRewards, }; + } else { + bossHpAtBattleEnd = Math.max( + 0, + bossHpBefore - partyDPS * timeToKillParty, + ); + bossNewHp = boss.maxHp; + partyHpRemaining = 0; + + boss.status = "available"; + boss.currentHp = boss.maxHp; + + // How close was the party to winning? (0 = hopeless, 1 = nearly won) + const victoryProgress = Math.min(1, timeToKillParty / timeToKillBoss); + // Casualty rate scales from ~0% (nearly won) to 60% (completely outmatched) + const casualtyFraction = (1 - victoryProgress) * 0.6; + + casualties = []; + for (const adventurer of state.adventurers) { + if (adventurer.count === 0) continue; + const killed = Math.floor(adventurer.count * casualtyFraction); + if (killed > 0) { + adventurer.count = Math.max(1, adventurer.count - killed); + casualties.push({ adventurerId: adventurer.id, killed }); + } + } } const now = Date.now(); @@ -96,5 +192,19 @@ bossRouter.post("/damage", async (context) => { data: { state: state as object, updatedAt: now }, }); - return context.json({ currentHp: boss.currentHp, defeated, rewards }); + const response: BossChallengeResponse = { + won, + partyDPS, + bossDPS, + bossHpBefore, + bossMaxHp: boss.maxHp, + bossHpAtBattleEnd, + bossNewHp, + partyMaxHp, + partyHpRemaining, + }; + if (rewards !== undefined) response.rewards = rewards; + if (casualties !== undefined) response.casualties = casualties; + + return context.json(response); }); diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index c6896a5..d23e4e9 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -1,6 +1,9 @@ import type { GameState, SaveRequest } from "@elysium/types"; import { Hono } from "hono"; import { prisma } from "../db/client.js"; +import { DEFAULT_ACHIEVEMENTS } from "../data/achievements.js"; +import { DEFAULT_ADVENTURERS } from "../data/adventurers.js"; +import { DEFAULT_EQUIPMENT } from "../data/equipment.js"; import { authMiddleware } from "../middleware/auth.js"; import { calculateOfflineGold } from "../services/offlineProgress.js"; @@ -18,6 +21,72 @@ gameRouter.get("/load", async (context) => { } const state = record.state as unknown as GameState; + + let needsBackfill = false; + + // Backfill combatPower on saves that predate the field + for (const adventurer of state.adventurers) { + if (adventurer.combatPower == null) { + const defaults = DEFAULT_ADVENTURERS.find((d) => d.id === adventurer.id); + adventurer.combatPower = defaults?.combatPower ?? 1; + needsBackfill = true; + } + } + + // Backfill equipment on saves that predate the feature + if (!Array.isArray(state.equipment) || state.equipment.length === 0) { + state.equipment = structuredClone(DEFAULT_EQUIPMENT); + needsBackfill = true; + } else { + // Merge in any equipment items missing from existing saves (new items added later) + for (const defaultItem of DEFAULT_EQUIPMENT) { + if (!state.equipment.some((e) => e.id === defaultItem.id)) { + state.equipment.push(structuredClone(defaultItem)); + needsBackfill = true; + } + } + } + + // Backfill achievements on saves that predate the feature + if (!Array.isArray(state.achievements) || state.achievements.length === 0) { + state.achievements = structuredClone(DEFAULT_ACHIEVEMENTS); + needsBackfill = true; + } else { + // Merge in any achievements missing from existing saves + for (const defaultAchievement of DEFAULT_ACHIEVEMENTS) { + if (!state.achievements.some((a) => a.id === defaultAchievement.id)) { + state.achievements.push(structuredClone(defaultAchievement)); + needsBackfill = true; + } + } + } + + // Backfill equipmentRewards on bosses that predate the field + for (const boss of state.bosses) { + if (!Array.isArray(boss.equipmentRewards)) { + boss.equipmentRewards = []; + needsBackfill = true; + } + } + + // Backfill new quests and upgrades from defaults (add missing ones) + const { DEFAULT_QUESTS } = await import("../data/quests.js"); + const { DEFAULT_UPGRADES } = await import("../data/upgrades.js"); + + for (const defaultQuest of DEFAULT_QUESTS) { + if (!state.quests.some((q) => q.id === defaultQuest.id)) { + state.quests.push(structuredClone(defaultQuest)); + needsBackfill = true; + } + } + + for (const defaultUpgrade of DEFAULT_UPGRADES) { + if (!state.upgrades.some((u) => u.id === defaultUpgrade.id)) { + state.upgrades.push(structuredClone(defaultUpgrade)); + needsBackfill = true; + } + } + const now = Date.now(); const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now); @@ -29,6 +98,13 @@ gameRouter.get("/load", async (context) => { state.lastTickAt = now; + if (needsBackfill || offlineGold > 0) { + await prisma.gameState.update({ + where: { discordId }, + data: { state: state as object, updatedAt: now }, + }); + } + return context.json({ state, offlineGold, offlineSeconds }); }); diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 3b4fd6a..9676d26 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -1,7 +1,7 @@ import type { AuthResponse, - BossDamageRequest, - BossDamageResponse, + BossChallengeRequest, + BossChallengeResponse, LoadResponse, PrestigeRequest, PrestigeResponse, @@ -61,10 +61,10 @@ export const saveGame = async (body: SaveRequest): Promise => body: JSON.stringify(body), }); -export const dealBossDamage = async ( - body: BossDamageRequest, -): Promise => - request("/boss/damage", { +export const challengeBoss = async ( + body: BossChallengeRequest, +): Promise => + request("/boss/challenge", { method: "POST", body: JSON.stringify(body), }); diff --git a/apps/web/src/components/game/AchievementPanel.tsx b/apps/web/src/components/game/AchievementPanel.tsx new file mode 100644 index 0000000..9f808ff --- /dev/null +++ b/apps/web/src/components/game/AchievementPanel.tsx @@ -0,0 +1,75 @@ +import type { Achievement } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; +import { formatNumber } from "../../utils/format.js"; + +const conditionDescription = (achievement: Achievement): string => { + const { condition } = achievement; + switch (condition.type) { + case "totalGoldEarned": + return `Earn ${formatNumber(condition.amount)} total gold`; + case "totalClicks": + return `Click ${formatNumber(condition.amount)} times`; + case "bossesDefeated": + return `Defeat ${condition.amount} boss${condition.amount > 1 ? "es" : ""}`; + case "questsCompleted": + return `Complete ${condition.amount} quest${condition.amount > 1 ? "s" : ""}`; + case "adventurerTotal": + return `Recruit ${formatNumber(condition.amount)} total adventurers`; + case "prestigeCount": + return `Prestige ${condition.amount} time${condition.amount > 1 ? "s" : ""}`; + case "equipmentOwned": + return `Own ${condition.amount} equipment item${condition.amount > 1 ? "s" : ""}`; + } +}; + +interface AchievementCardProps { + achievement: Achievement; +} + +const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Element => { + const isUnlocked = achievement.unlockedAt !== null; + + return ( +
+
{achievement.icon}
+
+

{achievement.name}

+

{achievement.description}

+

{conditionDescription(achievement)}

+ {achievement.reward?.crystals != null && ( +

💎 +{achievement.reward.crystals} Crystals

+ )} +
+
+ {isUnlocked ? ( + ✓ Unlocked + ) : ( + 🔒 + )} +
+
+ ); +}; + +export const AchievementPanel = (): React.JSX.Element => { + const { state } = useGame(); + + if (!state) return

Loading...

; + + const achievements = state.achievements ?? []; + const unlocked = achievements.filter((a) => a.unlockedAt !== null).length; + + return ( +
+

Achievements

+

+ {unlocked} / {achievements.length} unlocked +

+
+ {achievements.map((achievement) => ( + + ))} +
+
+ ); +}; diff --git a/apps/web/src/components/game/AchievementToast.tsx b/apps/web/src/components/game/AchievementToast.tsx new file mode 100644 index 0000000..76d5a66 --- /dev/null +++ b/apps/web/src/components/game/AchievementToast.tsx @@ -0,0 +1,48 @@ +import { useEffect } from "react"; +import type { Achievement } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; + +interface ToastItemProps { + achievement: Achievement; + onDismiss: (id: string) => void; +} + +const ToastItem = ({ achievement, onDismiss }: ToastItemProps): React.JSX.Element => { + useEffect(() => { + const timer = setTimeout(() => { + onDismiss(achievement.id); + }, 4000); + return () => { clearTimeout(timer); }; + }, [achievement.id, onDismiss]); + + return ( +
{ onDismiss(achievement.id); }}> + {achievement.icon} +
+ Achievement Unlocked! + {achievement.name} + {achievement.reward?.crystals != null && ( + 💎 +{achievement.reward.crystals} + )} +
+
+ ); +}; + +export const AchievementToast = (): React.JSX.Element | null => { + const { newAchievements, dismissAchievement } = useGame(); + + if (newAchievements.length === 0) return null; + + return ( +
+ {newAchievements.map((achievement) => ( + + ))} +
+ ); +}; diff --git a/apps/web/src/components/game/BattleModal.tsx b/apps/web/src/components/game/BattleModal.tsx new file mode 100644 index 0000000..1a1cacc --- /dev/null +++ b/apps/web/src/components/game/BattleModal.tsx @@ -0,0 +1,171 @@ +import type { BattleResult } from "../../context/GameContext.js"; +import { useEffect, useState } from "react"; + +interface BattleModalProps { + battle: BattleResult; + onDismiss: () => void; +} + +const formatNumber = (n: number): string => { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; + return Math.floor(n).toLocaleString(); +}; + +export const BattleModal = ({ + battle, + onDismiss, +}: BattleModalProps): React.JSX.Element => { + const { result, bossName } = battle; + + const [phase, setPhase] = useState<"animating" | "result">("animating"); + + // Starting HP percentages + const bossStartPercent = (result.bossHpBefore / result.bossMaxHp) * 100; + const partyStartPercent = 100; + + // Target HP percentages (after battle) + const bossEndPercent = (result.bossHpAtBattleEnd / result.bossMaxHp) * 100; + const partyEndPercent = result.partyMaxHp > 0 + ? (result.partyHpRemaining / result.partyMaxHp) * 100 + : 0; + + const [bossHpPercent, setBossHpPercent] = useState(bossStartPercent); + const [partyHpPercent, setPartyHpPercent] = useState(partyStartPercent); + + useEffect(() => { + // Brief delay so CSS transition has a starting point to animate from + const startAnimation = setTimeout(() => { + setBossHpPercent(bossEndPercent); + setPartyHpPercent(partyEndPercent); + }, 200); + + // Reveal result after animation completes + const revealResult = setTimeout(() => { + setPhase("result"); + }, 5_200); + + return () => { + clearTimeout(startAnimation); + clearTimeout(revealResult); + }; + }, [bossEndPercent, partyEndPercent]); + + const bossHpBarColour = bossHpPercent > 50 + ? "#e74c3c" + : bossHpPercent > 25 + ? "#e67e22" + : "#c0392b"; + + const partyHpBarColour = partyHpPercent > 50 + ? "#27ae60" + : partyHpPercent > 25 + ? "#f39c12" + : "#e74c3c"; + + return ( +
+
+

⚔️ Battle: {bossName}

+ +
+
+ Your Party DPS + {formatNumber(result.partyDPS)} +
+
vs
+
+ Boss DPS + {formatNumber(result.bossDPS)} +
+
+ +
+
+ 👹 {bossName} +
+
+
+ + {formatNumber(result.bossHpAtBattleEnd)} / {formatNumber(result.bossMaxHp)} + +
+ +
⚔️ VS ⚔️
+ +
+ 🛡️ Your Party +
+
+
+ + {formatNumber(result.partyHpRemaining)} / {formatNumber(result.partyMaxHp)} + +
+
+ + {phase === "animating" && ( +

Battling…

+ )} + + {phase === "result" && ( +
+ {result.won ? ( + <> +

🏆 Victory!

+ {result.rewards && ( +
+

Rewards:

+ 🪙 {formatNumber(result.rewards.gold)} gold + {result.rewards.essence > 0 && ( + ✨ {formatNumber(result.rewards.essence)} essence + )} + {result.rewards.crystals > 0 && ( + 💎 {formatNumber(result.rewards.crystals)} crystals + )} +
+ )} + + ) : ( + <> +

💀 Defeat

+

Your party was defeated. The boss has reset.

+ {result.casualties && result.casualties.length > 0 && ( +
+

Casualties:

+ {result.casualties.map((c) => ( + + ☠️ {c.killed} {c.adventurerId} lost + + ))} +
+ )} + + )} + +
+ )} +
+
+ ); +}; diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx index cb743d7..7149314 100644 --- a/apps/web/src/components/game/BossPanel.tsx +++ b/apps/web/src/components/game/BossPanel.tsx @@ -1,15 +1,25 @@ import type { Boss } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { formatNumber } from "../../utils/format.js"; interface BossCardProps { boss: Boss; prestigeCount: number; + onChallenge: (bossId: string) => void; + isChallenging: boolean; } -const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element => { - const { attackBoss } = useGame(); +const BossCard = ({ + boss, + prestigeCount, + onChallenge, + isChallenging, +}: BossCardProps): React.JSX.Element => { const hpPercent = (boss.currentHp / boss.maxHp) * 100; const isLocked = boss.prestigeRequirement > prestigeCount; + const canChallenge = + (boss.status === "available" || boss.status === "in_progress") && !isChallenging; return (
@@ -17,7 +27,9 @@ const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element =>

{boss.name}

{boss.description}

{isLocked && boss.status === "locked" && ( -

🔒 Requires Prestige {boss.prestigeRequirement}

+

+ 🔒 Requires Prestige {boss.prestigeRequirement} +

)}
@@ -30,26 +42,40 @@ const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element => />
- {boss.currentHp.toLocaleString()} / {boss.maxHp.toLocaleString()} HP + {formatNumber(boss.currentHp)} / {formatNumber(boss.maxHp)} HP
)} -
- 🪙 {boss.goldReward.toLocaleString()} - {boss.essenceReward > 0 && ✨ {boss.essenceReward.toLocaleString()}} - {boss.crystalReward > 0 && 💎 {boss.crystalReward.toLocaleString()}} +
+ 💢 Boss DPS: {formatNumber(boss.damagePerSecond)}
- {boss.status === "available" || boss.status === "in_progress" ? ( +
+ 🪙 {formatNumber(boss.goldReward)} + {boss.essenceReward > 0 && ( + ✨ {formatNumber(boss.essenceReward)} + )} + {boss.crystalReward > 0 && ( + 💎 {formatNumber(boss.crystalReward)} + )} + {(boss.equipmentRewards ?? []).length > 0 && ( + 🗡️ {boss.equipmentRewards.length} Equipment + )} +
+ + {(boss.status === "available" || boss.status === "in_progress") && ( - ) : null} + )} {boss.status === "defeated" && ( ☠️ Defeated @@ -59,19 +85,81 @@ const BossCard = ({ boss, prestigeCount }: BossCardProps): React.JSX.Element => }; export const BossPanel = (): React.JSX.Element => { - const { state } = useGame(); + const { state, challengeBoss } = useGame(); + const [challengingBossId, setChallengingBossId] = useState(null); if (!state) return

Loading...

; + // Calculate party combat stats including equipment multiplier + let globalMultiplier = 1; + for (const upgrade of state.upgrades) { + if (upgrade.purchased && upgrade.target === "global") { + globalMultiplier *= upgrade.multiplier; + } + } + const prestigeMultiplier = 1 + state.prestige.count * 0.1; + const equipmentCombatMultiplier = (state.equipment ?? []) + .filter((e) => e.equipped && e.bonus.combatMultiplier != null) + .reduce((mult, e) => mult * (e.bonus.combatMultiplier ?? 1), 1); + + let partyDPS = 0; + let partyHP = 0; + for (const adventurer of state.adventurers) { + if (adventurer.count === 0) continue; + let adventurerMultiplier = 1; + for (const upgrade of state.upgrades) { + if ( + upgrade.purchased && + upgrade.target === "adventurer" && + upgrade.adventurerId === adventurer.id + ) { + adventurerMultiplier *= upgrade.multiplier; + } + } + partyDPS += + adventurer.combatPower * + adventurer.count * + adventurerMultiplier * + globalMultiplier * + prestigeMultiplier; + partyHP += adventurer.level * 50 * adventurer.count; + } + partyDPS *= equipmentCombatMultiplier; + + const handleChallenge = async (bossId: string): Promise => { + setChallengingBossId(bossId); + try { + await challengeBoss(bossId); + } finally { + setChallengingBossId(null); + } + }; + return (

Boss Encounters

+ +
+
+ ⚔️ Party DPS + {formatNumber(partyDPS)} +
+
+ ❤️ Party HP + {formatNumber(partyHP)} +
+
+
{state.bosses.map((boss) => ( { + void handleChallenge(id); + }} /> ))}
diff --git a/apps/web/src/components/game/ClickArea.tsx b/apps/web/src/components/game/ClickArea.tsx index 6ad6308..e52e88e 100644 --- a/apps/web/src/components/game/ClickArea.tsx +++ b/apps/web/src/components/game/ClickArea.tsx @@ -1,8 +1,38 @@ +import { useCallback, useRef, useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { calculateClickPower } from "../../engine/tick.js"; +import { formatNumber } from "../../utils/format.js"; + +interface FloatText { + id: number; + x: number; + y: number; + text: string; +} export const ClickArea = (): React.JSX.Element => { const { state, handleClick } = useGame(); + const [floats, setFloats] = useState([]); + const nextIdRef = useRef(0); + + const handleClickWithFloat = useCallback( + (e: React.MouseEvent) => { + if (!state) return; + const rect = e.currentTarget.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const id = nextIdRef.current++; + const clickPower = calculateClickPower(state); + + setFloats((prev) => [...prev, { id, x, y, text: `+${formatNumber(clickPower)}` }]); + handleClick(); + + setTimeout(() => { + setFloats((prev) => prev.filter((f) => f.id !== id)); + }, 900); + }, + [state, handleClick], + ); if (!state) return
; @@ -11,15 +41,26 @@ export const ClickArea = (): React.JSX.Element => { return (

Guild Hall

- -

+{clickPower.toFixed(1)} gold per click

+
+ + {floats.map((float) => ( + + {float.text} + + ))} +
+

+{formatNumber(clickPower)} gold/click

); }; diff --git a/apps/web/src/components/game/EquipmentPanel.tsx b/apps/web/src/components/game/EquipmentPanel.tsx new file mode 100644 index 0000000..3807914 --- /dev/null +++ b/apps/web/src/components/game/EquipmentPanel.tsx @@ -0,0 +1,102 @@ +import type { Equipment, EquipmentType } from "@elysium/types"; +import { useGame } from "../../context/GameContext.js"; + +const RARITY_LABEL: Record = { + common: "Common", + rare: "Rare", + epic: "Epic", + legendary: "Legendary", +}; + +const TYPE_ICON: Record = { + weapon: "⚔️", + armour: "🛡️", + trinket: "💍", +}; + +const bonusDescription = (item: Equipment): string => { + const parts: string[] = []; + if (item.bonus.combatMultiplier != null) { + parts.push(`+${Math.round((item.bonus.combatMultiplier - 1) * 100)}% Combat`); + } + if (item.bonus.goldMultiplier != null) { + parts.push(`+${Math.round((item.bonus.goldMultiplier - 1) * 100)}% Gold/s`); + } + if (item.bonus.clickMultiplier != null) { + parts.push(`+${Math.round((item.bonus.clickMultiplier - 1) * 100)}% Click`); + } + return parts.join(", "); +}; + +interface EquipmentCardProps { + item: Equipment; +} + +const EquipmentCard = ({ item }: EquipmentCardProps): React.JSX.Element => { + const { equipItem } = useGame(); + + return ( +
+
{TYPE_ICON[item.type]}
+
+
+

{item.name}

+ {RARITY_LABEL[item.rarity]} +
+

{item.description}

+

{bonusDescription(item)}

+
+
+ {!item.owned && 🔒 Not yet obtained} + {item.owned && item.equipped && ✓ Equipped} + {item.owned && !item.equipped && ( + + )} +
+
+ ); +}; + +const SLOT_ORDER: EquipmentType[] = ["weapon", "armour", "trinket"]; +const SLOT_LABEL: Record = { + weapon: "⚔️ Weapons", + armour: "🛡️ Armour", + trinket: "💍 Trinkets", +}; + +export const EquipmentPanel = (): React.JSX.Element => { + const { state } = useGame(); + + if (!state) return

Loading...

; + + const equipment = state.equipment ?? []; + + return ( +
+

Equipment

+

+ Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time. +

+ + {SLOT_ORDER.map((slotType) => { + const items = equipment.filter((e) => e.type === slotType); + return ( +
+

{SLOT_LABEL[slotType]}

+
+ {items.map((item) => ( + + ))} +
+
+ ); + })} +
+ ); +}; diff --git a/apps/web/src/components/game/GameLayout.tsx b/apps/web/src/components/game/GameLayout.tsx index dd5e0cd..85a43f3 100644 --- a/apps/web/src/components/game/GameLayout.tsx +++ b/apps/web/src/components/game/GameLayout.tsx @@ -1,26 +1,32 @@ import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { ResourceBar } from "../ui/ResourceBar.js"; +import { AchievementPanel } from "./AchievementPanel.js"; +import { AchievementToast } from "./AchievementToast.js"; import { AdventurerPanel } from "./AdventurerPanel.js"; +import { BattleModal } from "./BattleModal.js"; import { BossPanel } from "./BossPanel.js"; import { ClickArea } from "./ClickArea.js"; +import { EquipmentPanel } from "./EquipmentPanel.js"; import { OfflineModal } from "./OfflineModal.js"; import { PrestigePanel } from "./PrestigePanel.js"; import { QuestPanel } from "./QuestPanel.js"; import { UpgradePanel } from "./UpgradePanel.js"; -type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "prestige"; +type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige"; const TABS: { id: Tab; label: string }[] = [ { id: "adventurers", label: "⚔️ Adventurers" }, { id: "upgrades", label: "🔧 Upgrades" }, { id: "quests", label: "📜 Quests" }, { id: "bosses", label: "👹 Bosses" }, + { id: "equipment", label: "🗡️ Equipment" }, + { id: "achievements", label: "🏆 Achievements" }, { id: "prestige", label: "⭐ Prestige" }, ]; export const GameLayout = (): React.JSX.Element => { - const { state, isLoading, error } = useGame(); + const { state, isLoading, error, battleResult, dismissBattle } = useGame(); const [activeTab, setActiveTab] = useState("adventurers"); if (isLoading) { @@ -48,6 +54,10 @@ export const GameLayout = (): React.JSX.Element => { prestigeCount={state.prestige.count} /> + + {battleResult && ( + + )}
diff --git a/apps/web/src/components/game/UpgradePanel.tsx b/apps/web/src/components/game/UpgradePanel.tsx index 4e24ea9..f2e2ac6 100644 --- a/apps/web/src/components/game/UpgradePanel.tsx +++ b/apps/web/src/components/game/UpgradePanel.tsx @@ -12,6 +12,23 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps) const canAfford = currentGold >= upgrade.costGold && currentEssence >= upgrade.costEssence; + if (!upgrade.unlocked) { + return ( +
+
+

🔒 {upgrade.name}

+

{upgrade.description}

+

×{upgrade.multiplier} multiplier

+
+
+ {upgrade.costGold > 0 && 🪙 {upgrade.costGold.toLocaleString()}} + {upgrade.costEssence > 0 && ✨ {upgrade.costEssence.toLocaleString()}} +
+ Locked +
+ ); + } + if (upgrade.purchased) { return (
@@ -49,16 +66,35 @@ export const UpgradePanel = (): React.JSX.Element => { if (!state) return

Loading...

; - const availableUpgrades = state.upgrades.filter((u) => u.unlocked); + const purchased = state.upgrades.filter((u) => u.purchased); + const available = state.upgrades.filter((u) => u.unlocked && !u.purchased); + const locked = state.upgrades.filter((u) => !u.unlocked); return (

Upgrades

- {availableUpgrades.length === 0 ? ( +

{purchased.length} / {state.upgrades.length} purchased

+ {state.upgrades.length === 0 ? (

No upgrades available yet — keep adventuring!

) : (
- {availableUpgrades.map((upgrade) => ( + {available.map((upgrade) => ( + + ))} + {purchased.map((upgrade) => ( + + ))} + {locked.map((upgrade) => ( { - if (value >= 1_000_000_000) { - return `${(value / 1_000_000_000).toFixed(2)}B`; - } - if (value >= 1_000_000) { - return `${(value / 1_000_000).toFixed(2)}M`; - } - if (value >= 1_000) { - return `${(value / 1_000).toFixed(2)}K`; - } - return value.toFixed(1); -}; - export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): React.JSX.Element => (
diff --git a/apps/web/src/context/GameContext.tsx b/apps/web/src/context/GameContext.tsx index 26428d7..2e37272 100644 --- a/apps/web/src/context/GameContext.tsx +++ b/apps/web/src/context/GameContext.tsx @@ -1,4 +1,4 @@ -import type { GameState } from "@elysium/types"; +import type { Achievement, BossChallengeResponse, GameState } from "@elysium/types"; import { createContext, useCallback, @@ -7,9 +7,14 @@ import { useRef, useState, } from "react"; -import { dealBossDamage, loadGame, saveGame } from "../api/client.js"; +import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js"; import { applyTick, calculateClickPower } from "../engine/tick.js"; +export interface BattleResult { + bossName: string; + result: BossChallengeResponse; +} + interface GameContextValue { state: GameState | null; isLoading: boolean; @@ -22,14 +27,24 @@ interface GameContextValue { buyUpgrade: (upgradeId: string) => void; /** Start a quest */ startQuest: (questId: string) => void; - /** Attack the active boss */ - attackBoss: (bossId: string) => void; + /** Challenge a boss — runs full server-side simulation */ + challengeBoss: (bossId: string) => Promise; + /** Equip an owned equipment item (auto-unequips the same slot) */ + equipItem: (equipmentId: string) => void; /** Reload state from the server */ reload: () => Promise; /** Offline gold earned on login */ offlineGold: number; /** Dismiss the offline gold notification */ dismissOfflineGold: () => void; + /** Battle result to display in the modal (null when no battle pending) */ + battleResult: BattleResult | null; + /** Dismiss the battle result modal */ + dismissBattle: () => void; + /** Queue of newly unlocked achievements (for toasts) */ + newAchievements: Achievement[]; + /** Remove an achievement from the toast queue */ + dismissAchievement: (id: string) => void; } const GameContext = createContext(null); @@ -41,9 +56,12 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [offlineGold, setOfflineGold] = useState(0); + const [battleResult, setBattleResult] = useState(null); + const [newAchievements, setNewAchievements] = useState([]); const stateRef = useRef(null); const lastSaveRef = useRef(Date.now()); const rafRef = useRef(null); + const newlyUnlockedRef = useRef([]); stateRef.current = state; @@ -79,9 +97,22 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React setState((prev) => { if (!prev) return prev; - return applyTick(prev, deltaSeconds); + const next = applyTick(prev, deltaSeconds); + + // Detect newly unlocked achievements + newlyUnlockedRef.current = next.achievements.filter((a, i) => { + const wasLocked = (prev.achievements ?? [])[i]?.unlockedAt === null; + return wasLocked && a.unlockedAt !== null; + }); + + return next; }); + if (newlyUnlockedRef.current.length > 0) { + setNewAchievements((prev) => [...prev, ...newlyUnlockedRef.current]); + newlyUnlockedRef.current = []; + } + // Auto-save every 30 seconds if (Date.now() - lastSaveRef.current >= AUTO_SAVE_INTERVAL_MS) { lastSaveRef.current = Date.now(); @@ -176,50 +207,107 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React }); }, []); - const attackBoss = useCallback(async (bossId: string) => { + const equipItem = useCallback((equipmentId: string) => { + setState((prev) => { + if (!prev) return prev; + const item = (prev.equipment ?? []).find((e) => e.id === equipmentId); + if (!item || !item.owned) return prev; + + return { + ...prev, + equipment: (prev.equipment ?? []).map((e) => { + if (e.id === equipmentId) return { ...e, equipped: true }; + // Unequip the previously-equipped item in the same slot + if (e.type === item.type && e.equipped) return { ...e, equipped: false }; + return e; + }), + }; + }); + }, []); + + const challengeBoss = useCallback(async (bossId: string) => { if (!stateRef.current) return; - const clickPower = calculateClickPower(stateRef.current); + const boss = stateRef.current.bosses.find((b) => b.id === bossId); + if (!boss) return; try { - const result = await dealBossDamage({ bossId, damage: clickPower }); + const result = await challengeBossApi({ bossId }); + // Update local state to match server result setState((prev) => { if (!prev) return prev; - return { - ...prev, - bosses: prev.bosses.map((b) => - b.id === bossId + + if (result.won) { + const bossIndex = prev.bosses.findIndex((b) => b.id === bossId); + return { + ...prev, + bosses: prev.bosses.map((b, idx) => { + if (b.id === bossId) { + return { ...b, status: "defeated" as const, currentHp: 0 }; + } + if ( + idx === bossIndex + 1 && + b.prestigeRequirement <= prev.prestige.count + ) { + return { ...b, status: "available" as const }; + } + return b; + }), + resources: result.rewards ? { - ...b, - status: result.defeated ? ("defeated" as const) : ("in_progress" as const), - currentHp: result.currentHp, - } - : b, - ), - ...(result.defeated && result.rewards - ? { - resources: { ...prev.resources, gold: prev.resources.gold + result.rewards.gold, essence: prev.resources.essence + result.rewards.essence, crystals: prev.resources.crystals + result.rewards.crystals, - }, - player: { + } + : prev.resources, + player: result.rewards + ? { ...prev.player, totalGoldEarned: prev.player.totalGoldEarned + result.rewards.gold, - }, - upgrades: prev.upgrades.map((u) => + } + : prev.player, + upgrades: result.rewards + ? prev.upgrades.map((u) => result.rewards!.upgradeIds.includes(u.id) ? { ...u, unlocked: true } : u, - ), - } - : {}), + ) + : prev.upgrades, + equipment: result.rewards + ? (prev.equipment ?? []).map((e) => { + if (!result.rewards!.equipmentIds.includes(e.id)) return e; + const slotEmpty = !(prev.equipment ?? []).some( + (other) => other.type === e.type && other.equipped, + ); + return { ...e, owned: true, equipped: slotEmpty || e.equipped }; + }) + : prev.equipment ?? [], + }; + } + + // Loss: reset boss HP and apply casualties + return { + ...prev, + bosses: prev.bosses.map((b) => + b.id === bossId + ? { ...b, status: "available" as const, currentHp: b.maxHp } + : b, + ), + adventurers: prev.adventurers.map((a) => { + const casualty = result.casualties?.find( + (c) => c.adventurerId === a.id, + ); + if (!casualty) return a; + return { ...a, count: Math.max(0, a.count - casualty.killed) }; + }), }; }); + + setBattleResult({ bossName: boss.name, result }); } catch { - // Rate limited or other error — silently ignore + // Silently ignore — server errors shouldn't crash the UI } }, []); @@ -227,6 +315,14 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React setOfflineGold(0); }, []); + const dismissBattle = useCallback(() => { + setBattleResult(null); + }, []); + + const dismissAchievement = useCallback((id: string) => { + setNewAchievements((prev) => prev.filter((a) => a.id !== id)); + }, []); + return ( {children} diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index 732dd07..3ba619b 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -1,4 +1,44 @@ -import type { GameState } from "@elysium/types"; +import type { Achievement, Equipment, GameState } from "@elysium/types"; + +/** + * Checks all achievements against the current game state and returns an updated + * achievements array, marking newly-met conditions with the current timestamp. + */ +const checkAchievements = (state: GameState): Achievement[] => { + const now = Date.now(); + return (state.achievements ?? []).map((achievement) => { + if (achievement.unlockedAt !== null) return achievement; + + const { condition } = achievement; + let met = false; + + switch (condition.type) { + case "totalGoldEarned": + met = state.player.totalGoldEarned >= condition.amount; + break; + case "totalClicks": + met = state.player.totalClicks >= condition.amount; + break; + case "bossesDefeated": + met = state.bosses.filter((b) => b.status === "defeated").length >= condition.amount; + break; + case "questsCompleted": + met = state.quests.filter((q) => q.status === "completed").length >= condition.amount; + break; + case "adventurerTotal": + met = state.adventurers.reduce((sum, a) => sum + a.count, 0) >= condition.amount; + break; + case "prestigeCount": + met = state.prestige.count >= condition.amount; + break; + case "equipmentOwned": + met = (state.equipment ?? []).filter((e) => e.owned).length >= condition.amount; + break; + } + + return met ? { ...achievement, unlockedAt: now } : achievement; + }); +}; /** * Pure function — applies one game tick to the state. @@ -6,6 +46,12 @@ import type { GameState } from "@elysium/types"; * Returns a new GameState (does not mutate the original). */ export const applyTick = (state: GameState, deltaSeconds: number): GameState => { + const equippedItems: Equipment[] = (state.equipment ?? []).filter((e) => e.equipped); + const equipmentGoldMultiplier = equippedItems.reduce( + (mult, e) => mult * (e.bonus.goldMultiplier ?? 1), + 1, + ); + let goldGained = 0; let essenceGained = 0; @@ -26,7 +72,12 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => const prestige = state.prestige.productionMultiplier; goldGained += - adventurer.goldPerSecond * adventurer.count * upgradeMultiplier * prestige * deltaSeconds; + adventurer.goldPerSecond * + adventurer.count * + upgradeMultiplier * + prestige * + equipmentGoldMultiplier * + deltaSeconds; essenceGained += adventurer.essencePerSecond * @@ -36,12 +87,16 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => deltaSeconds; } - // Complete active quests + // Complete active quests and apply their rewards const now = Date.now(); let questGold = 0; let questEssence = 0; let questCrystals = 0; + let updatedUpgrades = state.upgrades; + let updatedAdventurers = state.adventurers; + let updatedEquipment = state.equipment ?? []; + const updatedQuests = state.quests.map((quest) => { if ( quest.status !== "active" || @@ -51,7 +106,6 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => return quest; } - const completed = { ...quest, status: "completed" as const }; for (const reward of quest.rewards) { if (reward.type === "gold" && reward.amount != null) { questGold += reward.amount; @@ -59,15 +113,46 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => questEssence += reward.amount; } else if (reward.type === "crystals" && reward.amount != null) { questCrystals += reward.amount; + } else if (reward.type === "upgrade" && reward.targetId != null) { + updatedUpgrades = updatedUpgrades.map((u) => + u.id === reward.targetId ? { ...u, unlocked: true } : u, + ); + } else if (reward.type === "adventurer" && reward.targetId != null) { + updatedAdventurers = updatedAdventurers.map((a) => + a.id === reward.targetId ? { ...a, unlocked: true } : a, + ); + } else if (reward.type === "equipment" && reward.targetId != null) { + const targetId = reward.targetId; + updatedEquipment = updatedEquipment.map((e) => { + if (e.id !== targetId) return e; + const slotEmpty = !updatedEquipment.some( + (other) => other.type === e.type && other.equipped, + ); + return { ...e, owned: true, equipped: slotEmpty || e.equipped }; + }); } } - return completed; + + return { ...quest, status: "completed" as const }; + }); + + // Unlock quests whose prerequisites are now all completed + const completedIds = new Set( + updatedQuests.filter((q) => q.status === "completed").map((q) => q.id), + ); + const fullyUpdatedQuests = updatedQuests.map((quest) => { + if (quest.status !== "locked") return quest; + if (quest.prerequisiteIds.every((id) => completedIds.has(id))) { + return { ...quest, status: "available" as const }; + } + return quest; }); const newGold = state.resources.gold + goldGained + questGold; const newEssence = state.resources.essence + essenceGained + questEssence; + const newTotalGoldEarned = state.player.totalGoldEarned + goldGained + questGold; - return { + const partialState: GameState = { ...state, resources: { ...state.resources, @@ -77,20 +162,47 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState => }, player: { ...state.player, - totalGoldEarned: state.player.totalGoldEarned + goldGained + questGold, + totalGoldEarned: newTotalGoldEarned, }, - quests: updatedQuests, + quests: fullyUpdatedQuests, + upgrades: updatedUpgrades, + adventurers: updatedAdventurers, + equipment: updatedEquipment, lastTickAt: now, }; + + // Check achievements and apply crystal rewards for newly unlocked ones + const updatedAchievements = checkAchievements(partialState); + const crystalsFromAchievements = updatedAchievements.reduce((sum, a, i) => { + const wasLocked = (state.achievements ?? [])[i]?.unlockedAt === null; + const isNowUnlocked = a.unlockedAt !== null; + if (wasLocked && isNowUnlocked) { + return sum + (a.reward?.crystals ?? 0); + } + return sum; + }, 0); + + return { + ...partialState, + achievements: updatedAchievements, + resources: { + ...partialState.resources, + crystals: partialState.resources.crystals + crystalsFromAchievements, + }, + }; }; /** - * Calculates the effective click power, including upgrades. + * Calculates the effective click power, including upgrades and equipped trinkets. */ export const calculateClickPower = (state: GameState): number => { const clickMultiplier = state.upgrades .filter((u) => u.purchased && u.target === "click") .reduce((mult, upgrade) => mult * upgrade.multiplier, 1); - return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier; + const equipmentClickMultiplier = (state.equipment ?? []) + .filter((e) => e.equipped && e.bonus.clickMultiplier != null) + .reduce((mult, e) => mult * (e.bonus.clickMultiplier ?? 1), 1); + + return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier * equipmentClickMultiplier; }; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 758277e..519c4c6 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -291,6 +291,22 @@ body { opacity: 0.7; } +.upgrade-card.locked { + opacity: 0.45; +} + +.upgrade-locked-label { + color: var(--colour-text-muted); + font-size: 0.75rem; + white-space: nowrap; +} + +.upgrade-progress { + color: var(--colour-text-muted); + font-size: 0.85rem; + margin-bottom: 0.75rem; +} + .upgrade-info { flex: 1; } @@ -611,6 +627,475 @@ body { background: var(--colour-accent-light); } +/* ===================== BATTLE MODAL ===================== */ +.battle-modal { + max-width: 520px; +} + +.battle-stats { + align-items: center; + display: flex; + gap: 1rem; + justify-content: center; + margin-bottom: 1.5rem; +} + +.battle-stat { + background: var(--colour-bg); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 120px; + padding: 0.5rem 0.75rem; + text-align: center; +} + +.battle-stat .stat-label { + color: var(--colour-text-muted); + font-size: 0.75rem; + text-transform: uppercase; +} + +.battle-stat .stat-value { + color: var(--colour-accent-light); + font-size: 1.1rem; + font-weight: 700; +} + +.battle-stat-divider { + color: var(--colour-text-muted); + font-size: 0.85rem; +} + +.battle-bars { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.battle-bar-row { + align-items: center; + display: flex; + gap: 0.5rem; +} + +.battle-bar-row .bar-label { + font-size: 0.85rem; + min-width: 100px; + text-align: left; +} + +.hp-bar-container { + background: var(--colour-bg); + border: 1px solid var(--colour-border); + border-radius: 4px; + flex: 1; + height: 14px; + overflow: hidden; +} + +.hp-bar-fill { + border-radius: 4px; + height: 100%; +} + +.battle-bar-row .bar-hp { + color: var(--colour-text-muted); + font-size: 0.75rem; + min-width: 80px; + text-align: right; +} + +.vs-divider { + color: var(--colour-text-muted); + font-size: 0.85rem; + text-align: center; +} + +.battle-in-progress { + color: var(--colour-text-muted); + font-style: italic; +} + +.battle-outcome { + border-radius: var(--radius); + margin-top: 1rem; + padding: 1rem; +} + +.battle-outcome.victory { + background: rgba(39, 174, 96, 0.1); + border: 1px solid #27ae60; +} + +.battle-outcome.defeat { + background: rgba(231, 76, 60, 0.1); + border: 1px solid #e74c3c; +} + +.battle-outcome h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; +} + +.battle-rewards, +.battle-casualties { + display: flex; + flex-direction: column; + font-size: 0.9rem; + gap: 0.2rem; + margin: 0.5rem 0; +} + +.dismiss-button { + background: var(--colour-accent); + border: none; + border-radius: var(--radius); + color: #fff; + cursor: pointer; + font-size: 1rem; + font-weight: 700; + margin-top: 0.75rem; + padding: 0.5rem 2rem; + transition: background 0.15s; +} + +.dismiss-button:hover { + background: var(--colour-accent-light); +} + +/* Party combat stat bar in BossPanel */ +.party-combat-stats { + background: var(--colour-bg); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + gap: 2rem; + justify-content: center; + margin-bottom: 1.25rem; + padding: 0.75rem 1rem; +} + +.combat-stat { + display: flex; + flex-direction: column; + gap: 0.2rem; + text-align: center; +} + +.combat-stat .stat-label { + color: var(--colour-text-muted); + font-size: 0.75rem; + text-transform: uppercase; +} + +.combat-stat .stat-value { + color: var(--colour-accent-light); + font-size: 1rem; + font-weight: 700; +} + +.boss-meta { + color: var(--colour-text-muted); + font-size: 0.8rem; + margin-bottom: 0.5rem; +} + +/* ===================== CLICK FLOAT ===================== */ +@keyframes float-up { + 0% { opacity: 1; transform: translate(-50%, 0); } + 100% { opacity: 0; transform: translate(-50%, -70px); } +} + +.click-button-wrapper { + position: relative; + display: inline-block; +} + +.click-float { + animation: float-up 0.9s ease-out forwards; + color: var(--colour-gold); + font-size: 1rem; + font-weight: 700; + pointer-events: none; + position: absolute; + text-shadow: 0 1px 4px rgba(0,0,0,0.5); + user-select: none; +} + +/* ===================== EQUIPMENT ===================== */ +.equipment-intro { + color: var(--colour-text-muted); + font-size: 0.85rem; + margin-bottom: 1.25rem; +} + +.equipment-slot-section { + margin-bottom: 1.5rem; +} + +.slot-heading { + color: var(--colour-text-muted); + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; + text-transform: uppercase; +} + +.equipment-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.equipment-card { + align-items: center; + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-left: 3px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + gap: 0.75rem; + padding: 0.75rem; + transition: border-color 0.15s; +} + +.equipment-card.equipped { + border-color: var(--colour-success); + border-left-color: var(--colour-success); + box-shadow: 0 0 8px rgba(16, 185, 129, 0.15); +} + +.equipment-card.not-owned { + opacity: 0.45; +} + +/* Rarity border-left colours */ +.equipment-card.rarity-common { border-left-color: #9ca3af; } +.equipment-card.rarity-rare { border-left-color: #3b82f6; } +.equipment-card.rarity-epic { border-left-color: #a855f7; } +.equipment-card.rarity-legendary { border-left-color: #f59e0b; } + +.equipment-icon { + font-size: 1.5rem; + min-width: 2rem; + text-align: center; +} + +.equipment-info { + flex: 1; + min-width: 0; +} + +.equipment-name-row { + align-items: center; + display: flex; + gap: 0.5rem; + margin-bottom: 0.15rem; +} + +.equipment-name-row h3 { + font-size: 0.95rem; + margin: 0; +} + +.rarity-badge { + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + padding: 0.1rem 0.45rem; +} + +.rarity-badge.rarity-common { background: rgba(156, 163, 175, 0.2); color: #9ca3af; } +.rarity-badge.rarity-rare { background: rgba(59, 130, 246, 0.2); color: #60a5fa; } +.rarity-badge.rarity-epic { background: rgba(168, 85, 247, 0.2); color: #c084fc; } +.rarity-badge.rarity-legendary { background: rgba(245, 158, 11, 0.2); color: #fbbf24; } + +.equipment-description { + color: var(--colour-text-muted); + font-size: 0.8rem; + margin-bottom: 0.2rem; +} + +.equipment-bonus { + color: var(--colour-gold); + font-size: 0.8rem; + font-weight: 600; +} + +.equipment-action { + flex-shrink: 0; + text-align: right; +} + +.equipment-locked { + color: var(--colour-text-muted); + font-size: 0.8rem; +} + +.equipment-equipped-badge { + color: var(--colour-success); + font-size: 0.85rem; + font-weight: 600; +} + +.equip-button { + background: var(--colour-accent); + border: none; + border-radius: var(--radius); + color: #fff; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + padding: 0.3rem 0.8rem; + transition: background 0.15s; +} + +.equip-button:hover { + background: var(--colour-accent-light); +} + +/* ===================== ACHIEVEMENTS ===================== */ +.achievement-progress { + color: var(--colour-text-muted); + font-size: 0.85rem; + margin-bottom: 1rem; +} + +.achievement-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.achievement-card { + align-items: center; + background: var(--colour-surface); + border: 1px solid var(--colour-border); + border-radius: var(--radius); + display: flex; + gap: 0.75rem; + padding: 0.75rem; + transition: border-color 0.15s; +} + +.achievement-card.unlocked { + border-color: var(--colour-gold); + box-shadow: 0 0 8px rgba(245, 158, 11, 0.15); +} + +.achievement-card.locked { + opacity: 0.5; +} + +.achievement-icon { + font-size: 1.5rem; + min-width: 2rem; + text-align: center; +} + +.achievement-info { + flex: 1; +} + +.achievement-info h3 { + font-size: 0.95rem; + margin-bottom: 0.1rem; +} + +.achievement-info p { + color: var(--colour-text-muted); + font-size: 0.8rem; +} + +.achievement-condition { + font-style: italic; +} + +.achievement-reward { + color: var(--colour-crystal) !important; + font-weight: 600; +} + +.achievement-status { + flex-shrink: 0; +} + +.achievement-unlocked-badge { + color: var(--colour-gold); + font-size: 0.85rem; + font-weight: 700; +} + +.achievement-locked-badge { + color: var(--colour-text-muted); + font-size: 1rem; +} + +/* ===================== ACHIEVEMENT TOAST ===================== */ +@keyframes slide-in-right { + from { opacity: 0; transform: translateX(120%); } + to { opacity: 1; transform: translateX(0); } +} + +.achievement-toast-container { + bottom: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + position: fixed; + right: 1.5rem; + z-index: 200; +} + +.achievement-toast { + align-items: center; + animation: slide-in-right 0.35s ease-out; + background: var(--colour-surface); + border: 1px solid var(--colour-gold); + border-radius: var(--radius); + box-shadow: 0 4px 20px rgba(0,0,0,0.4); + cursor: pointer; + display: flex; + gap: 0.75rem; + max-width: 280px; + padding: 0.75rem 1rem; +} + +.toast-icon { + font-size: 1.5rem; + flex-shrink: 0; +} + +.toast-content { + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.toast-label { + color: var(--colour-gold); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.toast-name { + color: var(--colour-text); + font-size: 0.9rem; + font-weight: 600; +} + +.toast-reward { + color: var(--colour-crystal); + font-size: 0.8rem; +} + /* ===================== UTILITY ===================== */ .error { color: var(--colour-error); diff --git a/apps/web/src/utils/format.ts b/apps/web/src/utils/format.ts new file mode 100644 index 0000000..55c8c47 --- /dev/null +++ b/apps/web/src/utils/format.ts @@ -0,0 +1,20 @@ +/** + * Formats a number with K/M/B/T suffixes for display. + * Numbers below 1000 show one decimal place. + */ +export const formatNumber = (value: number): string => { + if (!isFinite(value) || isNaN(value)) return "0"; + if (value >= 1_000_000_000_000) { + return `${(value / 1_000_000_000_000).toFixed(2)}T`; + } + if (value >= 1_000_000_000) { + return `${(value / 1_000_000_000).toFixed(2)}B`; + } + if (value >= 1_000_000) { + return `${(value / 1_000_000).toFixed(2)}M`; + } + if (value >= 1_000) { + return `${(value / 1_000).toFixed(2)}K`; + } + return value.toFixed(1); +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1c72e64..3534afd 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,9 +1,15 @@ +export type { + Achievement, + AchievementCondition, + AchievementConditionType, + AchievementReward, +} from "./interfaces/Achievement.js"; export type { Adventurer, AdventurerClass } from "./interfaces/Adventurer.js"; export type { ApiError, AuthResponse, - BossDamageRequest, - BossDamageResponse, + BossChallengeRequest, + BossChallengeResponse, LoadResponse, PrestigeRequest, PrestigeResponse, @@ -12,6 +18,12 @@ export type { SaveResponse, } from "./interfaces/Api.js"; export type { Boss, BossStatus } from "./interfaces/Boss.js"; +export type { + Equipment, + EquipmentBonus, + EquipmentRarity, + EquipmentType, +} from "./interfaces/Equipment.js"; export type { GameState } from "./interfaces/GameState.js"; export type { Player } from "./interfaces/Player.js"; export type { PrestigeData } from "./interfaces/Prestige.js"; diff --git a/packages/types/src/interfaces/Achievement.ts b/packages/types/src/interfaces/Achievement.ts new file mode 100644 index 0000000..4eebaa6 --- /dev/null +++ b/packages/types/src/interfaces/Achievement.ts @@ -0,0 +1,28 @@ +export type AchievementConditionType = + | "totalGoldEarned" + | "totalClicks" + | "bossesDefeated" + | "questsCompleted" + | "adventurerTotal" + | "prestigeCount" + | "equipmentOwned"; + +export interface AchievementCondition { + type: AchievementConditionType; + amount: number; +} + +export interface AchievementReward { + crystals?: number; +} + +export interface Achievement { + id: string; + name: string; + description: string; + icon: string; + condition: AchievementCondition; + reward?: AchievementReward; + /** Unix timestamp when unlocked, null if not yet unlocked */ + unlockedAt: number | null; +} diff --git a/packages/types/src/interfaces/Adventurer.ts b/packages/types/src/interfaces/Adventurer.ts index 4e10401..a00b3bf 100644 --- a/packages/types/src/interfaces/Adventurer.ts +++ b/packages/types/src/interfaces/Adventurer.ts @@ -15,6 +15,8 @@ export interface Adventurer { goldPerSecond: number; /** Base essence generated per second */ essencePerSecond: number; + /** Combat power per unit — used in boss battle simulation */ + combatPower: number; count: number; unlocked: boolean; } diff --git a/packages/types/src/interfaces/Api.ts b/packages/types/src/interfaces/Api.ts index e8a100a..50f4e2a 100644 --- a/packages/types/src/interfaces/Api.ts +++ b/packages/types/src/interfaces/Api.ts @@ -23,20 +23,37 @@ export interface LoadResponse { offlineSeconds: number; } -export interface BossDamageRequest { +export interface BossChallengeRequest { bossId: string; - damage: number; } -export interface BossDamageResponse { - currentHp: number; - defeated: boolean; +export interface BossChallengeResponse { + won: boolean; + partyDPS: number; + bossDPS: number; + /** Boss HP immediately before the battle */ + bossHpBefore: number; + /** Boss maximum HP */ + bossMaxHp: number; + /** Boss HP at end of battle before any state reset (0 on win) */ + bossHpAtBattleEnd: number; + /** Boss HP stored in game after the result (0 on win, maxHp on loss) */ + bossNewHp: number; + /** Total party HP at start of battle */ + partyMaxHp: number; + /** Party HP remaining after battle (0 on loss) */ + partyHpRemaining: number; rewards?: { gold: number; essence: number; crystals: number; upgradeIds: string[]; + equipmentIds: string[]; }; + casualties?: Array<{ + adventurerId: string; + killed: number; + }>; } export interface PrestigeRequest { diff --git a/packages/types/src/interfaces/Boss.ts b/packages/types/src/interfaces/Boss.ts index 0cb375f..f7b4973 100644 --- a/packages/types/src/interfaces/Boss.ts +++ b/packages/types/src/interfaces/Boss.ts @@ -17,6 +17,8 @@ export interface Boss { crystalReward: number; /** IDs of upgrades unlocked on defeat */ upgradeRewards: string[]; + /** IDs of equipment items granted on defeat */ + equipmentRewards: string[]; /** Minimum prestige level required to access this boss */ prestigeRequirement: number; } diff --git a/packages/types/src/interfaces/Equipment.ts b/packages/types/src/interfaces/Equipment.ts new file mode 100644 index 0000000..6bddf32 --- /dev/null +++ b/packages/types/src/interfaces/Equipment.ts @@ -0,0 +1,25 @@ +export type EquipmentType = "weapon" | "armour" | "trinket"; + +export type EquipmentRarity = "common" | "rare" | "epic" | "legendary"; + +export interface EquipmentBonus { + /** Multiplier applied to all gold/s income (e.g. 1.1 = +10%) */ + goldMultiplier?: number; + /** Multiplier applied to all combat power (e.g. 1.25 = +25%) */ + combatMultiplier?: number; + /** Multiplier applied to click power (e.g. 1.5 = +50%) */ + clickMultiplier?: number; +} + +export interface Equipment { + id: string; + name: string; + description: string; + type: EquipmentType; + rarity: EquipmentRarity; + bonus: EquipmentBonus; + /** Whether the player has acquired this item */ + owned: boolean; + /** Whether this item is currently equipped (only one per type can be equipped) */ + equipped: boolean; +} diff --git a/packages/types/src/interfaces/GameState.ts b/packages/types/src/interfaces/GameState.ts index c93dcac..7ecee90 100644 --- a/packages/types/src/interfaces/GameState.ts +++ b/packages/types/src/interfaces/GameState.ts @@ -1,5 +1,7 @@ +import type { Achievement } from "./Achievement.js"; import type { Adventurer } from "./Adventurer.js"; import type { Boss } from "./Boss.js"; +import type { Equipment } from "./Equipment.js"; import type { Player } from "./Player.js"; import type { PrestigeData } from "./Prestige.js"; import type { Quest } from "./Quest.js"; @@ -13,6 +15,8 @@ export interface GameState { upgrades: Upgrade[]; quests: Quest[]; bosses: Boss[]; + equipment: Equipment[]; + achievements: Achievement[]; prestige: PrestigeData; /** Click power (gold per click, before upgrades) */ baseClickPower: number; diff --git a/packages/types/src/interfaces/Quest.ts b/packages/types/src/interfaces/Quest.ts index efae46a..cad3e29 100644 --- a/packages/types/src/interfaces/Quest.ts +++ b/packages/types/src/interfaces/Quest.ts @@ -1,6 +1,6 @@ export type QuestStatus = "locked" | "available" | "active" | "completed"; -export type QuestRewardType = "gold" | "essence" | "crystals" | "upgrade" | "adventurer"; +export type QuestRewardType = "gold" | "essence" | "crystals" | "upgrade" | "adventurer" | "equipment"; export interface QuestReward { type: QuestRewardType; diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index d3b0572..2671502 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -2,7 +2,8 @@ "extends": "@nhcarrigan/typescript-config", "compilerOptions": { "outDir": "./prod", - "rootDir": "." + "rootDir": ".", + "declaration": true }, "exclude": ["test/**/*.ts"] } -- 2.52.0 From 897eba5f641526d989acb6121fc2fb6cb869d6bc Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 13:42:40 -0800 Subject: [PATCH 03/84] feat: add zone system to bosses and quests --- apps/api/src/data/bosses.ts | 4 ++ apps/api/src/data/initialState.ts | 2 + apps/api/src/data/quests.ts | 9 +++ apps/api/src/data/zones.ts | 31 +++++++++ apps/api/src/routes/boss.ts | 7 ++ apps/api/src/routes/game.ts | 35 ++++++++++ apps/web/src/components/game/BossPanel.tsx | 20 +++++- apps/web/src/components/game/QuestPanel.tsx | 18 ++++- apps/web/src/components/game/ZoneSelector.tsx | 32 +++++++++ apps/web/src/styles.css | 67 +++++++++++++++++++ packages/types/src/index.ts | 1 + packages/types/src/interfaces/Boss.ts | 2 + packages/types/src/interfaces/GameState.ts | 2 + packages/types/src/interfaces/Quest.ts | 2 + packages/types/src/interfaces/Zone.ts | 11 +++ 15 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 apps/api/src/data/zones.ts create mode 100644 apps/web/src/components/game/ZoneSelector.tsx create mode 100644 packages/types/src/interfaces/Zone.ts diff --git a/apps/api/src/data/bosses.ts b/apps/api/src/data/bosses.ts index 21144eb..a5a2989 100644 --- a/apps/api/src/data/bosses.ts +++ b/apps/api/src/data/bosses.ts @@ -16,6 +16,7 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: ["click_2"], equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, + zoneId: "verdant_vale", }, { id: "lich_queen", @@ -32,6 +33,7 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: ["global_2"], equipmentRewards: ["enchanted_blade", "plate_armour", "arcane_orb"], prestigeRequirement: 0, + zoneId: "verdant_vale", }, { id: "elder_dragon", @@ -48,6 +50,7 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: ["click_3"], equipmentRewards: ["vorpal_sword", "dragon_scale"], prestigeRequirement: 1, + zoneId: "shattered_ruins", }, { id: "void_titan", @@ -64,5 +67,6 @@ export const DEFAULT_BOSSES: Boss[] = [ upgradeRewards: [], equipmentRewards: ["philosophers_stone"], prestigeRequirement: 3, + zoneId: "frozen_peaks", }, ]; diff --git a/apps/api/src/data/initialState.ts b/apps/api/src/data/initialState.ts index bc7a35a..73fa901 100644 --- a/apps/api/src/data/initialState.ts +++ b/apps/api/src/data/initialState.ts @@ -5,6 +5,7 @@ import { DEFAULT_BOSSES } from "./bosses.js"; import { DEFAULT_EQUIPMENT } from "./equipment.js"; import { DEFAULT_QUESTS } from "./quests.js"; import { DEFAULT_UPGRADES } from "./upgrades.js"; +import { DEFAULT_ZONES } from "./zones.js"; export const INITIAL_PRESTIGE: PrestigeData = { count: 0, @@ -33,6 +34,7 @@ export const INITIAL_GAME_STATE = (player: Player, characterName: string): GameS equipment: structuredClone(DEFAULT_EQUIPMENT), achievements: structuredClone(DEFAULT_ACHIEVEMENTS), prestige: INITIAL_PRESTIGE, + zones: structuredClone(DEFAULT_ZONES), baseClickPower: 1, lastTickAt: Date.now(), }); diff --git a/apps/api/src/data/quests.ts b/apps/api/src/data/quests.ts index d3a31c3..2f0706d 100644 --- a/apps/api/src/data/quests.ts +++ b/apps/api/src/data/quests.ts @@ -9,6 +9,7 @@ export const DEFAULT_QUESTS: Quest[] = [ durationSeconds: 60, rewards: [{ type: "gold", amount: 500 }], prerequisiteIds: [], + zoneId: "verdant_vale", }, { id: "goblin_camp", @@ -21,6 +22,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "essence", amount: 5 }, ], prerequisiteIds: ["first_steps"], + zoneId: "verdant_vale", }, { id: "haunted_mine", @@ -33,6 +35,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "global_1" }, ], prerequisiteIds: ["goblin_camp"], + zoneId: "verdant_vale", }, { id: "necromancer_tower", @@ -47,6 +50,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "cleric_1" }, ], prerequisiteIds: ["haunted_mine"], + zoneId: "verdant_vale", }, { id: "ancient_ruins", @@ -59,6 +63,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "click_2" }, ], prerequisiteIds: ["haunted_mine"], + zoneId: "verdant_vale", }, { id: "shadow_mere", @@ -72,6 +77,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "scout_1" }, ], prerequisiteIds: ["ancient_ruins"], + zoneId: "shattered_ruins", }, { id: "dragon_lair", @@ -86,6 +92,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "adventurer", targetId: "dragon_rider" }, ], prerequisiteIds: ["ancient_ruins"], + zoneId: "shattered_ruins", }, { id: "frozen_wastes", @@ -100,6 +107,7 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "global_3" }, ], prerequisiteIds: ["dragon_lair"], + zoneId: "frozen_peaks", }, { id: "void_rift", @@ -114,5 +122,6 @@ export const DEFAULT_QUESTS: Quest[] = [ { type: "upgrade", targetId: "knight_1" }, ], prerequisiteIds: ["frozen_wastes"], + zoneId: "frozen_peaks", }, ]; diff --git a/apps/api/src/data/zones.ts b/apps/api/src/data/zones.ts new file mode 100644 index 0000000..343b386 --- /dev/null +++ b/apps/api/src/data/zones.ts @@ -0,0 +1,31 @@ +import type { Zone } from "@elysium/types"; + +export const DEFAULT_ZONES: Zone[] = [ + { + id: "verdant_vale", + name: "The Verdant Vale", + description: + "Rolling green hills and ancient forests stretch to the horizon. This is where your guild takes its first steps — trade roads in need of clearing, goblin camps to rout, and an undead queen stirring in the north.", + emoji: "🌿", + status: "unlocked", + unlockBossId: null, + }, + { + id: "shattered_ruins", + name: "The Shattered Ruins", + description: + "The remnants of a civilisation long lost to war and dragonfire. Crumbling towers and cursed lakes hide treasures — and an elder dragon who claims these lands as his own.", + emoji: "🏛️", + status: "locked", + unlockBossId: "lich_queen", + }, + { + id: "frozen_peaks", + name: "The Frozen Peaks", + description: + "At the edge of the world, where the sun barely rises and the cold is a living thing, a tear in reality has drawn something ancient and terrible. Only the mightiest guilds dare tread here.", + emoji: "❄️", + status: "locked", + unlockBossId: "elder_dragon", + }, +]; diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index 683f4e8..26dfab7 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -152,6 +152,13 @@ bossRouter.post("/challenge", async (context) => { nextBoss.status = "available"; } + // Unlock any zone whose unlock condition is this boss + for (const zone of (state.zones ?? [])) { + if (zone.unlockBossId === body.bossId) { + zone.status = "unlocked"; + } + } + rewards = { gold: boss.goldReward, essence: boss.essenceReward, diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index d23e4e9..5708f2e 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -72,6 +72,8 @@ gameRouter.get("/load", async (context) => { // Backfill new quests and upgrades from defaults (add missing ones) const { DEFAULT_QUESTS } = await import("../data/quests.js"); const { DEFAULT_UPGRADES } = await import("../data/upgrades.js"); + const { DEFAULT_ZONES } = await import("../data/zones.js"); + const { DEFAULT_BOSSES } = await import("../data/bosses.js"); for (const defaultQuest of DEFAULT_QUESTS) { if (!state.quests.some((q) => q.id === defaultQuest.id)) { @@ -80,6 +82,15 @@ gameRouter.get("/load", async (context) => { } } + // Backfill zoneId on quests that predate the field + for (const quest of state.quests) { + if (!quest.zoneId) { + const defaults = DEFAULT_QUESTS.find((d) => d.id === quest.id); + quest.zoneId = defaults?.zoneId ?? "verdant_vale"; + needsBackfill = true; + } + } + for (const defaultUpgrade of DEFAULT_UPGRADES) { if (!state.upgrades.some((u) => u.id === defaultUpgrade.id)) { state.upgrades.push(structuredClone(defaultUpgrade)); @@ -87,6 +98,30 @@ gameRouter.get("/load", async (context) => { } } + // Backfill zones on saves that predate the feature + if (!Array.isArray(state.zones) || state.zones.length === 0) { + state.zones = structuredClone(DEFAULT_ZONES); + // Infer unlock state from defeated bosses + for (const zone of state.zones) { + if (zone.unlockBossId != null) { + const unlockBoss = state.bosses.find((b) => b.id === zone.unlockBossId); + if (unlockBoss?.status === "defeated") { + zone.status = "unlocked"; + } + } + } + needsBackfill = true; + } + + // Backfill zoneId on bosses that predate the field + for (const boss of state.bosses) { + if (!boss.zoneId) { + const defaults = DEFAULT_BOSSES.find((d) => d.id === boss.id); + boss.zoneId = defaults?.zoneId ?? "verdant_vale"; + needsBackfill = true; + } + } + const now = Date.now(); const { offlineGold, offlineSeconds } = calculateOfflineGold(state, now); diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx index 7149314..0bf2910 100644 --- a/apps/web/src/components/game/BossPanel.tsx +++ b/apps/web/src/components/game/BossPanel.tsx @@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { formatNumber } from "../../utils/format.js"; +import { ZoneSelector } from "./ZoneSelector.js"; interface BossCardProps { boss: Boss; @@ -17,7 +18,7 @@ const BossCard = ({ isChallenging, }: BossCardProps): React.JSX.Element => { const hpPercent = (boss.currentHp / boss.maxHp) * 100; - const isLocked = boss.prestigeRequirement > prestigeCount; + const isPrestigeLocked = boss.prestigeRequirement > prestigeCount; const canChallenge = (boss.status === "available" || boss.status === "in_progress") && !isChallenging; @@ -26,7 +27,7 @@ const BossCard = ({

{boss.name}

{boss.description}

- {isLocked && boss.status === "locked" && ( + {isPrestigeLocked && boss.status === "locked" && (

🔒 Requires Prestige {boss.prestigeRequirement}

@@ -87,6 +88,7 @@ const BossCard = ({ export const BossPanel = (): React.JSX.Element => { const { state, challengeBoss } = useGame(); const [challengingBossId, setChallengingBossId] = useState(null); + const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); if (!state) return

Loading...

; @@ -135,10 +137,19 @@ export const BossPanel = (): React.JSX.Element => { } }; + const zones = state.zones ?? []; + const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId); + return (

Boss Encounters

+ +
⚔️ Party DPS @@ -151,7 +162,7 @@ export const BossPanel = (): React.JSX.Element => {
- {state.bosses.map((boss) => ( + {zoneBosses.map((boss) => ( { }} /> ))} + {zoneBosses.length === 0 && ( +

No bosses in this zone yet.

+ )}
); diff --git a/apps/web/src/components/game/QuestPanel.tsx b/apps/web/src/components/game/QuestPanel.tsx index e127b03..39c503d 100644 --- a/apps/web/src/components/game/QuestPanel.tsx +++ b/apps/web/src/components/game/QuestPanel.tsx @@ -1,5 +1,7 @@ import type { Quest } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { ZoneSelector } from "./ZoneSelector.js"; const formatDuration = (seconds: number): string => { if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; @@ -62,16 +64,30 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => { export const QuestPanel = (): React.JSX.Element => { const { state } = useGame(); + const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); if (!state) return

Loading...

; + const zones = state.zones ?? []; + const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId); + return (

Quests

+ + +
- {state.quests.map((quest) => ( + {zoneQuests.map((quest) => ( ))} + {zoneQuests.length === 0 && ( +

No quests in this zone yet.

+ )}
); diff --git a/apps/web/src/components/game/ZoneSelector.tsx b/apps/web/src/components/game/ZoneSelector.tsx new file mode 100644 index 0000000..69ceeed --- /dev/null +++ b/apps/web/src/components/game/ZoneSelector.tsx @@ -0,0 +1,32 @@ +import type { Zone } from "@elysium/types"; + +interface ZoneSelectorProps { + zones: Zone[]; + activeZoneId: string; + onSelectZone: (zoneId: string) => void; +} + +export const ZoneSelector = ({ + zones, + activeZoneId, + onSelectZone, +}: ZoneSelectorProps): React.JSX.Element => ( +
+ {zones.map((zone) => ( + + ))} +
+); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 519c4c6..fe9f23f 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1107,6 +1107,73 @@ body { font-size: 0.85rem; } +/* ── Zone Selector ─────────────────────────────────────────────────────── */ + +.zone-selector { + display: flex; + gap: 0.5rem; + margin-bottom: 1.25rem; + overflow-x: auto; + padding-bottom: 0.25rem; +} + +.zone-tab { + align-items: center; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(147, 51, 234, 0.3); + border-radius: 0.5rem; + color: var(--colour-text-muted); + cursor: pointer; + display: flex; + flex-direction: column; + flex-shrink: 0; + font-family: inherit; + font-size: 0.8rem; + gap: 0.2rem; + padding: 0.6rem 1rem; + transition: all 0.2s; +} + +.zone-tab:hover:not(:disabled) { + background: rgba(147, 51, 234, 0.15); + border-color: rgba(147, 51, 234, 0.6); + color: var(--colour-text); +} + +.zone-tab-active { + background: rgba(147, 51, 234, 0.25); + border-color: var(--colour-primary); + color: var(--colour-text); +} + +.zone-tab-locked { + cursor: not-allowed; + opacity: 0.45; +} + +.zone-emoji { + font-size: 1.4rem; +} + +.zone-name { + font-size: 0.75rem; + font-weight: 600; + text-align: center; +} + +.zone-lock { + font-size: 0.7rem; +} + +.empty-zone { + color: var(--colour-text-muted); + font-style: italic; + padding: 1rem 0; + text-align: center; +} + +/* ── Loading / Error screens ───────────────────────────────────────────── */ + .loading-screen, .error-screen { align-items: center; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3534afd..f918072 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -38,3 +38,4 @@ export type { Upgrade, UpgradeTarget, } from "./interfaces/Upgrade.js"; +export type { Zone, ZoneStatus } from "./interfaces/Zone.js"; diff --git a/packages/types/src/interfaces/Boss.ts b/packages/types/src/interfaces/Boss.ts index f7b4973..f670fc0 100644 --- a/packages/types/src/interfaces/Boss.ts +++ b/packages/types/src/interfaces/Boss.ts @@ -21,4 +21,6 @@ export interface Boss { equipmentRewards: string[]; /** Minimum prestige level required to access this boss */ prestigeRequirement: number; + /** Zone this boss belongs to */ + zoneId: string; } diff --git a/packages/types/src/interfaces/GameState.ts b/packages/types/src/interfaces/GameState.ts index 7ecee90..5274ba6 100644 --- a/packages/types/src/interfaces/GameState.ts +++ b/packages/types/src/interfaces/GameState.ts @@ -7,6 +7,7 @@ import type { PrestigeData } from "./Prestige.js"; import type { Quest } from "./Quest.js"; import type { Resource } from "./Resource.js"; import type { Upgrade } from "./Upgrade.js"; +import type { Zone } from "./Zone.js"; export interface GameState { player: Player; @@ -18,6 +19,7 @@ export interface GameState { equipment: Equipment[]; achievements: Achievement[]; prestige: PrestigeData; + zones: Zone[]; /** Click power (gold per click, before upgrades) */ baseClickPower: number; /** Unix timestamp of the last client-side tick */ diff --git a/packages/types/src/interfaces/Quest.ts b/packages/types/src/interfaces/Quest.ts index cad3e29..fa47120 100644 --- a/packages/types/src/interfaces/Quest.ts +++ b/packages/types/src/interfaces/Quest.ts @@ -21,4 +21,6 @@ export interface Quest { rewards: QuestReward[]; /** IDs of quests that must be completed before this one unlocks */ prerequisiteIds: string[]; + /** Zone this quest belongs to */ + zoneId: string; } diff --git a/packages/types/src/interfaces/Zone.ts b/packages/types/src/interfaces/Zone.ts new file mode 100644 index 0000000..f93b5d2 --- /dev/null +++ b/packages/types/src/interfaces/Zone.ts @@ -0,0 +1,11 @@ +export type ZoneStatus = "locked" | "unlocked"; + +export interface Zone { + id: string; + name: string; + description: string; + emoji: string; + status: ZoneStatus; + /** Boss ID whose defeat unlocks this zone (null for the starter zone) */ + unlockBossId: string | null; +} -- 2.52.0 From f73417696553cc4407dc811e0875f6538e96e63d Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 13:46:28 -0800 Subject: [PATCH 04/84] feat: add show/hide locked toggle to all panels --- .../src/components/game/AchievementPanel.tsx | 20 +++++++-- apps/web/src/components/game/BossPanel.tsx | 21 ++++++++-- .../src/components/game/EquipmentPanel.tsx | 20 ++++++++- apps/web/src/components/game/QuestPanel.tsx | 21 ++++++++-- apps/web/src/components/game/UpgradePanel.tsx | 14 ++++++- apps/web/src/components/ui/LockToggle.tsx | 20 +++++++++ apps/web/src/styles.css | 41 +++++++++++++++++++ 7 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/components/ui/LockToggle.tsx diff --git a/apps/web/src/components/game/AchievementPanel.tsx b/apps/web/src/components/game/AchievementPanel.tsx index 9f808ff..4b21fcf 100644 --- a/apps/web/src/components/game/AchievementPanel.tsx +++ b/apps/web/src/components/game/AchievementPanel.tsx @@ -1,6 +1,8 @@ import type { Achievement } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { formatNumber } from "../../utils/format.js"; +import { LockToggle } from "../ui/LockToggle.js"; const conditionDescription = (achievement: Achievement): string => { const { condition } = achievement; @@ -53,20 +55,30 @@ const AchievementCard = ({ achievement }: AchievementCardProps): React.JSX.Eleme export const AchievementPanel = (): React.JSX.Element => { const { state } = useGame(); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; const achievements = state.achievements ?? []; - const unlocked = achievements.filter((a) => a.unlockedAt !== null).length; + const unlocked = achievements.filter((a) => a.unlockedAt !== null); + const locked = achievements.filter((a) => a.unlockedAt === null); + const visible = showLocked ? achievements : unlocked; return (
-

Achievements

+
+

Achievements

+ { setShowLocked((v) => !v); }} + /> +

- {unlocked} / {achievements.length} unlocked + {unlocked.length} / {achievements.length} unlocked

- {achievements.map((achievement) => ( + {visible.map((achievement) => ( ))}
diff --git a/apps/web/src/components/game/BossPanel.tsx b/apps/web/src/components/game/BossPanel.tsx index 0bf2910..8a35996 100644 --- a/apps/web/src/components/game/BossPanel.tsx +++ b/apps/web/src/components/game/BossPanel.tsx @@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; import { formatNumber } from "../../utils/format.js"; +import { LockToggle } from "../ui/LockToggle.js"; import { ZoneSelector } from "./ZoneSelector.js"; interface BossCardProps { @@ -89,6 +90,7 @@ export const BossPanel = (): React.JSX.Element => { const { state, challengeBoss } = useGame(); const [challengingBossId, setChallengingBossId] = useState(null); const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; @@ -139,10 +141,21 @@ export const BossPanel = (): React.JSX.Element => { const zones = state.zones ?? []; const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId); + const lockedCount = zoneBosses.filter((b) => b.status === "locked").length; + const visibleBosses = showLocked + ? zoneBosses + : zoneBosses.filter((b) => b.status !== "locked"); return (
-

Boss Encounters

+
+

Boss Encounters

+ { setShowLocked((v) => !v); }} + /> +
{
- {zoneBosses.map((boss) => ( + {visibleBosses.map((boss) => ( { }} /> ))} - {zoneBosses.length === 0 && ( -

No bosses in this zone yet.

+ {visibleBosses.length === 0 && ( +

No bosses to show in this zone.

)}
diff --git a/apps/web/src/components/game/EquipmentPanel.tsx b/apps/web/src/components/game/EquipmentPanel.tsx index 3807914..134c1f0 100644 --- a/apps/web/src/components/game/EquipmentPanel.tsx +++ b/apps/web/src/components/game/EquipmentPanel.tsx @@ -1,5 +1,7 @@ import type { Equipment, EquipmentType } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { LockToggle } from "../ui/LockToggle.js"; const RARITY_LABEL: Record = { common: "Common", @@ -72,20 +74,31 @@ const SLOT_LABEL: Record = { export const EquipmentPanel = (): React.JSX.Element => { const { state } = useGame(); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; const equipment = state.equipment ?? []; + const unownedCount = equipment.filter((e) => !e.owned).length; return (
-

Equipment

+
+

Equipment

+ { setShowLocked((v) => !v); }} + /> +

Equipment drops from bosses and grants passive bonuses. Only one item per slot can be equipped at a time.

{SLOT_ORDER.map((slotType) => { - const items = equipment.filter((e) => e.type === slotType); + const items = equipment.filter( + (e) => e.type === slotType && (showLocked || e.owned), + ); return (

{SLOT_LABEL[slotType]}

@@ -93,6 +106,9 @@ export const EquipmentPanel = (): React.JSX.Element => { {items.map((item) => ( ))} + {items.length === 0 && ( +

No items to show in this slot.

+ )}
); diff --git a/apps/web/src/components/game/QuestPanel.tsx b/apps/web/src/components/game/QuestPanel.tsx index 39c503d..00e73ec 100644 --- a/apps/web/src/components/game/QuestPanel.tsx +++ b/apps/web/src/components/game/QuestPanel.tsx @@ -1,6 +1,7 @@ import type { Quest } from "@elysium/types"; import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { LockToggle } from "../ui/LockToggle.js"; import { ZoneSelector } from "./ZoneSelector.js"; const formatDuration = (seconds: number): string => { @@ -65,15 +66,27 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => { export const QuestPanel = (): React.JSX.Element => { const { state } = useGame(); const [activeZoneId, setActiveZoneId] = useState("verdant_vale"); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; const zones = state.zones ?? []; const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId); + const lockedCount = zoneQuests.filter((q) => q.status === "locked").length; + const visibleQuests = showLocked + ? zoneQuests + : zoneQuests.filter((q) => q.status !== "locked"); return (
-

Quests

+
+

Quests

+ { setShowLocked((v) => !v); }} + /> +
{ />
- {zoneQuests.map((quest) => ( + {visibleQuests.map((quest) => ( ))} - {zoneQuests.length === 0 && ( -

No quests in this zone yet.

+ {visibleQuests.length === 0 && ( +

No quests to show in this zone.

)}
diff --git a/apps/web/src/components/game/UpgradePanel.tsx b/apps/web/src/components/game/UpgradePanel.tsx index f2e2ac6..3759ce3 100644 --- a/apps/web/src/components/game/UpgradePanel.tsx +++ b/apps/web/src/components/game/UpgradePanel.tsx @@ -1,5 +1,7 @@ import type { Upgrade } from "@elysium/types"; +import { useState } from "react"; import { useGame } from "../../context/GameContext.js"; +import { LockToggle } from "../ui/LockToggle.js"; interface UpgradeCardProps { upgrade: Upgrade; @@ -63,6 +65,7 @@ const UpgradeCard = ({ upgrade, currentGold, currentEssence }: UpgradeCardProps) export const UpgradePanel = (): React.JSX.Element => { const { state } = useGame(); + const [showLocked, setShowLocked] = useState(true); if (!state) return

Loading...

; @@ -72,7 +75,14 @@ export const UpgradePanel = (): React.JSX.Element => { return (
-

Upgrades

+
+

Upgrades

+ { setShowLocked((v) => !v); }} + /> +

{purchased.length} / {state.upgrades.length} purchased

{state.upgrades.length === 0 ? (

No upgrades available yet — keep adventuring!

@@ -94,7 +104,7 @@ export const UpgradePanel = (): React.JSX.Element => { currentEssence={state.resources.essence} /> ))} - {locked.map((upgrade) => ( + {showLocked && locked.map((upgrade) => ( void; + lockedCount: number; +} + +export const LockToggle = ({ + showLocked, + onToggle, + lockedCount, +}: LockToggleProps): React.JSX.Element => ( + +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index fe9f23f..8f9cd4b 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1107,6 +1107,47 @@ body { font-size: 0.85rem; } +/* ── Panel Header (title + lock toggle row) ────────────────────────────── */ + +.panel-header { + align-items: center; + display: flex; + gap: 1rem; + justify-content: space-between; + margin-bottom: 0.25rem; +} + +.panel-header h2 { + margin: 0; +} + +.lock-toggle { + align-items: center; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(147, 51, 234, 0.3); + border-radius: 1rem; + color: var(--colour-text-muted); + cursor: pointer; + display: flex; + font-family: inherit; + font-size: 0.75rem; + gap: 0.3rem; + padding: 0.3rem 0.75rem; + transition: all 0.2s; + white-space: nowrap; +} + +.lock-toggle:hover { + background: rgba(147, 51, 234, 0.15); + border-color: rgba(147, 51, 234, 0.6); + color: var(--colour-text); +} + +.lock-toggle-on { + border-color: rgba(147, 51, 234, 0.5); + color: var(--colour-text); +} + /* ── Zone Selector ─────────────────────────────────────────────────────── */ .zone-selector { -- 2.52.0 From 32c13f73c4b643ce1469c0d86f16b59bdf1e7fbe Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 13:49:14 -0800 Subject: [PATCH 05/84] feat: add public player profile page --- apps/web/src/App.tsx | 11 ++ apps/web/src/components/game/GameLayout.tsx | 3 + apps/web/src/components/game/ProfilePage.tsx | 115 ++++++++++++ apps/web/src/components/ui/ResourceBar.tsx | 12 +- apps/web/src/styles.css | 173 +++++++++++++++++++ 5 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/game/ProfilePage.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index a0694f1..4ae8727 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,6 +2,12 @@ import { useState } from "react"; import { GameProvider } from "./context/GameContext.js"; import { GameLayout } from "./components/game/GameLayout.js"; import { LoginPage } from "./components/game/LoginPage.js"; +import { ProfilePage } from "./components/game/ProfilePage.js"; + +const getProfileDiscordId = (): string | null => { + const match = /^\/profile\/(\d+)$/.exec(window.location.pathname); + return match?.[1] ?? null; +}; const handleAuthCallback = (): boolean => { if (window.location.pathname !== "/auth/callback") { @@ -27,6 +33,11 @@ const isAuthenticated = (): boolean => { export const App = (): React.JSX.Element => { const [loggedIn, setLoggedIn] = useState(isAuthenticated); + const profileDiscordId = getProfileDiscordId(); + if (profileDiscordId) { + return ; + } + if (!loggedIn) { return { setLoggedIn(true); }} />; } diff --git a/apps/web/src/components/game/GameLayout.tsx b/apps/web/src/components/game/GameLayout.tsx index 85a43f3..91bf0a8 100644 --- a/apps/web/src/components/game/GameLayout.tsx +++ b/apps/web/src/components/game/GameLayout.tsx @@ -47,11 +47,14 @@ export const GameLayout = (): React.JSX.Element => { if (!state) return

Loading...

; + const profileUrl = `/profile/${state.player.discordId}`; + return (
diff --git a/apps/web/src/components/game/ProfilePage.tsx b/apps/web/src/components/game/ProfilePage.tsx new file mode 100644 index 0000000..67cb820 --- /dev/null +++ b/apps/web/src/components/game/ProfilePage.tsx @@ -0,0 +1,115 @@ +import type { PublicProfileResponse } from "@elysium/types"; +import { useEffect, useState } from "react"; +import { formatNumber } from "../../utils/format.js"; + +interface ProfilePageProps { + discordId: string; +} + +export const ProfilePage = ({ discordId }: ProfilePageProps): React.JSX.Element => { + const [profile, setProfile] = useState(null); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + useEffect(() => { + fetch(`/api/profile/${discordId}`) + .then(async (res) => { + if (!res.ok) throw new Error("Player not found"); + return res.json() as Promise; + }) + .then(setProfile) + .catch((err: unknown) => { + setError(err instanceof Error ? err.message : "Failed to load profile"); + }); + }, [discordId]); + + const handleCopy = (): void => { + void navigator.clipboard.writeText(window.location.href).then(() => { + setCopied(true); + setTimeout(() => { setCopied(false); }, 2000); + }); + }; + + if (error) { + return ( +
+
+

⚠️ {error}

+ ← Play Elysium +
+
+ ); + } + + if (!profile) { + return ( +
+
Loading profile…
+
+ ); + } + + const avatarUrl = profile.avatar + ? `https://cdn.discordapp.com/avatars/${discordId}/${profile.avatar}.png?size=128` + : `https://cdn.discordapp.com/embed/avatars/${parseInt(discordId, 10) % 5}.png`; + + const memberSince = new Date(profile.createdAt).toLocaleDateString("en-GB", { + year: "numeric", + month: "long", + day: "numeric", + }); + + return ( +
+
+
+ {`${profile.username}'s +
+

{profile.characterName}

+

@{profile.username}

+ {profile.prestigeCount > 0 && ( + + ⭐ Prestige {profile.prestigeCount} + + )} +
+
+ +
+
+ 🪙 + {formatNumber(profile.totalGoldEarned)} + Total Gold Earned +
+
+ 👆 + {formatNumber(profile.totalClicks)} + Total Clicks +
+
+ 📅 + {memberSince} + Guild Founded +
+
+ +
+ + + ⚔️ Play Elysium + +
+
+
+ ); +}; diff --git a/apps/web/src/components/ui/ResourceBar.tsx b/apps/web/src/components/ui/ResourceBar.tsx index 93ea415..ebaf800 100644 --- a/apps/web/src/components/ui/ResourceBar.tsx +++ b/apps/web/src/components/ui/ResourceBar.tsx @@ -4,9 +4,10 @@ import { formatNumber } from "../../utils/format.js"; interface ResourceBarProps { resources: Resource; prestigeCount: number; + profileUrl: string; } -export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): React.JSX.Element => ( +export const ResourceBar = ({ resources, prestigeCount, profileUrl }: ResourceBarProps): React.JSX.Element => (
🪙 @@ -33,5 +34,14 @@ export const ResourceBar = ({ resources, prestigeCount }: ResourceBarProps): Rea ⭐ Prestige {prestigeCount}
)} + + 👤 Profile +
); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 8f9cd4b..14698f1 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1107,6 +1107,179 @@ body { font-size: 0.85rem; } +/* ── Profile link button in ResourceBar ────────────────────────────────── */ + +.profile-link-button { + align-items: center; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(147, 51, 234, 0.4); + border-radius: 1rem; + color: var(--colour-text-muted); + display: flex; + font-size: 0.8rem; + gap: 0.3rem; + margin-left: auto; + padding: 0.3rem 0.8rem; + text-decoration: none; + transition: all 0.2s; + white-space: nowrap; +} + +.profile-link-button:hover { + background: rgba(147, 51, 234, 0.2); + border-color: var(--colour-primary); + color: var(--colour-text); +} + +/* ── Public Profile Page ────────────────────────────────────────────────── */ + +.profile-page { + align-items: center; + background: var(--colour-bg); + display: flex; + justify-content: center; + min-height: 100vh; + padding: 2rem 1rem; +} + +.profile-card { + background: var(--colour-surface); + border: 1px solid rgba(147, 51, 234, 0.3); + border-radius: 1rem; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + max-width: 520px; + padding: 2rem; + width: 100%; +} + +.profile-header { + align-items: center; + display: flex; + gap: 1.5rem; + margin-bottom: 1.75rem; +} + +.profile-avatar { + border: 3px solid var(--colour-primary); + border-radius: 50%; + height: 96px; + width: 96px; +} + +.profile-identity { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.profile-character-name { + font-size: 1.6rem; + font-weight: 700; + margin: 0; +} + +.profile-username { + color: var(--colour-text-muted); + font-size: 0.9rem; + margin: 0; +} + +.profile-prestige-badge { + background: rgba(255, 215, 0, 0.15); + border: 1px solid rgba(255, 215, 0, 0.4); + border-radius: 1rem; + color: gold; + font-size: 0.8rem; + padding: 0.2rem 0.6rem; + width: fit-content; +} + +.profile-stats { + display: grid; + gap: 1rem; + grid-template-columns: repeat(3, 1fr); + margin-bottom: 1.75rem; +} + +.profile-stat { + align-items: center; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(147, 51, 234, 0.2); + border-radius: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.75rem 0.5rem; + text-align: center; +} + +.profile-stat-icon { + font-size: 1.4rem; +} + +.profile-stat-value { + font-size: 1rem; + font-weight: 700; +} + +.profile-stat-date { + font-size: 0.72rem; +} + +.profile-stat-label { + color: var(--colour-text-muted); + font-size: 0.7rem; +} + +.profile-actions { + display: flex; + gap: 0.75rem; +} + +.profile-share-button { + background: rgba(147, 51, 234, 0.2); + border: 1px solid var(--colour-primary); + border-radius: 0.5rem; + color: var(--colour-text); + cursor: pointer; + flex: 1; + font-family: inherit; + font-size: 0.9rem; + padding: 0.65rem 1rem; + transition: background 0.2s; +} + +.profile-share-button:hover { + background: rgba(147, 51, 234, 0.35); +} + +.profile-play-link { + background: var(--colour-primary); + border-radius: 0.5rem; + color: #fff; + flex: 1; + font-size: 0.9rem; + font-weight: 600; + padding: 0.65rem 1rem; + text-align: center; + text-decoration: none; + transition: opacity 0.2s; +} + +.profile-play-link:hover { + opacity: 0.85; +} + +.profile-loading, +.profile-error { + color: var(--colour-text-muted); + display: flex; + flex-direction: column; + font-size: 1rem; + gap: 1rem; + text-align: center; +} + /* ── Panel Header (title + lock toggle row) ────────────────────────────── */ .panel-header { -- 2.52.0 From 7e04daa07356fc530e4839f30c78c5f6defedd7d Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 13:56:12 -0800 Subject: [PATCH 06/84] feat: add profile editing (bio, display name, stat visibility) --- apps/api/prisma/schema.prisma | 2 + apps/api/src/routes/profile.ts | 57 ++++- apps/web/src/api/client.ts | 10 + .../src/components/game/EditProfileModal.tsx | 138 +++++++++++ apps/web/src/components/game/GameLayout.tsx | 6 + apps/web/src/components/game/ProfilePage.tsx | 57 +++-- apps/web/src/components/ui/ResourceBar.tsx | 36 ++- apps/web/src/styles.css | 214 +++++++++++++++++- packages/types/src/index.ts | 4 + packages/types/src/interfaces/Api.ts | 18 ++ .../types/src/interfaces/ProfileSettings.ts | 13 ++ 11 files changed, 525 insertions(+), 30 deletions(-) create mode 100644 apps/web/src/components/game/EditProfileModal.tsx create mode 100644 packages/types/src/interfaces/ProfileSettings.ts diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index c68de71..d739288 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -14,6 +14,8 @@ model Player { discriminator String avatar String? characterName String @default("") + bio String @default("") + profileSettings Json? createdAt Float lastSavedAt Float totalGoldEarned Float @default(0) diff --git a/apps/api/src/routes/profile.ts b/apps/api/src/routes/profile.ts index 0734a6e..bb73944 100644 --- a/apps/api/src/routes/profile.ts +++ b/apps/api/src/routes/profile.ts @@ -1,9 +1,32 @@ -import type { GameState } from "@elysium/types"; +import type { + GameState, + ProfileSettings, + UpdateProfileRequest, +} from "@elysium/types"; import { Hono } from "hono"; import { prisma } from "../db/client.js"; +import { DEFAULT_PROFILE_SETTINGS } from "@elysium/types"; +import { authMiddleware } from "../middleware/auth.js"; export const profileRouter = new Hono(); +const parseProfileSettings = (raw: unknown): ProfileSettings => { + if ( + raw !== null && + typeof raw === "object" && + !Array.isArray(raw) + ) { + const obj = raw as Record; + return { + showTotalGold: obj.showTotalGold !== false, + showTotalClicks: obj.showTotalClicks !== false, + showPrestige: obj.showPrestige !== false, + showGuildFounded: obj.showGuildFounded !== false, + }; + } + return { ...DEFAULT_PROFILE_SETTINGS }; +}; + profileRouter.get("/:discordId", async (context) => { const { discordId } = context.req.param(); @@ -18,14 +41,46 @@ profileRouter.get("/:discordId", async (context) => { const state = gameStateRecord?.state as unknown as GameState | undefined; const prestigeCount = state?.prestige.count ?? 0; + const profileSettings = parseProfileSettings(player.profileSettings); return context.json({ characterName: player.characterName, username: player.username, avatar: player.avatar ?? null, + bio: player.bio ?? "", + profileSettings, prestigeCount, totalGoldEarned: player.totalGoldEarned, totalClicks: player.totalClicks, createdAt: player.createdAt, }); }); + +profileRouter.put("/", authMiddleware, async (context) => { + const discordId = context.get("discordId") as string; + const body = await context.req.json(); + + const characterName = (body.characterName ?? "").trim().slice(0, 32); + const bio = (body.bio ?? "").trim().slice(0, 200); + const profileSettings: ProfileSettings = { + showTotalGold: body.profileSettings?.showTotalGold !== false, + showTotalClicks: body.profileSettings?.showTotalClicks !== false, + showPrestige: body.profileSettings?.showPrestige !== false, + showGuildFounded: body.profileSettings?.showGuildFounded !== false, + }; + + if (!characterName) { + return context.json({ error: "Character name cannot be empty" }, 400); + } + + const updated = await prisma.player.update({ + where: { discordId }, + data: { characterName, bio, profileSettings }, + }); + + return context.json({ + characterName: updated.characterName, + bio: updated.bio, + profileSettings, + }); +}); diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 9676d26..0ab8cee 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -8,6 +8,8 @@ import type { PublicProfileResponse, SaveRequest, SaveResponse, + UpdateProfileRequest, + UpdateProfileResponse, } from "@elysium/types"; const BASE_URL = "/api"; @@ -79,3 +81,11 @@ export const getPublicProfile = async ( discordId: string, ): Promise => request(`/profile/${discordId}`); + +export const updateProfile = async ( + body: UpdateProfileRequest, +): Promise => + request("/profile", { + method: "PUT", + body: JSON.stringify(body), + }); diff --git a/apps/web/src/components/game/EditProfileModal.tsx b/apps/web/src/components/game/EditProfileModal.tsx new file mode 100644 index 0000000..5c61d21 --- /dev/null +++ b/apps/web/src/components/game/EditProfileModal.tsx @@ -0,0 +1,138 @@ +import type { ProfileSettings } from "@elysium/types"; +import { useState } from "react"; +import { updateProfile } from "../../api/client.js"; +import { useGame } from "../../context/GameContext.js"; + +interface EditProfileModalProps { + onClose: () => void; +} + +const STAT_TOGGLES: { key: keyof ProfileSettings; label: string; icon: string }[] = [ + { key: "showTotalGold", label: "Total Gold Earned", icon: "🪙" }, + { key: "showTotalClicks", label: "Total Clicks", icon: "👆" }, + { key: "showPrestige", label: "Prestige Level", icon: "⭐" }, + { key: "showGuildFounded", label: "Guild Founded Date", icon: "📅" }, +]; + +export const EditProfileModal = ({ onClose }: EditProfileModalProps): React.JSX.Element => { + const { state } = useGame(); + const player = state?.player; + + const [characterName, setCharacterName] = useState(player?.characterName ?? ""); + const [bio, setBio] = useState(""); + const [settings, setSettings] = useState({ + showTotalGold: true, + showTotalClicks: true, + showPrestige: true, + showGuildFounded: true, + }); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [saved, setSaved] = useState(false); + + const handleSave = async (): Promise => { + setSaving(true); + setError(null); + try { + await updateProfile({ characterName, bio, profileSettings: settings }); + setSaved(true); + setTimeout(onClose, 900); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "Failed to save"); + } finally { + setSaving(false); + } + }; + + const toggleSetting = (key: keyof ProfileSettings): void => { + setSettings((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + return ( +
+
+
+

Edit Profile

+ +
+ +
+ + { setCharacterName(e.target.value); }} + /> + {characterName.length} / 32 + + +