diff --git a/.gitignore b/.gitignore index 28462a74..c223b48e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ Thumbs.db node_modules .sass-cache -.idea \ No newline at end of file +.idea + +dist +build \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..4ad3e6a4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +old + +# Ignore artifacts: +build +coverage + +pnpm-lock.yaml \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..c088126d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,73 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview +Astriarch is a space strategy game built as a TypeScript monorepo using pnpm workspaces. The game features real-time multiplayer gameplay with a Node.js backend and multiple frontend implementations. + +## Architecture +The project follows a monorepo structure with clear separation of concerns: + +- **packages/astriarch-engine**: Core game engine with business logic, game state management, and turn-based mechanics. Provides TypeScript types and game controllers used across all applications. +- **apps/astriarch-backend**: Express.js server with WebSocket support, MongoDB integration, and session management. Handles multiplayer game state and real-time communication. +- **apps/astriarch-frontend-sveltekit**: Primary SvelteKit frontend with component library, game canvas using Konva, and real-time multiplayer UI. +- **packages/astriarch-ui**: Shared React component library using Chakra UI for consistent styling across frontends. + +The engine exports game models, controllers, and messaging types that are consumed by both frontend and backend applications. The backend maintains authoritative game state while clients receive filtered views through the ClientGameModel. + +## Development Commands + +### Root Level (run from project root) +- `pnpm build` - Build all packages and apps +- `pnpm test` - Run tests across all workspaces +- `pnpm lint` - Lint all packages +- `pnpm format` - Format code across all packages +- `docker-compose up -d mongodb` - Start local MongoDB for development + +### Backend Development +- `pnpm --filter astriarch-backend dev` - Start backend in development mode with hot reload +- `pnpm --filter astriarch-backend build` - Build backend TypeScript to JavaScript +- `pnpm --filter astriarch-backend test` - Run backend tests +- `pnpm --filter astriarch-backend lint` - Lint backend code +- `pnpm --filter astriarch-backend type-check` - Run TypeScript type checking + +### SvelteKit Frontend Development +- `pnpm --filter astriarch-frontend-sveltekit dev` - Start SvelteKit dev server +- `pnpm --filter astriarch-frontend-sveltekit build` - Build SvelteKit app +- `pnpm --filter astriarch-frontend-sveltekit test` - Run Vitest unit tests +- `pnpm --filter astriarch-frontend-sveltekit lint` - Lint and format check +- `pnpm --filter astriarch-frontend-sveltekit storybook` - Start Storybook component development + +### Engine Package Development +- `pnpm --filter astriarch-engine build` - Build engine package +- `pnpm --filter astriarch-engine test` - Run engine unit tests +- `pnpm --filter astriarch-engine lint` - Lint engine code + +## Key Technical Details + +### Game State Management +- Game state is managed centrally in the astriarch-engine package +- Backend maintains authoritative GameModel, clients receive filtered ClientGameModel +- Real-time updates flow through WebSocket connections managed by the backend +- Game timing: MS_PER_TICK (200ms) for client updates, MS_PER_CYCLE (30s) for game turns + +### Database +- MongoDB is used for persistent game state and session management +- Start local MongoDB with `docker-compose up -d mongodb` for development +- Connection configuration in backend's config/ directory + +### WebSocket Communication +- Real-time multiplayer uses WebSocket connections through the backend's WebSocketServer +- Message types are defined in packages/astriarch-engine/src/messaging/MessageTypes +- Frontend websocket service handles connection management and message routing + +### Testing +- Backend uses Jest for unit testing +- SvelteKit frontend uses Vitest for component and unit testing +- Engine package has comprehensive Jest test suite +- Run individual package tests with `pnpm --filter test` + +## Current Branch Status +- Working on v2.x-svelte branch (SvelteKit migration) +- Main branch for PRs: master +- Recent development focuses on WebSocket multiplayer implementation and SvelteKit frontend \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d7f97335 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM node:18-alpine AS build + +WORKDIR /usr/src/app + +# Install pnpm globally +RUN npm install -g pnpm + +# Copy source code +COPY . . + +RUN pnpm install --frozen-lockfile + +RUN pnpm run --filter=astriarch-backend... build + +RUN pnpm deploy --filter=astriarch-backend --prod /prod/astriarch-backend + +FROM node:18-alpine AS backend + +COPY --from=build /prod/astriarch-backend /prod/astriarch-backend +WORKDIR /prod/astriarch-backend + +# Expose port +EXPOSE 8001 + +# Start the application +CMD ["pnpm", "start"] diff --git a/README.md b/README.md index 13f85a1e..dd2ed563 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,22 @@ -Astriarch -========= +# Astriarch - Ruler of the stars -Astriarch Ruler of the Stars, Space Strategy Game -http://www.astriarch.com/ +## Quickstart -This is the Open Source Multiplayer version of Astriarch. +`docker-compose up -d mongodb` -It uses NodeJS and MongoDB on the server and leverages WebSockets for communication to the client -Running the Server -================== -You'll need to install Node.js 0.10+ and MongoDB -Run `npm install` then `npm start` and then visit `http://localhost:8000` -Or the easiest way to get started is to use [Docker](https://www.docker.com/): -create a file `./config/local.json`: +https://blog.nrwl.io/setup-a-monorepo-with-pnpm-workspaces-and-speed-it-up-with-nx-bc5d97258a7e + ``` - { - "mongodb":{ - "host":"astriarch_mongo" - } - } +npx nx run-many --target=build --all ``` -Run `docker-compose build` then `docker-compose up` and then visit `http://localhost:8000` - -Overview -======== -Astriarch - Ruler of the Stars is a Turn-based, Single Player Space Strategy Game. -Build planetary improvements and star ships to capture planets and defeat your enemies. -Your ultimate goal is to become the master of the known universe, and earn the title of Astriarch! - -Background -========== -Developed in 2010 by Mastered Software, Astriarch combines aspects of other classic space strategy games such as Master of Orion 2 (MOO2), Stellar Conflict (1987 Amiga), and Star Control. -

-Currently Astriarch is realeased as a free casual web game.  Planned future enhancements include the ability to research and develop improvements, as well as galaxy special items and events. -

-The name Astriarch comes from the Ancient Greek words for star (�stron) and ruler (arkhos) -Other Versions -============== -Single Player Open Source Versions: +``` +pnpm --filter astriarch-frontend dev +``` -HTML5: https://github.com/mpalmerlee/Astriarch/tree/HTML5 +https://chakra-ui.com/getting-started/remix-guide -Silverlight: https://github.com/mpalmerlee/Astriarch/tree/Silverlight -Credits -======= -Astriarch - Ruler of the Stars, space strategy game designed and developed by Mastered Software, music by Resonant. Astriarch HTML 5/JavaScript version uses Stratiscape JavaScript Canvas Library and the jQuery Listbox plugin. -License -======= -This version of Astriarch - Ruler of the Stars is released under the MIT License. diff --git a/apps/astriarch-backend/.env.example b/apps/astriarch-backend/.env.example new file mode 100644 index 00000000..0c83242f --- /dev/null +++ b/apps/astriarch-backend/.env.example @@ -0,0 +1,20 @@ +# MongoDB Configuration +MONGODB_CONNECTION_STRING=mongodb://astriarch:astriarch_password@localhost:27017/astriarch_v2 +MONGODB_HOST=localhost +MONGODB_PORT=27017 +MONGODB_USERNAME=astriarch +MONGODB_PASSWORD=astriarch_password +MONGODB_DATABASE=astriarch_v2 + +# Server Configuration +NODE_ENV=development +HOST=localhost +PORT=8001 +WS_PORT=8001 +WS_PROTOCOL=ws + +# Session Configuration +SESSION_SECRET=astriarch-v2-development-secret + +# CORS Configuration +CORS_ORIGIN=http://localhost:5173,http://localhost:3000 diff --git a/apps/astriarch-backend/config/default.json b/apps/astriarch-backend/config/default.json new file mode 100644 index 00000000..3da2cd21 --- /dev/null +++ b/apps/astriarch-backend/config/default.json @@ -0,0 +1,34 @@ +{ + "loglevel": "INFO", + "mongodb": { + "gamedb_name": "astriarch_v2", + "sessiondb_name": "astriarch_v2", + "host": "127.0.0.1", + "port": "27017", + "username": null, + "password": null + }, + "cookie": { + "secret": "astriarch-v2-node-secret" + }, + "server": { + "port": 8001, + "host": "localhost" + }, + "websocket": { + "port": 8001, + "protocol": "ws", + "ping_frequency": 30000 + }, + "game": { + "client_timeout": 120000, + "cleanup_old_games": { + "enabled": true, + "check_interval_seconds": 7200 + } + }, + "cors": { + "origin": ["http://localhost:5173", "http://localhost:3000"], + "credentials": true + } +} diff --git a/apps/astriarch-backend/config/development.json b/apps/astriarch-backend/config/development.json new file mode 100644 index 00000000..2dd0285a --- /dev/null +++ b/apps/astriarch-backend/config/development.json @@ -0,0 +1,10 @@ +{ + "loglevel": "DEBUG", + "mongodb": { + "gamedb_name": "astriarch_v2_dev", + "sessiondb_name": "astriarch_v2_dev" + }, + "server": { + "port": 8001 + } +} diff --git a/apps/astriarch-backend/eslint.config.js b/apps/astriarch-backend/eslint.config.js new file mode 100644 index 00000000..0efbc9a6 --- /dev/null +++ b/apps/astriarch-backend/eslint.config.js @@ -0,0 +1,17 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + '@typescript-eslint/recommended', + ], + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + rules: { + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, +}; diff --git a/apps/astriarch-backend/package.json b/apps/astriarch-backend/package.json new file mode 100644 index 00000000..cb11aab9 --- /dev/null +++ b/apps/astriarch-backend/package.json @@ -0,0 +1,56 @@ +{ + "name": "astriarch-backend", + "version": "2.0.0", + "description": "Astriarch game server backend with TypeScript, Express, MongoDB, and WebSockets", + "main": "dist/app.js", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/app.ts", + "start": "node --enable-source-maps dist/app.js", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"", + "lint": "eslint src/**/*.ts", + "type-check": "tsc --noEmit", + "test": "jest", + "test:watch": "jest --watch" + }, + "dependencies": { + "astriarch-engine": "workspace:*", + "async": "^3.2.4", + "config": "^3.3.9", + "connect-mongo": "^5.1.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-session": "^1.17.3", + "helmet": "^7.1.0", + "mongoose": "^8.0.3", + "morgan": "^1.10.0", + "uuid": "^11.1.0", + "ws": "^8.14.2" + }, + "devDependencies": { + "@types/async": "^3.2.24", + "@types/config": "^3.3.3", + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-session": "^1.17.10", + "@types/morgan": "^1.9.9", + "@types/node": "^20.10.4", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.9", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", + "@types/jest": "^29.5.8", + "eslint": "^8.55.0", + "jest": "^29.7.0", + "prettier": "^3.6.2", + "ts-jest": "^29.1.1", + "tsx": "^4.6.2", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/apps/astriarch-backend/scripts/cleanup-sessions.js b/apps/astriarch-backend/scripts/cleanup-sessions.js new file mode 100644 index 00000000..0df1ce4a --- /dev/null +++ b/apps/astriarch-backend/scripts/cleanup-sessions.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +// Simple script to clean up corrupted sessions from MongoDB +const mongoose = require('mongoose'); + +async function cleanupSessions() { + const mongoUrl = 'mongodb://127.0.0.1:27017/astriarch_v2_dev'; + + console.log('Connecting to MongoDB...'); + + try { + await mongoose.connect(mongoUrl); + const db = mongoose.connection.db; + + // Clean up Express sessions (connect-mongo) + const expressSessionsCollection = db.collection('sessions'); + console.log('Cleaning up Express sessions...'); + + const result1 = await expressSessionsCollection.deleteMany({ sessionId: null }); + console.log(`Deleted ${result1.deletedCount} Express sessions with null sessionId`); + + const result2 = await expressSessionsCollection.deleteMany({ sessionId: { $exists: false } }); + console.log(`Deleted ${result2.deletedCount} Express sessions with undefined sessionId`); + + const result3 = await expressSessionsCollection.deleteMany({ sessionId: "" }); + console.log(`Deleted ${result3.deletedCount} Express sessions with empty sessionId`); + + const result4 = await expressSessionsCollection.deleteMany({ expires: { $exists: false } }); + console.log(`Deleted ${result4.deletedCount} Express sessions with undefined expires (old format)`); + + const result5 = await expressSessionsCollection.deleteMany({ expires: null }); + console.log(`Deleted ${result5.deletedCount} Express sessions with null expires`); + + const remainingExpress = await expressSessionsCollection.countDocuments(); + console.log(`Remaining Express sessions: ${remainingExpress}`); + + // Clean up game sessions + const gameSessionsCollection = db.collection('game_sessions'); + console.log('Cleaning up game sessions...'); + + const gameResult1 = await gameSessionsCollection.deleteMany({ sessionId: null }); + console.log(`Deleted ${gameResult1.deletedCount} game sessions with null sessionId`); + + const gameResult2 = await gameSessionsCollection.deleteMany({ sessionId: { $exists: false } }); + console.log(`Deleted ${gameResult2.deletedCount} game sessions with undefined sessionId`); + + const remainingGame = await gameSessionsCollection.countDocuments(); + console.log(`Remaining game sessions: ${remainingGame}`); + + // List remaining sessions for inspection if any + if (remainingExpress > 0) { + const sessions = await expressSessionsCollection.find({}).limit(5).toArray(); + console.log('Sample Express sessions:', sessions.map(s => ({ + _id: s._id, + sessionId: s.sessionId, + expires: s.expires + }))); + } + + if (remainingGame > 0) { + const gameSessions = await gameSessionsCollection.find({}).limit(5).toArray(); + console.log('Sample game sessions:', gameSessions.map(s => ({ + _id: s._id, + sessionId: s.sessionId, + playerName: s.playerName + }))); + } + + } catch (error) { + console.error('Error cleaning up sessions:', error); + } finally { + await mongoose.disconnect(); + console.log('Done.'); + } +} + +cleanupSessions(); diff --git a/apps/astriarch-backend/src/app.ts b/apps/astriarch-backend/src/app.ts new file mode 100644 index 00000000..bc91e9ae --- /dev/null +++ b/apps/astriarch-backend/src/app.ts @@ -0,0 +1,134 @@ +import express from "express"; +import http from "http"; +import cors from "cors"; +import helmet from "helmet"; +import morgan from "morgan"; +import cookieParser from "cookie-parser"; +import session from "express-session"; +import MongoStore from "connect-mongo"; +import config from "config"; + +import { connectDatabase } from "./database/connection"; +import { WebSocketServer } from "./websocket"; +import { healthRoutes } from "./routes/healthRoutes"; +import { GameController } from "./controllers/GameControllerWebSocket"; +import { logger } from "./utils/logger"; + +const app = express(); + +// Server configuration +const serverConfig = { + host: process.env.HOST || config.get("server.host") || "localhost", + port: process.env.PORT || config.get("server.port") || 8001, + wsPort: process.env.WS_PORT || config.get("websocket.port") || 8001, + wsProtocol: process.env.WS_PROTOCOL || config.get("websocket.protocol") || "ws", +}; + +// Security middleware +app.use( + helmet({ + contentSecurityPolicy: false, // Allow WebSocket connections + }), +); + +// CORS configuration +const corsOptions = config.get("cors") as cors.CorsOptions; +app.use(cors(corsOptions)); + +// Logging +app.use(morgan("combined")); + +// Body parsing middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +// Cookie parsing +const cookieSecret = config.get("cookie.secret") as string; +app.use(cookieParser(cookieSecret)); + +// Session configuration +app.use( + session({ + secret: cookieSecret, + store: MongoStore.create({ + mongoUrl: + process.env.MONGODB_CONNECTION_STRING || + `mongodb://${config.get("mongodb.host")}:${config.get("mongodb.port")}/${config.get("mongodb.sessiondb_name")}`, + touchAfter: 24 * 3600, // lazy session update + }), + resave: false, + saveUninitialized: true, // Changed to true to force session creation + cookie: { + maxAge: 6 * 30 * 24 * 60 * 60 * 1000, // 6 months + secure: process.env.NODE_ENV === "production", + httpOnly: true, // Add httpOnly for security + }, + }), +); + +// Routes - Only health check needed, everything else goes through WebSocket +app.use("/api/health", healthRoutes); + +// 404 handler +app.use("*", (req, res) => { + res.status(404).json({ error: "Not found" }); +}); + +// Error handler +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.error("Express error:", err); + res.status(500).json({ + error: process.env.NODE_ENV === "production" ? "Internal server error" : err.message, + }); +}); + +// Start server +async function startServer() { + try { + // Connect to database + await connectDatabase(); + + // Create HTTP server + const server = http.createServer(app); + + // Setup WebSocket server + const wss = new WebSocketServer(server); + + // Initialize game controller cleanup tasks + GameController.startGameCleanup(); + + // Start listening + server.listen(serverConfig.port, () => { + logger.info(`Astriarch Backend Server listening at: ${serverConfig.host}:${serverConfig.port}`); + logger.info( + `WebSocket server running on: ${serverConfig.wsProtocol}://${serverConfig.host}:${serverConfig.wsPort}`, + ); + }); + + // Graceful shutdown + process.on("SIGTERM", () => { + logger.info("SIGTERM received, shutting down gracefully"); + server.close(() => { + logger.info("Process terminated"); + process.exit(0); + }); + }); + } catch (error) { + logger.error("Failed to start server:", error); + process.exit(1); + } +} + +// Handle uncaught exceptions +process.on("uncaughtException", (error) => { + logger.error("Uncaught Exception:", error); + process.exit(1); +}); + +process.on("unhandledRejection", (reason, promise) => { + logger.error("Unhandled Rejection at:", promise, "reason:", reason); + process.exit(1); +}); + +// Start the server +startServer(); diff --git a/apps/astriarch-backend/src/controllers/GameControllerWebSocket.ts b/apps/astriarch-backend/src/controllers/GameControllerWebSocket.ts new file mode 100644 index 00000000..5fb79826 --- /dev/null +++ b/apps/astriarch-backend/src/controllers/GameControllerWebSocket.ts @@ -0,0 +1,1185 @@ +import config from "config"; +import { ServerGameModel, IGame, IPlayer } from "../models/Game"; +import { SessionModel } from "../models/Session"; +import { logger } from "../utils/logger"; +import { persistGame } from "../database/DocumentPersistence"; +import * as engine from "astriarch-engine"; +import { getPlayerId } from "../utils/player-id-helper"; +import { + GameModel, + Fleet, + StarShipType, + ResearchType, + TradingCenter, + TradeType, + TradingCenterResourceType, +} from "astriarch-engine"; + +export interface GameSettings { + maxPlayers?: number; + gameType?: string; + isPrivate?: boolean; +} + +export interface CreateGameData { + name: string; + players: Array<{ + name: string; + sessionId: string; + position: number; + }>; +} + +export interface JoinGameData { + gameId: string; + sessionId: string; + playerName: string; +} + +export interface StartGameData { + sessionId: string; + gameId: string; + gameOptions?: any; +} + +export interface ResumeGameData { + sessionId: string; + gameId: string; +} + +export interface GameResult { + success: boolean; + error?: string; + errorType?: string; + game?: IGame; + playerPosition?: number; + playerId?: string; + gameData?: any; + player?: IPlayer; + // End turn specific properties + allPlayersFinished?: boolean; + endOfTurnMessages?: any[]; + destroyedClientPlayers?: any[]; +} + +/** + * GameController that matches the old app.js architecture + * This provides WebSocket-compatible methods for game management + */ +export class GameController { + // ========================================== + // Game Management Methods (like old app.js) + // ========================================== + + /** + * List games available in the lobby + * Same logic as old game: find any game that is not started, or the current player is already in + */ + static async listLobbyGames(options: { sessionId: string }): Promise { + try { + // Find any game that is not started, or the current player is already in + // This matches the old game_controller.js query exactly + const query = { + $or: [ + { status: "waiting_for_players" }, // Not started games (anyone can join) + { + $and: [ + { status: "in_progress" }, // Started but not ended + { "players.sessionId": options.sessionId }, // Current player is in this game + { "players.destroyed": { $ne: true } }, // Player is not destroyed + ], + }, + ], + }; + + const games = await ServerGameModel.find(query).sort({ lastActivity: -1 }).limit(20); + + // Transform to match old app.js format - clean up docs, don't send sessionId to client + return games.map((game) => this.getGameSummaryFromGameDoc(game)); + } catch (error) { + logger.error("Error listing lobby games:", error); + throw error; + } + } + + /** + * Create a new game (WebSocket version) + */ + static async createGame(gameData: CreateGameData): Promise { + try { + const gameOptions = { + systemsToGenerate: 4, + planetsPerSystem: 4, + galaxySize: engine.GalaxySizeOption.SMALL, + distributePlanetsEvenly: true, + quickStart: false, + gameSpeed: engine.GameSpeed.NORMAL, + version: "2.0", + }; + const playerId = getPlayerId(gameData.players[0].position); + const gameModel = engine.createGame(playerId, gameData.players[0].name, gameOptions); + + const defaultGameOptions = engine.getDefaultServerGameOptions({}); + + // Create database record matching old app.js structure + const game = new ServerGameModel({ + name: gameData.name, + hostPlayerName: gameData.players[0]?.name || "Unknown", + players: gameData.players.map((p) => ({ + name: p.name, + sessionId: p.sessionId, + position: p.position, + Id: `player_${p.position}`, // Generate player ID + isActive: true, + isAI: false, + })), + gameOptions: defaultGameOptions, + gameState: gameModel.modelData, + status: "waiting_for_players", + createdAt: new Date(), + lastActivity: new Date(), + }); + + await persistGame(game); + logger.info(`Game "${gameData.name}" created by ${gameData.players[0]?.name}:`, game._id); + return game; + } catch (error) { + logger.error("Error creating game:", error); + throw error; + } + } + + /** + * Join an existing game (WebSocket version) + */ + static async joinGame(data: JoinGameData): Promise { + try { + const game = await ServerGameModel.findById(data.gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Check if player already in game + const existingPlayer = game.players?.find((p) => p.sessionId === data.sessionId); + if (existingPlayer) { + return { + success: true, + game, + playerPosition: existingPlayer.position, + playerId: existingPlayer.Id, + }; + } + + // Check if game is full + if (game.players && game.players.length >= (game.gameOptions?.systemsToGenerate || 4)) { + return { success: false, error: "Game is full" }; + } + + // Add player to game + const playerPosition = game.players ? game.players.length : 0; + const playerId = getPlayerId(playerPosition); + + const newPlayer = { + name: data.playerName, + sessionId: data.sessionId, + position: playerPosition, + Id: playerId, + isActive: true, + isAI: false, + }; + + if (!game.players) { + game.players = []; + } + game.players.push(newPlayer); + game.lastActivity = new Date(); + + await persistGame(game); + + logger.info(`${data.playerName} joined game ${data.gameId} as player ${playerPosition}`); + return { + success: true, + game, + playerPosition, + playerId, + }; + } catch (error) { + logger.error("Error joining game:", error); + return { success: false, error: "Failed to join game" }; + } + } + + /** + * Start a game (WebSocket version) + */ + static async startGame(data: StartGameData): Promise { + try { + // Find game by gameId from the payload + const game = await ServerGameModel.findById(data.gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Verify the session is associated with this game + const player = game.players?.find((p) => p.sessionId === data.sessionId); + if (!player) { + return { success: false, error: "Player not found in this game" }; + } + + // Add computer players based on opponentOptions + const gameOptions = game.gameOptions; + const allPlayers = [...(game.players || [])]; // Start with existing human players + + if (gameOptions?.opponentOptions) { + let computerNumber = 1; + + for (let i = 0; i < gameOptions.opponentOptions.length; i++) { + const opponentOption = gameOptions.opponentOptions[i]; + const playerType = opponentOption.type; + + if (playerType > 0) { + // Computer player (1=Easy, 2=Normal, 3=Hard, 4=Expert) + const computerPlayer = { + name: `Computer ${computerNumber++}`, + sessionId: undefined, // Computer players don't have sessions + position: i + 1, // Positions start from 1 (host is 0) + Id: getPlayerId(i + 1), + isActive: true, + isAI: true, + }; + allPlayers.push(computerPlayer); + } + // playerType === 0 means Human player (should already be in the game) + // playerType === -1 means Open slot (no player added) + // playerType === -2 means Closed slot (no player added) + } + } + + // Validate we have enough players to start + if (allPlayers.length < 2) { + return { success: false, error: "Not enough players to start game" }; + } + + // Create the engine players array + const enginePlayers = []; + for (const dbPlayer of allPlayers) { + if (dbPlayer.isAI) { + // Create computer player with appropriate AI level + const aiLevel = gameOptions?.opponentOptions?.[dbPlayer.position - 1]?.type || 2; // Default to Normal + enginePlayers.push( + engine.Player.constructPlayer( + dbPlayer.Id, + aiLevel, // AI difficulty level maps to PlayerType enum + dbPlayer.name, + engine.playerColors[dbPlayer.position] || engine.playerColors[0], + ), + ); + } else { + // Create human player + enginePlayers.push( + engine.Player.constructPlayer( + dbPlayer.Id, + engine.PlayerType.Human, + dbPlayer.name, + engine.playerColors[dbPlayer.position] || engine.playerColors[0], + ), + ); + } + } + + // Create the game model with all players + const gameModel = engine.GameModel.constructData(enginePlayers, { + systemsToGenerate: gameOptions?.systemsToGenerate || 4, + planetsPerSystem: gameOptions?.planetsPerSystem || 4, + galaxySize: gameOptions?.galaxySize || engine.GalaxySizeOption.SMALL, + distributePlanetsEvenly: gameOptions?.distributePlanetsEvenly ?? true, + quickStart: gameOptions?.quickStart ?? false, + gameSpeed: engine.GameSpeed.NORMAL, + version: "2.0", + }); + + // Update the game with the complete player list and game state + game.players = allPlayers; + game.gameState = gameModel.modelData; + game.status = "in_progress"; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info( + `Game ${game._id} started with ${allPlayers.length} players (${allPlayers.filter((p) => !p.isAI).length} human, ${allPlayers.filter((p) => p.isAI).length} computer)`, + ); + return { + success: true, + game, + }; + } catch (error) { + logger.error("Error starting game:", error); + return { success: false, error: "Failed to start game" }; + } + } + + /** + * Resume a game (WebSocket version) + */ + static async resumeGame(data: ResumeGameData): Promise { + try { + const game = await ServerGameModel.findById(data.gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + const player = game.players?.find((p) => p.sessionId === data.sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + return { + success: true, + gameData: game.gameState, + player, + }; + } catch (error) { + logger.error("Error resuming game:", error); + return { success: false, error: "Failed to resume game" }; + } + } + + // ========================================== + // Game Action Methods (to be implemented) + // ========================================== + + static async updateGameOptions(data: { + sessionId: string; + gameId: string; + gameOptions: any; + playerName?: string; + }): Promise { + try { + const { sessionId, gameId, gameOptions, playerName } = data; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Check if the player is the host (first player) + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Only the host (position 0) can change game options + if (player.position !== 0) { + return { success: false, error: "Only the game host can change options" }; + } + + // Update game options + game.gameOptions = { + ...game.gameOptions, + ...gameOptions, + }; + + // Update game name if provided + if (gameOptions.name) { + game.name = gameOptions.name; + } + + // Update main player name if provided + if (playerName && player) { + player.name = playerName; + game.hostPlayerName = playerName; + } + + game.lastActivity = new Date(); + await persistGame(game); + + logger.info(`Game options updated for game ${gameId}`); + return { success: true, game }; + } catch (error) { + logger.error("Error updating game options:", error); + return { success: false, error: "Failed to update game options" }; + } + } + + static async changePlayerName(data: { sessionId: string; gameId: string; playerName: string }): Promise { + try { + const { sessionId, gameId, playerName } = data; + + if (!playerName || playerName.trim() === "") { + return { success: false, error: "Player name cannot be empty" }; + } + + // Limit name to 20 characters like the old game + const trimmedName = playerName.substring(0, 20); + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find the player making the request + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Update the player's name + player.name = trimmedName; + + // Update the corresponding game options based on player position + if (player.position === 0) { + // Host player - update mainPlayerName and hostPlayerName + game.gameOptions.mainPlayerName = trimmedName; + game.hostPlayerName = trimmedName; + } else { + // Non-host player - update the corresponding opponentOptions entry + const opponentIndex = player.position - 1; + if (game.gameOptions.opponentOptions && game.gameOptions.opponentOptions[opponentIndex]) { + game.gameOptions.opponentOptions[opponentIndex].name = trimmedName; + } + } + + game.lastActivity = new Date(); + await persistGame(game); + + logger.info( + `Player name changed to "${trimmedName}" for player at position ${player.position} in game ${gameId}`, + ); + return { success: true, game }; + } catch (error) { + logger.error("Error changing player name:", error); + return { success: false, error: "Failed to change player name" }; + } + } + + static async sendShips(sessionId: string, data: any): Promise { + try { + const { gameId, planetIdSource, planetIdDest, data: shipsByType } = data; + + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the source planet in the game state + const sourcePlanet = gameModel.planets?.find((p: any) => p.id === planetIdSource); + if (!sourcePlanet) { + return { success: false, error: "Source planet not found" }; + } + + // Find the destination planet in the game state + const destinationPlanet = gameModel.planets?.find((p: any) => p.id === planetIdDest); + if (!destinationPlanet) { + return { success: false, error: "Destination planet not found" }; + } + + const gamePlayer = gameModel.players?.find((p: any) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Check if this player owns the source planet + const ownsSourcePlanet = GameModel.isPlanetOwnedByPlayer(gamePlayer, planetIdSource); + if (!ownsSourcePlanet) { + return { success: false, error: "You do not own the source planet" }; + } + + // Validate that we have ships to send + const totalShipsToSend = + shipsByType.scouts.length + + shipsByType.destroyers.length + + shipsByType.cruisers.length + + shipsByType.battleships.length; + + if (totalShipsToSend === 0) { + return { success: false, error: "No ships selected to send" }; + } + + // Create a helper function to split fleet by specific ship IDs + const splitFleetWithShipIds = ( + fleet: any, + scoutIds: number[], + destroyerIds: number[], + cruiserIds: number[], + battleshipIds: number[], + ) => { + const newFleet = Fleet.generateFleet([], fleet.locationHexMidPoint); + + const moveShipsToFleet = (shipIds: number[], targetType: any) => { + for (const shipId of shipIds) { + const shipIndex = fleet.starships.findIndex((s: any) => s.id === shipId && s.type === targetType); + if (shipIndex !== -1) { + const ship = fleet.starships.splice(shipIndex, 1)[0]; + newFleet.starships.push(ship); + } + } + }; + + moveShipsToFleet(scoutIds, StarShipType.Scout); + moveShipsToFleet(destroyerIds, StarShipType.Destroyer); + moveShipsToFleet(cruiserIds, StarShipType.Cruiser); + moveShipsToFleet(battleshipIds, StarShipType.Battleship); + + return newFleet; + }; + + // Split the fleet with the specific ship IDs + const newFleet = splitFleetWithShipIds( + sourcePlanet.planetaryFleet, + shipsByType.scouts, + shipsByType.destroyers, + shipsByType.cruisers, + shipsByType.battleships, + ); + + // Set the destination for the new fleet + Fleet.setDestination( + newFleet, + gameModelData.grid, + sourcePlanet.boundingHexMidPoint, + destinationPlanet.boundingHexMidPoint, + ); + + // Add the fleet to the source planet's outgoing fleets + sourcePlanet.outgoingFleets.push(newFleet); + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info( + `Player ${player.Id} sent ${totalShipsToSend} ships from planet ${planetIdSource} to planet ${planetIdDest} in game ${gameId}`, + ); + + return { + success: true, + game, + gameData: gameModel, + }; + } catch (error) { + logger.error("SendShips error:", error); + return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; + } + } + + static async startUpdatePlanet(data: any, payload: any): Promise { + // TODO: Implement planet update start + logger.warn("startUpdatePlanet not yet implemented"); + return { success: false, error: "Not implemented" }; + } + + static async updatePlanetOptions(sessionId: string, payload: any): Promise { + try { + const { gameId, planetId, farmerDiff, minerDiff, builderDiff } = payload; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the planet in the game state + const planet = gameModel.planets?.find((p: any) => p.id === planetId); + if (!planet) { + return { success: false, error: "Planet not found" }; + } + + const gamePlayer = gameModel.players?.find((p: any) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Check if this planet exists in the player's owned planets + const ownsPlanet = GameModel.isPlanetOwnedByPlayer(gamePlayer, planetId); + + if (!ownsPlanet) { + return { success: false, error: "You do not own this planet" }; + } + + // Update worker assignments using the new engine method that handles rebalancing + engine.Planet.updatePopulationWorkerTypes(planet, gamePlayer, farmerDiff || 0, minerDiff || 0, builderDiff || 0); + + // Save the updated game state + game.gameState = gameModel; + await persistGame(game); + + return { success: true, game }; + } catch (error) { + logger.error("updatePlanetOptions error:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error occurred while updating planet options", + }; + } + } + + static async updatePlanetBuildQueue(sessionId: string, payload: any): Promise { + try { + const { gameId, planetId, action, productionItem } = payload; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + // Construct a proper GameModel with grid from the gameState data + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; // The actual game state + + // Find the planet in the game state + const planet = gameModel.planets?.find((p: any) => p.id === planetId); + if (!planet) { + return { success: false, error: "Planet not found" }; + } + + const gamePlayer = gameModel.players?.find((p: any) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Check if this planet exists in the player's owned planets + const ownsPlanet = GameModel.isPlanetOwnedByPlayer(gamePlayer, planetId); + + if (!ownsPlanet) { + return { success: false, error: "You do not own this planet" }; + } + + // Handle different actions + if (action === "add") { + // Add item to build queue using engine method + const planetById = gameModel.planets.reduce((acc: any, p: any) => { + acc[p.id] = p; + return acc; + }, {}); + + const gamePlayer = gameModel.players?.find((p: any) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + const clientGameModel = engine.constructClientGameModel(gameModel, player.Id); + // Use engine method to add to queue and spend resources + const canBuild = engine.Player.enqueueProductionItemAndSpendResourcesIfPossible( + clientGameModel, + gameModelData.grid, // Use the grid from GameModelData + planet, + productionItem, + ); + + if (!canBuild) { + return { success: false, error: "Not enough resources to build item" }; + } + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + + // Use data access layer to save with automatic Mixed field handling + await persistGame(game); + + return { + success: true, + game, + gameData: gameModel, + }; + } else if (action === "remove") { + // TODO: Implement remove from queue + return { success: false, error: "Remove from queue not yet implemented" }; + } else { + return { success: false, error: "Invalid action" }; + } + } catch (error) { + logger.error("UpdatePlanetBuildQueue error:", error); + return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; + } + } + + static async clearWaypoint(sessionId: string, payload: any): Promise { + // TODO: Implement waypoint clearing + logger.warn("clearWaypoint not yet implemented"); + return { success: false, error: "Not implemented" }; + } + + static async adjustResearchPercent(sessionId: string, payload: any): Promise { + try { + if (!payload || typeof payload.researchPercent !== "number") { + return { success: false, error: "Invalid research percent value" }; + } + + if (payload.researchPercent < 0 || payload.researchPercent > 1) { + return { success: false, error: "Research percent must be between 0% and 100%" }; + } + + const { gameId } = payload; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the player in the game model + const gamePlayer = gameModel.players.find((p) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Update the player's research percent + if (!gamePlayer.research) { + gamePlayer.research = { + researchPercent: 0, + researchProgressByType: {}, + researchTypeInQueue: null, + }; + } + + gamePlayer.research.researchPercent = payload.researchPercent; + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info(`Player ${player.name} adjusted research percent to ${Math.round(payload.researchPercent * 100)}%`); + return { success: true, game, gameData: gameModel }; + } catch (error) { + logger.error("Error adjusting research percent:", error); + return { success: false, error: "Failed to adjust research percent" }; + } + } + + static async submitResearchItem(sessionId: string, payload: any): Promise { + try { + if (!payload || !payload.researchItem || typeof payload.researchItem.type !== "number") { + return { success: false, error: "Invalid research item" }; + } + + const { gameId } = payload; + const { type: researchType, data: researchData } = payload.researchItem; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the player in the game model + const gamePlayer = gameModel.players.find((p) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Get the research progress for this type + const researchProgress = gamePlayer.research.researchProgressByType[researchType as ResearchType]; + if (!researchProgress) { + return { success: false, error: "Research type not found" }; + } + + // Update research progress data with advantage/disadvantage data (for custom ships) + if (researchData) { + logger.info(`Updating research type ${researchType} with data:`, researchData); + engine.Research.updateResearchTypeProgressData(researchProgress, researchData); + logger.info( + `Research progress after update - base points: ${researchProgress.researchPointsBase}, data:`, + researchProgress.data, + ); + } + + // Set the research type in queue + gamePlayer.research.researchTypeInQueue = researchType; + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info(`Player ${player.name} started researching ${researchType}`); + return { success: true, game, gameData: gameModel }; + } catch (error) { + logger.error("Error submitting research item:", error); + return { success: false, error: "Failed to start research" }; + } + } + + static async cancelResearchItem(sessionId: string, payload: any): Promise { + try { + const { gameId } = payload; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the player in the game model + const gamePlayer = gameModel.players.find((p) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Clear the research queue + if (gamePlayer.research) { + gamePlayer.research.researchTypeInQueue = null; + } + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info(`Player ${player.name} cancelled research`); + return { success: true, game, gameData: gameModel }; + } catch (error) { + logger.error("Error cancelling research item:", error); + return { success: false, error: "Failed to cancel research" }; + } + } + + static async submitTrade(sessionId: string, payload: any): Promise { + try { + if ( + !payload || + !payload.gameId || + !payload.planetId || + typeof payload.tradeType !== "number" || + typeof payload.resourceType !== "number" || + typeof payload.amount !== "number" + ) { + return { success: false, error: "Invalid trade data" }; + } + + const { gameId, planetId, tradeType, resourceType, amount } = payload; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the player in the game model + const gamePlayer = gameModel.players.find((p) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Find the planet in the game model + const planet = gameModel.planets?.find((p: any) => p.id === planetId); + if (!planet) { + return { success: false, error: "Planet not found" }; + } + + // Verify player owns the planet + const ownsPlanet = GameModel.isPlanetOwnedByPlayer(gamePlayer, planetId); + if (!ownsPlanet) { + return { success: false, error: "Player does not own this planet" }; + } + + // Create the trade using the engine + const trade = TradingCenter.constructTrade( + gamePlayer.id, + planetId, + tradeType as TradeType, + resourceType as TradingCenterResourceType, + amount, + 5, // 5 second delay + ); + + // Add trade to the trading center + gameModel.tradingCenter.currentTrades.push(trade); + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info( + `Player ${player.name} submitted trade: ${TradeType[tradeType]} ${amount} ${TradingCenterResourceType[resourceType]}`, + ); + return { success: true, game, gameData: gameModel }; + } catch (error) { + logger.error("Error submitting trade:", error); + return { success: false, error: "Failed to submit trade" }; + } + } + + static async cancelTrade(sessionId: string, payload: any): Promise { + try { + if (!payload || !payload.gameId || !payload.tradeId) { + return { success: false, error: "Invalid cancellation data - gameId and tradeId required" }; + } + + const { gameId, tradeId } = payload; + + // Find the game + const game = await ServerGameModel.findById(gameId); + if (!game) { + return { success: false, error: "Game not found" }; + } + + // Find player by sessionId + const player = game.players?.find((p) => p.sessionId === sessionId); + if (!player) { + return { success: false, error: "Player not found in game" }; + } + + // Get the current game state + const gameModelData = GameModel.constructGridWithModelData(game.gameState as any); + const gameModel = gameModelData.modelData; + + // Find the player in the game model + const gamePlayer = gameModel.players.find((p) => p.id === player.Id); + if (!gamePlayer) { + return { success: false, error: "Player not found in game state" }; + } + + // Cancel the trade using the engine + const cancelled = TradingCenter.cancelTrade(gameModel.tradingCenter, tradeId, gamePlayer.id); + + if (!cancelled) { + return { success: false, error: "Trade not found or cannot be cancelled" }; + } + + // Save the updated game state + game.gameState = gameModel; + game.lastActivity = new Date(); + await persistGame(game); + + logger.info(`Player ${player.name} cancelled trade ${tradeId}`); + return { success: true, game, gameData: gameModel }; + } catch (error) { + logger.error("Error cancelling trade:", error); + return { success: false, error: "Failed to cancel trade" }; + } + } + + static async exitResign(sessionId: string, payload: any): Promise { + // TODO: Implement exit/resign + logger.warn("exitResign not yet implemented"); + return { success: false, error: "Not implemented" }; + } + + // ========================================== + // Chat and Session Management + // ========================================== + + static async getChatRoomWithSessions(gameId: string | null): Promise { + try { + if (gameId === null) { + // Lobby chat room - return all sessions not in games + const sessions = await SessionModel.find({ + $or: [{ gameId: { $exists: false } }, { gameId: null }], + }); + return { sessions }; + } else { + // Game-specific chat room + const sessions = await SessionModel.find({ gameId }); + return { sessions }; + } + } catch (error) { + logger.error("Error getting chat room with sessions:", error); + return null; + } + } + + static async joinChatRoom(gameId: string | null, session: any): Promise { + // TODO: Implement chat room joining like old app.js + logger.warn("joinChatRoom not yet implemented"); + return null; + } + + static async leaveChatRoom(sessionId: string, broadcast: boolean): Promise { + // TODO: Implement chat room leaving like old app.js + logger.warn("leaveChatRoom not yet implemented"); + return null; + } + + // ========================================== + // Utility Methods (like old app.js) + // ========================================== + + /** + * Converts a game document from the database to a game summary for the frontend + * This matches the IGame interface expected by the frontend + * Like the old game, we clean up the docs - we don't need to send everything to the client + */ + static getGameSummaryFromGameDoc(gameDoc: any): any { + const summary = { + _id: gameDoc._id.toString(), + name: gameDoc.name || "Unnamed Game", + hostPlayerName: gameDoc.hostPlayerName, // Include host information + status: gameDoc.status || "waiting", + players: [] as any[], // Will be populated below without sessionId + gameOptions: gameDoc.gameOptions || { + systemsToGenerate: 4, + planetsPerSystem: 4, + galaxySize: 4, + distributePlanetsEvenly: true, + quickStart: false, + gameSpeed: 3, + opponentOptions: [ + { name: "", type: -1 }, // Player 2: Open + { name: "", type: -2 }, // Player 3: Closed + { name: "", type: -2 }, // Player 4: Closed + ], + }, + createdAt: gameDoc.createdAt, + lastActivity: gameDoc.lastActivity, + started: gameDoc.status === "in_progress", + ended: gameDoc.status === "completed", + }; + + // Clean up player data - only send name and position, NOT sessionId + // This matches the old game_controller.js logic (line 280-283) + // Also add 'connected' field based on 'isActive' for frontend compatibility + if (gameDoc.players) { + for (const player of gameDoc.players) { + summary.players.push({ + name: player.name, + position: player.position, + connected: player.isActive || false, // Map isActive to connected for frontend + }); + } + } + + return summary; + } + + // ========================================== + // Session Management + // ========================================== + + static async touchSession(sessionId: string): Promise { + try { + await SessionModel.findOneAndUpdate({ sessionId }, { lastActivity: new Date() }, { upsert: true }); + } catch (error) { + logger.error("TouchSession Error:", error); + } + } + + static async cleanupExpiredSessions(timeout: number): Promise { + try { + const cutoffDate = new Date(Date.now() - timeout); + const result = await SessionModel.deleteMany({ + lastActivity: { $lt: cutoffDate }, + }); + + if (result.deletedCount > 0) { + logger.info(`Cleaned up ${result.deletedCount} expired sessions`); + } + } catch (error) { + logger.error("Error cleaning up expired sessions:", error); + } + } + + // ========================================== + // Legacy Cleanup Methods + // ========================================== + + static async cleanupOldGames(): Promise { + try { + const cleanupConfig = config.get("game.cleanup") as any; + const maxAge = cleanupConfig?.max_age_hours || 24; + const cutoffDate = new Date(Date.now() - maxAge * 60 * 60 * 1000); + + const result = await ServerGameModel.deleteMany({ + lastActivity: { $lt: cutoffDate }, + status: { $ne: "in_progress" }, + }); + + if (result.deletedCount > 0) { + logger.info(`Cleaned up ${result.deletedCount} old games`); + } + } catch (error) { + logger.error("Error during game cleanup:", error); + } + } + + static startGameCleanup(): void { + try { + const cleanupConfig = config.get("game.cleanup") as any; + + if (cleanupConfig && cleanupConfig.enabled) { + setInterval( + async () => { + await GameController.cleanupOldGames(); + }, + (cleanupConfig.check_interval_seconds || 3600) * 1000, + ); + + logger.info("Game cleanup scheduler started"); + } + } catch (error) { + logger.warn("Game cleanup config not found, skipping cleanup scheduler"); + } + } +} diff --git a/apps/astriarch-backend/src/database/DocumentPersistence.ts b/apps/astriarch-backend/src/database/DocumentPersistence.ts new file mode 100644 index 00000000..8f2b5270 --- /dev/null +++ b/apps/astriarch-backend/src/database/DocumentPersistence.ts @@ -0,0 +1,143 @@ +import { Document, Model } from "mongoose"; +import { logger } from "../utils/logger"; + +/** + * Data Access Layer for MongoDB document persistence. + * Automatically handles markModified() for Schema.Types.Mixed fields before saving. + */ +export class DocumentPersistence { + /** + * Save a document with automatic Mixed field marking. + * This replaces direct calls to document.save() + */ + static async saveDocument(document: T, mixedFields?: string[]): Promise { + try { + // Auto-detect Mixed fields if not explicitly provided + const fieldsToMark = mixedFields || DocumentPersistence.detectMixedFields(document); + + // Mark all Mixed type fields as modified + fieldsToMark.forEach((field) => { + document.markModified(field); + }); + + // Save and return the document + const savedDocument = await document.save(); + + logger.debug(`Document saved successfully. Mixed fields marked: [${fieldsToMark.join(", ")}]`); + return savedDocument; + } catch (error) { + logger.error("Error saving document:", error); + throw error; + } + } + + /** + * Save a Game document (convenience method) + */ + static async saveGame(gameDocument: Document): Promise { + return DocumentPersistence.saveDocument(gameDocument, ["gameState", "gameOptions"]); + } + + /** + * Save a PlayerAction document (convenience method) + */ + static async savePlayerAction(actionDocument: Document): Promise { + return DocumentPersistence.saveDocument(actionDocument, ["actionData", "changes"]); + } + + /** + * Save a GameEvent document (convenience method) + */ + static async saveGameEvent(eventDocument: Document): Promise { + return DocumentPersistence.saveDocument(eventDocument, ["eventData"]); + } + + /** + * Save a GameState document (convenience method) + */ + static async saveGameState(gameStateDocument: Document): Promise { + return DocumentPersistence.saveDocument(gameStateDocument, ["planets", "fleets", "players", "gameSettings"]); + } + + /** + * Auto-detect Mixed type fields from the document's schema + */ + private static detectMixedFields(document: Document): string[] { + const mixedFields: string[] = []; + + try { + const schema = (document.constructor as any).schema; + if (schema && schema.paths) { + for (const [path, schemaType] of Object.entries(schema.paths)) { + // Check if this path is a Mixed type + if ((schemaType as any).instance === "Mixed") { + mixedFields.push(path); + } + } + } + } catch (error) { + logger.warn("Could not auto-detect Mixed fields:", error); + } + + return mixedFields; + } + + /** + * Bulk save multiple documents with Mixed field handling + */ + static async saveDocuments(documents: { doc: T; mixedFields?: string[] }[]): Promise { + const savePromises = documents.map(({ doc, mixedFields }) => DocumentPersistence.saveDocument(doc, mixedFields)); + + return Promise.all(savePromises); + } + + /** + * Find and update a document with Mixed field handling + */ + static async findByIdAndUpdate( + model: Model, + id: string, + update: any, + mixedFields?: string[], + options: any = {}, + ): Promise { + try { + // First find the document + const document = await model.findById(id); + if (!document) { + return null; + } + + // Apply updates + Object.assign(document, update); + + // Save with Mixed field handling + return await DocumentPersistence.saveDocument(document, mixedFields); + } catch (error) { + logger.error("Error in findByIdAndUpdate:", error); + throw error; + } + } + + /** + * Create and save a new document with Mixed field handling + */ + static async createDocument(model: Model, data: any, mixedFields?: string[]): Promise { + try { + const document = new model(data); + return await DocumentPersistence.saveDocument(document, mixedFields); + } catch (error) { + logger.error("Error creating document:", error); + throw error; + } + } +} + +/** + * Convenience export for common usage patterns + */ +export const persist = DocumentPersistence.saveDocument; +export const persistGame = DocumentPersistence.saveGame; +export const persistPlayerAction = DocumentPersistence.savePlayerAction; +export const persistGameEvent = DocumentPersistence.saveGameEvent; +export const persistGameState = DocumentPersistence.saveGameState; diff --git a/apps/astriarch-backend/src/database/connection.ts b/apps/astriarch-backend/src/database/connection.ts new file mode 100644 index 00000000..ab1d21c2 --- /dev/null +++ b/apps/astriarch-backend/src/database/connection.ts @@ -0,0 +1,61 @@ +import mongoose from "mongoose"; +import config from "config"; +import { logger } from "../utils/logger"; + +export async function connectDatabase(): Promise { + let connectionString = ""; + try { + // Build connection string + const username = process.env.MONGODB_USERNAME || config.get("mongodb.username"); + const password = process.env.MONGODB_PASSWORD || config.get("mongodb.password"); + const host = process.env.MONGODB_HOST || config.get("mongodb.host"); + const port = process.env.MONGODB_PORT || config.get("mongodb.port"); + const database = process.env.MONGODB_DATABASE || config.get("mongodb.gamedb_name"); + + connectionString = process.env.MONGODB_CONNECTION_STRING || ""; + + if (!connectionString) { + const auth = username && password ? `${username}:${password}@` : ""; + connectionString = `mongodb://${auth}${host}:${port}/${database}`; + } + + logger.info( + `Attempting to connect to MongoDB with connection string: ${connectionString.replace(/\/\/.*:.*@/, "//***:***@")}`, + ); + + // MongoDB connection options + const options = { + maxPoolSize: 10, + serverSelectionTimeoutMS: 30000, + socketTimeoutMS: 45000, + }; + + await mongoose.connect(connectionString, options); + + logger.info(`Connected to MongoDB: ${database}`); + + // Connection event handlers + mongoose.connection.on("error", (error) => { + logger.error("MongoDB connection error:", error); + }); + + mongoose.connection.on("disconnected", () => { + logger.warn("MongoDB disconnected"); + }); + + mongoose.connection.on("reconnected", () => { + logger.info("MongoDB reconnected"); + }); + } catch (error) { + logger.error("Failed to connect to MongoDB:", { + error: error instanceof Error ? error.message : error, + stack: error instanceof Error ? error.stack : undefined, + connectionString: connectionString.replace(/\/\/.*:.*@/, "//***:***@"), + }); + throw error; + } +} + +export function disconnectDatabase(): Promise { + return mongoose.disconnect(); +} diff --git a/apps/astriarch-backend/src/models/ChatMessage.ts b/apps/astriarch-backend/src/models/ChatMessage.ts new file mode 100644 index 00000000..f5c4b8d1 --- /dev/null +++ b/apps/astriarch-backend/src/models/ChatMessage.ts @@ -0,0 +1,40 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface IChatMessage extends Document { + gameId: mongoose.Types.ObjectId; + playerName: string; + message: string; + timestamp: Date; + messageType: "public" | "team" | "system"; +} + +const ChatMessageSchema = new Schema({ + gameId: { + type: Schema.Types.ObjectId, + ref: "Game", + required: true, + }, + playerName: { + type: String, + required: true, + }, + message: { + type: String, + required: true, + maxlength: 500, + }, + timestamp: { + type: Date, + default: Date.now, + }, + messageType: { + type: String, + enum: ["public", "team", "system"], + default: "public", + }, +}); + +// Index for efficient chat retrieval +ChatMessageSchema.index({ gameId: 1, timestamp: -1 }); + +export const ChatMessageModel = mongoose.model("ChatMessage", ChatMessageSchema); diff --git a/apps/astriarch-backend/src/models/Game.ts b/apps/astriarch-backend/src/models/Game.ts new file mode 100644 index 00000000..319aa3a5 --- /dev/null +++ b/apps/astriarch-backend/src/models/Game.ts @@ -0,0 +1,92 @@ +import { ServerGameOptions } from "astriarch-engine"; +import mongoose, { Schema, Document } from "mongoose"; + +export interface IPlayer { + name: string; + sessionId?: string; // Optional for computer players + position: number; + Id: string; + isActive: boolean; + isAI: boolean; +} + +export interface IGame extends Document { + name: string; // Game name (for lobby display) + hostPlayerName: string; + maxPlayers: number; + gameType: string; + isPrivate: boolean; + gameState: object; + status: "waiting_for_players" | "in_progress" | "completed"; + players: IPlayer[]; // Array of players in the game + gameOptions: ServerGameOptions; // Game configuration options + createdAt: Date; + lastActivity: Date; +} + +const PlayerSchema = new Schema({ + name: { type: String, required: true }, + sessionId: { type: String, required: false }, // Not required for computer players + position: { type: Number, required: true }, + Id: { type: String, required: true }, + isActive: { type: Boolean, default: true }, + isAI: { type: Boolean, default: false }, +}); + +const GameSchema = new Schema({ + name: { + type: String, + }, + hostPlayerName: { + type: String, + required: true, + }, + maxPlayers: { + type: Number, + required: true, + default: 4, + }, + gameType: { + type: String, + required: true, + default: "standard", + }, + isPrivate: { + type: Boolean, + default: false, + }, + gameState: { + type: Schema.Types.Mixed, + required: true, + }, + status: { + type: String, + enum: ["waiting_for_players", "in_progress", "completed"], + default: "waiting_for_players", + }, + players: { + type: [PlayerSchema], + default: [], + }, + gameOptions: { + type: Schema.Types.Mixed, + default: () => ({}), + }, + createdAt: { + type: Date, + default: Date.now, + }, + lastActivity: { + type: Date, + default: Date.now, + }, +}); + +// Configure schema to preserve empty objects +GameSchema.set("minimize", false); + +// Index for efficient queries +GameSchema.index({ status: 1, createdAt: -1 }); +GameSchema.index({ lastActivity: 1 }); + +export const ServerGameModel = mongoose.model("Game", GameSchema); diff --git a/apps/astriarch-backend/src/models/GameEvent.ts b/apps/astriarch-backend/src/models/GameEvent.ts new file mode 100644 index 00000000..356a2831 --- /dev/null +++ b/apps/astriarch-backend/src/models/GameEvent.ts @@ -0,0 +1,106 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IGameEvent extends Document { + gameId: string; + timestamp: Date; + gameTime: number; // Game time when event occurred + eventType: string; + eventData: { + playerId?: string; + planetId?: string; + fleetId?: string; + message?: string; + severity?: "info" | "warning" | "error" | "critical"; + details?: any; + }; + affectedPlayers: string[]; // Which players should see this event + broadcasted: boolean; // Whether this event has been sent to clients + broadcastTime?: Date; + persistent: boolean; // Whether to store in player's event history + category: string; // For filtering and organization +} + +const GameEventSchema = new Schema( + { + gameId: { + type: String, + required: true, + index: true, + }, + timestamp: { + type: Date, + default: Date.now, + index: true, + }, + gameTime: { + type: Number, + required: true, + // Removed index: true - using schema-level index instead + }, + eventType: { + type: String, + required: true, + enum: [ + "game_start", + "game_end", + "player_join", + "player_leave", + "planet_colonized", + "planet_attacked", + "planet_conquered", + "fleet_arrived", + "fleet_destroyed", + "building_completed", + "research_completed", + "battle_result", + "diplomacy_change", + "trade_completed", + "game_message", + "system_notification", + "error_occurred", + ], + }, + eventData: { + type: Schema.Types.Mixed, + required: true, + }, + affectedPlayers: [ + { + type: String, + required: true, + }, + ], + broadcasted: { + type: Boolean, + required: true, + default: false, + index: true, + }, + broadcastTime: Date, + persistent: { + type: Boolean, + required: true, + default: true, + }, + category: { + type: String, + required: true, + enum: ["game_flow", "player_action", "battle", "economy", "diplomacy", "system", "error"], + index: true, + }, + }, + { + timestamps: true, + collection: "gameevents", + }, +); + +// Critical indexes for realtime event processing +GameEventSchema.index({ gameId: 1, timestamp: -1 }); +GameEventSchema.index({ gameId: 1, eventType: 1, timestamp: -1 }); +GameEventSchema.index({ gameId: 1, broadcasted: 1 }); +GameEventSchema.index({ gameId: 1, affectedPlayers: 1, timestamp: -1 }); +GameEventSchema.index({ gameId: 1, category: 1, timestamp: -1 }); +GameEventSchema.index({ gameTime: 1 }); + +export const GameEvent = model("GameEvent", GameEventSchema); diff --git a/apps/astriarch-backend/src/models/GameState.ts b/apps/astriarch-backend/src/models/GameState.ts new file mode 100644 index 00000000..392513c8 --- /dev/null +++ b/apps/astriarch-backend/src/models/GameState.ts @@ -0,0 +1,128 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IGameState extends Document { + gameId: string; + timestamp: Date; + gameTime: number; // Game time in milliseconds since start + planets: { + [planetId: string]: { + playerId: string | null; + population: number; + buildings: { + farms: number; + mines: number; + factories: number; + spacePlatforms: number; + }; + resources: { + food: number; + ore: number; + iridium: number; + }; + buildQueue: Array<{ + type: string; + turnsRemaining: number; + startTime: Date; + }>; + workingData?: any; // Temporary for migration compatibility + }; + }; + fleets: { + [fleetId: string]: { + playerId: string; + ships: { + scouts: number; + destroyers: number; + cruisers: number; + battleships: number; + }; + position: { + x: number; + y: number; + }; + destination?: { + planetId: string; + arrivalTime: Date; + }; + orders: string; + }; + }; + players: { + [playerId: string]: { + name: string; + isAI: boolean; + research: { + attack: number; + defense: number; + propulsion: number; + }; + resources: { + credits: number; + }; + isActive: boolean; + lastActionTime?: Date; + }; + }; + gameSettings: { + gameSpeed: number; + maxPlayers: number; + galaxySize: string; + aiDifficulty: string; + }; + version: string; // Schema version for migration tracking +} + +const GameStateSchema = new Schema( + { + gameId: { + type: String, + required: true, + index: true, + }, + timestamp: { + type: Date, + default: Date.now, + index: true, + }, + gameTime: { + type: Number, + required: true, + default: 0, + }, + planets: { + type: Schema.Types.Mixed, + required: true, + default: {}, + }, + fleets: { + type: Schema.Types.Mixed, + required: true, + default: {}, + }, + players: { + type: Schema.Types.Mixed, + required: true, + default: {}, + }, + gameSettings: { + type: Schema.Types.Mixed, + required: true, + }, + version: { + type: String, + required: true, + default: "2.0.0", + }, + }, + { + timestamps: true, + collection: "gamestates", + }, +); + +// Indexes for realtime performance +GameStateSchema.index({ gameId: 1, timestamp: -1 }); +GameStateSchema.index({ gameId: 1, gameTime: -1 }); +GameStateSchema.index({ "players.lastActionTime": -1 }); + +export const GameState = model("GameState", GameStateSchema); diff --git a/apps/astriarch-backend/src/models/PlayerAction.ts b/apps/astriarch-backend/src/models/PlayerAction.ts new file mode 100644 index 00000000..f17e4171 --- /dev/null +++ b/apps/astriarch-backend/src/models/PlayerAction.ts @@ -0,0 +1,105 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IPlayerAction extends Document { + gameId: string; + playerId: string; + timestamp: Date; + gameTime: number; // Game time when action was performed + actionType: string; + actionData: { + planetId?: string; + fleetId?: string; + targetPlanetId?: string; + buildingType?: string; + quantity?: number; + coordinates?: { + x: number; + y: number; + }; + [key: string]: any; // Allow flexible action data + }; + result: { + success: boolean; + message?: string; + changes?: any; // What changed as a result of this action + }; + processed: boolean; + processingTime?: Date; + clientTimestamp?: Date; // When client sent the action + serverLatency?: number; // Time between client and server processing +} + +const PlayerActionSchema = new Schema( + { + gameId: { + type: String, + required: true, + index: true, + }, + playerId: { + type: String, + required: true, + index: true, + }, + timestamp: { + type: Date, + default: Date.now, + index: true, + }, + gameTime: { + type: Number, + required: true, + }, + actionType: { + type: String, + required: true, + enum: [ + "build_structure", + "send_fleet", + "attack_planet", + "colonize_planet", + "research_upgrade", + "demolish_structure", + "fleet_orders", + "planet_management", + "diplomacy_action", + "trade_action", + ], + }, + actionData: { + type: Schema.Types.Mixed, + required: true, + }, + result: { + success: { + type: Boolean, + required: true, + default: false, + }, + message: String, + changes: Schema.Types.Mixed, + }, + processed: { + type: Boolean, + required: true, + default: false, + index: true, + }, + processingTime: Date, + clientTimestamp: Date, + serverLatency: Number, + }, + { + timestamps: true, + collection: "playeractions", + }, +); + +// Critical indexes for realtime performance +PlayerActionSchema.index({ gameId: 1, timestamp: -1 }); +PlayerActionSchema.index({ gameId: 1, playerId: 1, timestamp: -1 }); +PlayerActionSchema.index({ gameId: 1, processed: 1 }); +PlayerActionSchema.index({ gameId: 1, actionType: 1, timestamp: -1 }); +PlayerActionSchema.index({ gameTime: 1 }); + +export const PlayerAction = model("PlayerAction", PlayerActionSchema); diff --git a/apps/astriarch-backend/src/models/RealtimeConnection.ts b/apps/astriarch-backend/src/models/RealtimeConnection.ts new file mode 100644 index 00000000..44cc6271 --- /dev/null +++ b/apps/astriarch-backend/src/models/RealtimeConnection.ts @@ -0,0 +1,127 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IRealtimeConnection extends Document { + gameId: string; + sessionId: string; + playerId: string; + playerName: string; + connectionState: "connected" | "disconnected" | "reconnecting" | "timeout"; + websocketId?: string; // Internal WebSocket connection ID + connectedAt: Date; + lastActivity: Date; + disconnectedAt?: Date; + reconnectAttempts: number; + clientInfo: { + userAgent?: string; + ipAddress?: string; + version?: string; + }; + gameSync: { + lastSyncTime?: Date; + syncVersion?: string; + pendingActions?: number; // Number of unprocessed actions + }; + performance: { + latency?: number; // Average roundtrip latency in ms + packetsLost?: number; + reconnects?: number; + }; +} + +const RealtimeConnectionSchema = new Schema( + { + gameId: { + type: String, + required: true, + index: true, + }, + sessionId: { + type: String, + required: true, + unique: true, + // Removed index: true - using schema-level index instead + }, + playerId: { + type: String, + required: true, + index: true, + }, + playerName: { + type: String, + required: true, + }, + connectionState: { + type: String, + required: true, + enum: ["connected", "disconnected", "reconnecting", "timeout"], + default: "connected", + index: true, + }, + websocketId: { + type: String, + // Removed index: true - using schema-level index instead + }, + connectedAt: { + type: Date, + default: Date.now, + required: true, + }, + lastActivity: { + type: Date, + default: Date.now, + required: true, + // Removed index: true - using schema-level index instead + }, + disconnectedAt: Date, + reconnectAttempts: { + type: Number, + default: 0, + }, + clientInfo: { + userAgent: String, + ipAddress: String, + version: String, + }, + gameSync: { + lastSyncTime: Date, + syncVersion: String, + pendingActions: { + type: Number, + default: 0, + }, + }, + performance: { + latency: Number, + packetsLost: { + type: Number, + default: 0, + }, + reconnects: { + type: Number, + default: 0, + }, + }, + }, + { + timestamps: true, + collection: "realtimeconnections", + }, +); + +// Indexes for connection management and monitoring +RealtimeConnectionSchema.index({ gameId: 1, connectionState: 1 }); +RealtimeConnectionSchema.index({ gameId: 1, playerId: 1 }); +// Removed sessionId index - unique: true already creates an index +RealtimeConnectionSchema.index({ lastActivity: 1 }); // For cleanup of stale connections +RealtimeConnectionSchema.index({ websocketId: 1 }); + +// TTL index for automatic cleanup of old disconnected connections (24 hours) +RealtimeConnectionSchema.index( + { disconnectedAt: 1 }, + { + expireAfterSeconds: 24 * 60 * 60, + partialFilterExpression: { connectionState: "disconnected" }, + }, +); + +export const RealtimeConnection = model("RealtimeConnection", RealtimeConnectionSchema); diff --git a/apps/astriarch-backend/src/models/Session.ts b/apps/astriarch-backend/src/models/Session.ts new file mode 100644 index 00000000..aebd72e3 --- /dev/null +++ b/apps/astriarch-backend/src/models/Session.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface ISession extends Document { + sessionId: string; + playerName: string; + connectionStatus: "connected" | "disconnected"; + lastPing: Date; + createdAt: Date; +} + +const SessionSchema = new Schema({ + sessionId: { + type: String, + required: true, + unique: true, + }, + playerName: { + type: String, + required: true, + }, + connectionStatus: { + type: String, + enum: ["connected", "disconnected"], + default: "connected", + }, + lastPing: { + type: Date, + default: Date.now, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +// Indexes for efficient queries +SessionSchema.index({ lastPing: 1 }); + +export const SessionModel = mongoose.model("Session", SessionSchema, "game_sessions"); diff --git a/apps/astriarch-backend/src/models/index.ts b/apps/astriarch-backend/src/models/index.ts new file mode 100644 index 00000000..82637e02 --- /dev/null +++ b/apps/astriarch-backend/src/models/index.ts @@ -0,0 +1,7 @@ +export { ServerGameModel as Game, IGame } from "./Game"; +export { SessionModel as Session, ISession } from "./Session"; +export { ChatMessageModel as ChatMessage, IChatMessage } from "./ChatMessage"; +export { GameState, IGameState } from "./GameState"; +export { PlayerAction, IPlayerAction } from "./PlayerAction"; +export { GameEvent, IGameEvent } from "./GameEvent"; +export { RealtimeConnection, IRealtimeConnection } from "./RealtimeConnection"; diff --git a/apps/astriarch-backend/src/routes/gameRoutes.ts b/apps/astriarch-backend/src/routes/gameRoutes.ts new file mode 100644 index 00000000..9fdf4ce6 --- /dev/null +++ b/apps/astriarch-backend/src/routes/gameRoutes.ts @@ -0,0 +1,14 @@ +import { Router, Request, Response, type Router as ExpressRouter } from "express"; +import { logger } from "../utils/logger"; +import * as engine from "astriarch-engine"; + +const router: ExpressRouter = Router(); + +// Get available games list +router.get("/", async (req: Request, res: Response) => { + return res.json({ + engineVersion: engine.getEngineVersion(), + }); +}); + +export { router as gameRoutes }; diff --git a/apps/astriarch-backend/src/routes/healthRoutes.ts b/apps/astriarch-backend/src/routes/healthRoutes.ts new file mode 100644 index 00000000..317e7178 --- /dev/null +++ b/apps/astriarch-backend/src/routes/healthRoutes.ts @@ -0,0 +1,30 @@ +import { Router, Request, Response, type Router as ExpressRouter } from "express"; + +const router: ExpressRouter = Router(); + +interface HealthResponse { + status: string; + version: string; + timestamp: string; + uptime: number; + sessionId?: string; +} + +router.get("/", (req: Request, res: Response) => { + // Force session creation by setting a value + // This ensures that a session cookie will be created + if (req.session) { + (req.session as any).created = new Date().toISOString(); + } + console.log("Request session id: ", req.sessionID); + const response: HealthResponse = { + status: "OK", + version: process.env.npm_package_version || "2.0.0", + timestamp: new Date().toISOString(), + uptime: process.uptime(), + }; + + res.json(response); +}); + +export { router as healthRoutes }; diff --git a/apps/astriarch-backend/src/services/GameActionHandler.ts b/apps/astriarch-backend/src/services/GameActionHandler.ts new file mode 100644 index 00000000..f01cbf7d --- /dev/null +++ b/apps/astriarch-backend/src/services/GameActionHandler.ts @@ -0,0 +1,383 @@ +// Game action handler - processes game actions through business logic +import { GameState, PlayerAction, GameEvent } from "../models"; +import { logger } from "../utils/logger"; + +export interface GameActionResult { + success: boolean; + message?: string; + changes?: any; + gameStateUpdate?: any; +} + +export class GameActionHandler { + /** + * Process a building construction action + */ + static async processBuildStructure( + gameState: any, + playerId: string, + actionData: { + planetId: string; + buildingType: string; + quantity?: number; + }, + ): Promise { + try { + const { planetId, buildingType, quantity = 1 } = actionData; + + // Validate planet ownership + const planet = gameState.planets?.[planetId]; + if (!planet || planet.playerId !== playerId) { + return { + success: false, + message: "Planet not found or not owned by player", + }; + } + + // Validate building type + const validBuildings = ["farm", "mine", "factory", "spacePlatform"]; + if (!validBuildings.includes(buildingType)) { + return { + success: false, + message: `Invalid building type: ${buildingType}`, + }; + } + + // Check resource requirements (simplified for now) + const buildingCosts = { + farm: { ore: 10, iridium: 0 }, + mine: { ore: 15, iridium: 5 }, + factory: { ore: 20, iridium: 10 }, + spacePlatform: { ore: 50, iridium: 25 }, + }; + + const cost = buildingCosts[buildingType as keyof typeof buildingCosts]; + const totalCost = { + ore: cost.ore * quantity, + iridium: cost.iridium * quantity, + }; + + // Check if player has enough resources + if (planet.resources.ore < totalCost.ore || planet.resources.iridium < totalCost.iridium) { + return { + success: false, + message: "Insufficient resources", + }; + } + + // Deduct resources and add to build queue + planet.resources.ore -= totalCost.ore; + planet.resources.iridium -= totalCost.iridium; + + // Add to build queue (simplified - assuming immediate completion for now) + planet.buildings[buildingType] = (planet.buildings[buildingType] || 0) + quantity; + + const changes = { + type: "building_started", + planetId, + buildingType, + quantity, + newResources: planet.resources, + newBuildings: planet.buildings, + }; + + return { + success: true, + message: `${buildingType} construction started`, + changes, + gameStateUpdate: { planets: { [planetId]: planet } }, + }; + } catch (error) { + logger.error("Error processing build structure action:", error); + return { + success: false, + message: "Failed to process building construction", + }; + } + } + + /** + * Process a fleet movement action + */ + static async processSendFleet( + gameState: any, + playerId: string, + actionData: { + fromPlanetId: string; + toPlanetId: string; + ships: { + scouts?: number; + destroyers?: number; + cruisers?: number; + battleships?: number; + }; + orders: "attack" | "colonize" | "reinforce"; + }, + ): Promise { + try { + const { fromPlanetId, toPlanetId, ships, orders } = actionData; + + // Validate source planet + const fromPlanet = gameState.planets?.[fromPlanetId]; + if (!fromPlanet || fromPlanet.playerId !== playerId) { + return { + success: false, + message: "Source planet not found or not owned", + }; + } + + // Validate destination planet exists + const toPlanet = gameState.planets?.[toPlanetId]; + if (!toPlanet) { + return { + success: false, + message: "Destination planet not found", + }; + } + + // Calculate total ships being sent + const totalShips = Object.values(ships).reduce((sum, count) => sum + (count || 0), 0); + if (totalShips === 0) { + return { + success: false, + message: "No ships specified for fleet", + }; + } + + // Check if planet has enough ships (simplified - assume planet has ships) + // In real implementation, would check planet's available ships + + // Create fleet + const fleetId = `fleet_${Date.now()}_${playerId}`; + const fleet = { + id: fleetId, + playerId, + ships, + fromPlanetId, + toPlanetId, + orders, + position: fromPlanet.position || { x: 0, y: 0 }, + destination: toPlanet.position || { x: 100, y: 100 }, + arrivalTime: new Date(Date.now() + 60000), // 1 minute travel time + status: "in_transit", + }; + + // Add fleet to game state + if (!gameState.fleets) gameState.fleets = {}; + gameState.fleets[fleetId] = fleet; + + const changes = { + type: "fleet_dispatched", + fleetId, + fromPlanetId, + toPlanetId, + ships, + orders, + arrivalTime: fleet.arrivalTime, + }; + + return { + success: true, + message: `Fleet dispatched to ${toPlanetId}`, + changes, + gameStateUpdate: { fleets: { [fleetId]: fleet } }, + }; + } catch (error) { + logger.error("Error processing send fleet action:", error); + return { + success: false, + message: "Failed to dispatch fleet", + }; + } + } + + /** + * Process a research action + */ + static async processResearchUpgrade( + gameState: any, + playerId: string, + actionData: { + researchType: "attack" | "defense" | "propulsion"; + allocation: number; // Percentage of resources to allocate + }, + ): Promise { + try { + const { researchType, allocation } = actionData; + + // Validate research type + const validResearch = ["attack", "defense", "propulsion"]; + if (!validResearch.includes(researchType)) { + return { + success: false, + message: `Invalid research type: ${researchType}`, + }; + } + + // Validate allocation + if (allocation < 0 || allocation > 100) { + return { + success: false, + message: "Research allocation must be between 0 and 100", + }; + } + + // Get player data + const player = gameState.players?.[playerId]; + if (!player) { + return { + success: false, + message: "Player not found", + }; + } + + // Update research allocation + if (!player.research) { + player.research = { attack: 0, defense: 0, propulsion: 0 }; + } + + const previousAllocation = player.research[`${researchType}Allocation`] || 0; + player.research[`${researchType}Allocation`] = allocation; + + const changes = { + type: "research_allocation_changed", + researchType, + previousAllocation, + newAllocation: allocation, + }; + + return { + success: true, + message: `${researchType} research allocation set to ${allocation}%`, + changes, + gameStateUpdate: { players: { [playerId]: player } }, + }; + } catch (error) { + logger.error("Error processing research upgrade action:", error); + return { + success: false, + message: "Failed to update research", + }; + } + } + + /** + * Process planet management actions + */ + static async processPlanetManagement( + gameState: any, + playerId: string, + actionData: { + planetId: string; + action: "adjust_population" | "demolish_building" | "upgrade_building"; + parameters: any; + }, + ): Promise { + try { + const { planetId, action, parameters } = actionData; + + // Validate planet ownership + const planet = gameState.planets?.[planetId]; + if (!planet || planet.playerId !== playerId) { + return { + success: false, + message: "Planet not found or not owned by player", + }; + } + + let changes: any = { type: action, planetId }; + let gameStateUpdate: any = {}; + + switch (action) { + case "adjust_population": + // Adjust population allocation between different roles + const { workerAllocation } = parameters; + if (workerAllocation) { + planet.populationAllocation = workerAllocation; + changes.newAllocation = workerAllocation; + gameStateUpdate = { planets: { [planetId]: planet } }; + } + break; + + case "demolish_building": + // Remove a building and get some resources back + const { buildingType } = parameters; + if (planet.buildings[buildingType] > 0) { + planet.buildings[buildingType]--; + // Return some resources (50% of original cost) + const refundCosts = { + farm: { ore: 5, iridium: 0 }, + mine: { ore: 7, iridium: 2 }, + factory: { ore: 10, iridium: 5 }, + spacePlatform: { ore: 25, iridium: 12 }, + }; + const refund = refundCosts[buildingType as keyof typeof refundCosts]; + if (refund) { + planet.resources.ore += refund.ore; + planet.resources.iridium += refund.iridium; + } + changes.buildingType = buildingType; + changes.resourcesRefunded = refund; + gameStateUpdate = { planets: { [planetId]: planet } }; + } else { + return { + success: false, + message: `No ${buildingType} buildings to demolish`, + }; + } + break; + + default: + return { + success: false, + message: `Unknown planet management action: ${action}`, + }; + } + + return { + success: true, + message: `Planet management action completed: ${action}`, + changes, + gameStateUpdate, + }; + } catch (error) { + logger.error("Error processing planet management action:", error); + return { + success: false, + message: "Failed to process planet management action", + }; + } + } + + /** + * Main dispatcher for all game actions + */ + static async processAction( + gameState: any, + playerId: string, + actionType: string, + actionData: any, + ): Promise { + logger.debug(`Processing action: ${actionType} for player: ${playerId}`); + + switch (actionType) { + case "build_structure": + return await this.processBuildStructure(gameState, playerId, actionData); + + case "send_fleet": + return await this.processSendFleet(gameState, playerId, actionData); + + case "research_upgrade": + return await this.processResearchUpgrade(gameState, playerId, actionData); + + case "planet_management": + return await this.processPlanetManagement(gameState, playerId, actionData); + + default: + return { + success: false, + message: `Unknown action type: ${actionType}`, + }; + } + } +} diff --git a/apps/astriarch-backend/src/utils/logger.ts b/apps/astriarch-backend/src/utils/logger.ts new file mode 100644 index 00000000..69c4a63a --- /dev/null +++ b/apps/astriarch-backend/src/utils/logger.ts @@ -0,0 +1,52 @@ +import config from "config"; + +type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"; + +class Logger { + private logLevel: LogLevel; + + constructor() { + this.logLevel = (config.get("loglevel") as LogLevel) || "INFO"; + } + + private shouldLog(level: LogLevel): boolean { + const levels: Record = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + }; + return levels[level] >= levels[this.logLevel]; + } + + private formatPrefix(level: LogLevel): string { + const timestamp = new Date().toISOString(); + return `[${timestamp}] ${level}:`; + } + + debug(message: string, ...args: unknown[]): void { + if (this.shouldLog("DEBUG")) { + console.log(this.formatPrefix("DEBUG"), message, ...args); + } + } + + info(message: string, ...args: unknown[]): void { + if (this.shouldLog("INFO")) { + console.log(this.formatPrefix("INFO"), message, ...args); + } + } + + warn(message: string, ...args: unknown[]): void { + if (this.shouldLog("WARN")) { + console.warn(this.formatPrefix("WARN"), message, ...args); + } + } + + error(message: string, ...args: unknown[]): void { + if (this.shouldLog("ERROR")) { + console.error(this.formatPrefix("ERROR"), message, ...args); + } + } +} + +export const logger = new Logger(); diff --git a/apps/astriarch-backend/src/utils/player-id-helper.ts b/apps/astriarch-backend/src/utils/player-id-helper.ts new file mode 100644 index 00000000..293483f4 --- /dev/null +++ b/apps/astriarch-backend/src/utils/player-id-helper.ts @@ -0,0 +1 @@ +export const getPlayerId = (playerPosition: number) => `player_${playerPosition}`; diff --git a/apps/astriarch-backend/src/websocket/WebSocketServer.ts b/apps/astriarch-backend/src/websocket/WebSocketServer.ts new file mode 100644 index 00000000..2aead3d6 --- /dev/null +++ b/apps/astriarch-backend/src/websocket/WebSocketServer.ts @@ -0,0 +1,1434 @@ +import WebSocket from "ws"; +import { Server } from "http"; +import { logger } from "../utils/logger"; +import { Session, Game, IGame } from "../models"; +import { GameController } from "../controllers/GameControllerWebSocket"; +import { persistGame } from "../database/DocumentPersistence"; +import { v4 as uuidv4 } from "uuid"; +import { + MESSAGE_TYPE, + Message, + getMessageTypeName, + ERROR_TYPE, + CHAT_MESSAGE_TYPE, + type IMessage, + type IEventNotificationsPayload, + // Import available type guards + isCreateGameRequest, + isJoinGameRequest, + isStartGameRequest, + isListGamesResponse, + isGameStateUpdate, + isErrorMessage, + // Import available payload types + type ICreateGameRequestPayload, + type IJoinGameRequestPayload, + type IStartGameRequestPayload, + type IChangeGameOptionsPayload, + type IChangePlayerNamePayload, + constructClientGameModel, + advanceGameModelTime, + resetGameSnapshotTime, + GameModel, + ModelData, + Events, + type EventNotification, +} from "astriarch-engine"; +import { getPlayerId } from "../utils/player-id-helper"; + +export interface IConnectedClient { + ws: WebSocket; + sessionId: string; + gameId?: string; + playerId?: string; + playerName?: string; + lastPing: Date; + upgradeReq?: any; // For backward compatibility with cookie parsing +} + +export class WebSocketServer { + private static instance: WebSocketServer; + private wss: WebSocket.Server; + private clients: Map = new Map(); + private sessionLookup: Map = new Map(); // sessionId -> clientId mapping + private gameRooms: Map> = new Map(); // gameId -> sessionIds + private chatRooms: Map> = new Map(); // gameId -> sessionIds (null for lobby) + private eventSubscriptions: Set = new Set(); // track active event subscriptions by playerId + private pingInterval?: NodeJS.Timeout; + + constructor(server: Server) { + this.wss = new WebSocket.Server({ server }); + this.setupWebSocketServer(); + this.startPingInterval(); + WebSocketServer.instance = this; + } + + public static getInstance(): WebSocketServer | null { + return WebSocketServer.instance || null; + } + + // Event subscription management + private subscribeToPlayerEvents(playerId: string): void { + if (this.eventSubscriptions.has(playerId)) { + return; // Already subscribed + } + + Events.subscribe(playerId, (subscriptionPlayerId: string, events: EventNotification[]) => { + this.handlePlayerEvents(subscriptionPlayerId, events); + }); + + this.eventSubscriptions.add(playerId); + logger.info(`Subscribed to events for player: ${playerId}`); + } + + private handlePlayerEvents(playerId: string, events: EventNotification[]): void { + if (events.length === 0) { + return; + } + + logger.info(`Received ${events.length} events for player ${playerId}`); + + // Find the client for this player + let targetClient: IConnectedClient | null = null; + for (const client of this.clients.values()) { + if (client.playerId === playerId) { + targetClient = client; + break; + } + } + + if (!targetClient) { + logger.warn(`No connected client found for player ${playerId} to send events`); + return; + } + + // Send events to the client + const eventMessage = new Message(MESSAGE_TYPE.EVENT_NOTIFICATIONS, { + events, + }); + + this.broadcastToSession(targetClient.sessionId, eventMessage); + } + + private setupWebSocketServer(): void { + this.wss.on("connection", (ws: WebSocket, req: any) => { + const clientId = uuidv4(); + const sessionId = this.extractSessionId(req) || clientId; + + logger.info(`New WebSocket connection established. SessionId: ${sessionId}`); + + const client: IConnectedClient = { + ws, + sessionId, + lastPing: new Date(), + upgradeReq: req, // Store for cookie parsing compatibility + }; + + this.clients.set(clientId, client); + this.sessionLookup.set(sessionId, clientId); + + ws.on("message", (data: string) => { + this.handleMessage(clientId, data); + }); + + ws.on("close", () => { + this.handleDisconnection(clientId); + }); + + ws.on("error", (error) => { + logger.error(`WebSocket error for session ${sessionId}:`, error); + this.handleDisconnection(clientId); + }); + }); + } + + private extractSessionId(req: any): string | null { + // Extract session ID from signed cookies similar to old app.js + try { + if (req.headers.cookie) { + logger.info("Raw cookies received:", req.headers.cookie); + + const cookie = require("cookie"); + const signature = require("cookie-signature"); + const config = require("config"); + const cookieSecret = config.get("cookie.secret") as string; + + // Parse the cookies first + const cookies = cookie.parse(req.headers.cookie); + logger.info("Parsed cookies:", cookies); + + // Get the signed connect.sid cookie + const signedSessionCookie = cookies["connect.sid"]; + if (signedSessionCookie) { + logger.info("Signed cookie found:", signedSessionCookie); + + // Unsign the cookie using the same secret + if (signedSessionCookie.startsWith("s:")) { + try { + const unsigned = signature.unsign(signedSessionCookie.slice(2), cookieSecret); + logger.info("Unsigned session ID:", unsigned); + return unsigned; + } catch (error) { + logger.warn("Failed to unsign cookie:", error); + } + } + } + + logger.info("No valid signed session cookie found"); + } else { + logger.info("No cookies in WebSocket request headers"); + } + } catch (error) { + logger.warn("Error extracting session ID:", error); + } + return null; + } + + private async handleMessage(clientId: string, data: string): Promise { + const client = this.clients.get(clientId); + if (!client) { + logger.warn(`Received message from unknown client: ${clientId}`); + return; + } + + try { + // Touch session (like old app.js) + await this.touchSession(client.sessionId); + + const parsedMessage = JSON.parse(data); + const message: IMessage = { + type: parsedMessage.type, + payload: parsedMessage.payload || {}, + sessionId: client.sessionId, + timestamp: new Date(), + }; + + logger.info(`${getMessageTypeName(message.type)} Message received from ${client.sessionId}:`, data.toString()); + + // Update last ping + client.lastPing = new Date(); + + await this.processMessage(client, message); + } catch (error) { + logger.error(`Error parsing message from ${client.sessionId}:`, data, error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Invalid message format" })); + } + } + + private async processMessage(client: IConnectedClient, message: IMessage): Promise { + const clientId = this.getClientIdBySessionId(client.sessionId); + if (!clientId) return; + + // If this is a sync request and the client is in an active game, advance game time + if (this.isGameSyncRequired(message.type) && client.gameId) { + await this.advanceGameTimeForSync(client.gameId); + } + + switch (message.type) { + case MESSAGE_TYPE.NOOP: + // Echo back with server message (like old app.js) + const counter = + typeof message.payload === "object" && message.payload !== null && "counter" in message.payload + ? (message.payload as any).counter + : 0; + const response = { + ...(message.payload as object), + message: "Hello From the Server", + counter: counter + 1, + }; + this.sendToClient(clientId, new Message(MESSAGE_TYPE.NOOP, response)); + break; + + case MESSAGE_TYPE.PING: + // Send back PONG with updated client game model if player is in an active game + await this.handlePing(clientId); + break; + + case MESSAGE_TYPE.LIST_GAMES: + await this.handleListGames(clientId, message); + break; + + case MESSAGE_TYPE.CREATE_GAME: + if (!isCreateGameRequest(message)) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Invalid create game request" })); + return; + } + await this.handleCreateGame(clientId, message); + break; + + case MESSAGE_TYPE.JOIN_GAME: + if (!isJoinGameRequest(message)) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Invalid join game request" })); + return; + } + await this.handleJoinGame(clientId, message); + break; + + case MESSAGE_TYPE.START_GAME: + if (!isStartGameRequest(message)) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Invalid start game request" })); + return; + } + await this.handleStartGame(clientId, message); + break; + + case MESSAGE_TYPE.RESUME_GAME: + await this.handleResumeGame(clientId, message); + break; + + case MESSAGE_TYPE.CHANGE_GAME_OPTIONS: + await this.handleChangeGameOptions(clientId, message as IMessage); + break; + + case MESSAGE_TYPE.CHANGE_PLAYER_NAME: + await this.handleChangePlayerName(clientId, message as IMessage); + break; + + case MESSAGE_TYPE.SYNC_STATE: + await this.handleSyncState(clientId, message); + break; + + case MESSAGE_TYPE.SEND_SHIPS: + await this.handleSendShips(clientId, message); + break; + + case MESSAGE_TYPE.UPDATE_PLANET_OPTIONS: + await this.handleUpdatePlanetOptions(clientId, message); + break; + + case MESSAGE_TYPE.UPDATE_PLANET_BUILD_QUEUE: + await this.handleUpdatePlanetBuildQueue(clientId, message); + break; + + case MESSAGE_TYPE.CLEAR_WAYPOINT: + await this.handleClearWaypoint(clientId, message); + break; + + case MESSAGE_TYPE.ADJUST_RESEARCH_PERCENT: + await this.handleAdjustResearchPercent(clientId, message); + break; + + case MESSAGE_TYPE.SUBMIT_RESEARCH_ITEM: + await this.handleSubmitResearchItem(clientId, message); + break; + + case MESSAGE_TYPE.CANCEL_RESEARCH_ITEM: + await this.handleCancelResearchItem(clientId, message); + break; + + case MESSAGE_TYPE.SUBMIT_TRADE: + await this.handleSubmitTrade(clientId, message); + break; + + case MESSAGE_TYPE.CANCEL_TRADE: + await this.handleCancelTrade(clientId, message); + break; + + case MESSAGE_TYPE.CHAT_MESSAGE: + await this.handleChatMessage(clientId, message); + break; + + case MESSAGE_TYPE.EXIT_RESIGN: + await this.handleExitResign(clientId, message); + break; + + case MESSAGE_TYPE.LOGOUT: + await this.handleLogout(clientId, message); + break; + + default: + logger.error(`Unhandled Message Type: ${message.type}`); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Unknown message type" })); + break; + } + } + + // Helper methods + private async touchSession(sessionId: string): Promise { + try { + await Session.findOneAndUpdate({ sessionId }, { lastActivity: new Date() }, { upsert: true }); + } catch (error) { + logger.error("TouchSession Error:", error); + } + } + + private getClientIdBySessionId(sessionId: string): string | null { + return this.sessionLookup.get(sessionId) || null; + } + + // Real-time game management methods + private isGameSyncRequired(messageType: MESSAGE_TYPE): boolean { + // Only sync game state when explicitly requested via SYNC_STATE + // This allows the client (host) to control when game time advances + return messageType === MESSAGE_TYPE.SYNC_STATE; + } + + private async advanceGameTimeForSync(gameId: string): Promise { + try { + const game = await Game.findById(gameId); + if (!game || game.status !== "in_progress") { + return; + } + + // Use the engine's advanceGameModelTime function which handles time properly + // This will advance game time and process any AI/computer player actions + const gameModelData = GameModel.constructGridWithModelData(game.gameState as ModelData); + advanceGameModelTime(gameModelData); + + // Save the updated game state with automatic Mixed field handling + await persistGame(game); + + // Broadcast the updated game state to all connected players + await this.broadcastGameStateUpdate(gameId); + } catch (error) { + logger.error("Error advancing game time:", error); + } + } + + private async broadcastGameStateUpdate(gameId: string): Promise { + try { + logger.info(`Broadcasting game state update for game: ${gameId}`); + + const game = await Game.findById(gameId); + if (!game) { + logger.warn(`Game ${gameId} not found for state update`); + return; + } + + const gameRoom = this.gameRooms.get(gameId); + if (!gameRoom) { + logger.warn(`No game room found for game: ${gameId}`); + return; + } + + logger.info(`Game room has ${gameRoom.size} sessions: ${Array.from(gameRoom)}`); + + // Send updated client models to each connected player + for (const sessionId of gameRoom) { + const clientId = this.getClientIdBySessionId(sessionId); + const client = clientId ? this.clients.get(clientId) : null; + + logger.info(`Session ${sessionId}: clientId=${clientId}, client.playerId=${client?.playerId}`); + + if (client?.playerId && clientId) { + const clientGameModel = constructClientGameModel(game.gameState as any, client.playerId); + + // Debug: Log build queue data for the first planet + const firstPlanetId = Object.keys(clientGameModel.mainPlayerOwnedPlanets)[0]; + if (firstPlanetId) { + const firstPlanet = (clientGameModel.mainPlayerOwnedPlanets as any)[firstPlanetId]; + logger.info(`Build queue for planet ${firstPlanetId}: ${JSON.stringify(firstPlanet.buildQueue)}`); + } + + const updateMessage = new Message(MESSAGE_TYPE.GAME_STATE_UPDATE, { + clientGameModel, + currentCycle: (game.gameState as any).currentCycle || 0, + }); + this.sendToClient(clientId, updateMessage); + logger.info(`Sent game state update to session ${sessionId} with playerId ${client.playerId}`); + } else { + logger.warn(`Skipping session ${sessionId}: missing client or playerId`); + } + } + } catch (error) { + logger.error("Error broadcasting game state update:", error); + } + } + + private sendToClient(clientId: string, message: Message): void { + const client = this.clients.get(clientId); + if (!client || client.ws.readyState !== WebSocket.OPEN) return; + + try { + client.ws.send(JSON.stringify(message)); + } catch (error) { + logger.error(`Error sending message to ${clientId}:`, error); + } + } + + // Message handlers - Basic game management + private async handleListGames(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + const games = await GameController.listLobbyGames({ sessionId: client.sessionId }); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.LIST_GAMES, { games })); + } catch (error) { + logger.error("Error listing games:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Failed to list games" })); + } + } + + private async handleCreateGame(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + const { name, playerName } = message.payload; + + if (!name || !playerName) { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { message: "Game name and player name are required" }), + ); + return; + } + + const gameData = { + name, + players: [ + { + name: playerName.substring(0, 20), + sessionId: client.sessionId, + position: 0, + }, + ], + }; + + const game = await GameController.createGame(gameData); + + // Add client to game room + client.gameId = game._id.toString(); + client.playerName = playerName; + client.playerId = `player_0`; + + if (!this.gameRooms.has(game._id.toString())) { + this.gameRooms.set(game._id.toString(), new Set()); + } + this.gameRooms.get(game._id.toString())!.add(client.sessionId); + + // Send success response with game options to trigger view transition + const createResponse = { + gameId: game._id, + gameOptions: game.gameOptions, + name: game.name, + playerPosition: 0, + }; + + this.sendToClient(clientId, new Message(MESSAGE_TYPE.CREATE_GAME, createResponse)); + + // Update lobby players about new game + await this.sendUpdatedGameListToLobbyPlayers(game); + } catch (error) { + logger.error("Error creating game:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Failed to create game" })); + } + } + + private async handleJoinGame(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + const { gameId, playerName } = message.payload; + + if (!gameId || !playerName) { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { message: "Game ID and player name are required" }), + ); + return; + } + + const result = await GameController.joinGame({ + gameId, + sessionId: client.sessionId, + playerName: playerName.substring(0, 20), + }); + + if (result.success && result.game) { + const joinResponse = { + success: true, + gameId: gameId, + gameOptions: result.game.gameOptions, + name: result.game.name, + playerPosition: result.playerPosition, + _id: result.game._id, + }; + + this.sendToClient(clientId, new Message(MESSAGE_TYPE.JOIN_GAME, joinResponse)); + + // Update client info + client.gameId = gameId; + client.playerName = playerName; + client.playerId = result.playerId; + + // Add to game room + if (!this.gameRooms.has(gameId)) { + this.gameRooms.set(gameId, new Set()); + } + this.gameRooms.get(gameId)!.add(client.sessionId); + + // Broadcast to other players + this.broadcastToOtherPlayersInGame( + result.game, + client.sessionId, + new Message(MESSAGE_TYPE.CHANGE_GAME_OPTIONS, joinResponse), + ); + + // Update lobby + await this.sendUpdatedGameListToLobbyPlayers(result.game); + } else { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.JOIN_GAME, { + success: false, + error: result.error || "Failed to join game", + }), + ); + } + } catch (error) { + logger.error("Error joining game:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.JOIN_GAME, { + success: false, + error: "Failed to join game", + }), + ); + } + } + + private async handleStartGame(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + // Get gameId from payload + const gameId = message.payload.gameId; + + if (!gameId) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Game ID is required" })); + return; + } + + const result = await GameController.startGame({ + sessionId: client.sessionId, + gameId, + }); + + if (result.success && result.game) { + const playerId = getPlayerId(0); + const clientGameModel = constructClientGameModel(result.game.gameState as any, playerId); + // Send success response with game state + const startResponse = { + success: true, + gameState: clientGameModel, + }; + + this.sendToClient(clientId, new Message(MESSAGE_TYPE.START_GAME, startResponse)); + + // Send client models to each player (like old app.js) + if (result.game && result.game.players) { + for (const player of result.game.players) { + // Only send to human players (not AI) + if (!player.isAI && player.sessionId) { + // Update client info for this player + const playerClientId = this.getClientIdBySessionId(player.sessionId); + const playerClient = playerClientId ? this.clients.get(playerClientId) : null; + + if (playerClient) { + playerClient.gameId = gameId; + playerClient.playerId = getPlayerId(player.position || 0); + + // Subscribe to events for this player + this.subscribeToPlayerEvents(playerClient.playerId); + + // Add to game room + if (!this.gameRooms.has(gameId)) { + this.gameRooms.set(gameId, new Set()); + } + this.gameRooms.get(gameId)!.add(player.sessionId); + } + + // The game state might be a GameModelData object with a modelData property, + // or it might be the ModelData directly. Handle both cases. + let modelData: any; + if (result.game.gameState && typeof result.game.gameState === "object") { + modelData = (result.game.gameState as any).modelData || result.game.gameState; + } else { + logger.warn("Invalid game state structure, skipping client model construction"); + continue; + } + + // Construct the client-specific game model for this player + const clientGameModel = constructClientGameModel(modelData, player.Id); + + const startMessage = new Message(MESSAGE_TYPE.START_GAME, { + success: true, + gameState: clientGameModel, + }); + + this.broadcastToSession(player.sessionId, startMessage); + } + } + } + } else { + const errorResponse = { + success: false, + error: result.error || "Failed to start game", + }; + this.sendToClient(clientId, new Message(MESSAGE_TYPE.START_GAME, errorResponse)); + } + } catch (error) { + logger.error("Error starting game:", error); + const errorResponse = { + success: false, + error: "Failed to start game", + }; + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, errorResponse)); + } + } + + private async handleResumeGame(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + const payload = message.payload as any; + const gameId = payload.gameId; + + if (typeof gameId !== "string") { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Game ID is required" })); + return; + } + + // Always check if all human players are connected before resuming any in-progress game + const game = await Game.findById(gameId); + if (game && game.status === "in_progress") { + const allHumansConnected = await this.areAllHumanPlayersConnected(game); + + if (allHumansConnected) { + // Reset the snapshot time to prevent time jumps when resuming + await this.resetGameSnapshotTime(game); + } + // If not all humans are connected, we still allow the player to join + // but the game will remain in a paused state + } + + const result = await GameController.resumeGame({ + sessionId: client.sessionId, + gameId, + }); + + if (result.success && result.gameData && result.player) { + const clientGameModel = constructClientGameModel(result.gameData, result.player.Id); + + // Update client info for resumed game + client.gameId = gameId; + client.playerId = getPlayerId(result.player.position || 0); + + // Subscribe to events for this player + this.subscribeToPlayerEvents(client.playerId); + + // Add to game room if not already there + if (!this.gameRooms.has(gameId)) { + this.gameRooms.set(gameId, new Set()); + } + this.gameRooms.get(gameId)!.add(client.sessionId); + + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.RESUME_GAME, { + clientGameModel, + playerPosition: result.player.position, + }), + ); + + // Check if all human players are now connected after this player joined + if (game && game.status === "in_progress") { + const allHumansConnected = await this.areAllHumanPlayersConnected(game); + const gameRoom = this.gameRooms.get(gameId); + + if (gameRoom) { + if (allHumansConnected) { + // All players are now connected - notify everyone that the game has resumed + for (const sessionId of gameRoom) { + const notifyClientId = this.getClientIdBySessionId(sessionId); + if (notifyClientId && notifyClientId !== clientId) { + // Don't send to the player who just resumed + this.sendToClient( + notifyClientId, + new Message(MESSAGE_TYPE.GAME_RESUMED, { + gameId: gameId, + }), + ); + } + } + } else { + // Still waiting for other players - notify this player the game is paused + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.GAME_PAUSED, { + reason: "waiting_for_players", + gameId: gameId, + }), + ); + } + } + } + } else { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: result.error || "Unable to find Game to Resume", + }), + ); + } + } catch (error) { + logger.error("Error resuming game:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Failed to resume game" })); + } + } + + private async handleChangeGameOptions(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + const { gameId, gameOptions, playerName } = message.payload; + + if (!gameId || !gameOptions) { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { message: "Game ID and game options are required" }), + ); + return; + } + + const result = await GameController.updateGameOptions({ + sessionId: client.sessionId, + gameId, + gameOptions, + playerName, + }); + + if (result.success && result.game) { + const optionsResponse = { + gameOptions: result.game.gameOptions, + name: result.game.name, + gameId: result.game._id, + }; + + // Send success response to the player who changed options + this.sendToClient(clientId, new Message(MESSAGE_TYPE.CHANGE_GAME_OPTIONS, optionsResponse)); + + // Broadcast to other players in the game + this.broadcastToOtherPlayersInGame( + result.game, + client.sessionId, + new Message(MESSAGE_TYPE.CHANGE_GAME_OPTIONS, optionsResponse), + ); + + // Update lobby players about game changes + await this.sendUpdatedGameListToLobbyPlayers(result.game); + } else { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { message: result.error || "Failed to update game options" }), + ); + } + } catch (error) { + logger.error("Error changing game options:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Failed to update game options" })); + } + } + + private async handleChangePlayerName(clientId: string, message: IMessage): Promise { + try { + const client = this.clients.get(clientId); + if (!client) return; + + const { gameId, playerName } = message.payload; + + if (!gameId || !playerName) { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { message: "Game ID and player name are required" }), + ); + return; + } + + const result = await GameController.changePlayerName({ + sessionId: client.sessionId, + gameId, + playerName, + }); + + if (result.success && result.game) { + const optionsResponse = { + gameOptions: result.game.gameOptions, + name: result.game.name, + gameId: result.game._id, + }; + + // Send success response to the player who changed their name + this.sendToClient(clientId, new Message(MESSAGE_TYPE.CHANGE_GAME_OPTIONS, optionsResponse)); + + // Broadcast to other players in the game (following old game pattern) + this.broadcastToOtherPlayersInGame( + result.game, + client.sessionId, + new Message(MESSAGE_TYPE.CHANGE_GAME_OPTIONS, optionsResponse), + ); + + // Update lobby players about game changes + await this.sendUpdatedGameListToLobbyPlayers(result.game); + } else { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { message: result.error || "Failed to change player name" }), + ); + } + } catch (error) { + logger.error("Error in handleChangePlayerName:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Internal server error" })); + } + } + + private async handleSyncState(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + // NOTE: this doesn't really even have to do anything since we've already synced state + const response = new Message(MESSAGE_TYPE.SYNC_STATE, { + success: true, + }); + + this.sendToClient(clientId, response); + } catch (error) { + logger.error("handleSyncState error:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Sync state failed" })); + } + } + + private async handleSendShips(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.sendShips(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.SEND_SHIPS, { + success: true, + message: "Ships sent successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleSendShips error:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Send ships failed" })); + } + } + + private async handleUpdatePlanetOptions(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.updatePlanetOptions(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.UPDATE_PLANET_OPTIONS, { + success: true, + message: "Worker assignments updated successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleUpdatePlanetOptions error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: error instanceof Error ? error.message : "Unknown error occurred while updating worker assignments", + }), + ); + } + } + + private async handleUpdatePlanetBuildQueue(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.updatePlanetBuildQueue(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.UPDATE_PLANET_BUILD_QUEUE, { + success: true, + message: "Production item added to queue", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleUpdatePlanetBuildQueue error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: error instanceof Error ? error.message : "Unknown error occurred while updating build queue", + }), + ); + } + } + + private async handleClearWaypoint(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.clearWaypoint(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // If game has other players, broadcast the waypoint clearing + if (result.game && result.game.players) { + const response = new Message(MESSAGE_TYPE.CLEAR_WAYPOINT, message.payload); + this.broadcastToOtherPlayersInGame(result.game, client.sessionId, response); + } + } catch (error) { + logger.error("handleClearWaypoint error:", error); + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: "Clear waypoint failed" })); + } + } + + private async handleAdjustResearchPercent(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.adjustResearchPercent(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ADJUST_RESEARCH_PERCENT, { + success: true, + message: "Research allocation updated successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleAdjustResearchPercent error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: "Failed to adjust research percent", + }), + ); + } + } + + private async handleSubmitResearchItem(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.submitResearchItem(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.SUBMIT_RESEARCH_ITEM, { + success: true, + message: "Research started successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleSubmitResearchItem error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: "Failed to start research", + }), + ); + } + } + + private async handleCancelResearchItem(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.cancelResearchItem(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.CANCEL_RESEARCH_ITEM, { + success: true, + message: "Research cancelled successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleCancelResearchItem error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: "Failed to cancel research", + }), + ); + } + } + + private async handleSubmitTrade(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.submitTrade(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.SUBMIT_TRADE, { + success: true, + message: "Trade submitted successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleSubmitTrade error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: "Failed to submit trade", + }), + ); + } + } + + private async handleCancelTrade(clientId: string, message: IMessage): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + const result = await GameController.cancelTrade(client.sessionId, message.payload); + + if (!result.success) { + this.sendToClient(clientId, new Message(MESSAGE_TYPE.ERROR, { message: result.error })); + return; + } + + // Send success response to the requesting client + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.CANCEL_TRADE, { + success: true, + message: "Trade cancelled successfully", + }), + ); + + // Broadcast game state update to all players in the game + if (result.game && result.game._id) { + await this.broadcastGameStateUpdate(result.game._id.toString()); + } + } catch (error) { + logger.error("handleCancelTrade error:", error); + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.ERROR, { + message: "Failed to cancel trade", + }), + ); + } + } + + private async handleChatMessage(clientId: string, message: IMessage): Promise { + // TODO: Implement chat system like old app.js + logger.warn("handleChatMessage not yet implemented"); + } + + private async handleExitResign(clientId: string, message: IMessage): Promise { + // TODO: Implement based on GameController.exitResign + logger.warn("handleExitResign not yet implemented"); + } + + private async handleLogout(clientId: string, message: IMessage): Promise { + // TODO: Implement logout + logger.warn("handleLogout not yet implemented"); + } + + private async handlePing(clientId: string): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + // Always send a basic PONG response with session information + let pongPayload: any = { + timestamp: new Date().toISOString(), + }; + + // If client is in an active game, include their current game state + if (client.gameId && client.playerId) { + try { + const game = await Game.findById(client.gameId); + if (game && game.status === "in_progress") { + const clientGameModel = constructClientGameModel(game.gameState as any, client.playerId); + pongPayload = { + ...pongPayload, + clientGameModel, + currentCycle: (game.gameState as any).currentCycle || 0, + }; + logger.info(`Sending PONG with updated game state to session ${client.sessionId}`); + } + } catch (error) { + logger.error("Error getting game state for PING response:", error); + } + } + + this.sendToClient(clientId, new Message(MESSAGE_TYPE.PONG, pongPayload)); + } + + // Helper methods for game management + private async sendUpdatedGameListToLobbyPlayers(gameDoc: any): Promise { + logger.debug("sendUpdatedGameListToLobbyPlayers for game:", gameDoc._id); + + try { + // Get the complete updated games list instead of just the single game + const games = await GameController.listLobbyGames({ sessionId: "system" }); + const messageForPlayers = new Message(MESSAGE_TYPE.GAME_LIST_UPDATED, { games }); + + const chatRoom = await GameController.getChatRoomWithSessions(null); + if (chatRoom) { + for (const session of chatRoom.sessions) { + this.broadcastToSession(session.sessionId, messageForPlayers); + } + } + } catch (error) { + logger.error("sendUpdatedGameListToLobbyPlayers error:", error); + } + } + + private broadcastToOtherPlayersInGame(game: IGame, sessionId: string, message: Message): void { + const playersBySessionKey = this.getOtherPlayersBySessionKeyFromGame(game, sessionId); + this.broadcast(playersBySessionKey, message); + } + + private getOtherPlayersBySessionKeyFromGame( + game: IGame, + currentPlayerSessionKey: string | null, + ): Record { + const playersBySessionKey: Record = {}; + for (const player of game.players) { + if (player.sessionId && player.sessionId !== currentPlayerSessionKey) { + playersBySessionKey[player.sessionId] = player; + } + } + return playersBySessionKey; + } + + // Broadcasting methods (like old app.js) + public broadcast(playersBySessionKey: Record, message: Message): void { + const data = JSON.stringify(message); + for (const [sessionId, clientId] of this.sessionLookup.entries()) { + if (sessionId in playersBySessionKey) { + const client = this.clients.get(clientId); + if (client && client.ws.readyState === WebSocket.OPEN) { + client.ws.send(data); + } + } + } + } + + public broadcastToSession(playerSessionKey: string, message: Message): void { + const data = JSON.stringify(message); + const clientId = this.sessionLookup.get(playerSessionKey); + if (clientId) { + const client = this.clients.get(clientId); + if (client && client.ws.readyState === WebSocket.OPEN) { + client.ws.send(data); + } + } + } + + // Connection management + private async handleDisconnection(clientId: string): Promise { + const client = this.clients.get(clientId); + if (!client) return; + + try { + // Check if this is a human player in an active game + if (client.gameId && client.playerId) { + await this.handlePlayerDisconnection(client); + } + + // Update session status + await Session.findOneAndUpdate({ sessionId: client.sessionId }, { connectionStatus: "disconnected" }); + + // Remove from game room if in one + if (client.gameId) { + const gameRoom = this.gameRooms.get(client.gameId); + if (gameRoom) { + gameRoom.delete(client.sessionId); + if (gameRoom.size === 0) { + this.gameRooms.delete(client.gameId); + } + } + + // TODO: Handle chat room leaving like old app.js + } + + this.clients.delete(clientId); + this.sessionLookup.delete(client.sessionId); + logger.info(`Session ${client.sessionId} disconnected`); + } catch (error) { + logger.error("Error handling disconnection:", error); + } + } + + private async handlePlayerDisconnection(client: IConnectedClient): Promise { + const game = await Game.findById(client.gameId); + if (!game || game.status !== "in_progress") return; + + // Check if this is a human player + const player = game.players?.find((p) => p.sessionId === client.sessionId); + if (!player || player.isAI) return; + + // Notify other players about the disconnection (don't change game state) + await this.notifyForPlayerDisconnection(game, player); + } + + private async notifyForPlayerDisconnection(game: IGame, disconnectedPlayer: any): Promise { + // Just notify all connected players - don't change game status + const gameRoom = this.gameRooms.get(game.id); + if (gameRoom) { + for (const sessionId of gameRoom) { + const clientId = this.getClientIdBySessionId(sessionId); + if (clientId) { + this.sendToClient( + clientId, + new Message(MESSAGE_TYPE.PLAYER_DISCONNECTED, { + playerName: disconnectedPlayer.name, + gameId: game.id, + }), + ); + } + } + } + + logger.info(`Notified players about ${disconnectedPlayer.name} disconnection in game ${game.id}`); + } + + private async areAllHumanPlayersConnected(game: IGame): Promise { + const gameRoom = this.gameRooms.get(game.id); + if (!gameRoom) return false; + + const humanPlayers = game.players?.filter((p) => !p.isAI) || []; + return humanPlayers.every((player) => player.sessionId && gameRoom.has(player.sessionId)); + } + + private async resetGameSnapshotTime(game: IGame): Promise { + try { + // Reset the game snapshot time to prevent time jump when resuming + const gameModelData = GameModel.constructGridWithModelData(game.gameState as ModelData); + + // Use the engine's resetGameSnapshotTime function + const updatedGameModel = resetGameSnapshotTime(gameModelData); + + // Save the updated game state + game.gameState = updatedGameModel.modelData; + await persistGame(game); + + logger.info(`Reset snapshot time for game ${game.id} to prevent time jumps`); + } catch (error) { + logger.error("Error resetting game snapshot time:", error); + } + } + + private startPingInterval(): void { + this.pingInterval = setInterval(() => { + const now = new Date(); + const timeout = 30000; // 30 seconds + + for (const [clientId, client] of this.clients.entries()) { + if (now.getTime() - client.lastPing.getTime() > timeout) { + logger.warn(`Session ${client.sessionId} timed out`); + this.handleDisconnection(clientId); + } + } + }, 15000); // Check every 15 seconds + } + + public close(): void { + if (this.pingInterval) { + clearInterval(this.pingInterval); + } + this.wss.close(); + } +} diff --git a/apps/astriarch-backend/src/websocket/index.ts b/apps/astriarch-backend/src/websocket/index.ts new file mode 100644 index 00000000..b1749ea2 --- /dev/null +++ b/apps/astriarch-backend/src/websocket/index.ts @@ -0,0 +1,2 @@ +export { WebSocketServer } from "./WebSocketServer"; +export type { IConnectedClient } from "./WebSocketServer"; diff --git a/apps/astriarch-backend/tsconfig.json b/apps/astriarch-backend/tsconfig.json new file mode 100644 index 00000000..6672bc96 --- /dev/null +++ b/apps/astriarch-backend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "types": ["node"] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/tests/**/*" + ] +} diff --git a/apps/astriarch-frontend-sveltekit/.gitignore b/apps/astriarch-frontend-sveltekit/.gitignore new file mode 100644 index 00000000..42250352 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +*storybook.log +storybook-static diff --git a/apps/astriarch-frontend-sveltekit/.npmrc b/apps/astriarch-frontend-sveltekit/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/astriarch-frontend-sveltekit/.prettierignore b/apps/astriarch-frontend-sveltekit/.prettierignore new file mode 100644 index 00000000..7d74fe24 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/apps/astriarch-frontend-sveltekit/.prettierrc b/apps/astriarch-frontend-sveltekit/.prettierrc new file mode 100644 index 00000000..7ebb855b --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/apps/astriarch-frontend-sveltekit/.storybook/main.ts b/apps/astriarch-frontend-sveltekit/.storybook/main.ts new file mode 100644 index 00000000..921babac --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/.storybook/main.ts @@ -0,0 +1,11 @@ +import type { StorybookConfig } from '@storybook/sveltekit'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'], + addons: ['@storybook/addon-svelte-csf'], + framework: { + name: '@storybook/sveltekit', + options: {} + } +}; +export default config; diff --git a/apps/astriarch-frontend-sveltekit/.storybook/preview.ts b/apps/astriarch-frontend-sveltekit/.storybook/preview.ts new file mode 100644 index 00000000..9b795cc6 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/.storybook/preview.ts @@ -0,0 +1,14 @@ +import type { Preview } from '@storybook/sveltekit'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i + } + } + } +}; + +export default preview; diff --git a/apps/astriarch-frontend-sveltekit/README.md b/apps/astriarch-frontend-sveltekit/README.md new file mode 100644 index 00000000..b5b29507 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/apps/astriarch-frontend-sveltekit/components.json b/apps/astriarch-frontend-sveltekit/components.json new file mode 100644 index 00000000..f2c98482 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "gray" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/apps/astriarch-frontend-sveltekit/eslint.config.js b/apps/astriarch-frontend-sveltekit/eslint.config.js new file mode 100644 index 00000000..6284fab2 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/eslint.config.js @@ -0,0 +1,44 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from 'eslint-plugin-storybook'; + +import prettier from 'eslint-config-prettier'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + 'no-undef': 'off' + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + }, + storybook.configs['flat/recommended'] +); diff --git a/apps/astriarch-frontend-sveltekit/package.json b/apps/astriarch-frontend-sveltekit/package.json new file mode 100644 index 00000000..60055eb7 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/package.json @@ -0,0 +1,66 @@ +{ + "name": "astriarch-frontend-sveltekit", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "astriarch-engine": "workspace:*", + "konva": "^9.3.22" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@internationalized/date": "^3.8.2", + "@lucide/svelte": "^0.515.0", + "@storybook/addon-svelte-csf": "^5.0.6", + "@storybook/sveltekit": "^9.0.16", + "@sveltejs/adapter-cloudflare": "^7.0.0", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^22", + "bits-ui": "^2.8.10", + "clsx": "^2.1.1", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-storybook": "^9.0.16", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "shadcn-svelte": "^1.0.6", + "storybook": "^9.0.16", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.3.5", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^7.0.4", + "vitest": "^3.2.3" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + } +} diff --git a/apps/astriarch-frontend-sveltekit/src/app.css b/apps/astriarch-frontend-sveltekit/src/app.css new file mode 100644 index 00000000..9ef3f4ec --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/app.css @@ -0,0 +1,182 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; +@import './lib/styles/astriarch.css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.13 0.028 261.692); + --card: oklch(1 0 0); + --card-foreground: oklch(0.13 0.028 261.692); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.13 0.028 261.692); + --primary: oklch(0.21 0.034 264.665); + --primary-foreground: oklch(0.985 0.002 247.839); + --secondary: oklch(0.967 0.003 264.542); + --secondary-foreground: oklch(0.21 0.034 264.665); + --muted: oklch(0.967 0.003 264.542); + --muted-foreground: oklch(0.551 0.027 264.364); + --accent: oklch(0.967 0.003 264.542); + --accent-foreground: oklch(0.21 0.034 264.665); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.928 0.006 264.531); + --input: oklch(0.928 0.006 264.531); + --ring: oklch(0.707 0.022 261.325); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0.002 247.839); + --sidebar-foreground: oklch(0.13 0.028 261.692); + --sidebar-primary: oklch(0.21 0.034 264.665); + --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent: oklch(0.967 0.003 264.542); + --sidebar-accent-foreground: oklch(0.21 0.034 264.665); + --sidebar-border: oklch(0.928 0.006 264.531); + --sidebar-ring: oklch(0.707 0.022 261.325); +} + +.dark { + --background: oklch(0.13 0.028 261.692); + --foreground: oklch(0.985 0.002 247.839); + --card: oklch(0.21 0.034 264.665); + --card-foreground: oklch(0.985 0.002 247.839); + --popover: oklch(0.21 0.034 264.665); + --popover-foreground: oklch(0.985 0.002 247.839); + --primary: oklch(0.928 0.006 264.531); + --primary-foreground: oklch(0.21 0.034 264.665); + --secondary: oklch(0.278 0.033 256.848); + --secondary-foreground: oklch(0.985 0.002 247.839); + --muted: oklch(0.278 0.033 256.848); + --muted-foreground: oklch(0.707 0.022 261.325); + --accent: oklch(0.278 0.033 256.848); + --accent-foreground: oklch(0.985 0.002 247.839); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.034 264.665); + --sidebar-foreground: oklch(0.985 0.002 247.839); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent: oklch(0.278 0.033 256.848); + --sidebar-accent-foreground: oklch(0.985 0.002 247.839); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + font-family: + 'Orbitron', + system-ui, + -apple-system, + sans-serif; + } + + /* Default font family for all elements */ + html { + font-family: + 'Orbitron', + system-ui, + -apple-system, + sans-serif; + } + + /* Ensure buttons and inputs use Orbitron */ + button, + input, + select, + textarea { + font-family: + 'Orbitron', + system-ui, + -apple-system, + sans-serif; + } + + /* Headings use Orbitron with appropriate weights */ + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: + 'Orbitron', + system-ui, + -apple-system, + sans-serif; + font-weight: 600; + } + + /* Utility classes for font family */ + .font-orbitron { + font-family: + 'Orbitron', + system-ui, + -apple-system, + sans-serif; + } + + .font-game { + font-family: + 'Orbitron', + system-ui, + -apple-system, + sans-serif; + } +} + +@layer utilities { +} diff --git a/apps/astriarch-frontend-sveltekit/src/app.d.ts b/apps/astriarch-frontend-sveltekit/src/app.d.ts new file mode 100644 index 00000000..da08e6da --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/astriarch-frontend-sveltekit/src/app.html b/apps/astriarch-frontend-sveltekit/src/app.html new file mode 100644 index 00000000..5d63edeb --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/app.html @@ -0,0 +1,19 @@ + + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/demo.spec.ts b/apps/astriarch-frontend-sveltekit/src/demo.spec.ts new file mode 100644 index 00000000..e07cbbd7 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/demo.spec.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/battleship.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/battleship.svg new file mode 100644 index 00000000..cff9f3f6 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/battleship.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/builder.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/builder.svg new file mode 100644 index 00000000..bb35bbdd --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/builder.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/colony.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/colony.svg new file mode 100644 index 00000000..44b6aaf1 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/colony.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/cruiser.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/cruiser.svg new file mode 100644 index 00000000..37a13329 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/cruiser.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/defender.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/defender.svg new file mode 100644 index 00000000..b88a6f93 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/defender.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/destroyer.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/destroyer.svg new file mode 100644 index 00000000..8280c6e6 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/destroyer.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/energy.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/energy.svg new file mode 100644 index 00000000..dc66c625 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/energy.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/exit.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/exit.svg new file mode 100644 index 00000000..cc986b20 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/exit.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/factory.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/factory.svg new file mode 100644 index 00000000..8f584c0d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/factory.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/farm.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/farm.svg new file mode 100644 index 00000000..139b9eaa --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/farm.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/farmer.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/farmer.svg new file mode 100644 index 00000000..a8ab1843 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/farmer.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/food.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/food.svg new file mode 100644 index 00000000..ccec2d3f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/food.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/iridium.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/iridium.svg new file mode 100644 index 00000000..1bacfc5d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/iridium.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/mine.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/mine.svg new file mode 100644 index 00000000..d07c366f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/mine.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/miner.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/miner.svg new file mode 100644 index 00000000..6884d339 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/miner.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ore.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ore.svg new file mode 100644 index 00000000..3ab0fde4 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ore.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/population.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/population.svg new file mode 100644 index 00000000..9afa88a3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/population.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/production.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/production.svg new file mode 100644 index 00000000..4c99f84d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/production.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/research.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/research.svg new file mode 100644 index 00000000..d87ce532 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/research.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/scout.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/scout.svg new file mode 100644 index 00000000..28934621 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/scout.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_attack.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_attack.svg new file mode 100644 index 00000000..83fda240 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_attack.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_defense.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_defense.svg new file mode 100644 index 00000000..baaba029 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_defense.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_speed.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_speed.svg new file mode 100644 index 00000000..b67a3f7c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/ship_speed.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/space_platform.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/space_platform.svg new file mode 100644 index 00000000..40e6aa16 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/space_platform.svg @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/volume.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/volume.svg new file mode 100644 index 00000000..c94355b3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/volume.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/volume_muted.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/volume_muted.svg new file mode 100644 index 00000000..e0ed0d25 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/icons/volume_muted.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-arid.png b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-arid.png new file mode 100644 index 00000000..e3c98278 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-arid.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-asteroid.png b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-asteroid.png new file mode 100644 index 00000000..45f42dc6 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-asteroid.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-dead.png b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-dead.png new file mode 100644 index 00000000..0e52f5f2 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-dead.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-terestrial.png b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-terestrial.png new file mode 100644 index 00000000..94826a2e Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/lib/assets/images/planet-terestrial.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/logo/logo.svg b/apps/astriarch-frontend-sveltekit/src/lib/assets/logo/logo.svg new file mode 100644 index 00000000..3f092ab3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/logo/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgLarge.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgLarge.svelte new file mode 100644 index 00000000..bca557a9 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgLarge.svelte @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgMedium.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgMedium.svelte new file mode 100644 index 00000000..313d7a06 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgMedium.svelte @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgSmall.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgSmall.svelte new file mode 100644 index 00000000..b046a501 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/notification/NotificationSvgSmall.svelte @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineLarge.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineLarge.svelte new file mode 100644 index 00000000..19fed190 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineLarge.svelte @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineMedium.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineMedium.svelte new file mode 100644 index 00000000..9edc3d48 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineMedium.svelte @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineSmall.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineSmall.svelte new file mode 100644 index 00000000..12ec68be --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab-controller/TabControllerSvgBaselineSmall.svelte @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/assets/tab/TabSvgUnselected.svelte b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab/TabSvgUnselected.svelte new file mode 100644 index 00000000..65febc78 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/assets/tab/TabSvgUnselected.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/box/Box.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/box/Box.svelte new file mode 100644 index 00000000..d9d97d3e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/box/Box.svelte @@ -0,0 +1,11 @@ + + +
+ +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/Button.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/Button.svelte new file mode 100644 index 00000000..ab98b570 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/Button.svelte @@ -0,0 +1,100 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvg.svelte new file mode 100644 index 00000000..cc423d7c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvg.svelte @@ -0,0 +1,38 @@ + + +
+ +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgLargePrimary.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgLargePrimary.svelte new file mode 100644 index 00000000..e6070061 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgLargePrimary.svelte @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgLargeSecondary.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgLargeSecondary.svelte new file mode 100644 index 00000000..e2bb3c54 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgLargeSecondary.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgMediumPrimary.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgMediumPrimary.svelte new file mode 100644 index 00000000..a2461d19 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgMediumPrimary.svelte @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgMediumSecondary.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgMediumSecondary.svelte new file mode 100644 index 00000000..80846f3a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgMediumSecondary.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgSmallPrimary.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgSmallPrimary.svelte new file mode 100644 index 00000000..d1f78d9e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgSmallPrimary.svelte @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgSmallSecondary.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgSmallSecondary.svelte new file mode 100644 index 00000000..81e00dd8 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/button/ButtonSvgSmallSecondary.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/Card.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/Card.svelte new file mode 100644 index 00000000..ea7975de --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/Card.svelte @@ -0,0 +1,79 @@ + + + +
{ + if (onclick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + handleClick(); + } + }} + role={onclick ? 'button' : undefined} + tabindex={onclick ? 0 : undefined} + {...restProps} +> + + + + +
+ {#if label} +
+ {label} +
+ {/if} + {#if children} + {@render children()} + {/if} +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvg.svelte new file mode 100644 index 00000000..dc808787 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvg.svelte @@ -0,0 +1,14 @@ + + +
+ +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvgLarge.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvgLarge.svelte new file mode 100644 index 00000000..86d1fc75 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvgLarge.svelte @@ -0,0 +1,104 @@ + + +{#if !enabled} + + + + + + + + + + + + + + + + + +{:else} + + + + + + + + + + + + + + + + + + +{/if} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvgMedium.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvgMedium.svelte new file mode 100644 index 00000000..f80aacca --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/card/CardSvgMedium.svelte @@ -0,0 +1,79 @@ + + +{#if !enabled} + + + + + + + + + +{:else} + + + + + + + + + + + + + + + + + + +{/if} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dialog/Dialog.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dialog/Dialog.svelte new file mode 100644 index 00000000..493d2c29 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dialog/Dialog.svelte @@ -0,0 +1,47 @@ + + + + + + {title} + + Dialog content + +
+ {@render children?.()} +
+ + + + + +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dropdown/Dropdown.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dropdown/Dropdown.svelte new file mode 100644 index 00000000..2e0eef45 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dropdown/Dropdown.svelte @@ -0,0 +1,133 @@ + + +
+ + + + {displayText} + + + +
+ + + +
+ + +
+ + + {#each options as option} + + {option.label} + + {/each} + +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dropdown/DropdownSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dropdown/DropdownSvg.svelte new file mode 100644 index 00000000..2b44a7d3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/dropdown/DropdownSvg.svelte @@ -0,0 +1,61 @@ + + + + + {#if variant === 'primary'} + + + + + + + + {/if} + + + + {#if variant === 'primary'} + + {/if} + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/icon-image/IconImage.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/icon-image/IconImage.svelte new file mode 100644 index 00000000..40914575 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/icon-image/IconImage.svelte @@ -0,0 +1,71 @@ + + +
+ +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/index.ts new file mode 100644 index 00000000..db3891ce --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/index.ts @@ -0,0 +1,36 @@ +// Button components +export { default as Button } from './button/Button.svelte'; +export { default as ButtonSvg } from './button/ButtonSvg.svelte'; + +// Card components +export { default as Card } from './card/Card.svelte'; +export { default as CardSvg } from './card/CardSvg.svelte'; + +// Dropdown components +export { default as Dropdown } from './dropdown/Dropdown.svelte'; +export { default as DropdownSvg } from './dropdown/DropdownSvg.svelte'; + +// Icon components +export { default as IconImage } from './icon-image/IconImage.svelte'; + +// Text, Box, and Layout components +export { default as Text } from './text/Text.svelte'; +export { default as Box } from './box/Box.svelte'; +export { default as TopOverview } from './top-overview/TopOverview.svelte'; + +// Atoms +export { default as AvailablePlanetProductionItem } from '../controls/AvailablePlanetProductionItem.svelte'; +export { default as TopOverviewItem } from './top-overview-item/TopOverviewItem.svelte'; + +// Dialog and Notification components +export { default as Dialog } from './dialog/Dialog.svelte'; +export { default as Notification } from './notification/Notification.svelte'; + +// Navigation and Tab components +export { default as NavigationController } from './navigation-controller/NavigationController.svelte'; +export { default as NavigationTab } from './navigation-tab/NavigationTab.svelte'; +export { default as TabController } from './tab-controller/TabController.svelte'; +export { default as Tab } from './tab/Tab.svelte'; + +// Types +export type { Size, IconImageType, TabControllerTab } from './types.js'; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-controller/NavigationController.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-controller/NavigationController.svelte new file mode 100644 index 00000000..c088efcf --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-controller/NavigationController.svelte @@ -0,0 +1,51 @@ + + +
+
+ {#each items as item, i} + handleTabClick(i)} + /> + {/each} +
+ + +
+ {#if items[selectedIndex]?.content} + {@render items[selectedIndex].content()} + {/if} +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTab.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTab.svelte new file mode 100644 index 00000000..b25ba5a3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTab.svelte @@ -0,0 +1,29 @@ + + +
+ + {label} + + + {#if selected} + + {:else} + + {/if} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTabSelectedSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTabSelectedSvg.svelte new file mode 100644 index 00000000..32dd2b76 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTabSelectedSvg.svelte @@ -0,0 +1,72 @@ + + + + + e.key === 'Enter' && onclick?.()} + role="button" + tabindex="0" + d="M188.3 14H15.9625C15.0054 14 14.5951 15.2153 15.3565 15.7954L73.852 60.3635C75.2454 61.4251 76.9487 62 78.7004 62H251.037C251.995 62 252.405 60.7847 251.643 60.2046L193.148 15.6365C191.755 14.5749 190.051 14 188.3 14Z" + fill="#00FFFF" + /> + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTabUnselectedSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTabUnselectedSvg.svelte new file mode 100644 index 00000000..b41efa1c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/navigation-tab/NavigationTabUnselectedSvg.svelte @@ -0,0 +1,44 @@ + + + + e.key === 'Enter' && onclick?.()} + role="button" + tabindex="0" + opacity="0.9" + d="M61.0641 44.7726L4.92509 2H174.3C175.613 2 176.891 2.43121 177.936 3.22741L234.075 46H64.7004C63.3866 46 62.1091 45.5688 61.0641 44.7726ZM236.431 47.7954C236.43 47.7947 236.429 47.7939 236.428 47.7932L236.431 47.7954Z" + fill="#1B1F25" + stroke="url(#paint0_linear_143_69328)" + stroke-width="4" + /> + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/notification/Notification.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/notification/Notification.svelte new file mode 100644 index 00000000..9723e8cd --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/notification/Notification.svelte @@ -0,0 +1,57 @@ + + +
+ +
+ {#if children} + {@render children()} + {:else if label} + {label} + {/if} +
+ + +
+ {#if size === 'xs' || size === 'sm'} + + {:else if size === 'md'} + + {:else} + + {/if} +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/notification/NotificationItem.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/notification/NotificationItem.svelte new file mode 100644 index 00000000..568d31bf --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/notification/NotificationItem.svelte @@ -0,0 +1,126 @@ + + +
+ + + {getNotificationTypeLabel(notification.type)}: + + {notification.message} + +
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab-controller/TabController.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab-controller/TabController.svelte new file mode 100644 index 00000000..d7cf2e30 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab-controller/TabController.svelte @@ -0,0 +1,63 @@ + + +
+ +
+ {#each tabs as tab, i} + handleTabChange(i)} + style="z-index: {i === tabIndex ? tabs.length : tabs.length - i};" + /> + {/each} +
+ + +
+ {#if size === 'xs' || size === 'sm'} + + {:else if size === 'md'} + + {:else} + + {/if} +
+ + +
+ {#if tabs[tabIndex]?.children} + {@render tabs[tabIndex].children?.()} + {/if} +
+ + {#if children} + {@render children()} + {/if} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab/Tab.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab/Tab.svelte new file mode 100644 index 00000000..eea82cfa --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab/Tab.svelte @@ -0,0 +1,47 @@ + + +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onclick?.(); + } + }} + {...restProps} +> +
+ {label} +
+ +
+ {#if selected} + + {:else} + + {/if} +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab/TabSvgSelected.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab/TabSvgSelected.svelte new file mode 100644 index 00000000..b15e7216 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/tab/TabSvgSelected.svelte @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/text/Text.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/text/Text.svelte new file mode 100644 index 00000000..ae953d2b --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/text/Text.svelte @@ -0,0 +1,24 @@ + + + + {#if children} + {@render children()} + {/if} + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/TopOverviewButton.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/TopOverviewButton.svelte new file mode 100644 index 00000000..58e07959 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/TopOverviewButton.svelte @@ -0,0 +1,37 @@ + + +
+ + +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/TopOverviewButtonBackgroundSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/TopOverviewButtonBackgroundSvg.svelte new file mode 100644 index 00000000..5431b2fe --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/TopOverviewButtonBackgroundSvg.svelte @@ -0,0 +1,21 @@ + +
+ + + +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/VolumeButton.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/VolumeButton.svelte new file mode 100644 index 00000000..14d76785 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-button/VolumeButton.svelte @@ -0,0 +1,70 @@ + + +
+ + + + + + {#if showAudioControls} +
+ +
+ {/if} +
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-item/TopOverviewItem.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-item/TopOverviewItem.svelte new file mode 100644 index 00000000..183519d5 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview-item/TopOverviewItem.svelte @@ -0,0 +1,75 @@ + + + +
{ + if (onClick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + handleClick(); + } + }} + role={onClick ? 'button' : undefined} + tabindex={onClick ? 0 : undefined} + {...restProps} +> + + + {amountContent} + + {#if children} + {@render children()} + {/if} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview/TopOverview.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview/TopOverview.svelte new file mode 100644 index 00000000..e5b86d0e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview/TopOverview.svelte @@ -0,0 +1,86 @@ + + +
+ + + + + + + + + + + + {#if onExitGame} + + + + {/if} + + +
+ + + +
+ +
+ + +
+ +
+
+ + +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview/TopOverviewFrameSvg.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview/TopOverviewFrameSvg.svelte new file mode 100644 index 00000000..6d515830 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/top-overview/TopOverviewFrameSvg.svelte @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/types.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/types.ts new file mode 100644 index 00000000..20ac3699 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/astriarch/types.ts @@ -0,0 +1,47 @@ +import type { Snippet } from 'svelte'; + +export type Size = 'lg' | 'md' | 'sm' | 'xs'; + +export type IconImageType = + | 'population' + | 'food' + | 'energy' + | 'research' + | 'ore' + | 'iridium' + | 'exit' + | 'volume' + | 'volume_muted' + | 'ship_attack' + | 'ship_defense' + | 'ship_speed' + | 'farm' + | 'mine' + | 'colony' + | 'factory' + | 'space_platform' + | 'defender' + | 'scout' + | 'destroyer' + | 'cruiser' + | 'battleship'; + +export interface TabControllerTab { + label: string; + children?: Snippet; + onclick?: () => void; +} + +export interface PlanetResourceData { + food: number; + energy: number; + research: number; + ore: number; + iridium: number; + production: number; +} + +export interface ResourceData { + total: PlanetResourceData; + perTurn: PlanetResourceData; +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/AvailablePlanetProductionItem.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/AvailablePlanetProductionItem.svelte new file mode 100644 index 00000000..387d5f8a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/AvailablePlanetProductionItem.svelte @@ -0,0 +1,79 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/Logo.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/Logo.svelte new file mode 100644 index 00000000..49c3b65f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/Logo.svelte @@ -0,0 +1,63 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/WindowFrame.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/WindowFrame.svelte new file mode 100644 index 00000000..2c148227 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/WindowFrame.svelte @@ -0,0 +1,85 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/index.ts new file mode 100644 index 00000000..afe956b6 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/index.ts @@ -0,0 +1,3 @@ +// Atom components - the smallest reusable UI elements +export { default as Logo } from './Logo.svelte'; +export { default as WindowFrame } from './WindowFrame.svelte'; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Background.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Background.svelte new file mode 100644 index 00000000..71edae43 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Background.svelte @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke1.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke1.svelte new file mode 100644 index 00000000..a97b2c0e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke1.svelte @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke2.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke2.svelte new file mode 100644 index 00000000..dffc6ab1 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke2.svelte @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke3.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke3.svelte new file mode 100644 index 00000000..15c12c5c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/atoms/window-frame/Stroke3.svelte @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/audio/AudioControls.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/audio/AudioControls.svelte new file mode 100644 index 00000000..6363036e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/audio/AudioControls.svelte @@ -0,0 +1,354 @@ + + +
+ +
+ + + + + {#if showVolumeSlider && !compact} +
+ +
+ {/if} + + + {#if compact} + + {/if} +
+ + + {#if compact && showVolumePanel} +
+
+ + +
+ Phase: {$audioStatus.phase} +
+
+
+ {/if} + + + {#if !$audioStatus.enabled} +
+ + + + + +
+ {/if} +
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/controls/AvailablePlanetProductionItem.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/controls/AvailablePlanetProductionItem.svelte new file mode 100644 index 00000000..f7d21a7a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/controls/AvailablePlanetProductionItem.svelte @@ -0,0 +1,96 @@ + + + +{#snippet cardContent()} + +
+ +
+ {name} +
+ + + {#if displayCosts.length > 0} +
+ {#each displayCosts as [resource, amount]} +
+ +
+ + + {amount} + +
+ {/each} +
+ {/if} +
+
+{/snippet} + +{#if description} + + + + {@render cardContent()} + + +
+ {description} +
+
+
+
+{:else} + {@render cardContent()} +{/if} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/examples/LogoShowcase.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/examples/LogoShowcase.svelte new file mode 100644 index 00000000..a1ab8617 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/examples/LogoShowcase.svelte @@ -0,0 +1,95 @@ + + +
+
+

Logo Variations

+ + +
+

Text Logos

+
+ +
+

Different Sizes

+
+ + + + +
+
+ + +
+

+ Different Variants +

+
+ + + +
+
+
+
+ + +
+

Image Logos

+
+ +
+

Different Sizes

+
+ + + + +
+
+ + +
+

+ Different Variants +

+
+ + + +
+
+
+
+ + +
+

Usage in Context

+
+ +
+

+ Navigation Header +

+
+ +
Menu Items
+
+
+ + +
+

Login Card

+
+ +

+ Welcome back to the galaxy +

+
+
+
+
+
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/DrawnFleet.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/DrawnFleet.ts new file mode 100644 index 00000000..8b5ef7d1 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/DrawnFleet.ts @@ -0,0 +1,200 @@ +import type { FleetData, PlayerData, ClientModelData } from 'astriarch-engine'; +import Konva from 'konva'; + +const FLEET_ICON_SIZE = 11; +const ETA_TEXT_SIZE = 8; +const TRAVEL_LINE_WIDTH = 1.5; + +export function createDrawnFleet(fleetData: FleetData, gameModel: ClientModelData) { + return new DrawnFleet(fleetData, gameModel); +} + +export class DrawnFleet { + public group: Konva.Group; + private fleetData: FleetData; + private gameModel: ClientModelData; + private owner: PlayerData | null = null; + + // Visual elements + private travelLine!: Konva.Line; + private fleetIcon!: Konva.Rect; + private etaText!: Konva.Text; + + // Calculated position properties + private travelDistancePoint = { x: 0, y: 0 }; + + constructor(fleetData: FleetData, gameModel: ClientModelData) { + this.group = new Konva.Group(); + this.fleetData = fleetData; + this.gameModel = gameModel; + + this.createVisualElements(); + this.update(gameModel); + } + + private createVisualElements() { + // Travel line from origin to destination + this.travelLine = new Konva.Line({ + points: [0, 0, 0, 0], + stroke: 'green', + strokeWidth: TRAVEL_LINE_WIDTH, + visible: false + }); + this.group.add(this.travelLine); + + // Fleet icon (starship representation) + this.fleetIcon = new Konva.Rect({ + x: -FLEET_ICON_SIZE / 2, + y: -FLEET_ICON_SIZE / 2, + width: FLEET_ICON_SIZE, + height: FLEET_ICON_SIZE, + fill: 'green', + stroke: 'white', + strokeWidth: 1, + visible: false + }); + this.group.add(this.fleetIcon); + + // ETA text showing turns remaining + this.etaText = new Konva.Text({ + x: 8, + y: -4, + text: 'ETA', + fontSize: ETA_TEXT_SIZE, + fontFamily: 'Orbitron, monospace', + fontStyle: 'bold', + fill: 'green', + visible: false + }); + this.group.add(this.etaText); + } + + public update(clientGameModel: ClientModelData) { + this.gameModel = clientGameModel; + + // Find the owner of this fleet by looking through fleetsInTransit + this.owner = null; + + // Check main player first + for (const fleet of clientGameModel.mainPlayer.fleetsInTransit) { + if (fleet === this.fleetData) { + this.owner = clientGameModel.mainPlayer; + break; + } + } + + // Then check other client players (future enhancement) + // Note: ClientPlayer structure might not include fleetsInTransit + // Most fleets will likely belong to the main player for now + + this.updateTravelLine(); + this.updateVisualElements(); + } + + public updateTravelLine() { + // Only show travel elements if fleet is actually traveling + if (!this.fleetData.totalTravelDistance || this.fleetData.totalTravelDistance <= 0) { + this.hideAllElements(); + return; + } + + if ( + !this.fleetData.parsecsToDestination || + !this.fleetData.destinationHexMidPoint || + !this.fleetData.travelingFromHexMidPoint + ) { + this.hideAllElements(); + return; + } + + // Calculate travel progress + const traveled = + (this.fleetData.totalTravelDistance - this.fleetData.parsecsToDestination) / + this.fleetData.totalTravelDistance; + + // Calculate current position along travel line + const fromX = this.fleetData.travelingFromHexMidPoint.x; + const fromY = this.fleetData.travelingFromHexMidPoint.y; + const toX = this.fleetData.destinationHexMidPoint.x; + const toY = this.fleetData.destinationHexMidPoint.y; + + this.travelDistancePoint.x = fromX - (fromX - toX) * traveled; + this.travelDistancePoint.y = fromY - (fromY - toY) * traveled; + + // Update travel line from current position to destination + this.travelLine.points([this.travelDistancePoint.x, this.travelDistancePoint.y, toX, toY]); + + // Position fleet icon at current travel position + this.fleetIcon.x(this.travelDistancePoint.x - FLEET_ICON_SIZE / 2); + this.fleetIcon.y(this.travelDistancePoint.y - FLEET_ICON_SIZE / 2); + + // Calculate ETA + const turnsToDestination = this.calculateTurnsToDestination(); + const turnText = turnsToDestination > 1 ? ' Turns' : ' Turn'; + const etaString = `${turnsToDestination}${turnText}`; + this.etaText.text(etaString); + + // Position ETA text relative to fleet icon with smart offset + let offsetX = 8; + if (toX > fromX) { + // Traveling left to right, show text to the left of icon + if (turnsToDestination > 9) offsetX = -40; + else if (turnsToDestination === 1) offsetX = -30; + else offsetX = -35; + } + + this.etaText.x(this.travelDistancePoint.x + offsetX); + this.etaText.y(this.travelDistancePoint.y + 4); + + this.showAllElements(); + } + + private calculateTurnsToDestination(): number { + if (!this.fleetData.parsecsToDestination || !this.owner) { + return 0; + } + + // TODO: Get actual fleet speed from game engine + // For now, assume base speed of 1 parsec per turn + const fleetSpeed = 1; + return Math.ceil(this.fleetData.parsecsToDestination / fleetSpeed); + } + + private updateVisualElements() { + const color = this.owner?.color?.toString() || 'green'; + + // Update colors based on owner + this.travelLine.stroke(color); + this.fleetIcon.fill(color); + this.etaText.fill(color); + } + + private showAllElements() { + this.travelLine.visible(true); + this.fleetIcon.visible(true); + this.etaText.visible(true); + } + + private hideAllElements() { + this.travelLine.visible(false); + this.fleetIcon.visible(false); + this.etaText.visible(false); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public isInBounds(_x: number, _y: number): boolean { + // Fleet icons are typically not interactive for hit testing + // This matches the original implementation + return false; + } + + public getFleetData(): FleetData { + return this.fleetData; + } + + public destroyFleet() { + // Clean up Konva objects + this.group.destroyChildren(); + this.group.destroy(); + } +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/DrawnPlanet.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/DrawnPlanet.ts new file mode 100644 index 00000000..ea9c6872 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/DrawnPlanet.ts @@ -0,0 +1,811 @@ +import type { + PlanetData, + PlanetType, + ClientModelData, + PlayerData, + ClientPlayer, + StarshipData +} from 'astriarch-engine'; +import Konva from 'konva'; + +// Import planet images +import planetAsteroidImage from '$lib/assets/images/planet-asteroid.png'; +import planetDeadImage from '$lib/assets/images/planet-dead.png'; +import planetAridImage from '$lib/assets/images/planet-arid.png'; +import planetTerrestrialImage from '$lib/assets/images/planet-terestrial.png'; + +const PLANET_SIZE = 20; // Base planet size matching original +const PLANET_IMAGE_WIDTH = 36; // Width for planet images (wider aspect ratio) +const PLANET_IMAGE_HEIGHT = 32; // Height for planet images (shorter aspect ratio) +const SELECTION_COLOR = '#13f3fb'; // Cyan color for selection indicators + +export function createDrawnPlanet(planetData: PlanetData, gameModel: ClientModelData) { + return new DrawnPlanet(planetData, gameModel); +} + +export class DrawnPlanet { + public group: Konva.Group; + private planetData: PlanetData; + private knownPlanetType: PlanetType | null = null; + private gameModel: ClientModelData; + private owner: PlayerData | ClientPlayer | null = null; + private textBlockForeground = 'yellow'; + private textBlockStrengthForeground = 'yellow'; + private textBlockStrengthText = ''; + private productionItemStatusColor: string | null = null; + + // Visual elements + private planetImage: Konva.Image | null = null; + private planetRing: Konva.Circle | null = null; // Persistent ring for all planets + private nameText!: Konva.Text; + private strengthText!: Konva.Text; + private fleetStrengthIndicator: Konva.Group | null = null; // New visual fleet strength indicator + private platformIcon: Konva.Rect | null = null; + private statusIndicator: Konva.Circle | null = null; + private waypointLine: Konva.Line | null = null; + + // Selection state and elements + private isSelected: boolean = false; + private selectionRingOuter: Konva.Circle | null = null; + private selectionRingMiddle: Konva.Circle | null = null; + private selectionRingInner: Konva.Circle | null = null; + + constructor(planetData: PlanetData, gameModel: ClientModelData) { + this.group = new Konva.Group(); + this.planetData = planetData; + this.gameModel = gameModel; + + console.log('Creating DrawnPlanet:', { + id: planetData.id, + name: planetData.name, + boundingHexMidPoint: planetData.boundingHexMidPoint, + type: planetData.type, + hasFleet: !!planetData.planetaryFleet?.starships?.length, + hasBuildQueue: !!planetData.buildQueue?.length + }); + + // Position planet at its hex midpoint + this.group.x(planetData.boundingHexMidPoint.x); + this.group.y(planetData.boundingHexMidPoint.y); + + this.createVisualElements(); + this.update(gameModel); + + console.log('DrawnPlanet created and positioned at:', { + x: this.group.x(), + y: this.group.y() + }); + } + + private createVisualElements() { + // Planet image - will be shown when planet type is known + this.createPlanetImage(); + + // Planet ring - persistent ring for all planets (matches Figma design) + this.createPlanetRing(); + + // Planet name text - positioned below planet + this.nameText = new Konva.Text({ + x: -30, + y: PLANET_SIZE / 2 + 5, + width: 60, + text: this.planetData.name || this.planetData.id.toString(), + fontSize: 8, + fontFamily: 'Orbitron, monospace', + fontStyle: 'bold', + fill: this.textBlockForeground, + align: 'center' + }); + this.group.add(this.nameText); + + // Fleet strength text - positioned above planet + this.strengthText = new Konva.Text({ + x: -20, + y: -20, + width: 40, + text: this.textBlockStrengthText, + fontSize: 7, + fontFamily: 'Orbitron, monospace', + fontStyle: 'bold', + fill: this.textBlockStrengthForeground, + align: 'center' + }); + this.group.add(this.strengthText); + } + + update(gameModel: ClientModelData): void { + this.gameModel = gameModel; + + // Update planet data reference with fresh data from the game model + if (gameModel.mainPlayerOwnedPlanets[this.planetData.id]) { + // Update with owned planet data (full PlanetData) + this.planetData = gameModel.mainPlayerOwnedPlanets[this.planetData.id]; + } else { + // Check if this is a known client planet and update accordingly + const clientPlanet = gameModel.clientPlanets.find((cp) => cp.id === this.planetData.id); + if (clientPlanet) { + // Update the planet data with fresh client data while preserving the original type structure + this.planetData = { + ...this.planetData, // Keep existing data structure + name: clientPlanet.name, + originPoint: clientPlanet.originPoint, + boundingHexMidPoint: clientPlanet.boundingHexMidPoint, + // Only update type if clientPlanet has a non-null type + ...(clientPlanet.type && { type: clientPlanet.type }) + }; + } + } + + // Enhanced owner detection using multiple data sources + this.owner = null; + + // Check if this planet is owned by the main player + if (gameModel.mainPlayerOwnedPlanets[this.planetData.id]) { + this.owner = gameModel.mainPlayer; + //console.log(`Planet ${this.planetData.name} (${this.planetData.id}) is owned by main player`); + } else { + // Check for other players' ownership using lastKnownPlanetFleetStrength + const lastKnownData = gameModel.mainPlayer.lastKnownPlanetFleetStrength[this.planetData.id]; + if (lastKnownData?.lastKnownOwnerId) { + // Find the player with this ID + const knownOwner = gameModel.clientPlayers.find( + (player) => player.id === lastKnownData.lastKnownOwnerId + ); + if (knownOwner) { + this.owner = knownOwner; + //console.log(`Planet ${this.planetData.name} (${this.planetData.id}) last known owner: ${knownOwner.name} (cycle ${lastKnownData.cycleLastExplored})`); + } else { + //console.log(`Planet ${this.planetData.name} (${this.planetData.id}) has unknown owner ID: ${lastKnownData.lastKnownOwnerId}`); + } + } else { + if (lastKnownData) { + //console.log(`Planet ${this.planetData.name} (${this.planetData.id}) was explored but has no known owner (cycle ${lastKnownData.cycleLastExplored})`); + } else { + //console.log(`Planet ${this.planetData.name} (${this.planetData.id}) has never been explored`); + } + } + } + + // Update planet type if available and planet is known (explored) + // Check if this planet is in the player's known planets list + const isKnownPlanet = gameModel.mainPlayer.knownPlanetIds.includes(this.planetData.id); + + if (isKnownPlanet && this.planetData.type) { + this.knownPlanetType = this.planetData.type; + } else { + this.knownPlanetType = null; + } + + this.updateVisuals(); + } + + private updateVisuals(): void { + this.updatePlanetAppearance(); + this.updatePlanetImage(); + this.updatePlanetRing(); + this.updateFleetStrengthIndicator(); + this.updateProductionStatus(); + this.updateSelectionState(); + } + + private getPlanetImageUrl(planetType: PlanetType): string { + switch (planetType) { + case 1: // AsteroidBelt + return planetAsteroidImage; + case 2: // DeadPlanet + return planetDeadImage; + case 3: // PlanetClass1 (Arid) + return planetAridImage; + case 4: // PlanetClass2 (Terrestrial) + return planetTerrestrialImage; + default: + return planetAridImage; // fallback + } + } + + private createPlanetImage(): void { + // Planet image will be created/updated when we know the planet type + this.planetImage = null; + } + + private createPlanetRing(): void { + // Planet ring will be created for all planets + this.planetRing = null; + } + + private updatePlanetImage(): void { + // Only show planet image if we know the planet type (explored) + if (this.knownPlanetType) { + if (!this.planetImage) { + // Create new image + const imageObj = new Image(); + const imageUrl = this.getPlanetImageUrl(this.knownPlanetType); + + imageObj.onload = () => { + this.planetImage = new Konva.Image({ + x: -PLANET_IMAGE_WIDTH / 2, + y: -PLANET_IMAGE_HEIGHT / 2, + width: PLANET_IMAGE_WIDTH, + height: PLANET_IMAGE_HEIGHT, + image: imageObj, + visible: true + }); + + // Add image to the group + this.group.add(this.planetImage); + + // Proper layering: planet image at bottom, then ring on top, then UI elements + this.planetImage.moveToBottom(); + if (this.planetRing) { + this.planetRing.moveUp(); // Move ring above planet image + } + }; + + imageObj.onerror = (error) => { + console.error( + `Failed to load planet image for ${this.planetData.name}:`, + error, + imageUrl + ); + }; + + imageObj.src = imageUrl; + } else { + // Update existing image if planet type changed + const newImageUrl = this.getPlanetImageUrl(this.knownPlanetType); + + const imageObj = new Image(); + imageObj.onload = () => { + if (this.planetImage) { + this.planetImage.image(imageObj); + this.planetImage.visible(true); + } + }; + + imageObj.onerror = (error) => { + console.error( + `Failed to update planet image for ${this.planetData.name}:`, + error, + newImageUrl + ); + }; + + imageObj.src = newImageUrl; + } + } else { + // Hide planet image and show fallback circle for unexplored planets + if (this.planetImage) { + this.planetImage.visible(false); + } + } + } + + private updatePlanetRing(): void { + // Create persistent ring for all planets + const ringRadius = PLANET_SIZE / 2 + 4; // 4px larger than planet radius + + if (!this.planetRing) { + // Create sophisticated metallic ring with gradient matching Figma design + const ownerColor = this.owner ? this.getOwnerColor() : null; + + if (ownerColor) { + // For owned planets, create a gradient that incorporates the owner color + this.planetRing = new Konva.Circle({ + x: 0, + y: 0, + radius: ringRadius, + fill: 'transparent', + strokeLinearGradientStartPoint: { x: -ringRadius * 2, y: -ringRadius * 2 }, + strokeLinearGradientEndPoint: { x: ringRadius * 2, y: ringRadius * 2 }, + strokeLinearGradientColorStops: [ + 0, + '#313E46', // Dark metallic start + 0.31, + ownerColor + '88', // Owner color with transparency + 0.51, + '#FFFFFF', // White highlight + 1, + ownerColor + 'D4' // Owner color with more transparency + ], + strokeWidth: 2, + opacity: 0.8, + shadowColor: 'black', + shadowBlur: 2, + shadowOffset: { x: 0, y: 2 }, + shadowOpacity: 0.25, + visible: true + }); + } else { + // For unowned planets, use the original metallic gradient from Figma + this.planetRing = new Konva.Circle({ + x: 0, + y: 0, + radius: ringRadius, + fill: 'transparent', + strokeLinearGradientStartPoint: { x: -ringRadius * 2, y: -ringRadius * 2 }, + strokeLinearGradientEndPoint: { x: ringRadius * 2, y: ringRadius * 2 }, + strokeLinearGradientColorStops: [ + 0, + '#313E46', // Dark metallic start + 0.31, + '#A0B0BA88', // Semi-transparent light metallic + 0.51, + '#FFFFFF', // White highlight + 1, + '#69747BD4' // Semi-transparent dark metallic end + ], + strokeWidth: 2, + opacity: 0.7, + shadowColor: 'black', + shadowBlur: 2, + shadowOffset: { x: 0, y: 2 }, + shadowOpacity: 0.25, + visible: true + }); + } + + // Add ring and position it above planet image + this.group.add(this.planetRing); + + // Proper layering: planet image at bottom, then ring on top, then UI elements on top + if (this.planetImage) { + this.planetImage.moveToBottom(); + this.planetRing.moveUp(); // Move ring above planet image + } + } else { + // Update existing ring - need to recreate for gradient changes + this.planetRing.destroy(); + this.planetRing = null; + + // Recreate with new gradient + const ownerColor = this.owner ? this.getOwnerColor() : null; + + if (ownerColor) { + // For owned planets, create a gradient that incorporates the owner color + this.planetRing = new Konva.Circle({ + x: 0, + y: 0, + radius: ringRadius, + fill: 'transparent', + strokeLinearGradientStartPoint: { x: -ringRadius * 2, y: -ringRadius * 2 }, + strokeLinearGradientEndPoint: { x: ringRadius * 2, y: ringRadius * 2 }, + strokeLinearGradientColorStops: [ + 0, + '#313E46', // Dark metallic start + 0.31, + ownerColor + '88', // Owner color with transparency + 0.51, + '#FFFFFF', // White highlight + 1, + ownerColor + 'D4' // Owner color with more transparency + ], + strokeWidth: 2, + opacity: 0.8, + shadowColor: 'black', + shadowBlur: 2, + shadowOffset: { x: 0, y: 2 }, + shadowOpacity: 0.25, + visible: true + }); + } else { + // For unowned planets, use the original metallic gradient from Figma + this.planetRing = new Konva.Circle({ + x: 0, + y: 0, + radius: ringRadius, + fill: 'transparent', + strokeLinearGradientStartPoint: { x: -ringRadius * 2, y: -ringRadius * 2 }, + strokeLinearGradientEndPoint: { x: ringRadius * 2, y: ringRadius * 2 }, + strokeLinearGradientColorStops: [ + 0, + '#313E46', // Dark metallic start + 0.31, + '#A0B0BA88', // Semi-transparent light metallic + 0.51, + '#FFFFFF', // White highlight + 1, + '#69747BD4' // Semi-transparent dark metallic end + ], + strokeWidth: 2, + opacity: 0.7, + shadowColor: 'black', + shadowBlur: 2, + shadowOffset: { x: 0, y: 2 }, + shadowOpacity: 0.25, + visible: true + }); + } + + // Add to group and position properly + this.group.add(this.planetRing); + + // Proper layering: planet image at bottom, then ring on top, then UI elements on top + if (this.planetImage) { + this.planetImage.moveToBottom(); + this.planetRing.moveUp(); // Move ring above planet image + } + } + } + + private getOwnerColor(): string { + if (!this.owner) return '#FFFFFF'; + + const ownerColor = this.owner.color; + if (typeof ownerColor === 'string') { + // Ensure it's in hex format for gradient concatenation + const colorStr = ownerColor as string; + return colorStr.startsWith('#') + ? colorStr + : colorStr.startsWith('rgb') + ? this.rgbToHex(colorStr) + : colorStr; + } else if (ownerColor && typeof ownerColor === 'object') { + // Convert ColorRgbaData to hex + const r = Math.round(ownerColor.r || 0); + const g = Math.round(ownerColor.g || 0); + const b = Math.round(ownerColor.b || 0); + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + } + return '#FFFFFF'; + } + + private rgbToHex(rgb: string): string { + // Simple rgb() to hex conversion + const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (match) { + const r = parseInt(match[1]).toString(16).padStart(2, '0'); + const g = parseInt(match[2]).toString(16).padStart(2, '0'); + const b = parseInt(match[3]).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + return '#FFFFFF'; + } + + private updatePlanetAppearance(): void { + // This method previously updated planet circle colors + // Now that we use planet images and rings, this is a placeholder + // Could be used for future planet appearance customizations + } + + private calculateFleetStrength(): number { + let fleetStrength = 0; + + // For owned planets with planetary fleet data + if ( + this.owner && + this.owner.id === this.gameModel.mainPlayer.id && + this.planetData.planetaryFleet?.starships?.length + ) { + // Calculate total fleet strength (sum of all ship health/strength) + fleetStrength = this.planetData.planetaryFleet.starships.reduce( + (total: number, ship: StarshipData) => total + ship.health, + 0 + ); + } + // For non-owned or other players' planets, check last known fleet data + else { + const lastKnownData = + this.gameModel.mainPlayer.lastKnownPlanetFleetStrength[this.planetData.id]; + if (lastKnownData?.fleetData?.starships?.length) { + // Calculate last known fleet strength + fleetStrength = lastKnownData.fleetData.starships.reduce( + (total: number, ship: StarshipData) => total + ship.health, + 0 + ); + } + // Also check current planetary fleet for visible data + else if (this.planetData.planetaryFleet?.starships?.length) { + // We can see there are ships, use a default strength value + fleetStrength = this.planetData.planetaryFleet.starships.reduce( + (total: number, ship: StarshipData) => total + ship.health, + 0 + ); + } + } + + return fleetStrength; + } + + private updateFleetStrengthIndicator(): void { + const fleetStrength = this.calculateFleetStrength(); + + // Remove existing indicator + if (this.fleetStrengthIndicator) { + this.fleetStrengthIndicator.destroy(); + this.fleetStrengthIndicator = null; + } + + // Only show indicator if there's a fleet + if (fleetStrength > 0) { + this.createFleetStrengthIndicator(fleetStrength); + } + + // Clear the old text-based strength display + this.strengthText.text(''); + this.textBlockStrengthText = ''; + } + + private createFleetStrengthIndicator(fleetStrength: number): void { + // Calculate relative strength based on all known fleet strengths + const relativeStrength = this.calculateRelativeStrength(fleetStrength); + + // Get owner color for the dots + const ownerColor = this.owner ? this.getOwnerColor() : '#FFFFFF'; + + // Create group for the indicator - positioned to the left of the planet + this.fleetStrengthIndicator = new Konva.Group({ + x: -PLANET_SIZE / 2 - 8, // Position to the left of the planet + y: 0 // Centered vertically on the planet + }); + + // Larger, more visible dots + const dotSize = 4; // Increased from 2 + const dotSpacing = 2; // Spacing between dots + const maxDotsPerColumn = 5; // Maximum dots in a single column + const columnSpacing = 6; // Spacing between columns + + // Create dots in vertical columns, starting from bottom + for (let i = 0; i < relativeStrength; i++) { + const columnIndex = Math.floor(i / maxDotsPerColumn); + const dotInColumn = i % maxDotsPerColumn; + + // Position dots vertically from bottom up + const x = -(columnIndex * columnSpacing); + const y = + (maxDotsPerColumn - 1 - dotInColumn) * (dotSize + dotSpacing) - + ((maxDotsPerColumn - 1) * (dotSize + dotSpacing)) / 2; + + const dot = new Konva.Circle({ + x: x, + y: y, + radius: dotSize / 2, + fill: ownerColor, + stroke: 'black', + strokeWidth: 0.5, + opacity: 0.9 + }); + this.fleetStrengthIndicator.add(dot); + } + + // Add the indicator to the main group + this.group.add(this.fleetStrengthIndicator); + } + + private calculateRelativeStrength(fleetStrength: number): number { + // Collect all known fleet strengths across the galaxy to establish relative scale + const allFleetStrengths: number[] = []; + + // Add fleet strengths from owned planets + Object.entries(this.gameModel.mainPlayerOwnedPlanets).forEach(([, planetData]) => { + if (planetData?.planetaryFleet?.starships?.length) { + const strength = planetData.planetaryFleet.starships.reduce( + (total: number, ship: StarshipData) => total + ship.health, + 0 + ); + if (strength > 0) allFleetStrengths.push(strength); + } + }); + + // Add fleet strengths from last known data for all planets + Object.values(this.gameModel.mainPlayer.lastKnownPlanetFleetStrength).forEach((knownData) => { + if (knownData?.fleetData?.starships?.length) { + const strength = knownData.fleetData.starships.reduce( + (total: number, ship: StarshipData) => total + ship.health, + 0 + ); + if (strength > 0) allFleetStrengths.push(strength); + } + }); + + // If no other fleets are known, use a simple 1-dot system + if (allFleetStrengths.length <= 1) { + return 1; + } + + // Sort fleet strengths to establish percentiles + allFleetStrengths.sort((a, b) => a - b); + + // Calculate percentile of current fleet strength + const position = allFleetStrengths.findIndex((strength) => strength >= fleetStrength); + const percentile = position >= 0 ? position / (allFleetStrengths.length - 1) : 1; + + // Map percentile to dot count (1-15 dots, allowing for multiple columns) + // Bottom 20% = 1-2 dots, next 20% = 3-4 dots, etc. + if (percentile <= 0.1) return 1; // Bottom 10% + if (percentile <= 0.25) return 2; // Bottom 25% + if (percentile <= 0.4) return 3; // Bottom 40% + if (percentile <= 0.55) return 4; // Bottom 55% + if (percentile <= 0.7) return 5; // Bottom 70% + if (percentile <= 0.8) return 7; // Bottom 80% + if (percentile <= 0.9) return 9; // Bottom 90% + if (percentile <= 0.95) return 12; // Bottom 95% + return 15; // Top 5% - maximum dots + } + + private updateProductionStatus(): void { + // Show production indicator for owned planets with build queue + if (this.owner && this.planetData.buildQueue?.length > 0) { + // Determine production status color based on the first item in queue + const firstItem = this.planetData.buildQueue[0]; + if (firstItem) { + // Color based on production item type or completion status + if (firstItem.turnsToComplete <= 1) { + this.productionItemStatusColor = 'lime'; // Almost complete + } else if (firstItem.resourcesSpent) { + this.productionItemStatusColor = 'yellow'; // In progress + } else { + this.productionItemStatusColor = 'orange'; // Waiting for resources + } + } else { + this.productionItemStatusColor = 'gray'; // Unknown status + } + this.showProductionIndicator(); + } else if (this.statusIndicator) { + this.statusIndicator.destroy(); + this.statusIndicator = null; + this.productionItemStatusColor = null; + } + } + + private showProductionIndicator(): void { + if (!this.statusIndicator) { + this.statusIndicator = new Konva.Circle({ + x: PLANET_SIZE / 2 - 3, + y: -PLANET_SIZE / 2 + 3, + radius: 3, + fill: this.productionItemStatusColor || 'yellow', + stroke: 'black', + strokeWidth: 1 + }); + this.group.add(this.statusIndicator); + } + } + + private createSelectionRings(): void { + if (!this.selectionRingOuter) { + // Outer selection ring (matches Figma design: white stroke, 16% opacity, thick stroke) + this.selectionRingOuter = new Konva.Circle({ + x: 0, + y: 0, + radius: 26, + fill: 'transparent', + stroke: 'white', // White stroke like Figma + strokeWidth: 6, // Thick stroke like Figma: stroke-width="14" + opacity: 0.16, // Low opacity like Figma: stroke-opacity="0.16" + visible: false, + listening: false + }); + this.group.add(this.selectionRingOuter); + + // Middle selection ring (complex gradient design matching Figma) + this.selectionRingMiddle = new Konva.Circle({ + x: 0, + y: 0, + radius: 22, // Smaller than outer ring + fill: 'transparent', + strokeLinearGradientStartPoint: { x: -25, y: -25 }, + strokeLinearGradientEndPoint: { x: 25, y: 25 }, + strokeLinearGradientColorStops: [ + 0, + '#313E46', // Dark start + 0.31, + '#A0B0BA88', // Semi-transparent light gray + 0.51, + '#FFFFFF', // White middle + 1, + '#69747BD4' // Semi-transparent dark gray end + ], + strokeWidth: 1, + opacity: 1.0, // Full opacity since gradient handles transparency + shadowColor: 'black', + shadowBlur: 4, + shadowOffset: { x: 0, y: 4 }, + shadowOpacity: 0.25, + visible: false, + listening: false + }); + this.group.add(this.selectionRingMiddle); + + // Inner selection ring (cyan, thin) + this.selectionRingInner = new Konva.Circle({ + x: 0, + y: 0, + radius: 18, // Just outside the planet/ownership ring + fill: 'transparent', + stroke: SELECTION_COLOR, + strokeWidth: 1, + opacity: 0.6, + visible: false, + listening: false + }); + this.group.add(this.selectionRingInner); + } + } + + private updateSelectionState(): void { + if (this.isSelected) { + this.createSelectionRings(); + + if (this.selectionRingOuter) this.selectionRingOuter.visible(true); + if (this.selectionRingMiddle) this.selectionRingMiddle.visible(true); + if (this.selectionRingInner) this.selectionRingInner.visible(true); + + // Update text color to selection color when selected + this.nameText.fill(SELECTION_COLOR); + } else { + // Hide selection rings + if (this.selectionRingOuter) this.selectionRingOuter.visible(false); + if (this.selectionRingMiddle) this.selectionRingMiddle.visible(false); + if (this.selectionRingInner) this.selectionRingInner.visible(false); + + // Restore normal text color + this.nameText.fill(this.textBlockForeground); + } + } + + private lightenColor(color: string, factor: number): string { + // Simple color lightening - in a real implementation you'd want a proper color utility + if (color.startsWith('#')) { + const hex = color.slice(1); + const num = parseInt(hex, 16); + const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor)); + const g = Math.min( + 255, + Math.floor(((num >> 8) & 0x00ff) + (255 - ((num >> 8) & 0x00ff)) * factor) + ); + const b = Math.min(255, Math.floor((num & 0x0000ff) + (255 - (num & 0x0000ff)) * factor)); + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`; + } + return color; + } + + // Public methods + getPlanetData(): PlanetData { + return this.planetData; + } + + setSelected(selected: boolean): void { + this.isSelected = selected; + this.updateSelectionState(); + } + + getSelected(): boolean { + return this.isSelected; + } + + destroy(): void { + if (this.planetImage) { + this.planetImage.destroy(); + this.planetImage = null; + } + if (this.planetRing) { + this.planetRing.destroy(); + this.planetRing = null; + } + if (this.fleetStrengthIndicator) { + this.fleetStrengthIndicator.destroy(); + this.fleetStrengthIndicator = null; + } + if (this.selectionRingOuter) { + this.selectionRingOuter.destroy(); + this.selectionRingOuter = null; + } + if (this.selectionRingMiddle) { + this.selectionRingMiddle.destroy(); + this.selectionRingMiddle = null; + } + if (this.selectionRingInner) { + this.selectionRingInner.destroy(); + this.selectionRingInner = null; + } + if (this.group) { + this.group.destroy(); + } + } + + setVisible(visible: boolean): void { + if (this.group) { + this.group.visible(visible); + } + } +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/GalaxyCanvas.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/GalaxyCanvas.svelte new file mode 100644 index 00000000..6bcca26e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/galaxy/GalaxyCanvas.svelte @@ -0,0 +1,790 @@ + + + +
+ +
+
+ + + +
+ Zoom: {Math.round(zoomLevel * 100)}% +
+ + + {#if renderStats.currentFps > 0} +
+
+
FPS: {renderStats.currentFps}
+
Frame: {renderStats.avgFrameTime.toFixed(2)}ms
+
Min: {renderStats.minFrameTime === Infinity ? '0' : renderStats.minFrameTime.toFixed(2)}ms
+
Max: {renderStats.maxFrameTime.toFixed(2)}ms
+
Active: {renderStats.activeFrames}
+
Skipped: {renderStats.skippedFrames}
+
+
+
Layer Render Times:
+
+
G: {renderStats.layerRenderTimes.galaxy.toFixed(2)}ms
+
F: {renderStats.layerRenderTimes.fleet.toFixed(2)}ms
+
U: {renderStats.layerRenderTimes.ui.toFixed(2)}ms
+
+
+
+ {/if} + + +
+ Mouse wheel: zoom • Drag: pan +
+
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ActivityView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ActivityView.svelte new file mode 100644 index 00000000..bd09879c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ActivityView.svelte @@ -0,0 +1,693 @@ + + +
+ +
+ + +
+ +
+ +
+

+ Activity Center +

+
+ + +
+ + +
+
+ {#each filteredActivities as activity (activity.id)} +
+ +
handleActivityClick(activity)} + role="button" + tabindex="0" + onkeypress={(e) => e.key === 'Enter' && handleActivityClick(activity)} + > + +
+
+ {getNotificationIcon(activity.type)} +
+ + {getNotificationTypeLabel(activity.type)} + + {#if activity.planetName} + + • {activity.planetName} + + {/if} + + | + + + {formatTimestamp(activity.timestamp)} + + {#if hasAdditionalInfo(activity)} + + {expandedActivity === activity.id ? '▼' : '▶'} + + {/if} +
+ +
+ {activity.message} +
+
+ + + {#if expandedActivity === activity.id && hasAdditionalInfo(activity)} +
+ +
+ Time: + {formatDetailedTimestamp(activity.timestamp)} +
+ + + {#if activity.planetName && activity.planetId !== undefined} +
+ Planet: + {activity.planetName} (ID: {activity.planetId}) + +
+ {/if} + + + {#if activity.originalEvent && activity.originalEvent.data && isBattleNotification(activity)} + {@const conflictData = activity.originalEvent.data} +
+
BATTLE REPORT
+ + + {#if conflictData.resourcesLooted} +
+ {formatResourcesLooted(conflictData.resourcesLooted)} +
+ {/if} + + + {#if conflictData.attackingFleet} +
+
+ Attacking Fleet (Strength: {formatFleetStrength( + conflictData.attackingFleet + )}) + {#if conflictData.attackingFleetChances} + - Win Chance: {Math.round( + conflictData.attackingFleetChances * 100 + )}% + {/if} + {formatResearchBonus(conflictData.attackingFleetResearchBoost)} +
+
+ {formatFleetComposition(conflictData.attackingFleet)} +
+
+ {/if} + + + {#if conflictData.defendingFleet} +
+
+ Defending Fleet (Strength: {formatFleetStrength( + conflictData.defendingFleet + )}) + {formatResearchBonus(conflictData.defendingFleetResearchBoost)} +
+
+ {formatFleetComposition(conflictData.defendingFleet)} +
+
+ {/if} + + + {#if conflictData.winningFleet} +
+
Ships Remaining
+
+ {formatFleetComposition(conflictData.winningFleet)} +
+
+ {:else} +
All ships destroyed
+ {/if} +
+ {:else if activity.conflictData} + +
+
Battle Details:
+ {#if activity.conflictData.attackingFleetChances} +
+ • Attack success chance: {Math.round( + activity.conflictData.attackingFleetChances * 100 + )}% +
+ {/if} + {#if activity.conflictData.winningFleet} +
+ • Outcome: {activity.conflictData.winningFleet + ? 'Fleet survived' + : 'Fleet destroyed'} +
+ {/if} +
+ {/if} + + + {#if activity.originalEvent} +
+ Event Type: + {activity.originalEvent.type} + {#if activity.originalEvent.playerId} +
Player: {activity.originalEvent.playerId} + {/if} +
+ {/if} +
+ {/if} +
+ {:else} +
+
+ No {tabs[currentTabIndex]?.label.toLowerCase() || ''} activities yet +
+
+ {/each} +
+
+
+
+
+ + +
+ +
+ + +
+ +
+ {#each chatMessages as message (message.id)} +
+ +
+ + {message.sender} + + + | + + + {formatTimestamp(message.timestamp)} + +
+ +
+ {message.message} +
+
+ {/each} +
+ + +
+
+ +
|
+
+ +
+ + +
+
+
+
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/FleetCommandView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/FleetCommandView.svelte new file mode 100644 index 00000000..abad5a26 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/FleetCommandView.svelte @@ -0,0 +1,331 @@ + + + +
+ +
+
+

+ Send ships from Planet {currentSelectedPlanet?.name || 'Unknown'} +

+

+ Select one or multiple ships from your fleet to explore or attack the planet +

+
+ + +
+ {#if $fleetCommandStore.destinationPlanetId} + +
+
+
+

+ Destination: {destinationPlanet?.name || 'Unknown Planet'} +

+

+ Select ships above and click "Send Ships" to launch your fleet +

+
+ +
+
+ {:else} + +
+
+
+

+ Select a destination planet on the galaxy map +

+

+ Click on any planet in the galaxy view to set your fleet's destination +

+
+
+ + + +
+
+
+ {/if} +
+ + +
+ + +
+
+ + + {#if planetList.length > 1} +
+ +
+ {/if} + + +
+
+ {#each shipTypeTabs as tab} + + {/each} +
+ +
+
+ + +
+ {#if ships.length === 0} +
+ {#if selectedShipType === 'all'} + No mobile ships available on this planet + {:else} + No {shipTypeTabs.find((t) => t.type === selectedShipType)?.label?.toLowerCase() || + 'ships'} available on this planet + {/if} +
+ {:else} +
+
+ {#each ships as ship, index} + toggleShipSelection(ship.id)} + /> + {/each} +
+
+ {/if} +
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/PlanetInfoPanel.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/PlanetInfoPanel.svelte new file mode 100644 index 00000000..060e6236 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/PlanetInfoPanel.svelte @@ -0,0 +1,191 @@ + + + +{#if currentSelectedPlanet} +
+ +
+ + {currentSelectedPlanet.name} + +
+ + + {#if currentSelectedPlanet.type !== null} +
+ Type + + {GameTools.planetTypeToFriendlyName(currentSelectedPlanet.type)} + +
+ {/if} + + + {#if planetStatus === 'owned' && 'planetaryFleet' in currentSelectedPlanet} +
+ Status + Owned +
+ + + {#if currentSelectedPlanet.population && currentSelectedPlanet.population.length > 0} +
+ Population + + {currentSelectedPlanet.population.length.toLocaleString()} + +
+ {/if} + + + {#if currentSelectedPlanet.planetaryFleet} +
+ Fleet + + {formatFleetStrength(currentSelectedPlanet.planetaryFleet)} + +
+ {/if} + + + {:else if planetStatus === 'explored' && lastKnownData} +
+ Status + Explored +
+ + + {#if lastKnownData.lastKnownOwnerId && gameModel} + {@const owner = gameModel.clientPlayers.find( + (p) => p.id === lastKnownData.lastKnownOwnerId + )} +
+ Owner + + {owner ? owner.name : 'Unknown Player'} + +
+ {/if} + + +
+ Last Explored + + {getTurnsSinceExploration(lastKnownData)} + +
+ + +
+ Last Known Fleet + + {formatFleetStrength(lastKnownData)} + +
+ + + {:else if planetStatus === 'unexplored'} +
+ Status + Unexplored +
+ +
+ + Send a scout to explore this planet and discover its resources and defenses. + +
+ {/if} +
+{:else} + +
+ + Select a planet to view information + +
+{/if} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/PlanetOverviewView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/PlanetOverviewView.svelte new file mode 100644 index 00000000..28575b47 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/PlanetOverviewView.svelte @@ -0,0 +1,691 @@ + + +
+ {#if currentSelectedPlanet && isOwnedPlanet} + +
+
+ +
+

+ {currentSelectedPlanet.name} +

+ + {GameTools.planetTypeToFriendlyName(currentSelectedPlanet.type || 1)} + + {#if planetList.length > 1} + + {/if} +
+ {#if isFullPlanetData(currentSelectedPlanet)} + Pop: {currentSelectedPlanet.population?.length || 0} | Max: {currentSelectedPlanet.maxImprovements + + (currentSelectedPlanet.builtImprovements?.[2] || 0)} + {:else} + Planet Info: Limited View + {/if} +
+
+ + +
+ Resources: + {#if isFullPlanetData(currentSelectedPlanet)} +
+
+ {(currentSelectedPlanet.resources?.food || 0).toFixed(1)} + F +
+
+ {(currentSelectedPlanet.resources?.production || 0).toFixed(1)} + P +
+
+ {(currentSelectedPlanet.resources?.energy || 0).toFixed(1)} + E +
+
+ {(currentSelectedPlanet.resources?.ore || 0).toFixed(1)} + O +
+
+ {(currentSelectedPlanet.resources?.iridium || 0).toFixed(1)} + I +
+
+ {:else} + Resources not available + {/if} +
+ + +
+ Improvements: + {#if isFullPlanetData(currentSelectedPlanet)} +
+ {#if currentSelectedPlanet.builtImprovements?.[PlanetImprovementType.Farm] > 0} + + Farm: {currentSelectedPlanet.builtImprovements[PlanetImprovementType.Farm]} + + {/if} + {#if currentSelectedPlanet.builtImprovements?.[PlanetImprovementType.Mine] > 0} + + Mine: {currentSelectedPlanet.builtImprovements[PlanetImprovementType.Mine]} + + {/if} + {#if currentSelectedPlanet.builtImprovements?.[PlanetImprovementType.Factory] > 0} + + Factory: {currentSelectedPlanet.builtImprovements[PlanetImprovementType.Factory]} + + {/if} + {#if currentSelectedPlanet.builtImprovements?.[PlanetImprovementType.Colony] > 0} + + Colony: {currentSelectedPlanet.builtImprovements[PlanetImprovementType.Colony]} + + {/if} + {#if !currentSelectedPlanet.builtImprovements || Object.values(currentSelectedPlanet.builtImprovements).every((count) => count === 0)} + None + {/if} +
+ {:else} + Improvements not available + {/if} +
+
+
+ + +
+ +
+

Build Items

+ + +
+

+ Buildings +

+
+ {#each availableBuildings as building, index} + {@const availability = buildingAvailability.find((a) => a.type === building.type)} + addBuildingToQueue(building.type)} + /> + {/each} +
+
+ + +
+

+ Starships +

+
+ {#each availableShips as ship} + {@const availability = shipAvailability.find( + (a) => a.type === ship.type && a.isCustom === ship.isCustom + )} + addShipToQueue(ship.type, ship.customShipData)} + /> + {/each} +
+
+
+ + +
+

+ Production & Workers +

+ + +
+

+ Resource Production per Turn +

+
+
+ Food: + +2.4 / turn +
+
+ Energy: + +3.6 / turn +
+
+ Ore: + +1.8 / turn +
+
+ Iridium: + +0.9 / turn +
+
+
+ + +
+

+ Worker Assignment +

+
+
+ Farmers: +
+ + {workerAssignments.farmers} + +
+
+
+ Miners: +
+ + {workerAssignments.miners} + +
+
+
+ Builders: +
+ + {workerAssignments.builders} + +
+
+
+ + +
+
+ Total Population: + {isFullPlanetData(currentSelectedPlanet) + ? currentSelectedPlanet.population?.length || 0 + : 0} +
+
+ Unassigned: + {Math.max( + 0, + (isFullPlanetData(currentSelectedPlanet) + ? currentSelectedPlanet.population?.length || 0 + : 0) - + (workerAssignments.farmers + + workerAssignments.miners + + workerAssignments.builders) + )} +
+
+
+
+ + +
+

Build Queue

+ + {#if isFullPlanetData(currentSelectedPlanet) && currentSelectedPlanet.buildQueue && currentSelectedPlanet.buildQueue.length > 0} +
+ {#each currentSelectedPlanet.buildQueue as item, index} +
+
+
+ {#if item.itemType === 1 && item.improvementData?.type} + {GameTools.planetImprovementTypeToFriendlyName(item.improvementData.type)} + {:else if item.itemType === 2 && item.starshipData?.type} + {GameTools.starShipTypeToFriendlyName(item.starshipData.type)} + {:else} + Unknown Item + {/if} +
+ +
+ +
+ {index === 0 ? 'Building...' : `Queue position ${index + 1}`} +
+ + {#if index === 0} + +
+
+
+
+
+ {/if} + +
+ + {(item.productionCostComplete || 0).toFixed(1)}/{Math.round( + item.baseProductionCost || 1 + )} + + {#if index === 0} + + {item.turnsToComplete || '?'} turns + + {/if} +
+
+ {/each} +
+ {:else} +
+ Build queue is empty + + Select items from the left to build + +
+ {/if} +
+
+ {:else if currentSelectedPlanet && !isOwnedPlanet} + +
+
+ Planet "{currentSelectedPlanet.name}" is not under your control + + Only owned planets can be managed here + +
+
+ {:else} + +
+
+ No planets under your control + + Expand your empire to manage planets + +
+
+ {/if} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ResearchLabView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ResearchLabView.svelte new file mode 100644 index 00000000..31b5a6a3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ResearchLabView.svelte @@ -0,0 +1,640 @@ + + + +
+ +
+ + + + +
+ +
+
+ Allocated resources + + +
+
+
+ +
+ Energy + + {Math.round(energyPercent * 100)}% + +
+ +
handleBarClick(e, false)} + role="button" + tabindex="0" + > + {#each energyBars as filled} +
+ {/each} +
+
+ + +
+
+
+ +
+ Research + + {Math.round(researchPercent * 100)}% + +
+ +
handleBarClick(e, true)} + role="button" + tabindex="0" + > + {#each researchBars as filled} +
+ {/each} +
+
+
+
+ + +
+
+ + Custom ships and research improvements + + +
+ +
+ + Create Ships + + +
+ {#each shipTypes as ship} +
selectCustomShipResearch(ship.researchType)} + role="button" + tabindex="0" + > + + + + {#if getResearchLevel(ship.researchType) > 0} +
+ + {getResearchLevel(ship.researchType)} + +
+ {/if} +
+ {/each} +
+
+ + +
+ Ships + +
+ {#each improvements as improvement} +
submitResearchItem(improvement.researchType)} + role="button" + tabindex="0" + > + + + + {#if getResearchLevel(improvement.researchType) > 0} +
+ + {getResearchLevel(improvement.researchType)} + +
+ {/if} +
+ {/each} +
+
+ + +
+ Buildings + +
+ {#each infrastructure as infra} +
submitResearchItem(infra.researchType)} + role="button" + tabindex="0" + > + + + + {#if getResearchLevel(infra.researchType) > 0} +
+ + {getResearchLevel(infra.researchType)} + +
+ {/if} +
+ {/each} +
+
+ + +
+ {#if selectedCustomShipType && !isCustomShipResearch} + +
+ + Configuring {shipTypes.find((s) => s.researchType === selectedCustomShipType) + ?.name} Research + + +
+
+ + Advantage against + + + +
+ +
+ + Disadvantage against + + + +
+
+
+ +
+
+ {:else if currentResearchInfo} + +
+ + Currently researching + + +
+
+ +
+ +
+ + {currentResearchInfo.name}. + + + Estimated turns remaining: {currentResearchInfo.cyclesRemaining < 999 + ? currentResearchInfo.cyclesRemaining + : 'Infinity'} + + {#if currentResearchInfo.levelData} + + Progress: {(currentResearchInfo.levelData.percentComplete * 100).toFixed( + 1 + )}% complete + + +
+ {#each Array(20) as _, i} +
+ {/each} +
+ {/if} +
+
+ +
+ +
+
+ {:else} + +
+ + No active research project. Click on a research area below to have your scientists + and engineers start researching in that area. + +
+ {/if} +
+
+
+
+
+
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ShipCard.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ShipCard.svelte new file mode 100644 index 00000000..202327f2 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/ShipCard.svelte @@ -0,0 +1,185 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/TradingCenterView.md b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/TradingCenterView.md new file mode 100644 index 00000000..4827f388 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/TradingCenterView.md @@ -0,0 +1,308 @@ +# TradingCenterView Component + +## Overview + +The `TradingCenterView.svelte` component provides a comprehensive interface for the Galactic Trading Center, allowing players to buy and sell resources (Food, Ore, Iridium) in real-time. This component was built based on the Figma design specifications and integrates seamlessly with the Astriarch engine's trading system. + +## Features + +### Core Functionality + +- **Resource Trading**: Buy and sell Food, Ore, and Iridium +- **Real-time Pricing**: Dynamic price calculation based on supply and demand +- **Trade Management**: Submit, track, and cancel trades +- **Resource Overview**: View player stockpiles and trading center inventory +- **Visual Progress Bars**: Intuitive resource amount visualization + +### User Interface + +- **Responsive Design**: Adapts to different screen sizes +- **Figma-Based Styling**: Matches the design system specifications +- **Interactive Controls**: Slider-based amount selection, toggle buttons +- **Real-time Updates**: Reactive state management with Svelte stores + +## Component Structure + +### Props + +The component doesn't accept external props but relies on global stores: + +- `$clientGameModel` - Main game state +- `$selectedPlanet` - Currently selected planet + +### Key State Variables + +```typescript +// Trading configuration +let selectedTradeType: TradeType = TradeType.BUY; +let selectedResourceType: TradingCenterResourceType = TradingCenterResourceType.FOOD; +let tradeAmount = 0; +let estimatedCost = 0; +``` + +### Reactive Calculations + +- **playerTotalResources**: Player's total resources including pending trades +- **resourcePrices**: Current market prices for all resources +- **maxTradeAmount**: Maximum allowed trade amount based on availability +- **estimatedCost**: Real-time cost calculation for current trade + +## Usage + +### Integration + +The component is already integrated into the main game interface via the navigation: + +```svelte + +{:else if $currentView === 'trading'} +
+ +
+{/if} +``` + +### Navigation Access + +Players can access the trading center through the bottom navigation bar by clicking the "Trading" button. + +### Prerequisites + +For the trading center to function properly, ensure: + +1. **Game State**: A valid `$clientGameModel` with trading center data +2. **Planet Selection**: A selected planet (`$selectedPlanet`) +3. **WebSocket Connection**: Active connection for trade submission +4. **Engine Integration**: Proper trading center data from the backend + +## Trade Workflow + +### 1. Resource Selection + +```svelte + + +``` + +### 2. Trade Type Selection + +```svelte + + + +``` + +### 3. Amount Selection + +```svelte + + +``` + +### 4. Trade Submission + +```svelte + + +``` + +### 5. Trade Management + +```svelte + +{#each tradingCenter.mainPlayerTrades as trade, index} + +{/each} +``` + +## Engine Integration + +### Trading Center Data + +The component expects a `ClientTradingCenter` object with: + +```typescript +interface ClientTradingCenter { + energyAmount: number; + foodResource: TradingCenterResource; + oreResource: TradingCenterResource; + iridiumResource: TradingCenterResource; + mainPlayerTrades: TradeData[]; +} +``` + +### Resource Data Structure + +```typescript +interface TradingCenterResource { + amount: number; + currentPrice: number; + tradeAmountMax: number; + desiredAmount: number; +} +``` + +### Trade Data + +```typescript +interface TradeData { + playerId: number; + planetId: number; + tradeType: TradeType; + resourceType: TradingCenterResourceType; + amount: number; +} +``` + +## WebSocket Communication + +### Trade Submission + +```typescript +webSocketService.submitTrade(trade); +``` + +### Trade Cancellation + +```typescript +webSocketService.cancelTrade(index, planetId); +``` + +### Message Handling + +The component handles trade responses: + +- `MESSAGE_TYPE.SUBMIT_TRADE` - Trade submission results +- `MESSAGE_TYPE.CANCEL_TRADE` - Trade cancellation confirmations + +## Styling and Design + +### Color Scheme + +- **Primary**: Cyan accents (`#00FFFF`) +- **Background**: Dark gradients (`from-slate-900/90 to-slate-800/90`) +- **Resources**: Color-coded resource icons + - Food: Orange (`bg-orange-500`) + - Ore: Amber (`bg-amber-600`) + - Iridium: Purple (`bg-purple-500`) + +### Typography + +- **Font**: Orbitron for headings and labels +- **Sizes**: Responsive text scaling +- **Weight**: Bold for emphasis, normal for data + +### Layout + +- **Left Panel**: Resource overview (364px fixed width) +- **Right Panel**: Trading interface (flexible) +- **Bottom**: Floating submit button and trade list + +## Error Handling + +### Validation + +- **Amount Validation**: Prevents invalid trade amounts +- **Resource Availability**: Checks trading center stock +- **Player Resources**: Validates player has enough resources to sell + +### Error Display + +- Toast notifications for trade errors +- Disabled states for invalid actions +- Clear error messaging + +## Performance Considerations + +### Reactive Updates + +- Efficient reactive statements for calculations +- Minimal re-renders with proper dependencies +- Optimized store subscriptions + +### Memory Management + +- No memory leaks with proper cleanup +- Efficient data structures for trade tracking + +## Testing + +### Component Testing + +Test scenarios should include: + +- Resource selection and switching +- Trade type toggling +- Amount validation and limits +- Trade submission and cancellation +- Error state handling + +### Integration Testing + +- WebSocket message flow +- Store state updates +- Engine data synchronization + +## Future Enhancements + +### Potential Features + +- **Trade History**: Historical trade tracking +- **Price Charts**: Visual price trend analysis +- **Trade Alerts**: Notifications for price changes +- **Bulk Trading**: Multiple resource trades +- **Trade Presets**: Saved trade configurations + +### Performance Optimizations + +- Virtual scrolling for large trade lists +- Debounced calculations for slider changes +- Cached resource calculations + +## Troubleshooting + +### Common Issues + +1. **Trading Center Not Loading** + - Check `$clientGameModel.clientTradingCenter` exists + - Verify WebSocket connection + - Ensure backend trading center implementation + +2. **Trades Not Submitting** + - Check WebSocket connection status + - Verify `submitTrade` method implementation + - Check MESSAGE_TYPE constants + +3. **Price Calculations Wrong** + - Verify engine trading center calculations + - Check resource data structure + - Ensure proper reactive dependencies + +### Debug Information + +Enable debug logs to trace: + +- Trade submission attempts +- WebSocket message flow +- Resource calculation updates +- Store state changes + +## Dependencies + +### Required Packages + +- `astriarch-engine` - Trading center logic and types +- `svelte` - Component framework +- WebSocket service - Network communication + +### Store Dependencies + +- `clientGameModel` - Game state management +- `selectedPlanet` - Planet selection state +- `webSocketService` - Network communication + +## Conclusion + +The TradingCenterView component provides a complete trading interface that integrates seamlessly with the Astriarch game engine. It offers intuitive controls, real-time updates, and robust error handling while maintaining the game's visual design standards. diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/TradingCenterView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/TradingCenterView.svelte new file mode 100644 index 00000000..039ac54d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/game-views/TradingCenterView.svelte @@ -0,0 +1,561 @@ + + +
+ +
+

+ Galactic Trade Center, Trading from {currentPlanet?.name || 'Unknown'} +

+
+ +
+ +
+
+

+ Resources overview +

+ + +
+

Your Stockpile (After trade)

+
+
+ + + {Math.floor(playerTotalResources.food)} + +
+
+ + + {Math.floor(playerTotalResources.ore)} + +
+
+ + + {Math.floor(playerTotalResources.iridium)} + +
+
+
+ + +
+

+ Galaxy Trading Center Stockpile +

+
+
+ + + {tradingCenterAmounts.food} + +
+
+ + + {tradingCenterAmounts.ore} + +
+
+ + + {tradingCenterAmounts.iridium} + +
+
+
+ + +
+

Current Prices

+
+
+ + + {resourcePrices.food.toFixed(2)} + +
+
+ + + {resourcePrices.ore.toFixed(2)} + +
+
+ + + {resourcePrices.iridium.toFixed(2)} + +
+
+
+
+
+ + +
+ +
+ + +
+ + +
+ +
+

+ Select the resource you want to {selectedTradeType === TradeType.BUY + ? 'buy from' + : 'sell to'} GTC +

+ + +
+ + + + + +
+
+ + +
+
+ + + Amount of {getResourceName(selectedResourceType).toLowerCase()} + + + {tradeAmount} + +
+ + +
+ {#each tradeAmountBars as filled} +
+ {/each} +
+ + +
+ 0 + {maxTradeAmount} +
+ + +
+ + Estimated {selectedTradeType === TradeType.BUY ? 'cost' : 'gain'}: {estimatedCost.toFixed( + 0 + )} energy + +
+
+ + +
+

Galaxy trading energy amount

+ + {tradingCenterAmounts.energy.toFixed(0)} + +
+
+
+ + +
+ +
+
+ + + {#if tradingCenter?.mainPlayerTrades && tradingCenter.mainPlayerTrades.length > 0} +
+

Submitted Trades

+
+ {#each tradingCenter.mainPlayerTrades as trade, index} +
+ + {trade.tradeType === TradeType.BUY ? 'Buy' : 'Sell'} + {trade.amount} + {getResourceName(trade.resourceType)} + for {( + getResourceByType(trade.resourceType)?.currentPrice || 0 * trade.amount + ).toFixed(1)} energy + + +
+ {/each} +
+
+ {/if} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/CreateGameForm.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/CreateGameForm.svelte new file mode 100644 index 00000000..6bb4bf7f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/CreateGameForm.svelte @@ -0,0 +1,346 @@ + + +
+
+ Create New Game + + Set up your galactic empire + +
+ +
+ +
+ + e.key === 'Enter' && handleSubmit()} + /> + {#if errors.name} + + {errors.name} + + {/if} +
+ + +
+ + + {#if errors.description} + + {errors.description} + + {/if} +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + + {#if errors.maxPlayers} + + {errors.maxPlayers} + + {/if} +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + + {#if formData.isPrivate} +
+ + + {#if errors.password} + + {errors.password} + + {/if} +
+ {/if} + + +
+
+
+
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameDetails.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameDetails.svelte new file mode 100644 index 00000000..fec3fe98 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameDetails.svelte @@ -0,0 +1,342 @@ + + +
+ {#if !game} +
+ + Select a game to view details + +
+ {:else} +
+ +
+
+ + {game.name} + +
+ + {getStatusDisplay(game)} + +
+
+ + {#if game.description} + + {game.description} + + {/if} +
+ + +
+ + Game Settings + +
+
+ Galaxy Size + + {game.gameOptions?.galaxySize || 'Medium'} + +
+
+ Game Speed + + {game.gameOptions?.gameSpeed || 'Normal'} + +
+
+ Max Players + + {game.gameOptions?.maxPlayers || 4} + +
+
+ Victory Condition + + {game.gameOptions?.victoryCondition || 'Conquest'} + +
+
+
+ + +
+ + Players ({game.players?.length || 0}/{game.gameOptions?.maxPlayers || 4}) + +
+ {#if game.players && game.players.length > 0} + {#each game.players as player} +
+
+ + {player.username || player.name || 'Unknown Player'} + + {#if player.isHost} +
+ HOST +
+ {/if} +
+ {#if player.isReady !== undefined} +
+ + {player.isReady ? 'Ready' : 'Not Ready'} + +
+ {/if} +
+ {/each} + {:else} + No players yet + {/if} +
+
+ + +
+ + Game Information + +
+
+ Created + + {formatDate(game.createdAt)} + +
+ {#if game.lastActivity} +
+ Last Activity + + {formatDate(game.lastActivity)} + +
+ {/if} + {#if game.turn !== undefined} +
+ Turn + + {game.turn} + +
+ {/if} +
+
+ + +
+ {#if canJoinGame(game)} + + {/if} + + {#if canResumeGame(game)} + + {/if} + + {#if game.status === 'completed'} + + This game has ended + + {/if} +
+
+ {/if} +
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameList.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameList.svelte new file mode 100644 index 00000000..a6feb334 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameList.svelte @@ -0,0 +1,167 @@ + + +
+ {#if games.length === 0} +
+ + No games available. Create a new game to get started! + +
+ {:else} +
+ {#each games as game (game._id)} +
handleGameClick(game)} + on:keydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleGameClick(game); + } + }} + role="button" + tabindex="0" + > +
+ + {game.name} + +
+
+ +
+ + Status: {getStatusDisplay(game)} + + + Players: {game.players?.length || 0}/{game.gameOptions?.maxPlayers || 4} + + + Galaxy: {game.gameOptions?.galaxySize || 'Medium'} + +
+ +
+ + Created: {formatDate(game.createdAt)} + + {#if game.lastActivity} + + Last Active: {formatDate(game.lastActivity)} + + {/if} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameOptionsView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameOptionsView.svelte new file mode 100644 index 00000000..46e6920d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/GameOptionsView.svelte @@ -0,0 +1,646 @@ + + +
+
+
+
+ New Skirmish Game + + Please enter your name and choose the desired game settings. + +
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + {#if playerPosition === 0} + You ({formData.playerName || 'Host'}) + {:else} + + {getConnectedPlayerName(0)} (Host) + + {/if} +
+ +
+ +
+ {#if player1SlotOccupied} + + + {getConnectedPlayerName(1)} (Connected) + + {:else} + + + {getPlayerTypeName( + formData.opponentOptions[0]?.type, + formData.opponentOptions[0]?.name + )} + + {#if isHost} + + {/if} + {/if} +
+
+ + {#if formData.systemsToGenerate >= 3} +
+ +
+ {#if player2SlotOccupied} + + + {getConnectedPlayerName(2)} (Connected) + + {:else} + + + {getPlayerTypeName( + formData.opponentOptions[1]?.type, + formData.opponentOptions[1]?.name + )} + + {#if isHost} + + {/if} + {/if} +
+
+ {/if} + + {#if formData.systemsToGenerate >= 4} +
+ +
+ {#if player3SlotOccupied} + + + {getConnectedPlayerName(3)} (Connected) + + {:else} + + + {getPlayerTypeName( + formData.opponentOptions[2]?.type, + formData.opponentOptions[2]?.name + )} + + {#if isHost} + + {/if} + {/if} +
+
+ {/if} +
+ + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+ +
+ +
+ +
+
+
+
+ +
+
+
+
+
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/LobbyView.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/LobbyView.svelte new file mode 100644 index 00000000..c40cea02 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/LobbyView.svelte @@ -0,0 +1,325 @@ + + +
+ + {#if !isConnected} +
+ Connecting to server... +
+ {/if} + + + {#if currentView === 'lobby'} +
+ + ASTRIARCH LOBBY + + + Join an existing game or create a new one + + +
+ Debug: Connected: {isConnected}, Current View: {currentView}, Games: {games.length}, Game + ID: {gameId} +
+
+ {/if} + + +
+ {#if currentView === 'lobby'} + + +
+ +
+
+ + Available Games ({games.length}) + +
+
+ +
+
+ + +
+
+ Game Details +
+
+ +
+
+
+ {:else if currentView === 'game_options'} + + {#if currentGame && gameId} + + {:else} +
+ Loading game configuration... +
+ {/if} + {/if} +
+
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/index.ts new file mode 100644 index 00000000..5d14e53a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/lobby/index.ts @@ -0,0 +1,4 @@ +export { default as LobbyView } from './LobbyView.svelte'; +export { default as GameList } from './GameList.svelte'; +export { default as GameDetails } from './GameDetails.svelte'; +export { default as CreateGameForm } from './CreateGameForm.svelte'; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/AstriarchLogo.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/AstriarchLogo.svelte new file mode 100644 index 00000000..e69de29b diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/badge/badge.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 00000000..7c242f7f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/badge/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/badge/index.ts new file mode 100644 index 00000000..f05fb87f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from './badge.svelte'; +export { badgeVariants, type BadgeVariant } from './badge.svelte'; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/button/button.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/button/button.svelte new file mode 100644 index 00000000..16f1a3cc --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/button/button.svelte @@ -0,0 +1,80 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/button/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/button/index.ts new file mode 100644 index 00000000..5414d9d3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants +} from './button.svelte'; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-action.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 00000000..bbaafd22 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-content.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 00000000..1d60124a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-description.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 00000000..bfb0725b --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-footer.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 00000000..4e390bfd --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-header.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 00000000..9cdc602b --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-title.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 00000000..0eae8ec1 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card.svelte new file mode 100644 index 00000000..41123325 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/index.ts new file mode 100644 index 00000000..77d36747 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from './card.svelte'; +import Content from './card-content.svelte'; +import Description from './card-description.svelte'; +import Footer from './card-footer.svelte'; +import Header from './card-header.svelte'; +import Title from './card-title.svelte'; +import Action from './card-action.svelte'; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-close.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 00000000..e8a96a77 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-content.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 00000000..c0e54b88 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,43 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + + Close + + {/if} + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-description.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 00000000..c658420e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-footer.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 00000000..c457d563 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-header.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 00000000..5fe41452 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-overlay.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 00000000..938ab1e4 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-title.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 00000000..7073699d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-trigger.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 00000000..ac04d9fe --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/index.ts new file mode 100644 index 00000000..d9e5fb86 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from 'bits-ui'; + +import Title from './dialog-title.svelte'; +import Footer from './dialog-footer.svelte'; +import Header from './dialog-header.svelte'; +import Overlay from './dialog-overlay.svelte'; +import Content from './dialog-content.svelte'; +import Description from './dialog-description.svelte'; +import Trigger from './dialog-trigger.svelte'; +import Close from './dialog-close.svelte'; + +const Root = DialogPrimitive.Root; +const Portal = DialogPrimitive.Portal; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/input/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/input/index.ts new file mode 100644 index 00000000..15c0933d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from './input.svelte'; + +export { + Root, + // + Root as Input +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/input/input.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/input/input.svelte new file mode 100644 index 00000000..7598a865 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/input/input.svelte @@ -0,0 +1,51 @@ + + +{#if type === 'file'} + +{:else} + +{/if} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/label/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/label/index.ts new file mode 100644 index 00000000..808d1415 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from './label.svelte'; + +export { + Root, + // + Root as Label +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/label/label.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/label/label.svelte new file mode 100644 index 00000000..7fef72e9 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/index.ts new file mode 100644 index 00000000..bfa73d90 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import { Select as SelectPrimitive } from 'bits-ui'; + +import Group from './select-group.svelte'; +import Label from './select-label.svelte'; +import Item from './select-item.svelte'; +import Content from './select-content.svelte'; +import Trigger from './select-trigger.svelte'; +import Separator from './select-separator.svelte'; +import ScrollDownButton from './select-scroll-down-button.svelte'; +import ScrollUpButton from './select-scroll-up-button.svelte'; +import GroupHeading from './select-group-heading.svelte'; + +const Root = SelectPrimitive.Root; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-content.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 00000000..ca9d1b5d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,40 @@ + + + + + + + {@render children?.()} + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-group-heading.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 00000000..3ff70030 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-group.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 00000000..2520795d --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-item.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 00000000..06e48be3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-label.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 00000000..6c76ced6 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-scroll-down-button.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 00000000..35021483 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-scroll-up-button.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 00000000..3d35e04c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-separator.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 00000000..3624d4cb --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-trigger.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 00000000..f14abe05 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/separator/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/separator/index.ts new file mode 100644 index 00000000..768efac9 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from './separator.svelte'; + +export { + Root, + // + Root as Separator +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/separator/separator.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 00000000..6ceb6a52 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 00000000..273d831e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,21 @@ +import { Tooltip as TooltipPrimitive } from 'bits-ui'; +import Trigger from './tooltip-trigger.svelte'; +import Content from './tooltip-content.svelte'; + +const Root = TooltipPrimitive.Root; +const Provider = TooltipPrimitive.Provider; +const Portal = TooltipPrimitive.Portal; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/tooltip-content.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 00000000..60d68f51 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,47 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
+ {/snippet} +
+
+
diff --git a/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 00000000..5631d1b4 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/lib/design-system/examples.md b/apps/astriarch-frontend-sveltekit/src/lib/design-system/examples.md new file mode 100644 index 00000000..9f8e5870 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/design-system/examples.md @@ -0,0 +1,420 @@ +# Astriarch Design System + +Complete design system based on your Figma design with all colors and styles systematically organized. + +## Color Palette + +### Primary Colors + +```css +--astriarch-primary-fill: #00ffff; +--astriarch-primary-stroke: linear-gradient( + 299.62deg, + rgba(0, 0, 0, 0.5) 7.75%, + #00ffff 82.21%, + #ffffff 100% +); +``` + +### General UI Colors + +```css +--astriarch-ui-dark-blue: #1b1f25; +--astriarch-ui-dark-grey: #313e46; +--astriarch-ui-light-grey: #90a9ba; +--astriarch-ui-white: #ffffff; +--astriarch-ui-white-75: rgba(255, 255, 255, 0.75); +--astriarch-ui-white-50: rgba(255, 255, 255, 0.5); +--astriarch-ui-modal-bg: rgba(27, 31, 37, 0.9); +--astriarch-bg-glass: rgba(27, 31, 37, 0.8); +``` + +### Theme Colors + +```css +--astriarch-green-theme: #7bfe2b; +--astriarch-orange-theme: #ff9111; +--astriarch-purple-theme: #c240ff; +``` + +### Element/Resource Colors + +```css +--astriarch-food: #f87b54; +--astriarch-energy: #ffce22; +--astriarch-research: #00ffc2; +--astriarch-ore: #b6e827; +--astriarch-iridium: #aa77ff; +--astriarch-population: #23bdff; +``` + +### Status Colors + +```css +--astriarch-status-bad: #ff0000; +--astriarch-status-medium: #fff500; +--astriarch-status-good: #00ff38; +``` + +## Tailwind Utility Classes + +### Primary Colors + +```html + +
Primary
+
Gradient Background
+
Gradient Border
+``` + +### UI Colors + +```html + +
Dark Blue
+
Dark Grey
+
Glass Effect
+
Modal Background
+ + +Light Grey Text +White Text +``` + +### Theme Colors + +```html + +
Green Theme
+
Orange Theme
+
Purple Theme
+ +Green Text +Orange Text +Purple Text +``` + +### Resource/Element Colors + +```html + +
+ 🍎 Food + ⚡ Energy + 🔬 Research + ⛏️ Ore + 💎 Iridium + 👥 Population +
+ + +
Food Resource
+
Energy Resource
+ + +
Food Border
+
Energy Border
+``` + +### Status Colors + +```html + +✓ Good Status +⚠ Medium Status +✗ Bad Status + + +
Success
+
Warning
+
Error
+``` + +### Direct CSS Usage + +```css +/* Use the custom properties directly */ +.my-component { + background-color: var(--astriarch-primary-fill); + border: 1px solid var(--astriarch-primary-fill); +} + +.my-gradient-component { + background: var(--astriarch-primary-stroke); +} + +.resource-indicator { + color: var(--astriarch-food); + border-color: var(--astriarch-food); +} + +.status-indicator--good { + background-color: var(--astriarch-status-good); +} +``` + +## Practical Usage Examples + +### Resource Display Component + +```svelte + + +
+ {#each resources as resource} +
+ {resource.name}: + {resource.value.toFixed(1)} +
+ {/each} +
+``` + +### Status Indicator Component + +```svelte + + + + + +``` + +### Primary Button Component + +```svelte + + +{#if variant === 'primary'} + +{:else if variant === 'glass'} + +{:else if variant === 'gradient'} + +{/if} +``` + +### Theme Card Component + +```svelte + + +
+
+ +
+
+ +
+
+``` + +## Typography + +### Font Family + +```css +--astriarch-font-orbitron: 'Orbitron', system-ui, -apple-system, sans-serif; +``` + +The design system uses **Orbitron** as the primary font family for that sci-fi aesthetic. + +### Typography Scale + +#### Headlines + +```html + +

Main Title

+ + +

Section Header

+ + +

Subsection

+ + +

Small Header

+ + +
Component Title
+ + +
Card Title
+``` + +#### Body Text + +```html + +

Large body text for important content

+

Large body text emphasized

+ + +

Medium body text for general content

+

Medium body text emphasized

+ + +

Standard body text for most content

+

Standard body text emphasized

+ + +

Small body text for secondary content

+

Small body text emphasized

+``` + +#### Captions + +```html + +Standard caption text +Emphasized caption + + +Small caption text +Small emphasized caption +``` + +#### Call to Action + +```html + + + + + + + + +``` + +#### Labels + +```html + + +``` + +### Typography Usage Examples + +#### Section Header Component + +```svelte +
+

Planet Overview

+

+ Manage your planet's resources and development +

+
+``` + +#### Card Component with Typography + +```svelte +
+

Resource Production

+
+
+ Food: + +2.4 +
+ per turn +
+
+``` + +#### Button with Proper Typography + +```svelte + +``` + +#### Form with Labels + +```svelte +
+
+ + +
+
+``` + +## Next Steps + +This comprehensive design system provides: + +1. **Complete Color Palette** - All colors from your Figma design systematically organized +2. **Tailwind Integration** - Ready-to-use utility classes for rapid development +3. **CSS Custom Properties** - Direct access for custom styling +4. **Practical Examples** - Real-world component patterns +5. **Consistent Theming** - Unified approach across the entire application + +### Future Extensions + +- Animation/transition patterns +- Spacing scales (if needed beyond Tailwind defaults) +- Component-specific design tokens +- Dark/light mode variations +- Accessibility considerations (contrast ratios, etc.) diff --git a/apps/astriarch-frontend-sveltekit/src/lib/index.ts b/apps/astriarch-frontend-sveltekit/src/lib/index.ts new file mode 100644 index 00000000..856f2b6c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/astriarch-frontend-sveltekit/src/lib/services/audioService.ts b/apps/astriarch-frontend-sveltekit/src/lib/services/audioService.ts new file mode 100644 index 00000000..45a12bc3 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/services/audioService.ts @@ -0,0 +1,336 @@ +export type GamePhase = 'StartMenu' | 'InGame' | 'GameOver'; + +export interface AudioTrack { + element: HTMLAudioElement; + name: string; + loop: boolean; +} + +export class AudioService { + private volume = 1.0; + private muted = false; + private enabled = true; + private currentPhase: GamePhase = 'StartMenu'; + private currentInGameTrack = 0; + private fadingPlayer: HTMLAudioElement | null = null; + + // Audio tracks + private tracks: Map = new Map(); + + constructor(enabled = true) { + this.enabled = enabled; + // Only initialize audio tracks if we're in the browser + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + this.initializeAudioTracks(); + } + } + + private initializeAudioTracks() { + if (!this.enabled || typeof document === 'undefined') return; + + // Background music - don't auto-play menu music to avoid browser blocking + this.createAudioTrack('menu-start', 'audio/menu-start', true, false, false); + this.createAudioTrack('in-game1', 'audio/in-game1', false, true); + this.createAudioTrack('in-game2', 'audio/in-game2', false, true); + this.createAudioTrack('in-game3', 'audio/in-game3', false, true); + this.createAudioTrack('in-game4', 'audio/in-game4', false, true); + this.createAudioTrack('game-over', 'audio/game-over', true, false); + + // Sound effects + this.createAudioTrack('turn-start', 'audio/sfx-turn-start', false, false); + this.createAudioTrack('turn-end', 'audio/sfx-turn-end', false, false); + } + + private createAudioTrack( + name: string, + fileName: string, + loop: boolean, + addEndedEvent: boolean, + playImmediately = false + ) { + if (typeof document === 'undefined') return; + + const audioElement = document.createElement('audio'); + audioElement.loop = loop; + audioElement.controls = false; + audioElement.preload = 'auto'; + + // Support both OGG and MP3 formats + const supportsOgg = audioElement.canPlayType('audio/ogg; codecs="vorbis"'); + const extension = supportsOgg ? 'ogg' : 'mp3'; + audioElement.src = `${fileName}.${extension}`; + + if (playImmediately) { + audioElement.addEventListener( + 'canplaythrough', + () => { + audioElement.play().catch((error) => { + console.warn('Audio autoplay blocked:', error); + }); + }, + false + ); + } + + if (addEndedEvent) { + audioElement.addEventListener( + 'ended', + () => { + this.onMediaEnded(); + }, + false + ); + } + + // Attempt to preload + audioElement.load(); + + this.tracks.set(name, { + element: audioElement, + name, + loop + }); + } + + private onMediaEnded() { + if (this.currentPhase === 'InGame') { + this.currentInGameTrack++; + if (this.currentInGameTrack >= 4) { + this.currentInGameTrack = 0; + } + } + + const currentTrack = this.getCurrentTrack(); + if (currentTrack) { + currentTrack.element.currentTime = 0; + currentTrack.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + currentTrack.element.volume = this.volume; + } + } + + private getCurrentTrack(): AudioTrack | undefined { + if (!this.enabled) return undefined; + + switch (this.currentPhase) { + case 'StartMenu': + return this.tracks.get('menu-start'); + case 'GameOver': + return this.tracks.get('game-over'); + case 'InGame': { + const trackNames = ['in-game1', 'in-game2', 'in-game3', 'in-game4']; + return this.tracks.get(trackNames[this.currentInGameTrack]); + } + default: + return undefined; + } + } + + private fadeOutTick() { + if (!this.enabled || !this.fadingPlayer) return; + + if (this.fadingPlayer.volume <= 0.1) { + this.fadingPlayer.volume = 0; + this.fadingPlayer.pause(); + this.fadingPlayer.currentTime = 0; + this.fadingPlayer = null; + } else { + this.fadingPlayer.volume = Math.max(0, this.fadingPlayer.volume - 0.1); + setTimeout(() => this.fadeOutTick(), 100); + } + } + + private startFadeOut() { + const currentTrack = this.getCurrentTrack(); + if (currentTrack) { + this.fadingPlayer = currentTrack.element; + setTimeout(() => this.fadeOutTick(), 100); + } + } + + // Public methods + toggleMute(): boolean { + this.muted = !this.muted; + + if (this.enabled) { + this.tracks.forEach((track) => { + track.element.muted = this.muted; + }); + } + + return this.muted; + } + + setVolume(volume: number) { + this.volume = Math.max(0, Math.min(1, volume)); + + const currentTrack = this.getCurrentTrack(); + if (currentTrack) { + currentTrack.element.volume = this.volume; + } + } + + getVolume(): number { + return this.volume; + } + + isMuted(): boolean { + return this.muted; + } + + isEnabled(): boolean { + return this.enabled; + } + + getCurrentPhase(): GamePhase { + return this.currentPhase; + } + + // Enable audio after user interaction + enableUserInteraction() { + // This method should be called after the first user interaction + // to enable audio playback in browsers that require user gesture + if (this.currentPhase === 'StartMenu') { + this.startMenu(); + } + } + + // Phase transitions + startMenu() { + if (!this.enabled) { + console.log('AudioService: startMenu() called but audio not enabled'); + return; + } + + console.log('AudioService: startMenu() called, currentPhase:', this.currentPhase); + + // Only fade out if we're switching from a different phase + if (this.currentPhase !== 'StartMenu') { + this.startFadeOut(); + } + + this.currentPhase = 'StartMenu'; + + const track = this.tracks.get('menu-start'); + if (track) { + console.log('AudioService: Found menu-start track, attempting to play...'); + track.element.currentTime = 0; + track.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + track.element.volume = this.volume; + console.log('AudioService: Menu track volume set to:', this.volume); + } else { + console.warn('AudioService: menu-start track not found!'); + } + } + + beginGame() { + if (!this.enabled) return; + + // Only fade out if we're switching from a different phase + if (this.currentPhase !== 'InGame') { + this.startFadeOut(); + } + + this.currentPhase = 'InGame'; + this.currentInGameTrack = 0; + + const track = this.tracks.get('in-game1'); + if (track) { + track.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + track.element.volume = this.volume; + } + } + + endGame() { + if (!this.enabled) return; + + // Only fade out if we're switching from a different phase + if (this.currentPhase !== 'GameOver') { + this.startFadeOut(); + } + + this.currentPhase = 'GameOver'; + + const track = this.tracks.get('game-over'); + if (track) { + track.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + track.element.volume = this.volume; + } + } + + // Sound effects + playTurnStart() { + if (!this.enabled || this.muted) return; + + const track = this.tracks.get('turn-start'); + if (track) { + track.element.currentTime = 0; + track.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + track.element.volume = this.volume; + } + } + + playTurnEnd() { + if (!this.enabled || this.muted) return; + + const track = this.tracks.get('turn-end'); + if (track) { + track.element.currentTime = 0; + track.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + track.element.volume = this.volume; + } + } + + // Enable user interaction with audio (required for autoplay policies) + enableAudio() { + this.enabled = true; + // Only initialize if we're in the browser and haven't already initialized + if ( + typeof window !== 'undefined' && + typeof document !== 'undefined' && + this.tracks.size === 0 + ) { + this.initializeAudioTracks(); + } + + // Try to play the current phase's audio + const currentTrack = this.getCurrentTrack(); + if (currentTrack && currentTrack.element.paused) { + currentTrack.element.play().catch((error) => { + console.warn('Audio play failed:', error); + }); + } + } + + // Cleanup + destroy() { + this.tracks.forEach((track) => { + track.element.pause(); + track.element.src = ''; + track.element.remove(); + }); + this.tracks.clear(); + this.fadingPlayer = null; + } +} + +// Singleton instance - only create in browser environment +let audioServiceInstance: AudioService | null = null; + +export const audioService = (() => { + if (typeof window !== 'undefined' && !audioServiceInstance) { + audioServiceInstance = new AudioService(true); + } + return audioServiceInstance; +})(); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/services/websocket.ts b/apps/astriarch-frontend-sveltekit/src/lib/services/websocket.ts new file mode 100644 index 00000000..01b208f5 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/services/websocket.ts @@ -0,0 +1,1249 @@ +import { get } from 'svelte/store'; +import { + MESSAGE_TYPE, + Message, + type IMessage, + type IGame, + type ServerGameOptions, + type IEventNotificationsPayload, + type EventNotification, + EventNotificationType, + isCreateGameResponse, + isJoinGameResponse, + isListGamesResponse, + isStartGameResponse, + isGameStateUpdate, + isErrorMessage +} from 'astriarch-engine'; + +// Import the main game stores to update them when receiving multiplayer game state +import { clientGameModel, isGameRunning, gameActions } from '$lib/stores/gameStore'; +import { PlayerStorage } from '$lib/utils/playerStorage'; +import type { ClientModelData } from 'astriarch-engine'; + +// Import multiplayer store types and functionality from the centralized store +import { multiplayerGameStore, type ChatMessage } from '$lib/stores/multiplayerGameStore'; + +// Import activity store for enhanced event logging +import { activityStore } from '$lib/stores/activityStore'; + +// Re-export types from engine for convenience +export type { IGame, ServerGameOptions }; + +// WebSocket connection management +class WebSocketService { + private ws: WebSocket | null = null; + private reconnectAttempts = 0; + private maxReconnectAttempts = 5; + private reconnectDelay = 1000; + private messageQueue: IMessage[] = []; + private isConnecting = false; + private pingInterval: number | null = null; + private readonly pingIntervalMs = 20000; // 20 seconds (less than backend's 30s timeout) + private gameSyncInterval: number | null = null; + private readonly gameSyncIntervalMs = 1000; // 1 second for game state synchronization + + constructor(private gameStore: typeof multiplayerGameStore) {} + + // Helper method to get current gameId from store + private getCurrentGameId(): string | null { + return get(this.gameStore).gameId; + } + + // Helper method to get current gameId with error handling + private requireGameId(): string { + const gameId = this.getCurrentGameId(); + if (!gameId) { + throw new Error('No active game session - gameId not found in store'); + } + return gameId; + } + + // Helper method to check if current player is the host (position 0) + private isHost(): boolean { + const storeState = get(this.gameStore); + return storeState.playerPosition === 0; + } + + // Helper method to establish session via HTTP before WebSocket connection + private async establishSession(): Promise { + try { + // Make a simple HTTP request to the health endpoint to establish a session + // This will create a session and set the connect.sid cookie + const response = await fetch('http://localhost:8001/api/health', { + method: 'GET', + credentials: 'include' // Important: include cookies in the request + }); + + if (!response.ok) { + console.warn('Failed to establish session via health check:', response.status); + } else { + console.log('Session established via HTTP health check'); + } + } catch (error) { + console.warn('Error establishing session:', error); + } + } + + connect(url: string = 'ws://localhost:8001'): Promise { + if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) { + return Promise.resolve(); + } + + this.isConnecting = true; + + // First establish a session via HTTP, then connect WebSocket + return this.establishSession().then(() => { + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.isConnecting = false; + this.reconnectAttempts = 0; + this.gameStore.setConnected(true); + + // Start the ping interval to keep the session alive + this.startPingInterval(); + + // Load player name from localStorage, or use default + const storedPlayerName = PlayerStorage.getPlayerNameWithFallback('Player'); + this.gameStore.setPlayerName(storedPlayerName); + + // Send queued messages + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift(); + if (message) { + this.send(message); + } + } + + resolve(); + }; + + this.ws.onmessage = (event) => { + try { + const message: IMessage = JSON.parse(event.data); + this.handleMessage(message); + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + }; + + this.ws.onclose = () => { + console.log('WebSocket disconnected'); + this.isConnecting = false; + this.gameStore.setConnected(false); + this.stopPingInterval(); + this.stopGameSyncInterval(); + this.attemptReconnect(); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + this.isConnecting = false; + this.gameStore.addNotification({ + type: 'error', + message: 'WebSocket connection error', + timestamp: Date.now() + }); + reject(error); + }; + } catch (error) { + this.isConnecting = false; + reject(error); + } + }); + }); + } + + private attemptReconnect() { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + console.log( + `Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...` + ); + + setTimeout(() => { + this.connect().catch((error) => { + console.error('Reconnection failed:', error); + }); + }, this.reconnectDelay * this.reconnectAttempts); + } else { + this.gameStore.addNotification({ + type: 'error', + message: 'Failed to reconnect to server after multiple attempts', + timestamp: Date.now() + }); + } + } + + private handleEventNotifications(events: EventNotification[]) { + let shouldUpdateFleets = false; + let shouldUpdatePlanets = false; + + for (const event of events) { + // Convert EventNotificationType to a user-friendly notification type + let notificationType: + | 'info' + | 'success' + | 'warning' + | 'error' + | 'battle' + | 'research' + | 'construction' + | 'fleet' + | 'planet' + | 'diplomacy' = 'info'; + + switch (event.type) { + case EventNotificationType.ResearchComplete: + case EventNotificationType.ResearchStolen: + case EventNotificationType.ResearchQueueEmpty: + notificationType = 'research'; + break; + case EventNotificationType.ImprovementBuilt: + case EventNotificationType.ShipBuilt: + case EventNotificationType.ImprovementDemolished: + notificationType = 'construction'; + if (event.type === EventNotificationType.ShipBuilt) { + shouldUpdateFleets = true; + } + shouldUpdatePlanets = true; + break; + case EventNotificationType.DefendedAgainstAttackingFleet: + case EventNotificationType.AttackingFleetLost: + case EventNotificationType.PlanetCaptured: + case EventNotificationType.PlanetLost: + notificationType = 'battle'; + shouldUpdateFleets = true; + shouldUpdatePlanets = true; + break; + case EventNotificationType.PopulationGrowth: + case EventNotificationType.PopulationStarvation: + case EventNotificationType.PlanetLostDueToStarvation: + notificationType = 'planet'; + shouldUpdatePlanets = true; + break; + case EventNotificationType.InsufficientFood: + case EventNotificationType.FoodShortageRiots: + case EventNotificationType.CitizensProtesting: + notificationType = 'warning'; + break; + case EventNotificationType.TradesExecuted: + notificationType = 'success'; + break; + case EventNotificationType.TradesNotExecuted: + notificationType = 'error'; + break; + default: + notificationType = 'info'; + } + + // Add to the transient notification system (for popups) + this.gameStore.addNotification({ + type: notificationType, + message: event.message, + timestamp: Date.now() + }); + + // Also add to the persistent activity log with full event data + activityStore.addNotificationWithEventData( + { + type: notificationType, + message: event.message, + timestamp: Date.now() + }, + event // Pass the original EventNotification for rich interactions + ); + } + + // Notify galaxy canvas of changes after processing all events + if (shouldUpdateFleets) { + gameActions.notifyFleetSent(); + } + if (shouldUpdatePlanets) { + gameActions.notifyPlanetsChanged(); + } + } + + private handleMessage(message: IMessage) { + console.log('Received message:', message); + + switch (message.type) { + case MESSAGE_TYPE.LIST_GAMES: + if (isListGamesResponse(message)) { + this.gameStore.setAvailableGames(message.payload.games); + } else { + console.warn('LIST_GAMES payload missing games array:', message.payload); + } + break; + + case MESSAGE_TYPE.GAME_LIST_UPDATED: + if (isListGamesResponse(message)) { + this.gameStore.setAvailableGames(message.payload.games); + } else { + console.warn('GAME_LIST_UPDATED payload missing games array:', message.payload); + } + break; + + case MESSAGE_TYPE.GAME_STATE_UPDATE: + if (isGameStateUpdate(message)) { + if (message.payload.clientGameModel) { + // Update the client game model with the real-time data from server + const updatedClientGameModel = message.payload.clientGameModel as ClientModelData; + clientGameModel.set(updatedClientGameModel); + console.log('Received real-time game state update from server'); + + // Notify galaxy canvas that game state has changed (including fleets) + gameActions.notifyFleetSent(); + } else if (message.payload.changes) { + // TODO: Apply incremental changes if needed + console.log('Received incremental changes (not implemented):', message.payload.changes); + } + } else { + console.warn('Unexpected GAME_STATE_UPDATE payload format:', message.payload); + } + break; + + case MESSAGE_TYPE.START_GAME: + if (isStartGameResponse(message)) { + if (message.payload.success) { + this.gameStore.setCurrentView('game'); + + // Update the main game stores for multiplayer + if (message.payload.gameState) { + // The server sends a ClientModelData (player-specific view of the game) + const clientGameState = message.payload.gameState as ClientModelData; + clientGameModel.set(clientGameState); + isGameRunning.set(true); + gameActions.selectHomePlanet(); + + // For multiplayer, we don't have access to the full GameModelData on the client + // The clientGameModel contains everything the player needs to see + console.log('Multiplayer game started! Updated client game state.'); + + // Start the client-side game loop for real-time updates + // Note: In multiplayer, time advancement happens on the server + // The client loop only updates the UI and sends player actions + gameActions.resumeGame(); + } + + this.gameStore.addNotification({ + type: 'success', + message: 'Game started!', + timestamp: Date.now() + }); + + // Start game sync interval if this player is the host + this.startGameSyncInterval(); + } else { + this.gameStore.addNotification({ + type: 'error', + message: message.payload.error || 'Failed to start game', + timestamp: Date.now() + }); + } + } else { + console.warn('Unexpected START_GAME payload format:', message.payload); + } + break; + + case MESSAGE_TYPE.RESUME_GAME: + // Handle resume game response - similar to START_GAME but for existing games + if (message.payload && typeof message.payload === 'object') { + const payload = message.payload as { + clientGameModel?: ClientModelData; + playerPosition?: number; + sessionId?: string; + }; + if (payload.clientGameModel) { + this.gameStore.setCurrentView('game'); + + // Store session ID if provided + if (payload.sessionId && typeof payload.sessionId === 'string') { + this.gameStore.setSessionId(payload.sessionId); + } + + // Update the main game stores for multiplayer + const clientGameState = payload.clientGameModel as ClientModelData; + clientGameModel.set(clientGameState); + + // Don't automatically set game as running - wait for server to tell us + // if the game is paused or resumed based on player connectivity + gameActions.selectHomePlanet(); + + this.gameStore.addNotification({ + type: 'success', + message: 'Rejoined game successfully!', + timestamp: Date.now() + }); + + console.log( + 'Game rejoined successfully! Updated client game state. Waiting for server status...' + ); + + // If player position is provided, update it + if (typeof payload.playerPosition === 'number') { + this.gameStore.setPlayerPosition(payload.playerPosition); + } + + // Start game sync interval if this player is the host and game is in progress + this.startGameSyncInterval(); + } else { + this.gameStore.addNotification({ + type: 'error', + message: 'Failed to resume game - invalid response', + timestamp: Date.now() + }); + } + } else { + console.warn('Unexpected RESUME_GAME payload format:', message.payload); + } + break; + + case MESSAGE_TYPE.CREATE_GAME: + if (isCreateGameResponse(message)) { + this.gameStore.setGameId(message.payload.gameId); + this.gameStore.setGameJoined(true); + this.gameStore.setCurrentView('game_options'); + + // Game creator is always at position 0 (following old game pattern) + this.gameStore.setPlayerPosition(0); + console.log('Created game as host at position 0'); + + // As the host, we may need to start sync interval when the game actually starts + // (The interval will only start if we're in an active game) + + // Store session ID if provided + const payload = message.payload as unknown as Record; + if (payload.sessionId && typeof payload.sessionId === 'string') { + this.gameStore.setSessionId(payload.sessionId); + } + + this.gameStore.addNotification({ + type: 'success', + message: `Game created successfully!`, + timestamp: Date.now() + }); + } else if (isErrorMessage(message)) { + this.gameStore.addNotification({ + type: 'error', + message: message.payload.error || 'Failed to create game', + timestamp: Date.now() + }); + } else { + console.warn('Unexpected CREATE_GAME payload format:', message.payload); + this.gameStore.addNotification({ + type: 'error', + message: 'Unexpected response from server', + timestamp: Date.now() + }); + } + break; + + case MESSAGE_TYPE.JOIN_GAME: + if (isJoinGameResponse(message)) { + if (message.payload.success) { + this.gameStore.setGameId(message.payload.gameId || ''); + this.gameStore.setGameJoined(true); + this.gameStore.setCurrentView('game_options'); + + // Store player position if provided (following old game pattern) + const payload = message.payload as unknown as Record; + if (typeof payload.playerPosition === 'number') { + this.gameStore.setPlayerPosition(payload.playerPosition); + console.log('Joined game at player position:', payload.playerPosition); + } + + // Store session ID if provided + if (payload.sessionId && typeof payload.sessionId === 'string') { + this.gameStore.setSessionId(payload.sessionId); + } + + this.gameStore.addNotification({ + type: 'success', + message: 'Successfully joined game!', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'error', + message: message.payload.error || 'Failed to join game', + timestamp: Date.now() + }); + } + } else { + console.warn('Unexpected JOIN_GAME payload format:', message.payload); + } + break; + + case MESSAGE_TYPE.CHANGE_GAME_OPTIONS: + // Game options have been updated + console.log('Received CHANGE_GAME_OPTIONS:', message.payload); + + // The backend automatically sends updated game list to lobby players + // so the current game will be updated with new options + + this.gameStore.addNotification({ + type: 'info', + message: 'Game options updated', + timestamp: Date.now() + }); + break; + + case MESSAGE_TYPE.CHANGE_PLAYER_NAME: + // Player name has been updated + console.log('Received CHANGE_PLAYER_NAME:', message.payload); + + // The backend automatically sends updated game list to lobby players + // so the current game will be updated with the new player name + + this.gameStore.addNotification({ + type: 'info', + message: 'Player name updated', + timestamp: Date.now() + }); + break; + + case MESSAGE_TYPE.CHAT_MESSAGE: { + const chatData = message.payload as unknown as ChatMessage; + this.gameStore.addChatMessage(chatData); + break; + } + + case MESSAGE_TYPE.EVENT_NOTIFICATIONS: { + const eventData = message.payload as IEventNotificationsPayload; + this.handleEventNotifications(eventData.events as EventNotification[]); + break; + } + + case MESSAGE_TYPE.CHAT_ROOM_SESSIONS_UPDATED: + this.gameStore.addNotification({ + type: 'info', + message: `Player activity updated`, + timestamp: Date.now() + }); + break; + + case MESSAGE_TYPE.GAME_OVER: + this.gameStore.addNotification({ + type: 'info', + message: `Game has ended`, + timestamp: Date.now() + }); + break; + + case MESSAGE_TYPE.PLAYER_DISCONNECTED: + if ( + message.payload && + typeof message.payload === 'object' && + 'playerName' in message.payload + ) { + const payload = message.payload as { playerName: string; gameId: string }; + this.gameStore.addNotification({ + type: 'warning', + message: `${payload.playerName} has disconnected. Game paused.`, + timestamp: Date.now() + }); + // Pause the client-side game loop + gameActions.pauseGame(); + } + break; + + case MESSAGE_TYPE.GAME_PAUSED: + if (message.payload && typeof message.payload === 'object' && 'reason' in message.payload) { + const payload = message.payload as { reason: string; gameId: string }; + this.gameStore.addNotification({ + type: 'warning', + message: `Game paused - ${payload.reason === 'waiting_for_players' ? 'waiting for disconnected player to reconnect' : payload.reason}`, + timestamp: Date.now() + }); + // Pause the client-side game loop and set game as not running + isGameRunning.set(false); + gameActions.pauseGame(); + // Stop game sync interval when game is paused + this.stopGameSyncInterval(); + } + break; + + case MESSAGE_TYPE.GAME_RESUMED: + if (message.payload && typeof message.payload === 'object' && 'gameId' in message.payload) { + this.gameStore.addNotification({ + type: 'success', + message: 'All players reconnected. Game resumed.', + timestamp: Date.now() + }); + // Resume the client-side game loop and set game as running + isGameRunning.set(true); + gameActions.resumeGame(); + // Start game sync interval if this player is the host + this.startGameSyncInterval(); + } + break; + + case MESSAGE_TYPE.PLAYER_RECONNECTED: + if ( + message.payload && + typeof message.payload === 'object' && + 'playerName' in message.payload + ) { + const payload = message.payload as { + playerName: string; + gameId: string; + allPlayersConnected?: boolean; + }; + if (payload.allPlayersConnected) { + this.gameStore.addNotification({ + type: 'success', + message: `All players have reconnected. You can now resume the game.`, + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'info', + message: `${payload.playerName} has reconnected.`, + timestamp: Date.now() + }); + } + } + break; + + case MESSAGE_TYPE.UPDATE_PLANET_BUILD_QUEUE: + // Handle build queue update response + if ( + message.payload && + typeof message.payload === 'object' && + 'success' in message.payload + ) { + const payload = message.payload as { + success: boolean; + error?: string; + gameData?: unknown; + }; + if (payload.success) { + this.gameStore.addNotification({ + type: 'success', + message: 'Build queue updated successfully', + timestamp: Date.now() + }); + + // Game state updates are handled via GAME_STATE_UPDATE messages + } else { + this.gameStore.addNotification({ + type: 'error', + message: payload.error || 'Failed to update build queue', + timestamp: Date.now() + }); + } + } else { + console.warn('Unexpected UPDATE_PLANET_BUILD_QUEUE payload format:', message.payload); + } + break; + + case MESSAGE_TYPE.SEND_SHIPS: + // Handle send ships response + if (message.payload && typeof message.payload === 'object') { + if ('error' in message.payload) { + const errorPayload = message.payload as { error?: string }; + this.gameStore.addNotification({ + type: 'error', + message: errorPayload.error || 'Failed to send ships', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'success', + message: 'Ships sent successfully', + timestamp: Date.now() + }); + } + } else { + // Success case - ships sent successfully + this.gameStore.addNotification({ + type: 'success', + message: 'Ships sent successfully', + timestamp: Date.now() + }); + } + break; + + case MESSAGE_TYPE.ERROR: + if (isErrorMessage(message)) { + this.gameStore.addNotification({ + type: 'error', + message: message.payload.error || 'An error occurred', + timestamp: Date.now() + }); + } else { + console.warn('Unexpected ERROR payload format:', message.payload); + } + break; + + case MESSAGE_TYPE.PONG: + // Server responded to our ping - session is alive + console.log('Received pong from server'); + + // Check if PONG includes session ID or updated game state + if (message.payload && typeof message.payload === 'object') { + const payload = message.payload as Record; + + // Store session ID if provided + if (payload.sessionId && typeof payload.sessionId === 'string') { + this.gameStore.setSessionId(payload.sessionId); + console.log('Session ID stored:', payload.sessionId); + } + + if (payload.clientGameModel) { + console.log('PONG included updated game state, updating client model'); + + // Update the main game store with the fresh data from the server + clientGameModel.set(payload.clientGameModel as ClientModelData); + + // Don't automatically set game as running - let explicit game state messages handle that + // The PONG is just providing updated data, not changing game state + + // Optionally update current cycle if provided + if (payload.currentCycle !== undefined) { + // You could add a currentCycle store if needed + console.log('Current game cycle:', payload.currentCycle); + } + } + } + break; + + case MESSAGE_TYPE.SYNC_STATE: + // Handle server state sync response + console.log('Received synchronized game state from server'); + break; + + case MESSAGE_TYPE.ADJUST_RESEARCH_PERCENT: + // Handle research percent adjustment response + if (message.payload && typeof message.payload === 'object') { + if ('error' in message.payload) { + const errorPayload = message.payload as { error?: string }; + this.gameStore.addNotification({ + type: 'error', + message: errorPayload.error || 'Failed to adjust research percent', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'success', + message: 'Research allocation updated successfully', + timestamp: Date.now() + }); + // Game state updates are handled via GAME_STATE_UPDATE messages + } + } + break; + + case MESSAGE_TYPE.SUBMIT_RESEARCH_ITEM: + // Handle submit research item response + if (message.payload && typeof message.payload === 'object') { + if ('error' in message.payload) { + const errorPayload = message.payload as { error?: string }; + this.gameStore.addNotification({ + type: 'error', + message: errorPayload.error || 'Failed to start research', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'success', + message: 'Research started successfully', + timestamp: Date.now() + }); + // Game state updates are handled via GAME_STATE_UPDATE messages + } + } + break; + + case MESSAGE_TYPE.CANCEL_RESEARCH_ITEM: + // Handle cancel research item response + if (message.payload && typeof message.payload === 'object') { + if ('error' in message.payload) { + const errorPayload = message.payload as { error?: string }; + this.gameStore.addNotification({ + type: 'error', + message: errorPayload.error || 'Failed to cancel research', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'success', + message: 'Research cancelled successfully', + timestamp: Date.now() + }); + // Game state updates are handled via GAME_STATE_UPDATE messages + } + } + break; + + case MESSAGE_TYPE.SUBMIT_TRADE: + // Handle submit trade response + if (message.payload && typeof message.payload === 'object') { + if ('error' in message.payload) { + const errorPayload = message.payload as { error?: string }; + this.gameStore.addNotification({ + type: 'error', + message: errorPayload.error || 'Failed to submit trade', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'success', + message: 'Trade submitted successfully', + timestamp: Date.now() + }); + // Game state updates are handled via GAME_STATE_UPDATE messages + } + } + break; + + case MESSAGE_TYPE.CANCEL_TRADE: + // Handle cancel trade response + if (message.payload && typeof message.payload === 'object') { + if ('error' in message.payload) { + const errorPayload = message.payload as { error?: string }; + this.gameStore.addNotification({ + type: 'error', + message: errorPayload.error || 'Failed to cancel trade', + timestamp: Date.now() + }); + } else { + this.gameStore.addNotification({ + type: 'success', + message: 'Trade cancelled successfully', + timestamp: Date.now() + }); + // Game state updates are handled via GAME_STATE_UPDATE messages + } + } + break; + + default: + console.log('Unhandled message type:', message.type); + } + } + + send(message: IMessage) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } else { + // Queue message for when connection is established + this.messageQueue.push(message); + + if (!this.isConnecting) { + this.connect().catch((error) => { + console.error('Failed to connect for sending message:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'Failed to send message - not connected', + timestamp: Date.now() + }); + }); + } + } + } + + // Game-specific methods + listGames() { + this.send(new Message(MESSAGE_TYPE.LIST_GAMES, {})); + } + + createGame(gameOptions: ServerGameOptions) { + // Use player name from game options, fallback to store or default + const playerName = gameOptions.mainPlayerName || get(this.gameStore).playerName || 'Player'; + + // Update the store with the actual player name being used + this.gameStore.setPlayerName(playerName); + + console.log('Creating game with player name:', playerName); // Send both game name and player name as the backend expects + const payload: Record = { + name: gameOptions.name || 'Unnamed Game', + playerName: playerName, + gameOptions: { + name: gameOptions.name || 'Unnamed Game', + mainPlayerName: playerName, + systemsToGenerate: 4, // Default to 4 systems + planetsPerSystem: gameOptions.planetsPerSystem || 4, + galaxySize: + typeof gameOptions.galaxySize === 'string' + ? gameOptions.galaxySize === 'Small' + ? 2 + : gameOptions.galaxySize === 'Medium' + ? 3 + : gameOptions.galaxySize === 'Large' + ? 4 + : 4 + : gameOptions.galaxySize || 4, + distributePlanetsEvenly: gameOptions.distributePlanetsEvenly ?? true, + quickStart: gameOptions.quickStart ?? false, + gameSpeed: 3, // Default to no time limit + opponentOptions: [ + { name: '', type: -1 }, // Player 2: Open + { name: '', type: -2 }, // Player 3: Closed + { name: '', type: -2 } // Player 4: Closed + ] + } + }; + + console.log('Sending CREATE_GAME with payload:', payload); + this.send(new Message(MESSAGE_TYPE.CREATE_GAME, payload)); + } + + joinGame(gameId: string, playerName?: string) { + // Get current player name from store or use a default + const currentPlayerName = playerName || get(this.gameStore).playerName || 'Player'; + this.send(new Message(MESSAGE_TYPE.JOIN_GAME, { gameId, playerName: currentPlayerName })); + } + + startGame() { + try { + const gameId = this.requireGameId(); + console.log('Starting game:', gameId); + this.send(new Message(MESSAGE_TYPE.START_GAME, { gameId })); + } catch (error) { + console.error('Failed to start game:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'No game selected to start', + timestamp: Date.now() + }); + } + } + + resumeGame(gameId: string) { + console.log('Resuming game:', gameId); + this.gameStore.setGameId(gameId); + this.send(new Message(MESSAGE_TYPE.RESUME_GAME, { gameId })); + } + + leaveGame() { + this.stopGameSyncInterval(); + this.send(new Message(MESSAGE_TYPE.EXIT_RESIGN, {})); + } + + sendChatMessage(message: string) { + this.send(new Message(MESSAGE_TYPE.CHAT_MESSAGE, { message })); + } + + changeGameOptions( + gameId: string, + gameOptions: { + name: string; + playerName: string; + systemsToGenerate: number; + planetsPerSystem: number; + galaxySize: number; + distributePlanetsEvenly: boolean; + quickStart: boolean; + gameSpeed: number; + opponentOptions: Array<{ name: string; type: number }>; + } + ) { + const payload = { + gameId, + gameOptions: { + name: gameOptions.name, + mainPlayerName: gameOptions.playerName, + systemsToGenerate: gameOptions.systemsToGenerate, + planetsPerSystem: gameOptions.planetsPerSystem, + galaxySize: gameOptions.galaxySize, + distributePlanetsEvenly: gameOptions.distributePlanetsEvenly, + quickStart: gameOptions.quickStart, + gameSpeed: gameOptions.gameSpeed, + opponentOptions: gameOptions.opponentOptions + }, + playerName: gameOptions.playerName + }; + + console.log('Sending CHANGE_GAME_OPTIONS with payload:', payload); + this.send(new Message(MESSAGE_TYPE.CHANGE_GAME_OPTIONS, payload)); + } + + changePlayerName(gameId: string, playerName: string) { + const trimmedName = playerName.substring(0, 20); // Limit to 20 characters like old game + + const payload = { + gameId, + playerName: trimmedName + }; + + // Update local store (which will also save to localStorage) + this.gameStore.setPlayerName(trimmedName); + + console.log('Sending CHANGE_PLAYER_NAME with payload:', payload); + this.send(new Message(MESSAGE_TYPE.CHANGE_PLAYER_NAME, payload)); + } + + updatePlanetBuildQueue( + planetId: number, + action: 'add' | 'remove', + productionItem?: unknown, + index?: number + ) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + planetId, + action, + productionItem, + index + }; + + console.log('Sending UPDATE_PLANET_BUILD_QUEUE with payload:', payload); + this.send(new Message(MESSAGE_TYPE.UPDATE_PLANET_BUILD_QUEUE, payload)); + } catch (error) { + console.error('Failed to update planet build queue:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'Cannot update build queue - no active game session', + timestamp: Date.now() + }); + } + } + + updatePlanetWorkerAssignments( + planetId: number, + farmerDiff: number, + minerDiff: number, + builderDiff: number + ) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + planetId, + farmerDiff, + minerDiff, + builderDiff + }; + + console.log('Sending UPDATE_PLANET_OPTIONS with payload:', payload); + this.send(new Message(MESSAGE_TYPE.UPDATE_PLANET_OPTIONS, payload)); + } catch (error) { + console.error('Failed to update planet worker assignments:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'Cannot update worker assignments - no active game session', + timestamp: Date.now() + }); + } + } + + sendShips( + planetIdSource: number, + planetIdDest: number, + shipsByType: { + scouts: number[]; + destroyers: number[]; + cruisers: number[]; + battleships: number[]; + } + ) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + planetIdSource, + planetIdDest, + data: shipsByType + }; + + console.log('Sending SEND_SHIPS with payload:', payload); + this.send(new Message(MESSAGE_TYPE.SEND_SHIPS, payload)); + } catch (error) { + console.error('Failed to send ships:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'Cannot send ships - no active game session', + timestamp: Date.now() + }); + } + } + + requestStateSync() { + try { + const gameId = this.requireGameId(); + console.log('Requesting server state synchronization for game:', gameId); + const payload = { gameId }; + this.send(new Message(MESSAGE_TYPE.SYNC_STATE, payload)); + } catch (error) { + console.error('Failed to request state sync:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'Cannot sync state - no active game session', + timestamp: Date.now() + }); + } + } + + private startPingInterval() { + this.stopPingInterval(); // Clear any existing interval + this.pingInterval = window.setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + console.log('Sending ping to keep session alive'); + this.send(new Message(MESSAGE_TYPE.PING, {})); + } + }, this.pingIntervalMs); + } + + private stopPingInterval() { + if (this.pingInterval !== null) { + window.clearInterval(this.pingInterval); + this.pingInterval = null; + } + } + + private startGameSyncInterval() { + // Only start sync interval if player is the host and there's an active game + if (!this.isHost() || !this.getCurrentGameId()) { + return; + } + + this.stopGameSyncInterval(); // Clear any existing interval + console.log('Starting game sync interval (host player)'); + + this.gameSyncInterval = window.setInterval(() => { + if ( + this.ws && + this.ws.readyState === WebSocket.OPEN && + this.getCurrentGameId() && + this.isHost() + ) { + console.log('Sending SYNC_STATE to advance game time for AI players'); + this.send(new Message(MESSAGE_TYPE.SYNC_STATE, {})); + } else { + // Stop interval if conditions are no longer met + this.stopGameSyncInterval(); + } + }, this.gameSyncIntervalMs); + } + + private stopGameSyncInterval() { + if (this.gameSyncInterval !== null) { + console.log('Stopping game sync interval'); + window.clearInterval(this.gameSyncInterval); + this.gameSyncInterval = null; + } + } + + // Research methods + adjustResearchPercent(researchPercent: number) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + researchPercent: Math.max(0, Math.min(1, researchPercent)) + }; + this.send(new Message(MESSAGE_TYPE.ADJUST_RESEARCH_PERCENT, payload)); + } catch (error) { + console.error('Failed to adjust research percent:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'No game selected to adjust research', + timestamp: Date.now() + }); + } + } + + submitResearchItem(researchType: number, data: Record = {}) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + researchItem: { + type: researchType, + data: data + } + }; + this.send(new Message(MESSAGE_TYPE.SUBMIT_RESEARCH_ITEM, payload)); + } catch (error) { + console.error('Failed to submit research item:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'No game selected to start research', + timestamp: Date.now() + }); + } + } + + cancelResearchItem() { + try { + const gameId = this.requireGameId(); + const payload = { gameId }; + this.send(new Message(MESSAGE_TYPE.CANCEL_RESEARCH_ITEM, payload)); + } catch (error) { + console.error('Failed to cancel research item:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'No game selected to cancel research', + timestamp: Date.now() + }); + } + } + + // Trading methods + submitTrade(planetId: number, tradeType: number, resourceType: number, amount: number) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + planetId, + tradeType, + resourceType, + amount + }; + console.log('Sending SUBMIT_TRADE with payload:', payload); + this.send(new Message(MESSAGE_TYPE.SUBMIT_TRADE, payload)); + } catch (error) { + console.error('Failed to submit trade:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'No game selected to submit trade', + timestamp: Date.now() + }); + } + } + + cancelTrade(tradeId: string) { + try { + const gameId = this.requireGameId(); + const payload = { + gameId, + tradeId + }; + console.log('Sending CANCEL_TRADE with payload:', payload); + this.send(new Message(MESSAGE_TYPE.CANCEL_TRADE, payload)); + } catch (error) { + console.error('Failed to cancel trade:', error); + this.gameStore.addNotification({ + type: 'error', + message: 'No game selected to cancel trade', + timestamp: Date.now() + }); + } + } + + disconnect() { + this.stopPingInterval(); + this.stopGameSyncInterval(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.gameStore.reset(); + } +} + +// Create singleton instances +export const webSocketService = new WebSocketService(multiplayerGameStore); + +// Export connection utility +export function connectToGame(url?: string) { + return webSocketService.connect(url); +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/stores/activityStore.ts b/apps/astriarch-frontend-sveltekit/src/lib/stores/activityStore.ts new file mode 100644 index 00000000..e83fa4fb --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/stores/activityStore.ts @@ -0,0 +1,191 @@ +import { writable, derived } from 'svelte/store'; +import { multiplayerGameStore, type GameNotification } from './multiplayerGameStore'; +import type { EventNotification, PlanetaryConflictData, PlanetData } from 'astriarch-engine'; + +export interface ActivityLogEntry { + id: string; + type: GameNotification['type']; + message: string; + timestamp: number; + category?: string; + read?: boolean; + // Original event notification data for rich interactions + originalEvent?: EventNotification; + // Parsed planet information for easy access + planetId?: number; + planetName?: string; + // Additional structured data + conflictData?: PlanetaryConflictData; +} + +export interface ActivityStore { + activityLog: ActivityLogEntry[]; + unreadCount: number; +} + +function createActivityStore() { + const initialState: ActivityStore = { + activityLog: [], + unreadCount: 0 + }; + + const { subscribe, set, update } = writable(initialState); + + // Subscribe to notifications from the multiplayerGameStore to automatically add them to the activity log + multiplayerGameStore.notifications.subscribe((notifications) => { + // For each notification, check if it's already in our activity log + notifications.forEach((notification) => { + update((store) => { + // Check if this notification is already in the activity log + const exists = store.activityLog.some((entry) => entry.id === notification.id); + if (!exists) { + // Add new notification to activity log + const newEntry: ActivityLogEntry = { + id: notification.id, + type: notification.type, + message: notification.message, + timestamp: notification.timestamp, + read: false + }; + + return { + ...store, + activityLog: [newEntry, ...store.activityLog], // Add to beginning for newest first + unreadCount: store.unreadCount + 1 + }; + } + return store; + }); + }); + }); + + return { + subscribe, + set, + update, + + // Add a notification with enhanced EventNotification data + addNotificationWithEventData: ( + notification: Omit, + originalEvent?: EventNotification + ) => { + const newEntry: ActivityLogEntry = { + id: `activity-${Date.now()}-${Math.random()}`, + type: notification.type, + message: notification.message, + timestamp: notification.timestamp, + read: false, + originalEvent, + // Extract planet info if available + planetId: originalEvent?.planet?.id, + planetName: originalEvent?.planet?.name, + // Extract conflict data if available + conflictData: originalEvent?.data as PlanetaryConflictData + }; + + update((store) => ({ + ...store, + activityLog: [newEntry, ...store.activityLog], + unreadCount: store.unreadCount + 1 + })); + }, + + // Add a manual activity entry (for system events, chat events, etc.) + addActivity: (entry: Omit) => { + const newEntry: ActivityLogEntry = { + ...entry, + id: `activity-${Date.now()}-${Math.random()}`, + read: false + }; + + update((store) => ({ + ...store, + activityLog: [newEntry, ...store.activityLog], + unreadCount: store.unreadCount + 1 + })); + }, + + // Mark activities as read + markAsRead: (activityIds: string[]) => { + update((store) => { + const updatedLog = store.activityLog.map((entry) => + activityIds.includes(entry.id) ? { ...entry, read: true } : entry + ); + const newUnreadCount = updatedLog.filter((entry) => !entry.read).length; + + return { + ...store, + activityLog: updatedLog, + unreadCount: newUnreadCount + }; + }); + }, + + // Mark all activities as read + markAllAsRead: () => { + update((store) => ({ + ...store, + activityLog: store.activityLog.map((entry) => ({ ...entry, read: true })), + unreadCount: 0 + })); + }, + + // Clear old activities (keep last N entries) + clearOldActivities: (keepCount: number = 100) => { + update((store) => { + const sortedLog = [...store.activityLog].sort((a, b) => b.timestamp - a.timestamp); + const keptLog = sortedLog.slice(0, keepCount); + const newUnreadCount = keptLog.filter((entry) => !entry.read).length; + + return { + ...store, + activityLog: keptLog, + unreadCount: newUnreadCount + }; + }); + }, + + // Clear all activities + clearAll: () => { + set(initialState); + }, + + // Reset store + reset: () => { + set(initialState); + } + }; +} + +export const activityStore = createActivityStore(); + +// Derived stores for convenience +export const activityLog = derived(activityStore, ($store) => $store.activityLog); +export const unreadCount = derived(activityStore, ($store) => $store.unreadCount); + +// Filtered activity logs +export const importantActivities = derived(activityLog, ($log) => + $log.filter((entry) => ['error', 'warning', 'battle'].includes(entry.type)) +); + +export const combatActivities = derived( + activityLog, + ($log) => + $log.filter( + (entry) => + entry.type === 'battle' || + (entry.originalEvent && + (entry.originalEvent.type === 13 || // DefendedAgainstAttackingFleet + entry.originalEvent.type === 14 || // AttackingFleetLost + entry.originalEvent.type === 15 || // PlanetCaptured + entry.originalEvent.type === 19)) + ) // PlanetLost +); + +export const generalActivities = derived(activityLog, ($log) => + $log.filter((entry) => + ['info', 'success', 'research', 'construction', 'planet', 'fleet', 'diplomacy'].includes( + entry.type + ) + ) +); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/stores/audioStore.ts b/apps/astriarch-frontend-sveltekit/src/lib/stores/audioStore.ts new file mode 100644 index 00000000..5f251620 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/stores/audioStore.ts @@ -0,0 +1,109 @@ +import { writable, readable, derived } from 'svelte/store'; +import { audioService, type GamePhase } from '$lib/services/audioService'; + +// Audio service state stores - with safe defaults for SSR +export const isAudioEnabled = writable(audioService?.isEnabled() ?? false); +export const isAudioMuted = writable(audioService?.isMuted() ?? false); +export const audioVolume = writable(audioService?.getVolume() ?? 1.0); +export const currentAudioPhase = writable( + audioService?.getCurrentPhase() ?? 'StartMenu' +); + +// Audio service actions +export const audioActions = { + toggleMute: () => { + if (!audioService) return false; + const muted = audioService.toggleMute(); + isAudioMuted.set(muted); + return muted; + }, + + setVolume: (volume: number) => { + if (!audioService) return; + audioService.setVolume(volume); + audioVolume.set(volume); + }, + + startMenu: () => { + if (!audioService) return; + audioService.startMenu(); + currentAudioPhase.set('StartMenu'); + }, + + beginGame: () => { + if (!audioService) return; + audioService.beginGame(); + currentAudioPhase.set('InGame'); + }, + + endGame: () => { + if (!audioService) return; + audioService.endGame(); + currentAudioPhase.set('GameOver'); + }, + + playTurnStart: () => { + if (!audioService) return; + audioService.playTurnStart(); + }, + + playTurnEnd: () => { + if (!audioService) return; + audioService.playTurnEnd(); + }, + + enableAudio: () => { + if (!audioService) return; + audioService.enableUserInteraction(); + // Also explicitly start menu music to ensure it plays after user interaction + audioService.startMenu(); + isAudioEnabled.set(true); + } +}; + +// Derived store for audio status display +export const audioStatus = derived( + [isAudioEnabled, isAudioMuted, audioVolume, currentAudioPhase], + ([$isEnabled, $isMuted, $volume, $phase]) => ({ + enabled: $isEnabled, + muted: $isMuted, + volume: $volume, + phase: $phase, + volumePercent: Math.round($volume * 100) + }) +); + +// Browser compatibility check +export const audioSupported = readable( + typeof window !== 'undefined' && typeof Audio !== 'undefined', + () => { + // No cleanup needed for this static check + } +); + +// Auto-enable audio on first user interaction (required for modern browsers) +let hasUserInteracted = false; + +export function enableAudioOnFirstInteraction() { + // Only run in browser environment + if (typeof window === 'undefined' || typeof document === 'undefined') return; + + if (hasUserInteracted) return; + + const enableAudio = () => { + if (!hasUserInteracted) { + hasUserInteracted = true; + audioActions.enableAudio(); + + // Remove listeners after first interaction + document.removeEventListener('click', enableAudio); + document.removeEventListener('touchstart', enableAudio); + document.removeEventListener('keydown', enableAudio); + } + }; + + // Listen for various user interaction events + document.addEventListener('click', enableAudio, { once: true }); + document.addEventListener('touchstart', enableAudio, { once: true }); + document.addEventListener('keydown', enableAudio, { once: true }); +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/stores/fleetCommandStore.ts b/apps/astriarch-frontend-sveltekit/src/lib/stores/fleetCommandStore.ts new file mode 100644 index 00000000..d9a14d05 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/stores/fleetCommandStore.ts @@ -0,0 +1,51 @@ +import { writable } from 'svelte/store'; + +export interface FleetCommandState { + isSelectingDestination: boolean; + sourcePlanetId: number | null; + selectedShipIds: Set; + destinationPlanetId: number | null; +} + +const initialState: FleetCommandState = { + isSelectingDestination: false, + sourcePlanetId: null, + selectedShipIds: new Set(), + destinationPlanetId: null +}; + +function createFleetCommandStore() { + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + + // Actions + startSelectingDestination: (sourcePlanetId: number, selectedShipIds: Set) => + update((state) => ({ + ...state, + isSelectingDestination: true, + sourcePlanetId, + selectedShipIds: new Set(selectedShipIds), + destinationPlanetId: null + })), + + setDestinationPlanet: (destinationPlanetId: number) => + update((state) => ({ + ...state, + destinationPlanetId, + isSelectingDestination: false + })), + + cancelDestinationSelection: () => + update((state) => ({ + ...state, + isSelectingDestination: false, + destinationPlanetId: null + })), + + reset: () => set(initialState) + }; +} + +export const fleetCommandStore = createFleetCommandStore(); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/stores/gameStore.ts b/apps/astriarch-frontend-sveltekit/src/lib/stores/gameStore.ts new file mode 100644 index 00000000..0b17199a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/stores/gameStore.ts @@ -0,0 +1,412 @@ +import { writable, derived } from 'svelte/store'; +import { + advanceClientGameModelTime, + getPlayerTotalResources, + getPlayerTotalResourceProductionPerTurn, + getPlayerTotalPopulation, + Grid, + Player, + Planet, + type ClientModelData, + type PlanetProductionItemData, + ResearchType, + subscribeToEvents, + EventNotificationType, + type EventNotification, + GALAXY_WIDTH, + GALAXY_HEIGHT +} from 'astriarch-engine'; +import type { ClientPlanet } from 'astriarch-engine/src/model/clientModel'; +import type { PlanetData } from 'astriarch-engine/src/model/planet'; +import { webSocketService } from '$lib/services/websocket'; + +// Game state stores +export const clientGameModel = writable(null); +export const gameGrid = writable(null); +export const notifications = writable([]); +export const isGameRunning = writable(true); +export const selectedPlanetId = writable(null); + +// Galaxy canvas reference for event-driven updates +interface GalaxyCanvasComponent { + markPlanetsChanged(): void; + markFleetsChanged(): void; + markSelectionChanged(): void; + markViewChanged(): void; + markGameStateChanged(): void; +} + +let galaxyCanvasRef: GalaxyCanvasComponent | null = null; +export function setGalaxyCanvasRef(ref: GalaxyCanvasComponent | null) { + galaxyCanvasRef = ref; + console.log('Galaxy canvas reference set for event-driven updates'); +} + +// Helper function to trigger galaxy canvas updates +function notifyGalaxyCanvas(method: keyof GalaxyCanvasComponent) { + if (galaxyCanvasRef && typeof galaxyCanvasRef[method] === 'function') { + galaxyCanvasRef[method](); + } +} + +// Resource data derived from client game model +export const resourceData = derived(clientGameModel, ($clientGameModel) => { + if (!$clientGameModel) { + return { + total: { + food: 0, + energy: 0, + research: 0, + ore: 0, + iridium: 0, + production: 0 + }, + perTurn: { + food: 0, + energy: 0, + research: 0, + ore: 0, + iridium: 0, + production: 0 + } + }; + } + + // Get actual resources from the game model using the correct API + const totalResources = getPlayerTotalResources( + $clientGameModel.mainPlayer, + $clientGameModel.mainPlayerOwnedPlanets + ); + const perTurnResources = getPlayerTotalResourceProductionPerTurn( + $clientGameModel.mainPlayer, + $clientGameModel.mainPlayerOwnedPlanets + ); + + return { + total: totalResources, + perTurn: perTurnResources + }; +}); + +// Population derived from client game model +export const population = derived(clientGameModel, ($clientGameModel) => { + if (!$clientGameModel) return 0; + + return getPlayerTotalPopulation( + $clientGameModel.mainPlayer, + $clientGameModel.mainPlayerOwnedPlanets + ); +}); + +// Current cycle derived from client game model +export const currentCycle = derived(clientGameModel, ($clientGameModel) => { + if (!$clientGameModel) return 0; + return $clientGameModel.currentCycle; +}); + +// Research progress derived from client game model (creates new objects for reactivity) +export const researchProgress = derived(clientGameModel, ($clientGameModel) => { + if (!$clientGameModel) return {}; + // Create new objects to ensure Svelte reactivity works with nested mutations + const progress = $clientGameModel.mainPlayer.research.researchProgressByType; + const computed: Record = {}; + for (const [key, value] of Object.entries(progress)) { + computed[key] = { ...value }; + } + return computed; +}); + +// Current research type derived from client game model +export const currentResearchType = derived(clientGameModel, ($clientGameModel) => { + if (!$clientGameModel) return null; + return $clientGameModel.mainPlayer.research.researchTypeInQueue; +}); + +// Friendly time display derived from current cycle +export const gameTime = derived(currentCycle, ($currentCycle) => { + // Convert cycles to a more meaningful time representation + const hours = Math.floor($currentCycle / 100); // Rough conversion + const minutes = Math.floor(($currentCycle % 100) * 0.6); // Convert to 60-minute scale + return { + cycle: $currentCycle, + timeString: `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`, + stardate: (2387 + $currentCycle * 0.001).toFixed(3) + }; +}); +export const currentResearch = derived(clientGameModel, ($clientGameModel) => { + if (!$clientGameModel || !$clientGameModel.mainPlayer.research.researchTypeInQueue) { + return 'Nothing'; + } + + const researchType = $clientGameModel.mainPlayer.research.researchTypeInQueue; + const researchNames: Record = { + [ResearchType.NEW_SHIP_TYPE_DEFENDER]: 'Defender Ship', + [ResearchType.NEW_SHIP_TYPE_SCOUT]: 'Scout Ship', + [ResearchType.NEW_SHIP_TYPE_DESTROYER]: 'Destroyer Ship', + [ResearchType.NEW_SHIP_TYPE_CRUISER]: 'Cruiser Ship', + [ResearchType.NEW_SHIP_TYPE_BATTLESHIP]: 'Battleship', + [ResearchType.COMBAT_IMPROVEMENT_ATTACK]: 'Combat Attack', + [ResearchType.COMBAT_IMPROVEMENT_DEFENSE]: 'Combat Defense', + [ResearchType.PROPULSION_IMPROVEMENT]: 'Propulsion', + [ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS]: 'Farm Efficiency', + [ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES]: 'Mine Efficiency', + [ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES]: 'Colony Growth', + [ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES]: 'Factory Efficiency', + [ResearchType.SPACE_PLATFORM_IMPROVEMENT]: 'Space Platforms' + }; + + return researchNames[researchType] || 'Unknown Research'; +}); + +// Selected planet derived from selectedPlanetId and clientGameModel +export const selectedPlanet = derived( + [selectedPlanetId, clientGameModel], + ([$selectedPlanetId, $clientGameModel]): PlanetData | ClientPlanet | null => { + if (!$selectedPlanetId || !$clientGameModel) return null; + + // First check if it's an owned planet (full PlanetData) + const ownedPlanet = $clientGameModel.mainPlayerOwnedPlanets[$selectedPlanetId]; + if (ownedPlanet) { + return ownedPlanet; + } + + // Then check if it's a known client planet (ClientPlanet) + const clientPlanet = $clientGameModel.clientPlanets.find((p) => p.id === $selectedPlanetId); + if (clientPlanet) { + return clientPlanet; + } + + return null; + } +); + +// Game actions +export const gameActions = { + clearNotifications() { + notifications.set([]); + }, + + // Planet selection actions + selectPlanet(planetId: number | null) { + console.log('gameActions.selectPlanet called with ID:', planetId); + selectedPlanetId.set(planetId); + notifyGalaxyCanvas('markSelectionChanged'); + }, + + selectHomePlanet() { + const cgm = get(clientGameModel); + + const homePlanetId = cgm?.mainPlayer.homePlanetId; + if (!homePlanetId) { + selectedPlanetId.set(null); + notifyGalaxyCanvas('markSelectionChanged'); + return; + } + + selectedPlanetId.set(homePlanetId); + notifyGalaxyCanvas('markSelectionChanged'); + }, + + pauseGame() { + isGameRunning.set(false); + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId); + animationFrameId = null; + } + }, + + resumeGame() { + isGameRunning.set(true); + startGameLoop(); + }, + + // Validate with engine then add item to planet's build queue if resources are sufficient + addToPlanetBuildQueueOptimistic(planetId: number, item: PlanetProductionItemData): boolean { + const cgm = get(clientGameModel); + if (!cgm || !cgm.mainPlayerOwnedPlanets[planetId]) return false; + + const grid = get(gameGrid); + if (!grid) return false; + + const planet = cgm.mainPlayerOwnedPlanets[planetId]; + + // Use engine validation to check if we can build this item + const canBuild = Player.enqueueProductionItemAndSpendResourcesIfPossible( + cgm, + grid, + planet, + item + ); + + if (canBuild) { + // The engine method already updated the planet's build queue and spent resources + // We need to trigger a store update to notify components + clientGameModel.update((current) => (current ? { ...current } : null)); + notifyGalaxyCanvas('markPlanetsChanged'); + } + + return canBuild; + }, + + // Remove item from planet's build queue with refund + removeFromPlanetBuildQueueOptimistic(planetId: number, itemIndex: number): boolean { + const cgm = get(clientGameModel); + if (!cgm || !cgm.mainPlayerOwnedPlanets[planetId]) return false; + + const planet = cgm.mainPlayerOwnedPlanets[planetId]; + + // Use engine method to remove item and handle refund + const success = Planet.removeBuildQueueItemForRefund(planet, itemIndex); + + if (success) { + // Trigger a store update to notify components + clientGameModel.update((current) => (current ? { ...current } : null)); + notifyGalaxyCanvas('markPlanetsChanged'); + } + + return success; + }, + + // Notify galaxy canvas when fleets are sent (call this after sending via WebSocket) + notifyFleetSent() { + notifyGalaxyCanvas('markFleetsChanged'); + }, + + // Notify galaxy canvas when planets are changed (call this after planet-related events) + notifyPlanetsChanged() { + notifyGalaxyCanvas('markPlanetsChanged'); + } +}; + +// Helper to get current store values +function get(store: { subscribe: (fn: (value: T) => void) => () => void }): T { + let value: T; + const unsubscribe = store.subscribe((v) => (value = v)); + unsubscribe(); + return value!; +} + +// Animation frame loop for continuous game time advancement +let animationFrameId: number | null = null; + +function startGameLoop() { + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId); + } + + function gameLoop() { + if (!get(isGameRunning)) { + animationFrameId = null; + return; + } + + const cgm = get(clientGameModel); + const grid = get(gameGrid); + + if (cgm && grid) { + // Advance the client game model time continuously + const updatedClientModelData = advanceClientGameModelTime(cgm, grid); + clientGameModel.set(updatedClientModelData.clientGameModel); + if (updatedClientModelData.fleetsArrivingOnUnownedPlanets.length > 0) { + // request data sync with server + console.log('Fleets arriving on unowned planets, requesting state synchronization'); + webSocketService.requestStateSync(); + } + } + + animationFrameId = requestAnimationFrame(gameLoop); + } + + animationFrameId = requestAnimationFrame(gameLoop); +} + +// Construct the grid once when a game is started/resumed +clientGameModel.subscribe((cgm) => { + const currentGrid = get(gameGrid); + + if (cgm && cgm.gameOptions && !currentGrid) { + // Only construct grid once when we have game options but no grid yet + const grid = new Grid(GALAXY_WIDTH, GALAXY_HEIGHT, cgm.gameOptions); + gameGrid.set(grid); + console.log('Client-side grid constructed with', grid.hexes.length, 'hexes'); + notifyGalaxyCanvas('markGameStateChanged'); + } else if (!cgm) { + // Clear the grid when no game model + gameGrid.set(null); + } else if (cgm) { + // Game model updated - mark everything as potentially changed + notifyGalaxyCanvas('markGameStateChanged'); + } +}); + +// Events that should trigger a server sync when they occur +const SYNC_TRIGGERING_EVENTS = new Set([ + EventNotificationType.ShipBuilt, + EventNotificationType.ImprovementBuilt, + EventNotificationType.ResearchComplete, + EventNotificationType.DefendedAgainstAttackingFleet, + EventNotificationType.AttackingFleetLost, + EventNotificationType.PlanetCaptured, + EventNotificationType.PlanetLost, + EventNotificationType.PopulationGrowth, + EventNotificationType.TradesExecuted +]); + +// Subscribe to engine events when we have a client game model +let eventSubscriptionActive = false; +clientGameModel.subscribe((cgm) => { + if (cgm && cgm.mainPlayer && !eventSubscriptionActive) { + // Subscribe to events for the main player + subscribeToEvents(cgm.mainPlayer.id, (playerId: string, events: EventNotification[]) => { + // Check if any events require server sync + const needsSync = events.some((event) => SYNC_TRIGGERING_EVENTS.has(event.type)); + + if (needsSync) { + console.log('Events require server sync, requesting state synchronization'); + webSocketService.requestStateSync(); + } + + // Notify galaxy canvas of specific changes + events.forEach((event) => { + switch (event.type) { + case EventNotificationType.ShipBuilt: + case EventNotificationType.ImprovementBuilt: + case EventNotificationType.PopulationGrowth: + notifyGalaxyCanvas('markPlanetsChanged'); + break; + case EventNotificationType.PlanetCaptured: + case EventNotificationType.PlanetLost: + notifyGalaxyCanvas('markGameStateChanged'); + break; + case EventNotificationType.DefendedAgainstAttackingFleet: + case EventNotificationType.AttackingFleetLost: + notifyGalaxyCanvas('markFleetsChanged'); + break; + default: + // For other events, mark general game state changed + notifyGalaxyCanvas('markGameStateChanged'); + } + }); + }); + eventSubscriptionActive = true; + console.log('Subscribed to client-side engine events for player:', cgm.mainPlayer.id); + } else if (!cgm && eventSubscriptionActive) { + // Reset subscription state when no game model + eventSubscriptionActive = false; + } +}); + +// Start the game loop when both client game model and grid are loaded +clientGameModel.subscribe((cgm) => { + const grid = get(gameGrid); + if (cgm && grid && get(isGameRunning) && animationFrameId === null) { + startGameLoop(); + } +}); + +gameGrid.subscribe((grid) => { + const cgm = get(clientGameModel); + if (cgm && grid && get(isGameRunning) && animationFrameId === null) { + startGameLoop(); + } +}); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/stores/multiplayerGameStore.ts b/apps/astriarch-frontend-sveltekit/src/lib/stores/multiplayerGameStore.ts new file mode 100644 index 00000000..a6ee6548 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/stores/multiplayerGameStore.ts @@ -0,0 +1,203 @@ +import { writable, derived } from 'svelte/store'; +import type { IGame } from 'astriarch-engine'; +import { PlayerStorage } from '$lib/utils/playerStorage'; + +// WebSocket multiplayer game state interface +export interface MultiplayerGameState { + sessionId: string | null; + gameId: string | null; + currentPlayer: string | null; + playerName: string | null; + playerPosition: number | null; // Store the player position returned by the backend + connected: boolean; + gameJoined: boolean; + currentView: 'lobby' | 'game_options' | 'game'; + availableGames: IGame[]; + selectedGame: IGame | null; + selectedPlanet: string | null; + selectedFleet: string | null; +} + +// Chat and notifications +export interface ChatMessage { + id: string; + playerId: string; + playerName: string; + message: string; + timestamp: number; +} + +export interface GameNotification { + id: string; + type: + | 'info' + | 'success' + | 'warning' + | 'error' + | 'battle' + | 'research' + | 'construction' + | 'fleet' + | 'planet' + | 'diplomacy'; + message: string; + timestamp: number; + actionText?: string; + actionType?: string; + duration?: number; // Duration in milliseconds before auto-removal (default: 5000ms) +} + +// Create the multiplayer game store +function createMultiplayerGameStore() { + const initialState: MultiplayerGameState = { + sessionId: null, + gameId: null, + currentPlayer: null, + playerName: PlayerStorage.getPlayerNameWithFallback('Player'), // Load from localStorage + playerPosition: null, + connected: false, + gameJoined: false, + currentView: 'lobby', + availableGames: [], + selectedGame: null, + selectedPlanet: null, + selectedFleet: null + }; + + const { subscribe, set, update } = writable(initialState); + + // Separate stores for chat and notifications + const chatMessages = writable([]); + const notifications = writable([]); + + return { + subscribe, + set, + update, + chatMessages, + notifications, + + // Connection management + setConnected: (connected: boolean) => + update((store) => ({ + ...store, + connected + })), + + setSessionId: (sessionId: string) => + update((store) => ({ + ...store, + sessionId + })), + + setPlayerName: (playerName: string) => { + // Save to localStorage for persistence + PlayerStorage.setPlayerName(playerName); + + return update((store) => ({ + ...store, + playerName + })); + }, + + setPlayerPosition: (playerPosition: number | null) => + update((store) => ({ + ...store, + playerPosition + })), + + // Game state actions - removed setGameModel as game data should come from gameStore + setCurrentPlayer: (playerId: string) => + update((store) => ({ + ...store, + currentPlayer: playerId + })), + + setGameId: (gameId: string | null) => + update((store) => ({ + ...store, + gameId + })), + + setGameJoined: (gameJoined: boolean) => + update((store) => ({ + ...store, + gameJoined + })), + + setCurrentView: (currentView: 'lobby' | 'game_options' | 'game') => + update((store) => ({ + ...store, + currentView + })), + + // Lobby management + setAvailableGames: (games: IGame[]) => + update((store) => ({ + ...store, + availableGames: games + })), + + setSelectedGame: (game: IGame | null) => + update((store) => ({ + ...store, + selectedGame: game + })), + + // Selection actions + selectPlanet: (planetId: string | null) => + update((store) => ({ + ...store, + selectedPlanet: planetId + })), + + selectFleet: (fleetId: string | null) => + update((store) => ({ + ...store, + selectedFleet: fleetId + })), + + // Chat actions + addChatMessage: (message: ChatMessage) => { + chatMessages.update((prev) => [...prev, message]); + }, + + clearChatMessages: () => { + chatMessages.set([]); + }, + + // Notification actions + addNotification: (notification: Omit) => { + const newNotification: GameNotification = { + ...notification, + id: `notification-${Date.now()}-${Math.random()}`, + duration: notification.duration ?? 5000 // Default to 5 seconds + }; + notifications.update((prev) => [...prev, newNotification]); + + // Set up auto-removal timer if enabled + if (newNotification.duration) { + setTimeout(() => { + notifications.update((prev) => prev.filter((n) => n.id !== newNotification.id)); + }, newNotification.duration); + } + }, + + dismissNotification: (notificationId: string) => { + notifications.update((prev) => prev.filter((n) => n.id !== notificationId)); + }, + + clearNotifications: () => { + notifications.set([]); + }, + + // Reset actions + reset: () => { + set(initialState); + chatMessages.set([]); + notifications.set([]); + } + }; +} + +export const multiplayerGameStore = createMultiplayerGameStore(); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/stores/navigationStore.ts b/apps/astriarch-frontend-sveltekit/src/lib/stores/navigationStore.ts new file mode 100644 index 00000000..c370ad2f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/stores/navigationStore.ts @@ -0,0 +1,11 @@ +import { writable } from 'svelte/store'; + +export type GameView = 'planets' | 'fleet' | 'research' | 'trading' | 'activity'; + +export const currentView = writable('planets'); + +export const navigationActions = { + setView(view: GameView) { + currentView.set(view); + } +}; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/styles/astriarch.css b/apps/astriarch-frontend-sveltekit/src/lib/styles/astriarch.css new file mode 100644 index 00000000..44215575 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/styles/astriarch.css @@ -0,0 +1,567 @@ +/* Astriarch Design System */ + +:root { + /* Astriarch Design System Colors */ + + /* Primary Color Palette */ + --astriarch-primary-fill: #00ffff; + --astriarch-primary-stroke: linear-gradient( + 299.62deg, + rgba(0, 0, 0, 0.5) 7.75%, + #00ffff 82.21%, + #ffffff 100% + ); + + /* General UI Colors */ + --astriarch-ui-dark-blue: #1b1f25; + --astriarch-ui-dark-grey: #313e46; + --astriarch-ui-light-grey: #90a9ba; + --astriarch-ui-white: #ffffff; + --astriarch-ui-white-75: rgba(255, 255, 255, 0.75); + --astriarch-ui-white-50: rgba(255, 255, 255, 0.5); + --astriarch-ui-modal-bg: rgba(27, 31, 37, 0.9); + + /* Theme Colors */ + --astriarch-green-theme: #7bfe2b; + --astriarch-orange-theme: #ff9111; + --astriarch-purple-theme: #c240ff; + + /* Element Colors */ + --astriarch-food: #f87b54; + --astriarch-energy: #ffce22; + --astriarch-research: #00ffc2; + --astriarch-ore: #b6e827; + --astriarch-iridium: #aa77ff; + --astriarch-population: #23bdff; + + /* Status Colors */ + --astriarch-status-bad: #ff0000; + --astriarch-status-medium: #fff500; + --astriarch-status-good: #00ff38; + + /* Background */ + --astriarch-bg-glass: rgba(27, 31, 37, 0.8); + + /* Typography */ + --astriarch-font-orbitron: 'Orbitron', system-ui, -apple-system, sans-serif; + + /* Typography Scale - Headlines */ + --astriarch-headline-72-size: 4.5rem; /* 72px */ + --astriarch-headline-72-weight: 600; + --astriarch-headline-72-line-height: 5.375rem; /* 86px */ + --astriarch-headline-72-letter-spacing: 0rem; + + --astriarch-headline-56-size: 3.5rem; /* 56px */ + --astriarch-headline-56-weight: 700; + --astriarch-headline-56-line-height: 3.875rem; /* 62px */ + --astriarch-headline-56-letter-spacing: 0.07rem; /* 1.12px */ + + --astriarch-headline-42-size: 2.625rem; /* 42px */ + --astriarch-headline-42-weight: 700; + --astriarch-headline-42-line-height: 2.875rem; /* 46px */ + --astriarch-headline-42-letter-spacing: 0.0525rem; /* 0.84px */ + + --astriarch-headline-38-size: 2.375rem; /* 38px */ + --astriarch-headline-38-weight: 700; + --astriarch-headline-38-line-height: 2.625rem; /* 42px */ + --astriarch-headline-38-letter-spacing: 0.0475rem; /* 0.76px */ + + --astriarch-headline-32-size: 2rem; /* 32px */ + --astriarch-headline-32-weight: 700; + --astriarch-headline-32-line-height: 2.5rem; /* 40px */ + --astriarch-headline-32-letter-spacing: 0.04rem; /* 0.64px */ + + --astriarch-headline-24-size: 1.5rem; /* 24px */ + --astriarch-headline-24-weight: 700; + --astriarch-headline-24-line-height: 2rem; /* 32px */ + --astriarch-headline-24-letter-spacing: 0.03rem; /* 0.48px */ + + /* Typography Scale - Body */ + --astriarch-body-19-size: 1.1875rem; /* 19px */ + --astriarch-body-19-weight: 400; + --astriarch-body-19-line-height: 1.8125rem; /* 29px */ + --astriarch-body-19-letter-spacing: 0.00594rem; /* 0.095px */ + + --astriarch-body-19-semibold-size: 1.1875rem; /* 19px */ + --astriarch-body-19-semibold-weight: 600; + --astriarch-body-19-semibold-line-height: 1.375rem; /* 22px */ + --astriarch-body-19-semibold-letter-spacing: 0.00594rem; /* 0.095px */ + + --astriarch-body-18-size: 1.125rem; /* 18px */ + --astriarch-body-18-weight: 400; + --astriarch-body-18-line-height: 1.625rem; /* 26px */ + --astriarch-body-18-letter-spacing: 0.00563rem; /* 0.09px */ + + --astriarch-body-18-semibold-size: 1.125rem; /* 18px */ + --astriarch-body-18-semibold-weight: 600; + --astriarch-body-18-semibold-line-height: 1.625rem; /* 26px */ + --astriarch-body-18-semibold-letter-spacing: 0.00563rem; /* 0.09px */ + + --astriarch-body-16-size: 1rem; /* 16px */ + --astriarch-body-16-weight: 400; + --astriarch-body-16-line-height: 1.5rem; /* 24px */ + --astriarch-body-16-letter-spacing: 0.01rem; /* 0.16px */ + + --astriarch-body-16-semibold-size: 1rem; /* 16px */ + --astriarch-body-16-semibold-weight: 600; + --astriarch-body-16-semibold-line-height: 1.5rem; /* 24px */ + --astriarch-body-16-semibold-letter-spacing: 0.005rem; /* 0.08px */ + + --astriarch-body-14-size: 0.875rem; /* 14px */ + --astriarch-body-14-weight: 400; + --astriarch-body-14-line-height: 1.75rem; /* 28px */ + --astriarch-body-14-letter-spacing: 0.00875rem; /* 0.14px */ + + --astriarch-body-14-semibold-size: 0.875rem; /* 14px */ + --astriarch-body-14-semibold-weight: 600; + --astriarch-body-14-semibold-line-height: 1.5rem; /* 24px */ + --astriarch-body-14-semibold-letter-spacing: 0.00438rem; /* 0.07px */ + + /* Typography Scale - Caption */ + --astriarch-caption-12-size: 0.75rem; /* 12px */ + --astriarch-caption-12-weight: 400; + --astriarch-caption-12-line-height: 1rem; /* 16px */ + --astriarch-caption-12-letter-spacing: 0.03125rem; /* 0.5px */ + + --astriarch-caption-12-semibold-size: 0.75rem; /* 12px */ + --astriarch-caption-12-semibold-weight: 600; + --astriarch-caption-12-semibold-line-height: 1rem; /* 16px */ + --astriarch-caption-12-semibold-letter-spacing: 0.03125rem; /* 0.5px */ + + --astriarch-caption-10-size: 0.625rem; /* 10px */ + --astriarch-caption-10-weight: 400; + --astriarch-caption-10-line-height: 0.875rem; /* 14px */ + --astriarch-caption-10-letter-spacing: 0.03125rem; /* 0.5px */ + + --astriarch-caption-10-semibold-size: 0.625rem; /* 10px */ + --astriarch-caption-10-semibold-weight: 600; + --astriarch-caption-10-semibold-line-height: 0.875rem; /* 14px */ + --astriarch-caption-10-semibold-letter-spacing: 0.01563rem; /* 0.25px */ + + /* Typography Scale - Call to Action */ + --astriarch-cta-16-size: 1rem; /* 16px */ + --astriarch-cta-16-weight: 800; + --astriarch-cta-16-line-height: 1.25rem; /* 20px */ + --astriarch-cta-16-letter-spacing: 0.125rem; /* 2px */ + + --astriarch-cta-14-size: 0.875rem; /* 14px */ + --astriarch-cta-14-weight: 800; + --astriarch-cta-14-line-height: 1.25rem; /* 20px */ + --astriarch-cta-14-letter-spacing: 0.125rem; /* 2px */ + + --astriarch-cta-12-size: 0.75rem; /* 12px */ + --astriarch-cta-12-weight: 800; + --astriarch-cta-12-line-height: 1rem; /* 16px */ + --astriarch-cta-12-letter-spacing: 0.125rem; /* 2px */ + + /* Typography Scale - Label */ + --astriarch-label-12-size: 0.75rem; /* 12px */ + --astriarch-label-12-weight: 400; + --astriarch-label-12-line-height: 1rem; /* 16px */ + --astriarch-label-12-letter-spacing: 0.1875rem; /* 3px */ +} + +@theme inline { + /* Astriarch Design System Utilities for Tailwind */ + --color-astriarch-primary: var(--astriarch-primary-fill); + --color-astriarch-ui-dark-blue: var(--astriarch-ui-dark-blue); + --color-astriarch-ui-dark-grey: var(--astriarch-ui-dark-grey); + --color-astriarch-ui-light-grey: var(--astriarch-ui-light-grey); + --color-astriarch-ui-white: var(--astriarch-ui-white); + --color-astriarch-green-theme: var(--astriarch-green-theme); + --color-astriarch-orange-theme: var(--astriarch-orange-theme); + --color-astriarch-purple-theme: var(--astriarch-purple-theme); + --color-astriarch-food: var(--astriarch-food); + --color-astriarch-energy: var(--astriarch-energy); + --color-astriarch-research: var(--astriarch-research); + --color-astriarch-ore: var(--astriarch-ore); + --color-astriarch-iridium: var(--astriarch-iridium); + --color-astriarch-population: var(--astriarch-population); + --color-astriarch-status-bad: var(--astriarch-status-bad); + --color-astriarch-status-medium: var(--astriarch-status-medium); + --color-astriarch-status-good: var(--astriarch-status-good); +} + +@layer utilities { + /* Astriarch Design System Utilities */ + + /* Primary Color */ + .bg-astriarch-primary { + background-color: var(--astriarch-primary-fill); + } + + .text-astriarch-primary { + color: var(--astriarch-primary-fill); + } + + .border-astriarch-primary { + border-color: var(--astriarch-primary-fill); + } + + .bg-astriarch-primary-stroke { + background: var(--astriarch-primary-stroke); + } + + /* UI Colors */ + .bg-astriarch-ui-dark-blue { + background-color: var(--astriarch-ui-dark-blue); + } + + .bg-astriarch-ui-dark-grey { + background-color: var(--astriarch-ui-dark-grey); + } + + .bg-astriarch-ui-light-grey { + background-color: var(--astriarch-ui-light-grey); + } + + .text-astriarch-ui-light-grey { + color: var(--astriarch-ui-light-grey); + } + + .bg-astriarch-ui-white { + background-color: var(--astriarch-ui-white); + } + + .text-astriarch-ui-white { + color: var(--astriarch-ui-white); + } + + .bg-astriarch-glass { + background-color: var(--astriarch-bg-glass); + backdrop-filter: blur(8px); + } + + .bg-astriarch-modal { + background-color: var(--astriarch-ui-modal-bg); + backdrop-filter: blur(12px); + } + + /* Theme Colors */ + .bg-astriarch-green { + background-color: var(--astriarch-green-theme); + } + + .text-astriarch-green { + color: var(--astriarch-green-theme); + } + + .bg-astriarch-orange { + background-color: var(--astriarch-orange-theme); + } + + .text-astriarch-orange { + color: var(--astriarch-orange-theme); + } + + .bg-astriarch-purple { + background-color: var(--astriarch-purple-theme); + } + + .text-astriarch-purple { + color: var(--astriarch-purple-theme); + } + + /* Element/Resource Colors */ + .bg-astriarch-food { + background-color: var(--astriarch-food); + } + + .text-astriarch-food { + color: var(--astriarch-food); + } + + .bg-astriarch-energy { + background-color: var(--astriarch-energy); + } + + .text-astriarch-energy { + color: var(--astriarch-energy); + } + + .bg-astriarch-research { + background-color: var(--astriarch-research); + } + + .text-astriarch-research { + color: var(--astriarch-research); + } + + .bg-astriarch-ore { + background-color: var(--astriarch-ore); + } + + .text-astriarch-ore { + color: var(--astriarch-ore); + } + + .bg-astriarch-iridium { + background-color: var(--astriarch-iridium); + } + + .text-astriarch-iridium { + color: var(--astriarch-iridium); + } + + .bg-astriarch-population { + background-color: var(--astriarch-population); + } + + .text-astriarch-population { + color: var(--astriarch-population); + } + + /* Status Colors */ + .bg-astriarch-status-bad { + background-color: var(--astriarch-status-bad); + } + + .text-astriarch-status-bad { + color: var(--astriarch-status-bad); + } + + .bg-astriarch-status-medium { + background-color: var(--astriarch-status-medium); + } + + .text-astriarch-status-medium { + color: var(--astriarch-status-medium); + } + + .bg-astriarch-status-good { + background-color: var(--astriarch-status-good); + } + + .text-astriarch-status-good { + color: var(--astriarch-status-good); + } + + /* Border Utilities */ + .border-astriarch-food { + border-color: var(--astriarch-food); + } + + .border-astriarch-energy { + border-color: var(--astriarch-energy); + } + + .border-astriarch-research { + border-color: var(--astriarch-research); + } + + .border-astriarch-ore { + border-color: var(--astriarch-ore); + } + + .border-astriarch-iridium { + border-color: var(--astriarch-iridium); + } + + .border-astriarch-population { + border-color: var(--astriarch-population); + } + + /* For use as border image or pseudo-element backgrounds */ + .border-gradient-astriarch { + position: relative; + } + + .border-gradient-astriarch::before { + content: ''; + position: absolute; + inset: 0; + padding: 1px; /* Border width */ + background: var(--astriarch-primary-stroke); + border-radius: inherit; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + -webkit-mask-composite: xor; + } + + /* Typography Utilities - Headlines */ + .text-astriarch-headline-72 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-headline-72-size); + font-weight: var(--astriarch-headline-72-weight); + line-height: var(--astriarch-headline-72-line-height); + letter-spacing: var(--astriarch-headline-72-letter-spacing); + } + + .text-astriarch-headline-56 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-headline-56-size); + font-weight: var(--astriarch-headline-56-weight); + line-height: var(--astriarch-headline-56-line-height); + letter-spacing: var(--astriarch-headline-56-letter-spacing); + } + + .text-astriarch-headline-42 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-headline-42-size); + font-weight: var(--astriarch-headline-42-weight); + line-height: var(--astriarch-headline-42-line-height); + letter-spacing: var(--astriarch-headline-42-letter-spacing); + } + + .text-astriarch-headline-38 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-headline-38-size); + font-weight: var(--astriarch-headline-38-weight); + line-height: var(--astriarch-headline-38-line-height); + letter-spacing: var(--astriarch-headline-38-letter-spacing); + } + + .text-astriarch-headline-32 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-headline-32-size); + font-weight: var(--astriarch-headline-32-weight); + line-height: var(--astriarch-headline-32-line-height); + letter-spacing: var(--astriarch-headline-32-letter-spacing); + } + + .text-astriarch-headline-24 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-headline-24-size); + font-weight: var(--astriarch-headline-24-weight); + line-height: var(--astriarch-headline-24-line-height); + letter-spacing: var(--astriarch-headline-24-letter-spacing); + } + + /* Typography Utilities - Body Text */ + .text-astriarch-body-19 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-19-size); + font-weight: var(--astriarch-body-19-weight); + line-height: var(--astriarch-body-19-line-height); + letter-spacing: var(--astriarch-body-19-letter-spacing); + } + + .text-astriarch-body-19-semibold { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-19-semibold-size); + font-weight: var(--astriarch-body-19-semibold-weight); + line-height: var(--astriarch-body-19-semibold-line-height); + letter-spacing: var(--astriarch-body-19-semibold-letter-spacing); + } + + .text-astriarch-body-18 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-18-size); + font-weight: var(--astriarch-body-18-weight); + line-height: var(--astriarch-body-18-line-height); + letter-spacing: var(--astriarch-body-18-letter-spacing); + } + + .text-astriarch-body-18-semibold { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-18-semibold-size); + font-weight: var(--astriarch-body-18-semibold-weight); + line-height: var(--astriarch-body-18-semibold-line-height); + letter-spacing: var(--astriarch-body-18-semibold-letter-spacing); + } + + .text-astriarch-body-16 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-16-size); + font-weight: var(--astriarch-body-16-weight); + line-height: var(--astriarch-body-16-line-height); + letter-spacing: var(--astriarch-body-16-letter-spacing); + } + + .text-astriarch-body-16-semibold { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-16-semibold-size); + font-weight: var(--astriarch-body-16-semibold-weight); + line-height: var(--astriarch-body-16-semibold-line-height); + letter-spacing: var(--astriarch-body-16-semibold-letter-spacing); + } + + .text-astriarch-body-14 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-14-size); + font-weight: var(--astriarch-body-14-weight); + line-height: var(--astriarch-body-14-line-height); + letter-spacing: var(--astriarch-body-14-letter-spacing); + } + + .text-astriarch-body-14-semibold { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-body-14-semibold-size); + font-weight: var(--astriarch-body-14-semibold-weight); + line-height: var(--astriarch-body-14-semibold-line-height); + letter-spacing: var(--astriarch-body-14-semibold-letter-spacing); + } + + /* Typography Utilities - Captions */ + .text-astriarch-caption-12 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-caption-12-size); + font-weight: var(--astriarch-caption-12-weight); + line-height: var(--astriarch-caption-12-line-height); + letter-spacing: var(--astriarch-caption-12-letter-spacing); + } + + .text-astriarch-caption-12-semibold { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-caption-12-semibold-size); + font-weight: var(--astriarch-caption-12-semibold-weight); + line-height: var(--astriarch-caption-12-semibold-line-height); + letter-spacing: var(--astriarch-caption-12-semibold-letter-spacing); + } + + .text-astriarch-caption-10 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-caption-10-size); + font-weight: var(--astriarch-caption-10-weight); + line-height: var(--astriarch-caption-10-line-height); + letter-spacing: var(--astriarch-caption-10-letter-spacing); + } + + .text-astriarch-caption-10-semibold { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-caption-10-semibold-size); + font-weight: var(--astriarch-caption-10-semibold-weight); + line-height: var(--astriarch-caption-10-semibold-line-height); + letter-spacing: var(--astriarch-caption-10-semibold-letter-spacing); + } + + /* Typography Utilities - Call to Action */ + .text-astriarch-cta-16 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-cta-16-size); + font-weight: var(--astriarch-cta-16-weight); + line-height: var(--astriarch-cta-16-line-height); + letter-spacing: var(--astriarch-cta-16-letter-spacing); + text-transform: uppercase; + } + + .text-astriarch-cta-14 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-cta-14-size); + font-weight: var(--astriarch-cta-14-weight); + line-height: var(--astriarch-cta-14-line-height); + letter-spacing: var(--astriarch-cta-14-letter-spacing); + text-transform: uppercase; + } + + .text-astriarch-cta-12 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-cta-12-size); + font-weight: var(--astriarch-cta-12-weight); + line-height: var(--astriarch-cta-12-line-height); + letter-spacing: var(--astriarch-cta-12-letter-spacing); + text-transform: uppercase; + } + + /* Typography Utilities - Labels */ + .text-astriarch-label-12 { + font-family: var(--astriarch-font-orbitron); + font-size: var(--astriarch-label-12-size); + font-weight: var(--astriarch-label-12-weight); + line-height: var(--astriarch-label-12-line-height); + letter-spacing: var(--astriarch-label-12-letter-spacing); + text-transform: uppercase; + } +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/utils.ts b/apps/astriarch-frontend-sveltekit/src/lib/utils.ts new file mode 100644 index 00000000..f92bfcbb --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/apps/astriarch-frontend-sveltekit/src/lib/utils/number-helper.spec.ts b/apps/astriarch-frontend-sveltekit/src/lib/utils/number-helper.spec.ts new file mode 100644 index 00000000..93c30e14 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/utils/number-helper.spec.ts @@ -0,0 +1,121 @@ +import { describe, it, expect } from 'vitest'; +import { toShortNumberString } from './number-helper'; + +describe('toShortNumberString', () => { + describe('special cases', () => { + it('should handle zero', () => { + expect(toShortNumberString(0)).toBe('0'); + }); + + it('should handle infinity', () => { + expect(toShortNumberString(Infinity)).toBe('Infinity'); + expect(toShortNumberString(-Infinity)).toBe('-Infinity'); + }); + + it('should handle NaN', () => { + expect(toShortNumberString(NaN)).toBe('NaN'); + }); + }); + + describe('numbers less than 1000', () => { + it('should show 1 decimal place for small positive numbers', () => { + expect(toShortNumberString(0.1)).toBe('0.1'); + expect(toShortNumberString(1)).toBe('1.0'); + expect(toShortNumberString(42.7)).toBe('42.7'); + expect(toShortNumberString(123.456)).toBe('123.5'); // rounded + expect(toShortNumberString(999.9)).toBe('999.9'); + }); + + it('should show 1 decimal place for small negative numbers', () => { + expect(toShortNumberString(-0.1)).toBe('-0.1'); + expect(toShortNumberString(-42.7)).toBe('-42.7'); + expect(toShortNumberString(-123.456)).toBe('-123.5'); // rounded + expect(toShortNumberString(-999.9)).toBe('-999.9'); + }); + + it('should handle edge case just below 1000', () => { + expect(toShortNumberString(999.99)).toBe('1000.0'); + }); + }); + + describe('thousands (K)', () => { + it('should format thousands with K suffix', () => { + expect(toShortNumberString(1000)).toBe('1.0K'); + expect(toShortNumberString(1123.2)).toBe('1.1K'); + expect(toShortNumberString(1500)).toBe('1.5K'); + expect(toShortNumberString(9999)).toBe('10.0K'); + expect(toShortNumberString(50000)).toBe('50.0K'); + expect(toShortNumberString(999999)).toBe('1000.0K'); + }); + + it('should format negative thousands with K suffix', () => { + expect(toShortNumberString(-1000)).toBe('-1.0K'); + expect(toShortNumberString(-1500)).toBe('-1.5K'); + expect(toShortNumberString(-50000)).toBe('-50.0K'); + }); + }); + + describe('millions (M)', () => { + it('should format millions with M suffix', () => { + expect(toShortNumberString(1000000)).toBe('1.0M'); + expect(toShortNumberString(2456789)).toBe('2.5M'); + expect(toShortNumberString(15000000)).toBe('15.0M'); + expect(toShortNumberString(999999999)).toBe('1000.0M'); + }); + + it('should format negative millions with M suffix', () => { + expect(toShortNumberString(-1000000)).toBe('-1.0M'); + expect(toShortNumberString(-2456789)).toBe('-2.5M'); + expect(toShortNumberString(-15000000)).toBe('-15.0M'); + }); + }); + + describe('billions (B)', () => { + it('should format billions with B suffix', () => { + expect(toShortNumberString(1000000000)).toBe('1.0B'); + expect(toShortNumberString(3789123456)).toBe('3.8B'); + expect(toShortNumberString(15000000000)).toBe('15.0B'); + expect(toShortNumberString(999999999999)).toBe('1000.0B'); + }); + + it('should format negative billions with B suffix', () => { + expect(toShortNumberString(-1000000000)).toBe('-1.0B'); + expect(toShortNumberString(-3789123456)).toBe('-3.8B'); + }); + }); + + describe('trillions (T)', () => { + it('should format trillions with T suffix', () => { + expect(toShortNumberString(1000000000000)).toBe('1.0T'); + expect(toShortNumberString(4567890123456)).toBe('4.6T'); + expect(toShortNumberString(15000000000000)).toBe('15.0T'); + }); + + it('should format negative trillions with T suffix', () => { + expect(toShortNumberString(-1000000000000)).toBe('-1.0T'); + expect(toShortNumberString(-4567890123456)).toBe('-4.6T'); + }); + + it('should handle very large numbers', () => { + expect(toShortNumberString(1234567890123456)).toBe('1234.6T'); + }); + }); + + describe('rounding behavior', () => { + it('should round to 1 decimal place correctly', () => { + expect(toShortNumberString(1149)).toBe('1.1K'); // 1.149K rounds to 1.1K + expect(toShortNumberString(1151)).toBe('1.2K'); // 1.151K rounds to 1.2K + expect(toShortNumberString(1234567)).toBe('1.2M'); // 1.234567M rounds to 1.2M + expect(toShortNumberString(1250000)).toBe('1.3M'); // 1.25M rounds to 1.3M + }); + }); + + describe('boundary conditions', () => { + it('should handle exact boundary values', () => { + expect(toShortNumberString(999.99999)).toBe('1000.0'); + expect(toShortNumberString(999999.99999)).toBe('1000.0K'); + expect(toShortNumberString(999999999.99999)).toBe('1000.0M'); + expect(toShortNumberString(999999999999)).toBe('1000.0B'); + }); + }); +}); diff --git a/apps/astriarch-frontend-sveltekit/src/lib/utils/number-helper.ts b/apps/astriarch-frontend-sveltekit/src/lib/utils/number-helper.ts new file mode 100644 index 00000000..32f89660 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/utils/number-helper.ts @@ -0,0 +1,38 @@ +/** + * Formats a number into a short, readable string with appropriate abbreviations. + * - Numbers < 1000: Shows 1 decimal place (e.g., 123.4) + * - Numbers >= 1000: Shows abbreviated form with K, M, B, T suffixes (e.g., 1.1K, 2.3M) + */ +export function toShortNumberString(value: number): string { + // Handle special cases + if (value === 0) return '0'; + if (!isFinite(value)) return value.toString(); + + const absValue = Math.abs(value); + const sign = value < 0 ? '-' : ''; + + // Less than 1000: show 1 decimal place + if (absValue < 1000) { + return sign + absValue.toFixed(1); + } + + // Define the suffixes and their corresponding values + const suffixes = [ + { value: 1e12, suffix: 'T' }, // Trillion + { value: 1e9, suffix: 'B' }, // Billion + { value: 1e6, suffix: 'M' }, // Million + { value: 1e3, suffix: 'K' } // Thousand + ]; + + // Find the appropriate suffix + for (const { value: threshold, suffix } of suffixes) { + if (absValue >= threshold) { + const scaledValue = absValue / threshold; + // Show 1 decimal place for the abbreviated form + return sign + scaledValue.toFixed(1) + suffix; + } + } + + // Fallback (should never reach here given our logic above) + return sign + absValue.toFixed(1); +} diff --git a/apps/astriarch-frontend-sveltekit/src/lib/utils/playerStorage.ts b/apps/astriarch-frontend-sveltekit/src/lib/utils/playerStorage.ts new file mode 100644 index 00000000..c2fefffb --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/lib/utils/playerStorage.ts @@ -0,0 +1,75 @@ +/** + * Player storage utilities for persisting player preferences in localStorage + * Following the pattern of the old Astriarch game + */ + +const STORAGE_KEYS = { + PLAYER_NAME: 'astriarch_player_name' +} as const; + +export class PlayerStorage { + /** + * Get the stored player name from localStorage + * @returns The stored player name or null if not found + */ + static getPlayerName(): string | null { + if (typeof window === 'undefined') { + // SSR safety - return null on server + return null; + } + + try { + return localStorage.getItem(STORAGE_KEYS.PLAYER_NAME); + } catch (error) { + console.warn('Failed to read player name from localStorage:', error); + return null; + } + } + + /** + * Store the player name in localStorage + * @param playerName The player name to store + */ + static setPlayerName(playerName: string): void { + if (typeof window === 'undefined') { + // SSR safety - do nothing on server + return; + } + + if (!playerName || playerName.trim().length === 0) { + // Don't store empty names + return; + } + + try { + localStorage.setItem(STORAGE_KEYS.PLAYER_NAME, playerName.trim()); + } catch (error) { + console.warn('Failed to save player name to localStorage:', error); + } + } + + /** + * Clear the stored player name from localStorage + */ + static clearPlayerName(): void { + if (typeof window === 'undefined') { + // SSR safety - do nothing on server + return; + } + + try { + localStorage.removeItem(STORAGE_KEYS.PLAYER_NAME); + } catch (error) { + console.warn('Failed to clear player name from localStorage:', error); + } + } + + /** + * Get the default player name with localStorage fallback + * @param fallback The fallback name if no stored name exists + * @returns The stored player name or the fallback + */ + static getPlayerNameWithFallback(fallback: string = 'Player'): string { + return PlayerStorage.getPlayerName() || fallback; + } +} diff --git a/apps/astriarch-frontend-sveltekit/src/routes/+layout.svelte b/apps/astriarch-frontend-sveltekit/src/routes/+layout.svelte new file mode 100644 index 00000000..03b1ba5e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/routes/+layout.svelte @@ -0,0 +1,17 @@ + + +{@render children()} diff --git a/apps/astriarch-frontend-sveltekit/src/routes/+page.svelte b/apps/astriarch-frontend-sveltekit/src/routes/+page.svelte new file mode 100644 index 00000000..8b8bd244 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/routes/+page.svelte @@ -0,0 +1,486 @@ + + + + Astriarch - Space Strategy Game + + + +
+ +
+
+ +
+
+
+
+
+
+ + +
+
+
+ + + + {#if gameStarted} +
+ + Cycle {$gameTime.cycle} • {$gameTime.timeString} + + + Stardate {$gameTime.stardate} + +
+ +
+ +
+ {/if} +
+ +
+ {#if gameStarted} + + {$isGameRunning ? '● RUNNING' : '⏸ PAUSED'} + + {:else} + +
+
+
+ + +
+ {#if gameStarted} + +
+ +
+ +
+ {#if GalaxyCanvas} + + {/if} +
+ + +
+ +
+ + + {#if $currentView === 'fleet'} +
+ +
+ {:else if $currentView === 'planets'} +
+ +
+ {:else if $currentView === 'research'} +
+ +
+ {:else if $currentView === 'trading'} +
+ +
+ {:else if $currentView === 'activity'} +
+ +
+ {/if} +
+ + +
+ +
+
+ {:else if showLobby} + +
+
+
+
+ +
+
+ {:else} + +
+
+
+ +
+ + Command vast fleets across the galaxy in real-time. Manage planetary resources and + populations. Research advanced technologies. Forge alliances or crush your enemies. + + + The galaxy awaits your strategic genius in this real-time space conquest. + +
+
+
+
+ {/if} +
+ + + {#if $notifications.length > 0} +
+ {#each $notifications.slice(-5) as notification, i (notification.id)} + multiplayerGameStore.dismissNotification(notification.id)} + /> + {/each} +
+ {:else} + +
+ No notifications ({$notifications.length}) +
+ {/if} +
diff --git a/apps/astriarch-frontend-sveltekit/src/routes/test/+page.svelte b/apps/astriarch-frontend-sveltekit/src/routes/test/+page.svelte new file mode 100644 index 00000000..b15372d4 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/routes/test/+page.svelte @@ -0,0 +1,895 @@ + + +
+
+

+ Astriarch - Ruler of the Stars +

+ + +
+

🎨 Logo Atom Component

+ +
+ +
+

Text Logos

+
+
+ Small: + +
+
+ Medium: + +
+
+ Large: + +
+
+ XL: + +
+
+
+ + +
+

Image Logos

+
+
+ Small: + +
+
+ Medium: + +
+
+ Large: + +
+
+ XL: + +
+
+
+
+ + +
+ +
+

Header Usage

+
+ +
Navigation Menu
+
+
+ + +
+

Login Card Usage

+
+ +

Welcome back to the galaxy

+
+
+
+
+ + +
+

+ 🖼️ Window Frame Atom Component +

+ +
+ +
+

Basic Window Frame

+
+ +
+

Window Content Area

+
+
+
+
+ + +
+

SVG Components Mode

+
+ +
+

+ Planet Overview +

+
+
Food: 1,250
+
Energy: 890
+
Ore: 670
+
Research: 340
+
+
+
+
+
+
+ + +
+ +
+

Orange Theme

+
+ +
+

Orange Panel

+
+
+
+
+ + +
+

Green Theme

+
+ +
+

Green Panel

+
+
+
+
+ + +
+

Purple Theme

+
+ +
+

+ Purple Panel +

+
+
+
+
+
+ + +
+ +
+

CSS Fallback Mode

+
+ +
+

Simple CSS Styling

+
+
+
+
+ + +
+

SVG Components Mode

+
+ +
+

+ Pixel-Perfect Figma Design +

+
+
+
+
+
+
+ + +
+

+ 🚀 Astriarch Custom SVG Components +

+ +
+ +
+

Buttons

+ + + + +
+ + +
+

Cards

+ + + +
+ + +
+

Resource Icons

+
+
+ + Energy +
+
+ + Population +
+
+ + Research +
+
+ + Ore +
+
+
+ + +
+

Game UI Sample

+
+ +
+ + +50/turn +
+
+ +
+
+ + +
+

TopOverview Component

+
+ +
+
+ + +
+

+ Individual TopOverviewItem Components +

+
+ console.log('Food clicked')} + /> + + + + +
+
+ + +
+
+

Text Component

+
+ Large Bold Title + + Cyan Subtitle Text + + + This is regular body text that demonstrates the Text component's ability to render + styled content with proper spacing and readability. + + + Small Caps Warning Text + +
+
+ +
+

Box Component

+
+ + + Styled Box Container + + + This Box component can contain any content and be styled with custom CSS. + + + + + + Success Message Box + + + + + + Warning Message Box + + +
+
+
+ + +
+

+ Navigation Tab Components +

+
+ console.log('Fleet Command clicked')} + /> + console.log('Planet Overview clicked')} + /> + console.log('Research Lab clicked')} + /> +
+
+ + +
+

+ Individual Tab Components +

+
+ console.log('Fleet tab clicked')} + /> + console.log('Research tab clicked')} + /> + console.log('Planet tab clicked')} + /> +
+
+ + +
+

Dropdown Components

+
+ +
+

Ship Type

+ { + selectedShipType = value; + console.log('Selected ship:', value); + }} + /> +

Selected: {selectedShipType || 'None'}

+
+ + +
+

Research

+ { + selectedResearch = value; + console.log('Selected research:', value); + }} + /> +

Selected: {selectedResearch}

+
+ + +
+

Primary Variant

+ console.log('Primary dropdown:', value)} + /> + console.log('Disabled dropdown:', value)} + /> +
+
+
+
+ +

+ 🎨 shadcn-svelte Components Integration +

+ + +
+ + + + + Planet Xerion + Inhabited + + + A thriving world in the outer rim + + + +
+
+ Population: + 12.5M +
+
+ Production: + +450/turn +
+
+ Energy: + +120/turn +
+
+
+ + + +
+ + + + + + Battleship Alpha + Combat Ready + + Heavy assault vessel + + +
+
+ Attack: + 85 +
+
+ Defense: + 67 +
+
+ Health: + 100% +
+
+
+ + + + +
+ + + + + Game Controls + Interact with the galaxy + + + + + + + Research Tech + + + + Research Laboratory + + Choose your next technological advancement + + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + +
+
+
+ Credits + 15,750 +
+
+ Energy + +450/turn +
+
+ Research + +125/turn +
+
+ Turn + 42 +
+
+
+ + +

+ 🚀 New Astriarch Components +

+ +
+ +
+

Astriarch Dialog

+

Custom game-themed dialog with SVG styling.

+ + + {#if showAstriarchDialog} + { + showAstriarchDialog = false; + }} + > +
+

Your fleet is ready for deployment, Admiral.

+
+
+ Battleships: + 12 +
+
+ Cruisers: + 25 +
+
+ Fighters: + 150 +
+
+
+
+ {/if} +
+ + +
+

Notifications

+

Game notifications with different sizes.

+
+ + + +
+
+ + +
+

Navigation Controller

+

Game navigation with custom styling.

+ +
+ + +
+

Tab Controller

+

Tabbed interface for game sections.

+ +
+
+

Selected tab content:

+
+
+
Ships
+
42 vessels
+
+
+
Buildings
+
15 structures
+
+
+
Resources
+
Abundant
+
+
+
+
+
+
+
+ + +
+

Production Items

+
+ + console.log('Farm selected')} + /> + console.log('Mine selected')} + /> + console.log('Factory selected')} + /> + + + console.log('Scout selected')} + /> + console.log('Destroyer selected')} + /> + console.log('Battleship selected')} + /> +
+
+ +
+

+ ✨ Built with SvelteKit + + shadcn-svelte +

+

+ No more SVG containment issues! Professional UI components out of the box. +

+
+
+
diff --git a/apps/astriarch-frontend-sveltekit/src/routes/test/websocket/+page.svelte b/apps/astriarch-frontend-sveltekit/src/routes/test/websocket/+page.svelte new file mode 100644 index 00000000..b381c441 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/routes/test/websocket/+page.svelte @@ -0,0 +1,422 @@ + + + + Astriarch WebSocket Test + + +
+

Astriarch WebSocket Test Client

+ + +
+

Connection

+
+ + + + {connected ? 'Connected' : 'Disconnected'} + +
+
+ + +
+

Basic Actions

+
+ + + +
+
+ + +
+

Game Management

+
+ + + +
+ +
+ + +
+ + {#if currentGameId} +
+ Current Game ID: + {currentGameId} +
+ {/if} +
+ + +
+

Raw Message Testing

+
+ + + +
+
+ + +
+
+

Message Log

+ +
+
+ {#each messageLog as message} +
{message}
+ {/each} + {#if messageLog.length === 0} +
No messages yet...
+ {/if} +
+
+
+ + diff --git a/apps/astriarch-frontend-sveltekit/src/stories/Button.stories.svelte b/apps/astriarch-frontend-sveltekit/src/stories/Button.stories.svelte new file mode 100644 index 00000000..373922aa --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/Button.stories.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/stories/Button.svelte b/apps/astriarch-frontend-sveltekit/src/stories/Button.svelte new file mode 100644 index 00000000..a4abdc61 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/Button.svelte @@ -0,0 +1,30 @@ + + + diff --git a/apps/astriarch-frontend-sveltekit/src/stories/Header.stories.svelte b/apps/astriarch-frontend-sveltekit/src/stories/Header.stories.svelte new file mode 100644 index 00000000..3ea062d9 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/Header.stories.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/apps/astriarch-frontend-sveltekit/src/stories/Header.svelte b/apps/astriarch-frontend-sveltekit/src/stories/Header.svelte new file mode 100644 index 00000000..aa5e14a4 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/Header.svelte @@ -0,0 +1,45 @@ + + +
+
+
+ + + + + + + +

Acme

+
+
+ {#if user} + + Welcome, {user.name}! + +
+
+
diff --git a/apps/astriarch-frontend-sveltekit/src/stories/Page.stories.svelte b/apps/astriarch-frontend-sveltekit/src/stories/Page.stories.svelte new file mode 100644 index 00000000..4cd9c246 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/Page.stories.svelte @@ -0,0 +1,32 @@ + + + { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await waitFor(() => expect(loginButton).not.toBeInTheDocument()); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }} +/> + + diff --git a/apps/astriarch-frontend-sveltekit/src/stories/Page.svelte b/apps/astriarch-frontend-sveltekit/src/stories/Page.svelte new file mode 100644 index 00000000..c88695c7 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/Page.svelte @@ -0,0 +1,70 @@ + + +
+
(user = { name: 'Jane Doe' })} + onLogout={() => (user = undefined)} + onCreateAccount={() => (user = { name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a + + component-driven + + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page data + in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at + + Storybook tutorials + + . Read more in the + docs + . +

+
+ Tip + Adjust the width of the canvas with the + + + + + + Viewports addon in the toolbar +
+
+
diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/accessibility.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/accessibility.png new file mode 100644 index 00000000..6ffe6fea Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/accessibility.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/accessibility.svg b/apps/astriarch-frontend-sveltekit/src/stories/assets/accessibility.svg new file mode 100644 index 00000000..107e93f8 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/assets/accessibility.svg @@ -0,0 +1 @@ +Accessibility \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/addon-library.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/addon-library.png new file mode 100644 index 00000000..95deb38a Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/addon-library.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/assets.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/assets.png new file mode 100644 index 00000000..cfba6817 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/assets.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/avif-test-image.avif b/apps/astriarch-frontend-sveltekit/src/stories/assets/avif-test-image.avif new file mode 100644 index 00000000..530709bc Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/avif-test-image.avif differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/context.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/context.png new file mode 100644 index 00000000..e5cd249a Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/context.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/discord.svg b/apps/astriarch-frontend-sveltekit/src/stories/assets/discord.svg new file mode 100644 index 00000000..d638958b --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/assets/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/docs.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/docs.png new file mode 100644 index 00000000..a749629d Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/docs.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/figma-plugin.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/figma-plugin.png new file mode 100644 index 00000000..8f79b08c Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/figma-plugin.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/github.svg b/apps/astriarch-frontend-sveltekit/src/stories/assets/github.svg new file mode 100644 index 00000000..dc513528 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/assets/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/share.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/share.png new file mode 100644 index 00000000..8097a370 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/share.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/styling.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/styling.png new file mode 100644 index 00000000..d341e826 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/styling.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/testing.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/testing.png new file mode 100644 index 00000000..d4ac39a0 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/testing.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/theming.png b/apps/astriarch-frontend-sveltekit/src/stories/assets/theming.png new file mode 100644 index 00000000..1535eb9b Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/src/stories/assets/theming.png differ diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/tutorials.svg b/apps/astriarch-frontend-sveltekit/src/stories/assets/tutorials.svg new file mode 100644 index 00000000..b492a9c6 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/assets/tutorials.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/stories/assets/youtube.svg b/apps/astriarch-frontend-sveltekit/src/stories/assets/youtube.svg new file mode 100644 index 00000000..a7515d7e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/assets/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/src/stories/button.css b/apps/astriarch-frontend-sveltekit/src/stories/button.css new file mode 100644 index 00000000..4fc38a29 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/button.css @@ -0,0 +1,30 @@ +.storybook-button { + display: inline-block; + cursor: pointer; + border: 0; + border-radius: 3em; + font-weight: 700; + line-height: 1; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.storybook-button--primary { + background-color: #555ab9; + color: white; +} +.storybook-button--secondary { + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; + background-color: transparent; + color: #333; +} +.storybook-button--small { + padding: 10px 16px; + font-size: 12px; +} +.storybook-button--medium { + padding: 11px 20px; + font-size: 14px; +} +.storybook-button--large { + padding: 12px 24px; + font-size: 16px; +} diff --git a/apps/astriarch-frontend-sveltekit/src/stories/header.css b/apps/astriarch-frontend-sveltekit/src/stories/header.css new file mode 100644 index 00000000..d511c66f --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/header.css @@ -0,0 +1,32 @@ +.storybook-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-header svg { + display: inline-block; + vertical-align: top; +} + +.storybook-header h1 { + display: inline-block; + vertical-align: top; + margin: 6px 0 6px 10px; + font-weight: 700; + font-size: 20px; + line-height: 1; +} + +.storybook-header button + button { + margin-left: 10px; +} + +.storybook-header .welcome { + margin-right: 10px; + color: #333; + font-size: 14px; +} diff --git a/apps/astriarch-frontend-sveltekit/src/stories/page.css b/apps/astriarch-frontend-sveltekit/src/stories/page.css new file mode 100644 index 00000000..67f2bb2c --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/src/stories/page.css @@ -0,0 +1,68 @@ +.storybook-page { + margin: 0 auto; + padding: 48px 20px; + max-width: 600px; + color: #333; + font-size: 14px; + line-height: 24px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-page h2 { + display: inline-block; + vertical-align: top; + margin: 0 0 4px; + font-weight: 700; + font-size: 32px; + line-height: 1; +} + +.storybook-page p { + margin: 1em 0; +} + +.storybook-page a { + color: inherit; +} + +.storybook-page ul { + margin: 1em 0; + padding-left: 30px; +} + +.storybook-page li { + margin-bottom: 8px; +} + +.storybook-page .tip { + display: inline-block; + vertical-align: top; + margin-right: 10px; + border-radius: 1em; + background: #e7fdd8; + padding: 4px 12px; + color: #357a14; + font-weight: 700; + font-size: 11px; + line-height: 12px; +} + +.storybook-page .tip-wrapper { + margin-top: 40px; + margin-bottom: 40px; + font-size: 13px; + line-height: 20px; +} + +.storybook-page .tip-wrapper svg { + display: inline-block; + vertical-align: top; + margin-top: 3px; + margin-right: 4px; + width: 12px; + height: 12px; +} + +.storybook-page .tip-wrapper svg path { + fill: #1ea7fd; +} diff --git a/public/audio/game-over.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/game-over.mp3 similarity index 100% rename from public/audio/game-over.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/game-over.mp3 diff --git a/public/audio/game-over.ogg b/apps/astriarch-frontend-sveltekit/static/audio/game-over.ogg similarity index 100% rename from public/audio/game-over.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/game-over.ogg diff --git a/public/audio/in-game1.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/in-game1.mp3 similarity index 100% rename from public/audio/in-game1.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/in-game1.mp3 diff --git a/public/audio/in-game1.ogg b/apps/astriarch-frontend-sveltekit/static/audio/in-game1.ogg similarity index 100% rename from public/audio/in-game1.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/in-game1.ogg diff --git a/public/audio/in-game2.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/in-game2.mp3 similarity index 100% rename from public/audio/in-game2.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/in-game2.mp3 diff --git a/public/audio/in-game2.ogg b/apps/astriarch-frontend-sveltekit/static/audio/in-game2.ogg similarity index 100% rename from public/audio/in-game2.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/in-game2.ogg diff --git a/public/audio/in-game3.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/in-game3.mp3 similarity index 100% rename from public/audio/in-game3.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/in-game3.mp3 diff --git a/public/audio/in-game3.ogg b/apps/astriarch-frontend-sveltekit/static/audio/in-game3.ogg similarity index 100% rename from public/audio/in-game3.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/in-game3.ogg diff --git a/public/audio/in-game4.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/in-game4.mp3 similarity index 100% rename from public/audio/in-game4.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/in-game4.mp3 diff --git a/public/audio/in-game4.ogg b/apps/astriarch-frontend-sveltekit/static/audio/in-game4.ogg similarity index 100% rename from public/audio/in-game4.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/in-game4.ogg diff --git a/public/audio/menu-start.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/menu-start.mp3 similarity index 100% rename from public/audio/menu-start.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/menu-start.mp3 diff --git a/public/audio/menu-start.ogg b/apps/astriarch-frontend-sveltekit/static/audio/menu-start.ogg similarity index 100% rename from public/audio/menu-start.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/menu-start.ogg diff --git a/public/audio/sfx-turn-end.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-end.mp3 similarity index 100% rename from public/audio/sfx-turn-end.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-end.mp3 diff --git a/public/audio/sfx-turn-end.ogg b/apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-end.ogg similarity index 100% rename from public/audio/sfx-turn-end.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-end.ogg diff --git a/public/audio/sfx-turn-start.mp3 b/apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-start.mp3 similarity index 100% rename from public/audio/sfx-turn-start.mp3 rename to apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-start.mp3 diff --git a/public/audio/sfx-turn-start.ogg b/apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-start.ogg similarity index 100% rename from public/audio/sfx-turn-start.ogg rename to apps/astriarch-frontend-sveltekit/static/audio/sfx-turn-start.ogg diff --git a/apps/astriarch-frontend-sveltekit/static/favicon.svg b/apps/astriarch-frontend-sveltekit/static/favicon.svg new file mode 100644 index 00000000..cc5dc66a --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/static/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/apps/astriarch-frontend-sveltekit/static/logo/astriarch-logo.png b/apps/astriarch-frontend-sveltekit/static/logo/astriarch-logo.png new file mode 100644 index 00000000..0d12b3d8 Binary files /dev/null and b/apps/astriarch-frontend-sveltekit/static/logo/astriarch-logo.png differ diff --git a/apps/astriarch-frontend-sveltekit/svelte.config.js b/apps/astriarch-frontend-sveltekit/svelte.config.js new file mode 100644 index 00000000..612cde97 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-cloudflare'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { adapter: adapter() } +}; + +export default config; diff --git a/apps/astriarch-frontend-sveltekit/tsconfig.json b/apps/astriarch-frontend-sveltekit/tsconfig.json new file mode 100644 index 00000000..0b2d8865 --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/apps/astriarch-frontend-sveltekit/vite.config.ts b/apps/astriarch-frontend-sveltekit/vite.config.ts new file mode 100644 index 00000000..184d986b --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/vite.config.ts @@ -0,0 +1,20 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + test: { + projects: [ + { + extends: './vite.config.ts', + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] + } + } + ] + } +}); diff --git a/apps/astriarch-frontend-sveltekit/vitest-setup-client.ts b/apps/astriarch-frontend-sveltekit/vitest-setup-client.ts new file mode 100644 index 00000000..570b9f0e --- /dev/null +++ b/apps/astriarch-frontend-sveltekit/vitest-setup-client.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/docker-compose.yml b/docker-compose.yml index 024c5299..47f56653 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,31 +1,42 @@ -# see ./scripts/docker/README.md -version: '2' +version: '3.8' + services: - astriarchapp: - container_name: astriarch_app - build: - context: . - dockerfile: Dockerfile-local - image: astriarch - depends_on: - - "db" + mongodb: + image: mongo:7.0 + container_name: astriarch_mongodb + restart: unless-stopped ports: - - '8000:8000' - volumes: - - .:/usr/src/app - working_dir: /usr/src/app - command: npm start + - '27017:27017' environment: - NODE_ENV: local + MONGO_INITDB_DATABASE: astriarch_v2 + volumes: + - mongodb_data:/data/db networks: - astriarch_net - db: - image: mongo:3.2.8 - container_name: astriarch_mongo + # Backend service (optional - can run locally during development) + astriarch-backend: + build: + context: . + dockerfile: Dockerfile + container_name: astriarch_backend + restart: unless-stopped + ports: + - '8001:8001' + environment: + NODE_ENV: development + MONGODB_CONNECTION_STRING: mongodb://astriarch:astriarch_password@mongodb:27017/astriarch_v2 + HOST: 0.0.0.0 + PORT: 8001 + depends_on: + - mongodb networks: - - astriarch_net + - astriarch_net + command: npm run dev +volumes: + mongodb_data: networks: astriarch_net: + driver: bridge diff --git a/mongo-init/init-mongo.sh b/mongo-init/init-mongo.sh new file mode 100644 index 00000000..69cbf7c0 --- /dev/null +++ b/mongo-init/init-mongo.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +mongosh --eval " +db = db.getSiblingDB('astriarch_v2'); + +db.createUser({ + user: 'astriarch', + pwd: 'astriarch_password', + roles: [ + { + role: 'readWrite', + db: 'astriarch_v2' + } + ] +}); + +db.createCollection('games'); +db.createCollection('sessions'); +db.createCollection('chatmessages'); + +// Create indexes for better performance +db.games.createIndex({ 'createdAt': 1 }); +db.games.createIndex({ 'status': 1 }); +db.sessions.createIndex({ 'sessionId': 1 }, { unique: true }); +db.sessions.createIndex({ 'gameId': 1 }); +db.chatmessages.createIndex({ 'gameId': 1, 'timestamp': 1 }); + +print('Database initialization completed successfully'); +" diff --git a/Dockerfile-local b/old/Dockerfile-local similarity index 100% rename from Dockerfile-local rename to old/Dockerfile-local diff --git a/LICENSE.md b/old/LICENSE.md similarity index 100% rename from LICENSE.md rename to old/LICENSE.md diff --git a/Procfile b/old/Procfile similarity index 100% rename from Procfile rename to old/Procfile diff --git a/old/README.md b/old/README.md new file mode 100644 index 00000000..13f85a1e --- /dev/null +++ b/old/README.md @@ -0,0 +1,55 @@ +Astriarch +========= + +Astriarch Ruler of the Stars, Space Strategy Game +http://www.astriarch.com/ + +This is the Open Source Multiplayer version of Astriarch. + +It uses NodeJS and MongoDB on the server and leverages WebSockets for communication to the client + +Running the Server +================== +You'll need to install Node.js 0.10+ and MongoDB +Run `npm install` then `npm start` and then visit `http://localhost:8000` + +Or the easiest way to get started is to use [Docker](https://www.docker.com/): +create a file `./config/local.json`: +``` + { + "mongodb":{ + "host":"astriarch_mongo" + } + } +``` +Run `docker-compose build` then `docker-compose up` and then visit `http://localhost:8000` + +Overview +======== +Astriarch - Ruler of the Stars is a Turn-based, Single Player Space Strategy Game. +Build planetary improvements and star ships to capture planets and defeat your enemies. +Your ultimate goal is to become the master of the known universe, and earn the title of Astriarch! + +Background +========== +Developed in 2010 by Mastered Software, Astriarch combines aspects of other classic space strategy games such as Master of Orion 2 (MOO2), Stellar Conflict (1987 Amiga), and Star Control. +

+Currently Astriarch is realeased as a free casual web game.  Planned future enhancements include the ability to research and develop improvements, as well as galaxy special items and events. +

+The name Astriarch comes from the Ancient Greek words for star (�stron) and ruler (arkhos) + +Other Versions +============== +Single Player Open Source Versions: + +HTML5: https://github.com/mpalmerlee/Astriarch/tree/HTML5 + +Silverlight: https://github.com/mpalmerlee/Astriarch/tree/Silverlight + +Credits +======= +Astriarch - Ruler of the Stars, space strategy game designed and developed by Mastered Software, music by Resonant. Astriarch HTML 5/JavaScript version uses Stratiscape JavaScript Canvas Library and the jQuery Listbox plugin. + +License +======= +This version of Astriarch - Ruler of the Stars is released under the MIT License. diff --git a/app.js b/old/app.js similarity index 100% rename from app.js rename to old/app.js diff --git a/client_file_registry.js b/old/client_file_registry.js similarity index 100% rename from client_file_registry.js rename to old/client_file_registry.js diff --git a/compile-scripts.js b/old/compile-scripts.js similarity index 100% rename from compile-scripts.js rename to old/compile-scripts.js diff --git a/config.rb b/old/config.rb similarity index 100% rename from config.rb rename to old/config.rb diff --git a/config/default.json b/old/config/default.json similarity index 100% rename from config/default.json rename to old/config/default.json diff --git a/config/runtime.json b/old/config/runtime.json similarity index 100% rename from config/runtime.json rename to old/config/runtime.json diff --git a/controllers/game_controller.js b/old/controllers/game_controller.js similarity index 100% rename from controllers/game_controller.js rename to old/controllers/game_controller.js diff --git a/old/docker-compose.yml b/old/docker-compose.yml new file mode 100644 index 00000000..024c5299 --- /dev/null +++ b/old/docker-compose.yml @@ -0,0 +1,31 @@ +# see ./scripts/docker/README.md +version: '2' +services: + astriarchapp: + container_name: astriarch_app + build: + context: . + dockerfile: Dockerfile-local + image: astriarch + depends_on: + - "db" + ports: + - '8000:8000' + volumes: + - .:/usr/src/app + working_dir: /usr/src/app + command: npm start + environment: + NODE_ENV: local + networks: + - astriarch_net + + db: + image: mongo:3.2.8 + container_name: astriarch_mongo + networks: + - astriarch_net + + +networks: + astriarch_net: diff --git a/models/models.js b/old/models/models.js similarity index 100% rename from models/models.js rename to old/models/models.js diff --git a/npm-shrinkwrap.json b/old/npm-shrinkwrap.json similarity index 100% rename from npm-shrinkwrap.json rename to old/npm-shrinkwrap.json diff --git a/old/package.json b/old/package.json new file mode 100644 index 00000000..6e441c3a --- /dev/null +++ b/old/package.json @@ -0,0 +1,32 @@ +{ + "name": "Astriarch", + "version": "1.9.2", + "private": true, + "dependencies": { + "async": "2.6.4", + "body-parser": "^1.18.3", + "config": "0.4.34", + "connect-mongo": "^2.0.1", + "console-ten": "0.0.5", + "cookie-parser": "^1.4.3", + "errorhandler": "^1.5.0", + "express": "^4.16.4", + "express-session": "^1.15.6", + "extend": "2.0.2", + "mongoose": "^5.7.5", + "morgan": "^1.9.1", + "pug": "^3.0.1", + "serve-favicon": "^2.5.0", + "ws": "^6.2.2" + }, + "devDependencies": { + "mocha": "^5.2.0", + "prettier": "^1.14.3", + "should": "4.6.0" + }, + "scripts": { + "prettier-write": "prettier --write \"{.,models,controllers,test,public/js,public/js/astriarch,}/*.js\" --print-width 120", + "start": "node app.js", + "test": "./node_modules/.bin/mocha --reporter list" + } +} diff --git a/public/about.html b/old/public/about.html similarity index 100% rename from public/about.html rename to old/public/about.html diff --git a/old/public/audio/game-over.mp3 b/old/public/audio/game-over.mp3 new file mode 100644 index 00000000..7a0206ea Binary files /dev/null and b/old/public/audio/game-over.mp3 differ diff --git a/old/public/audio/game-over.ogg b/old/public/audio/game-over.ogg new file mode 100644 index 00000000..b061ec47 Binary files /dev/null and b/old/public/audio/game-over.ogg differ diff --git a/old/public/audio/in-game1.mp3 b/old/public/audio/in-game1.mp3 new file mode 100644 index 00000000..ea82d280 Binary files /dev/null and b/old/public/audio/in-game1.mp3 differ diff --git a/old/public/audio/in-game1.ogg b/old/public/audio/in-game1.ogg new file mode 100644 index 00000000..4482da20 Binary files /dev/null and b/old/public/audio/in-game1.ogg differ diff --git a/old/public/audio/in-game2.mp3 b/old/public/audio/in-game2.mp3 new file mode 100644 index 00000000..ad523ff7 Binary files /dev/null and b/old/public/audio/in-game2.mp3 differ diff --git a/old/public/audio/in-game2.ogg b/old/public/audio/in-game2.ogg new file mode 100644 index 00000000..861a1a22 Binary files /dev/null and b/old/public/audio/in-game2.ogg differ diff --git a/old/public/audio/in-game3.mp3 b/old/public/audio/in-game3.mp3 new file mode 100644 index 00000000..9384ed4d Binary files /dev/null and b/old/public/audio/in-game3.mp3 differ diff --git a/old/public/audio/in-game3.ogg b/old/public/audio/in-game3.ogg new file mode 100644 index 00000000..c97c7151 Binary files /dev/null and b/old/public/audio/in-game3.ogg differ diff --git a/old/public/audio/in-game4.mp3 b/old/public/audio/in-game4.mp3 new file mode 100644 index 00000000..8cff368f Binary files /dev/null and b/old/public/audio/in-game4.mp3 differ diff --git a/old/public/audio/in-game4.ogg b/old/public/audio/in-game4.ogg new file mode 100644 index 00000000..dd0ebd01 Binary files /dev/null and b/old/public/audio/in-game4.ogg differ diff --git a/old/public/audio/menu-start.mp3 b/old/public/audio/menu-start.mp3 new file mode 100644 index 00000000..cd61556e Binary files /dev/null and b/old/public/audio/menu-start.mp3 differ diff --git a/old/public/audio/menu-start.ogg b/old/public/audio/menu-start.ogg new file mode 100644 index 00000000..888bfbad Binary files /dev/null and b/old/public/audio/menu-start.ogg differ diff --git a/old/public/audio/sfx-turn-end.mp3 b/old/public/audio/sfx-turn-end.mp3 new file mode 100644 index 00000000..7d2697a0 Binary files /dev/null and b/old/public/audio/sfx-turn-end.mp3 differ diff --git a/old/public/audio/sfx-turn-end.ogg b/old/public/audio/sfx-turn-end.ogg new file mode 100644 index 00000000..a1fce06b Binary files /dev/null and b/old/public/audio/sfx-turn-end.ogg differ diff --git a/old/public/audio/sfx-turn-start.mp3 b/old/public/audio/sfx-turn-start.mp3 new file mode 100644 index 00000000..97e62cb2 Binary files /dev/null and b/old/public/audio/sfx-turn-start.mp3 differ diff --git a/old/public/audio/sfx-turn-start.ogg b/old/public/audio/sfx-turn-start.ogg new file mode 100644 index 00000000..f2b548cd Binary files /dev/null and b/old/public/audio/sfx-turn-start.ogg differ diff --git a/public/css/astriarch.css b/old/public/css/astriarch.css similarity index 100% rename from public/css/astriarch.css rename to old/public/css/astriarch.css diff --git a/public/css/custom-theme/images/ui-bg_gloss-wave_16_808080_500x100.png b/old/public/css/custom-theme/images/ui-bg_gloss-wave_16_808080_500x100.png similarity index 100% rename from public/css/custom-theme/images/ui-bg_gloss-wave_16_808080_500x100.png rename to old/public/css/custom-theme/images/ui-bg_gloss-wave_16_808080_500x100.png diff --git a/public/css/custom-theme/images/ui-bg_highlight-hard_55_555555_1x100.png b/old/public/css/custom-theme/images/ui-bg_highlight-hard_55_555555_1x100.png similarity index 100% rename from public/css/custom-theme/images/ui-bg_highlight-hard_55_555555_1x100.png rename to old/public/css/custom-theme/images/ui-bg_highlight-hard_55_555555_1x100.png diff --git a/public/css/custom-theme/images/ui-bg_highlight-soft_15_595959_1x100.png b/old/public/css/custom-theme/images/ui-bg_highlight-soft_15_595959_1x100.png similarity index 100% rename from public/css/custom-theme/images/ui-bg_highlight-soft_15_595959_1x100.png rename to old/public/css/custom-theme/images/ui-bg_highlight-soft_15_595959_1x100.png diff --git a/public/css/custom-theme/images/ui-bg_highlight-soft_35_595959_1x100.png b/old/public/css/custom-theme/images/ui-bg_highlight-soft_35_595959_1x100.png similarity index 100% rename from public/css/custom-theme/images/ui-bg_highlight-soft_35_595959_1x100.png rename to old/public/css/custom-theme/images/ui-bg_highlight-soft_35_595959_1x100.png diff --git a/public/css/custom-theme/images/ui-bg_highlight-soft_60_393939_1x100.png b/old/public/css/custom-theme/images/ui-bg_highlight-soft_60_393939_1x100.png similarity index 100% rename from public/css/custom-theme/images/ui-bg_highlight-soft_60_393939_1x100.png rename to old/public/css/custom-theme/images/ui-bg_highlight-soft_60_393939_1x100.png diff --git a/public/css/custom-theme/images/ui-bg_inset-soft_15_808080_1x100.png b/old/public/css/custom-theme/images/ui-bg_inset-soft_15_808080_1x100.png similarity index 100% rename from public/css/custom-theme/images/ui-bg_inset-soft_15_808080_1x100.png rename to old/public/css/custom-theme/images/ui-bg_inset-soft_15_808080_1x100.png diff --git a/public/css/custom-theme/images/ui-icons_121212_256x240.png b/old/public/css/custom-theme/images/ui-icons_121212_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_121212_256x240.png rename to old/public/css/custom-theme/images/ui-icons_121212_256x240.png diff --git a/public/css/custom-theme/images/ui-icons_bbbbbb_256x240.png b/old/public/css/custom-theme/images/ui-icons_bbbbbb_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_bbbbbb_256x240.png rename to old/public/css/custom-theme/images/ui-icons_bbbbbb_256x240.png diff --git a/public/css/custom-theme/images/ui-icons_cccccc_256x240.png b/old/public/css/custom-theme/images/ui-icons_cccccc_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_cccccc_256x240.png rename to old/public/css/custom-theme/images/ui-icons_cccccc_256x240.png diff --git a/public/css/custom-theme/images/ui-icons_cd0a0a_256x240.png b/old/public/css/custom-theme/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_cd0a0a_256x240.png rename to old/public/css/custom-theme/images/ui-icons_cd0a0a_256x240.png diff --git a/public/css/custom-theme/images/ui-icons_dddddd_256x240.png b/old/public/css/custom-theme/images/ui-icons_dddddd_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_dddddd_256x240.png rename to old/public/css/custom-theme/images/ui-icons_dddddd_256x240.png diff --git a/public/css/custom-theme/images/ui-icons_eeeeee_256x240.png b/old/public/css/custom-theme/images/ui-icons_eeeeee_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_eeeeee_256x240.png rename to old/public/css/custom-theme/images/ui-icons_eeeeee_256x240.png diff --git a/public/css/custom-theme/images/ui-icons_ffffff_256x240.png b/old/public/css/custom-theme/images/ui-icons_ffffff_256x240.png similarity index 100% rename from public/css/custom-theme/images/ui-icons_ffffff_256x240.png rename to old/public/css/custom-theme/images/ui-icons_ffffff_256x240.png diff --git a/public/css/custom-theme/jquery-ui.min.css b/old/public/css/custom-theme/jquery-ui.min.css similarity index 100% rename from public/css/custom-theme/jquery-ui.min.css rename to old/public/css/custom-theme/jquery-ui.min.css diff --git a/public/css/joyride-2.1.css b/old/public/css/joyride-2.1.css similarity index 100% rename from public/css/joyride-2.1.css rename to old/public/css/joyride-2.1.css diff --git a/public/css/jquery.ui.adjust.css b/old/public/css/jquery.ui.adjust.css similarity index 100% rename from public/css/jquery.ui.adjust.css rename to old/public/css/jquery.ui.adjust.css diff --git a/public/css/js-listbox-style.css b/old/public/css/js-listbox-style.css similarity index 100% rename from public/css/js-listbox-style.css rename to old/public/css/js-listbox-style.css diff --git a/public/css/main.css b/old/public/css/main.css similarity index 100% rename from public/css/main.css rename to old/public/css/main.css diff --git a/public/css/main.scss b/old/public/css/main.scss similarity index 100% rename from public/css/main.scss rename to old/public/css/main.scss diff --git a/public/css/theme.css b/old/public/css/theme.css similarity index 100% rename from public/css/theme.css rename to old/public/css/theme.css diff --git a/public/help.html b/old/public/help.html similarity index 100% rename from public/help.html rename to old/public/help.html diff --git a/public/img/SystemMasterLogo.png b/old/public/img/SystemMasterLogo.png similarity index 100% rename from public/img/SystemMasterLogo.png rename to old/public/img/SystemMasterLogo.png diff --git a/public/img/Thumbs.db b/old/public/img/Thumbs.db similarity index 100% rename from public/img/Thumbs.db rename to old/public/img/Thumbs.db diff --git a/public/img/background-sprites.jpg b/old/public/img/background-sprites.jpg similarity index 100% rename from public/img/background-sprites.jpg rename to old/public/img/background-sprites.jpg diff --git a/public/img/favicon.ico b/old/public/img/favicon.ico similarity index 100% rename from public/img/favicon.ico rename to old/public/img/favicon.ico diff --git a/public/img/h2.gif b/old/public/img/h2.gif similarity index 100% rename from public/img/h2.gif rename to old/public/img/h2.gif diff --git a/public/img/icon-10x16-sf4a6be44db.png b/old/public/img/icon-10x16-sf4a6be44db.png similarity index 100% rename from public/img/icon-10x16-sf4a6be44db.png rename to old/public/img/icon-10x16-sf4a6be44db.png diff --git a/public/img/icon-10x16/Builder.png b/old/public/img/icon-10x16/Builder.png similarity index 100% rename from public/img/icon-10x16/Builder.png rename to old/public/img/icon-10x16/Builder.png diff --git a/public/img/icon-10x16/Farmer.png b/old/public/img/icon-10x16/Farmer.png similarity index 100% rename from public/img/icon-10x16/Farmer.png rename to old/public/img/icon-10x16/Farmer.png diff --git a/public/img/icon-10x16/Miner.png b/old/public/img/icon-10x16/Miner.png similarity index 100% rename from public/img/icon-10x16/Miner.png rename to old/public/img/icon-10x16/Miner.png diff --git a/public/img/icon-10x16/PopulationSmallEmpty.png b/old/public/img/icon-10x16/PopulationSmallEmpty.png similarity index 100% rename from public/img/icon-10x16/PopulationSmallEmpty.png rename to old/public/img/icon-10x16/PopulationSmallEmpty.png diff --git a/public/img/icon-10x16/PopulationSmallFilled.png b/old/public/img/icon-10x16/PopulationSmallFilled.png similarity index 100% rename from public/img/icon-10x16/PopulationSmallFilled.png rename to old/public/img/icon-10x16/PopulationSmallFilled.png diff --git a/public/img/icon-10x16/PopulationSmallFilled_orange.png b/old/public/img/icon-10x16/PopulationSmallFilled_orange.png similarity index 100% rename from public/img/icon-10x16/PopulationSmallFilled_orange.png rename to old/public/img/icon-10x16/PopulationSmallFilled_orange.png diff --git a/public/img/icon-10x16/PopulationSmallFilled_red.png b/old/public/img/icon-10x16/PopulationSmallFilled_red.png similarity index 100% rename from public/img/icon-10x16/PopulationSmallFilled_red.png rename to old/public/img/icon-10x16/PopulationSmallFilled_red.png diff --git a/public/img/icon-16x16-s3575b47bff.png b/old/public/img/icon-16x16-s3575b47bff.png similarity index 100% rename from public/img/icon-16x16-s3575b47bff.png rename to old/public/img/icon-16x16-s3575b47bff.png diff --git a/public/img/icon-16x16/PlanetaryImprovementSlotEmpty.png b/old/public/img/icon-16x16/PlanetaryImprovementSlotEmpty.png similarity index 100% rename from public/img/icon-16x16/PlanetaryImprovementSlotEmpty.png rename to old/public/img/icon-16x16/PlanetaryImprovementSlotEmpty.png diff --git a/public/img/icon-16x16/PlanetaryImprovementSlotFilled.png b/old/public/img/icon-16x16/PlanetaryImprovementSlotFilled.png similarity index 100% rename from public/img/icon-16x16/PlanetaryImprovementSlotFilled.png rename to old/public/img/icon-16x16/PlanetaryImprovementSlotFilled.png diff --git a/public/img/icon-16x16/build-queue-add.png b/old/public/img/icon-16x16/build-queue-add.png similarity index 100% rename from public/img/icon-16x16/build-queue-add.png rename to old/public/img/icon-16x16/build-queue-add.png diff --git a/public/img/icon-16x16/build-queue-down.png b/old/public/img/icon-16x16/build-queue-down.png similarity index 100% rename from public/img/icon-16x16/build-queue-down.png rename to old/public/img/icon-16x16/build-queue-down.png diff --git a/public/img/icon-16x16/build-queue-remove.png b/old/public/img/icon-16x16/build-queue-remove.png similarity index 100% rename from public/img/icon-16x16/build-queue-remove.png rename to old/public/img/icon-16x16/build-queue-remove.png diff --git a/public/img/icon-16x16/build-queue-up.png b/old/public/img/icon-16x16/build-queue-up.png similarity index 100% rename from public/img/icon-16x16/build-queue-up.png rename to old/public/img/icon-16x16/build-queue-up.png diff --git a/public/img/icon-16x16/credits.png b/old/public/img/icon-16x16/credits.png similarity index 100% rename from public/img/icon-16x16/credits.png rename to old/public/img/icon-16x16/credits.png diff --git a/public/img/icon-16x16/demolish.png b/old/public/img/icon-16x16/demolish.png similarity index 100% rename from public/img/icon-16x16/demolish.png rename to old/public/img/icon-16x16/demolish.png diff --git a/public/img/icon-16x16/food.png b/old/public/img/icon-16x16/food.png similarity index 100% rename from public/img/icon-16x16/food.png rename to old/public/img/icon-16x16/food.png diff --git a/public/img/icon-16x16/iridium.png b/old/public/img/icon-16x16/iridium.png similarity index 100% rename from public/img/icon-16x16/iridium.png rename to old/public/img/icon-16x16/iridium.png diff --git a/public/img/icon-16x16/main-menu.png b/old/public/img/icon-16x16/main-menu.png similarity index 100% rename from public/img/icon-16x16/main-menu.png rename to old/public/img/icon-16x16/main-menu.png diff --git a/public/img/icon-16x16/ore.png b/old/public/img/icon-16x16/ore.png similarity index 100% rename from public/img/icon-16x16/ore.png rename to old/public/img/icon-16x16/ore.png diff --git a/public/img/icon-16x16/population.png b/old/public/img/icon-16x16/population.png similarity index 100% rename from public/img/icon-16x16/population.png rename to old/public/img/icon-16x16/population.png diff --git a/public/img/icon-16x16/research.png b/old/public/img/icon-16x16/research.png similarity index 100% rename from public/img/icon-16x16/research.png rename to old/public/img/icon-16x16/research.png diff --git a/public/img/icon-16x16/spaceplatform.png b/old/public/img/icon-16x16/spaceplatform.png similarity index 100% rename from public/img/icon-16x16/spaceplatform.png rename to old/public/img/icon-16x16/spaceplatform.png diff --git a/public/img/icon-16x16/speaker-off.png b/old/public/img/icon-16x16/speaker-off.png similarity index 100% rename from public/img/icon-16x16/speaker-off.png rename to old/public/img/icon-16x16/speaker-off.png diff --git a/public/img/icon-16x16/speaker-on.png b/old/public/img/icon-16x16/speaker-on.png similarity index 100% rename from public/img/icon-16x16/speaker-on.png rename to old/public/img/icon-16x16/speaker-on.png diff --git a/public/img/icon-16x16/starship.png b/old/public/img/icon-16x16/starship.png similarity index 100% rename from public/img/icon-16x16/starship.png rename to old/public/img/icon-16x16/starship.png diff --git a/public/img/icon-16x16/trading.png b/old/public/img/icon-16x16/trading.png similarity index 100% rename from public/img/icon-16x16/trading.png rename to old/public/img/icon-16x16/trading.png diff --git a/public/img/icon-32x32-s927148ad5f.png b/old/public/img/icon-32x32-s927148ad5f.png similarity index 100% rename from public/img/icon-32x32-s927148ad5f.png rename to old/public/img/icon-32x32-s927148ad5f.png diff --git a/public/img/icon-32x32/BattleshipCustomLarge.png b/old/public/img/icon-32x32/BattleshipCustomLarge.png similarity index 100% rename from public/img/icon-32x32/BattleshipCustomLarge.png rename to old/public/img/icon-32x32/BattleshipCustomLarge.png diff --git a/public/img/icon-32x32/BattleshipLarge.png b/old/public/img/icon-32x32/BattleshipLarge.png similarity index 100% rename from public/img/icon-32x32/BattleshipLarge.png rename to old/public/img/icon-32x32/BattleshipLarge.png diff --git a/public/img/icon-32x32/ColonyLarge.png b/old/public/img/icon-32x32/ColonyLarge.png similarity index 100% rename from public/img/icon-32x32/ColonyLarge.png rename to old/public/img/icon-32x32/ColonyLarge.png diff --git a/public/img/icon-32x32/CruiserCustomLarge.png b/old/public/img/icon-32x32/CruiserCustomLarge.png similarity index 100% rename from public/img/icon-32x32/CruiserCustomLarge.png rename to old/public/img/icon-32x32/CruiserCustomLarge.png diff --git a/public/img/icon-32x32/CruiserLarge.png b/old/public/img/icon-32x32/CruiserLarge.png similarity index 100% rename from public/img/icon-32x32/CruiserLarge.png rename to old/public/img/icon-32x32/CruiserLarge.png diff --git a/public/img/icon-32x32/DefenderCustomLarge.png b/old/public/img/icon-32x32/DefenderCustomLarge.png similarity index 100% rename from public/img/icon-32x32/DefenderCustomLarge.png rename to old/public/img/icon-32x32/DefenderCustomLarge.png diff --git a/public/img/icon-32x32/DefenderLarge.png b/old/public/img/icon-32x32/DefenderLarge.png similarity index 100% rename from public/img/icon-32x32/DefenderLarge.png rename to old/public/img/icon-32x32/DefenderLarge.png diff --git a/public/img/icon-32x32/DestroyerCustomLarge.png b/old/public/img/icon-32x32/DestroyerCustomLarge.png similarity index 100% rename from public/img/icon-32x32/DestroyerCustomLarge.png rename to old/public/img/icon-32x32/DestroyerCustomLarge.png diff --git a/public/img/icon-32x32/DestroyerLarge.png b/old/public/img/icon-32x32/DestroyerLarge.png similarity index 100% rename from public/img/icon-32x32/DestroyerLarge.png rename to old/public/img/icon-32x32/DestroyerLarge.png diff --git a/public/img/icon-32x32/FactoryLarge.png b/old/public/img/icon-32x32/FactoryLarge.png similarity index 100% rename from public/img/icon-32x32/FactoryLarge.png rename to old/public/img/icon-32x32/FactoryLarge.png diff --git a/public/img/icon-32x32/FarmLarge.png b/old/public/img/icon-32x32/FarmLarge.png similarity index 100% rename from public/img/icon-32x32/FarmLarge.png rename to old/public/img/icon-32x32/FarmLarge.png diff --git a/public/img/icon-32x32/FoodLarge.png b/old/public/img/icon-32x32/FoodLarge.png similarity index 100% rename from public/img/icon-32x32/FoodLarge.png rename to old/public/img/icon-32x32/FoodLarge.png diff --git a/public/img/icon-32x32/IridiumLarge.png b/old/public/img/icon-32x32/IridiumLarge.png similarity index 100% rename from public/img/icon-32x32/IridiumLarge.png rename to old/public/img/icon-32x32/IridiumLarge.png diff --git a/public/img/icon-32x32/MineLarge.png b/old/public/img/icon-32x32/MineLarge.png similarity index 100% rename from public/img/icon-32x32/MineLarge.png rename to old/public/img/icon-32x32/MineLarge.png diff --git a/public/img/icon-32x32/OreLarge.png b/old/public/img/icon-32x32/OreLarge.png similarity index 100% rename from public/img/icon-32x32/OreLarge.png rename to old/public/img/icon-32x32/OreLarge.png diff --git a/public/img/icon-32x32/PlanetAsteroid.png b/old/public/img/icon-32x32/PlanetAsteroid.png similarity index 100% rename from public/img/icon-32x32/PlanetAsteroid.png rename to old/public/img/icon-32x32/PlanetAsteroid.png diff --git a/public/img/icon-32x32/PlanetClass1.png b/old/public/img/icon-32x32/PlanetClass1.png similarity index 100% rename from public/img/icon-32x32/PlanetClass1.png rename to old/public/img/icon-32x32/PlanetClass1.png diff --git a/public/img/icon-32x32/PlanetClass2.png b/old/public/img/icon-32x32/PlanetClass2.png similarity index 100% rename from public/img/icon-32x32/PlanetClass2.png rename to old/public/img/icon-32x32/PlanetClass2.png diff --git a/public/img/icon-32x32/PlanetDead.png b/old/public/img/icon-32x32/PlanetDead.png similarity index 100% rename from public/img/icon-32x32/PlanetDead.png rename to old/public/img/icon-32x32/PlanetDead.png diff --git a/public/img/icon-32x32/ProductionLarge.png b/old/public/img/icon-32x32/ProductionLarge.png similarity index 100% rename from public/img/icon-32x32/ProductionLarge.png rename to old/public/img/icon-32x32/ProductionLarge.png diff --git a/public/img/icon-32x32/ResearchAttack.png b/old/public/img/icon-32x32/ResearchAttack.png similarity index 100% rename from public/img/icon-32x32/ResearchAttack.png rename to old/public/img/icon-32x32/ResearchAttack.png diff --git a/public/img/icon-32x32/ResearchDefense.png b/old/public/img/icon-32x32/ResearchDefense.png similarity index 100% rename from public/img/icon-32x32/ResearchDefense.png rename to old/public/img/icon-32x32/ResearchDefense.png diff --git a/public/img/icon-32x32/ResearchPropulsion.png b/old/public/img/icon-32x32/ResearchPropulsion.png similarity index 100% rename from public/img/icon-32x32/ResearchPropulsion.png rename to old/public/img/icon-32x32/ResearchPropulsion.png diff --git a/public/img/icon-32x32/ScoutCustomLarge.png b/old/public/img/icon-32x32/ScoutCustomLarge.png similarity index 100% rename from public/img/icon-32x32/ScoutCustomLarge.png rename to old/public/img/icon-32x32/ScoutCustomLarge.png diff --git a/public/img/icon-32x32/ScoutLarge.png b/old/public/img/icon-32x32/ScoutLarge.png similarity index 100% rename from public/img/icon-32x32/ScoutLarge.png rename to old/public/img/icon-32x32/ScoutLarge.png diff --git a/public/img/icon-32x32/SpacePlatformLarge.png b/old/public/img/icon-32x32/SpacePlatformLarge.png similarity index 100% rename from public/img/icon-32x32/SpacePlatformLarge.png rename to old/public/img/icon-32x32/SpacePlatformLarge.png diff --git a/public/img/large_background.jpg b/old/public/img/large_background.jpg similarity index 100% rename from public/img/large_background.jpg rename to old/public/img/large_background.jpg diff --git a/public/img/li.gif b/old/public/img/li.gif similarity index 100% rename from public/img/li.gif rename to old/public/img/li.gif diff --git a/public/img/spinner-green.gif b/old/public/img/spinner-green.gif similarity index 100% rename from public/img/spinner-green.gif rename to old/public/img/spinner-green.gif diff --git a/public/img/spinner-small.gif b/old/public/img/spinner-small.gif similarity index 100% rename from public/img/spinner-small.gif rename to old/public/img/spinner-small.gif diff --git a/public/img/spinner.gif b/old/public/img/spinner.gif similarity index 100% rename from public/img/spinner.gif rename to old/public/img/spinner.gif diff --git a/public/img/tile-bg.png b/old/public/img/tile-bg.png similarity index 100% rename from public/img/tile-bg.png rename to old/public/img/tile-bg.png diff --git a/public/js/astriarch/astriarch_ai.js b/old/public/js/astriarch/astriarch_ai.js similarity index 100% rename from public/js/astriarch/astriarch_ai.js rename to old/public/js/astriarch/astriarch_ai.js diff --git a/public/js/astriarch/astriarch_alert.js b/old/public/js/astriarch/astriarch_alert.js similarity index 100% rename from public/js/astriarch/astriarch_alert.js rename to old/public/js/astriarch/astriarch_alert.js diff --git a/public/js/astriarch/astriarch_base.js b/old/public/js/astriarch/astriarch_base.js similarity index 100% rename from public/js/astriarch/astriarch_base.js rename to old/public/js/astriarch/astriarch_base.js diff --git a/public/js/astriarch/astriarch_battlesimulator.js b/old/public/js/astriarch/astriarch_battlesimulator.js similarity index 100% rename from public/js/astriarch/astriarch_battlesimulator.js rename to old/public/js/astriarch/astriarch_battlesimulator.js diff --git a/public/js/astriarch/astriarch_clientmodel.js b/old/public/js/astriarch/astriarch_clientmodel.js similarity index 100% rename from public/js/astriarch/astriarch_clientmodel.js rename to old/public/js/astriarch/astriarch_clientmodel.js diff --git a/public/js/astriarch/astriarch_commcontrol.js b/old/public/js/astriarch/astriarch_commcontrol.js similarity index 100% rename from public/js/astriarch/astriarch_commcontrol.js rename to old/public/js/astriarch/astriarch_commcontrol.js diff --git a/public/js/astriarch/astriarch_dialog.js b/old/public/js/astriarch/astriarch_dialog.js similarity index 100% rename from public/js/astriarch/astriarch_dialog.js rename to old/public/js/astriarch/astriarch_dialog.js diff --git a/public/js/astriarch/astriarch_drawnfleet.js b/old/public/js/astriarch/astriarch_drawnfleet.js similarity index 100% rename from public/js/astriarch/astriarch_drawnfleet.js rename to old/public/js/astriarch/astriarch_drawnfleet.js diff --git a/public/js/astriarch/astriarch_drawnplanet.js b/old/public/js/astriarch/astriarch_drawnplanet.js similarity index 100% rename from public/js/astriarch/astriarch_drawnplanet.js rename to old/public/js/astriarch/astriarch_drawnplanet.js diff --git a/public/js/astriarch/astriarch_exitconfirmcontrol.js b/old/public/js/astriarch/astriarch_exitconfirmcontrol.js similarity index 100% rename from public/js/astriarch/astriarch_exitconfirmcontrol.js rename to old/public/js/astriarch/astriarch_exitconfirmcontrol.js diff --git a/public/js/astriarch/astriarch_fleet.js b/old/public/js/astriarch/astriarch_fleet.js similarity index 100% rename from public/js/astriarch/astriarch_fleet.js rename to old/public/js/astriarch/astriarch_fleet.js diff --git a/public/js/astriarch/astriarch_gamecontroller.js b/old/public/js/astriarch/astriarch_gamecontroller.js similarity index 100% rename from public/js/astriarch/astriarch_gamecontroller.js rename to old/public/js/astriarch/astriarch_gamecontroller.js diff --git a/public/js/astriarch/astriarch_gameovercontrol.js b/old/public/js/astriarch/astriarch_gameovercontrol.js similarity index 100% rename from public/js/astriarch/astriarch_gameovercontrol.js rename to old/public/js/astriarch/astriarch_gameovercontrol.js diff --git a/public/js/astriarch/astriarch_globals.js b/old/public/js/astriarch/astriarch_globals.js similarity index 100% rename from public/js/astriarch/astriarch_globals.js rename to old/public/js/astriarch/astriarch_globals.js diff --git a/public/js/astriarch/astriarch_grid.js b/old/public/js/astriarch/astriarch_grid.js similarity index 100% rename from public/js/astriarch/astriarch_grid.js rename to old/public/js/astriarch/astriarch_grid.js diff --git a/public/js/astriarch/astriarch_hexagon.js b/old/public/js/astriarch/astriarch_hexagon.js similarity index 100% rename from public/js/astriarch/astriarch_hexagon.js rename to old/public/js/astriarch/astriarch_hexagon.js diff --git a/public/js/astriarch/astriarch_loader.js b/old/public/js/astriarch/astriarch_loader.js similarity index 100% rename from public/js/astriarch/astriarch_loader.js rename to old/public/js/astriarch/astriarch_loader.js diff --git a/public/js/astriarch/astriarch_lobbycontrol.js b/old/public/js/astriarch/astriarch_lobbycontrol.js similarity index 100% rename from public/js/astriarch/astriarch_lobbycontrol.js rename to old/public/js/astriarch/astriarch_lobbycontrol.js diff --git a/public/js/astriarch/astriarch_localstorageinterface.js b/old/public/js/astriarch/astriarch_localstorageinterface.js similarity index 100% rename from public/js/astriarch/astriarch_localstorageinterface.js rename to old/public/js/astriarch/astriarch_localstorageinterface.js diff --git a/public/js/astriarch/astriarch_model.js b/old/public/js/astriarch/astriarch_model.js similarity index 100% rename from public/js/astriarch/astriarch_model.js rename to old/public/js/astriarch/astriarch_model.js diff --git a/public/js/astriarch/astriarch_newgamecontrol.js b/old/public/js/astriarch/astriarch_newgamecontrol.js similarity index 100% rename from public/js/astriarch/astriarch_newgamecontrol.js rename to old/public/js/astriarch/astriarch_newgamecontrol.js diff --git a/public/js/astriarch/astriarch_planet.js b/old/public/js/astriarch/astriarch_planet.js similarity index 100% rename from public/js/astriarch/astriarch_planet.js rename to old/public/js/astriarch/astriarch_planet.js diff --git a/public/js/astriarch/astriarch_planetaryconflictcontrol.js b/old/public/js/astriarch/astriarch_planetaryconflictcontrol.js similarity index 100% rename from public/js/astriarch/astriarch_planetaryconflictcontrol.js rename to old/public/js/astriarch/astriarch_planetaryconflictcontrol.js diff --git a/public/js/astriarch/astriarch_planetview.js b/old/public/js/astriarch/astriarch_planetview.js similarity index 100% rename from public/js/astriarch/astriarch_planetview.js rename to old/public/js/astriarch/astriarch_planetview.js diff --git a/public/js/astriarch/astriarch_player.js b/old/public/js/astriarch/astriarch_player.js similarity index 100% rename from public/js/astriarch/astriarch_player.js rename to old/public/js/astriarch/astriarch_player.js diff --git a/public/js/astriarch/astriarch_research.js b/old/public/js/astriarch/astriarch_research.js similarity index 100% rename from public/js/astriarch/astriarch_research.js rename to old/public/js/astriarch/astriarch_research.js diff --git a/public/js/astriarch/astriarch_researchcontrol.js b/old/public/js/astriarch/astriarch_researchcontrol.js similarity index 100% rename from public/js/astriarch/astriarch_researchcontrol.js rename to old/public/js/astriarch/astriarch_researchcontrol.js diff --git a/public/js/astriarch/astriarch_savedgameinterface.js b/old/public/js/astriarch/astriarch_savedgameinterface.js similarity index 100% rename from public/js/astriarch/astriarch_savedgameinterface.js rename to old/public/js/astriarch/astriarch_savedgameinterface.js diff --git a/public/js/astriarch/astriarch_sendshipscontrol.js b/old/public/js/astriarch/astriarch_sendshipscontrol.js similarity index 100% rename from public/js/astriarch/astriarch_sendshipscontrol.js rename to old/public/js/astriarch/astriarch_sendshipscontrol.js diff --git a/public/js/astriarch/astriarch_servercontroller.js b/old/public/js/astriarch/astriarch_servercontroller.js similarity index 100% rename from public/js/astriarch/astriarch_servercontroller.js rename to old/public/js/astriarch/astriarch_servercontroller.js diff --git a/public/js/astriarch/astriarch_shared.js b/old/public/js/astriarch/astriarch_shared.js similarity index 100% rename from public/js/astriarch/astriarch_shared.js rename to old/public/js/astriarch/astriarch_shared.js diff --git a/public/js/astriarch/astriarch_tradingcenter.js b/old/public/js/astriarch/astriarch_tradingcenter.js similarity index 100% rename from public/js/astriarch/astriarch_tradingcenter.js rename to old/public/js/astriarch/astriarch_tradingcenter.js diff --git a/public/js/astriarch/astriarch_tradingcontrol.js b/old/public/js/astriarch/astriarch_tradingcontrol.js similarity index 100% rename from public/js/astriarch/astriarch_tradingcontrol.js rename to old/public/js/astriarch/astriarch_tradingcontrol.js diff --git a/public/js/astriarch/astriarch_turneventmessage.js b/old/public/js/astriarch/astriarch_turneventmessage.js similarity index 100% rename from public/js/astriarch/astriarch_turneventmessage.js rename to old/public/js/astriarch/astriarch_turneventmessage.js diff --git a/public/js/astriarch/astriarch_view.js b/old/public/js/astriarch/astriarch_view.js similarity index 100% rename from public/js/astriarch/astriarch_view.js rename to old/public/js/astriarch/astriarch_view.js diff --git a/public/js/dist/astriarch.js b/old/public/js/dist/astriarch.js similarity index 100% rename from public/js/dist/astriarch.js rename to old/public/js/dist/astriarch.js diff --git a/public/js/dist/external.js b/old/public/js/dist/external.js similarity index 100% rename from public/js/dist/external.js rename to old/public/js/dist/external.js diff --git a/public/js/ext/audio-interface.js b/old/public/js/ext/audio-interface.js similarity index 100% rename from public/js/ext/audio-interface.js rename to old/public/js/ext/audio-interface.js diff --git a/public/js/ext/jCanvas.js b/old/public/js/ext/jCanvas.js similarity index 100% rename from public/js/ext/jCanvas.js rename to old/public/js/ext/jCanvas.js diff --git a/public/js/ext/jquery-1.7.1.min.js b/old/public/js/ext/jquery-1.7.1.min.js similarity index 100% rename from public/js/ext/jquery-1.7.1.min.js rename to old/public/js/ext/jquery-1.7.1.min.js diff --git a/public/js/ext/jquery-ui.min.js b/old/public/js/ext/jquery-ui.min.js similarity index 100% rename from public/js/ext/jquery-ui.min.js rename to old/public/js/ext/jquery-ui.min.js diff --git a/public/js/ext/jquery.joyride-2.1.js b/old/public/js/ext/jquery.joyride-2.1.js similarity index 100% rename from public/js/ext/jquery.joyride-2.1.js rename to old/public/js/ext/jquery.joyride-2.1.js diff --git a/public/js/ext/js-cardlist.js b/old/public/js/ext/js-cardlist.js similarity index 100% rename from public/js/ext/js-cardlist.js rename to old/public/js/ext/js-cardlist.js diff --git a/public/js/ext/js-inherit.js b/old/public/js/ext/js-inherit.js similarity index 100% rename from public/js/ext/js-inherit.js rename to old/public/js/ext/js-inherit.js diff --git a/public/js/ext/js-listbox.js b/old/public/js/ext/js-listbox.js similarity index 100% rename from public/js/ext/js-listbox.js rename to old/public/js/ext/js-listbox.js diff --git a/public/js/ext/mousetrap.min.js b/old/public/js/ext/mousetrap.min.js similarity index 100% rename from public/js/ext/mousetrap.min.js rename to old/public/js/ext/mousetrap.min.js diff --git a/public/js/ext/reconnecting-websocket.js b/old/public/js/ext/reconnecting-websocket.js similarity index 100% rename from public/js/ext/reconnecting-websocket.js rename to old/public/js/ext/reconnecting-websocket.js diff --git a/public/js/server_comm.js b/old/public/js/server_comm.js similarity index 100% rename from public/js/server_comm.js rename to old/public/js/server_comm.js diff --git a/public/news.html b/old/public/news.html similarity index 100% rename from public/news.html rename to old/public/news.html diff --git a/server.js b/old/server.js similarity index 100% rename from server.js rename to old/server.js diff --git a/test/battlesimulation_test.js b/old/test/battlesimulation_test.js similarity index 100% rename from test/battlesimulation_test.js rename to old/test/battlesimulation_test.js diff --git a/test/fleet_test.js b/old/test/fleet_test.js similarity index 100% rename from test/fleet_test.js rename to old/test/fleet_test.js diff --git a/test/mocha_debugger.js b/old/test/mocha_debugger.js similarity index 100% rename from test/mocha_debugger.js rename to old/test/mocha_debugger.js diff --git a/test/servercontroller_test.js b/old/test/servercontroller_test.js similarity index 100% rename from test/servercontroller_test.js rename to old/test/servercontroller_test.js diff --git a/test/tradingcenter_test.js b/old/test/tradingcenter_test.js similarity index 100% rename from test/tradingcenter_test.js rename to old/test/tradingcenter_test.js diff --git a/views/astriarch.pug b/old/views/astriarch.pug similarity index 100% rename from views/astriarch.pug rename to old/views/astriarch.pug diff --git a/views/layout.pug b/old/views/layout.pug similarity index 100% rename from views/layout.pug rename to old/views/layout.pug diff --git a/views/test.pug b/old/views/test.pug similarity index 100% rename from views/test.pug rename to old/views/test.pug diff --git a/wss_interface.js b/old/wss_interface.js similarity index 100% rename from wss_interface.js rename to old/wss_interface.js diff --git a/package.json b/package.json index 6e441c3a..c3cbdc38 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,29 @@ { - "name": "Astriarch", - "version": "1.9.2", - "private": true, - "dependencies": { - "async": "2.6.4", - "body-parser": "^1.18.3", - "config": "0.4.34", - "connect-mongo": "^2.0.1", - "console-ten": "0.0.5", - "cookie-parser": "^1.4.3", - "errorhandler": "^1.5.0", - "express": "^4.16.4", - "express-session": "^1.15.6", - "extend": "2.0.2", - "mongoose": "^5.7.5", - "morgan": "^1.9.1", - "pug": "^3.0.1", - "serve-favicon": "^2.5.0", - "ws": "^6.2.2" + "name": "astriarch", + "version": "1.0.0", + "description": "Astriarch space strategy game monorepo", + "scripts": { + "clean": "rm -rf node_modules && pnpm run -r clean", + "build": "pnpm run -r build", + "test": "pnpm run -r test", + "format": "pnpm run -r format", + "format:check": "pnpm run -r format:check", + "lint": "pnpm run -r lint", + "lint:check": "pnpm run -r lint:check", + "prepare": "husky" }, + "keywords": [ + + ], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912", "devDependencies": { - "mocha": "^5.2.0", - "prettier": "^1.14.3", - "should": "4.6.0" + "husky": "^9.1.7" }, - "scripts": { - "prettier-write": "prettier --write \"{.,models,controllers,test,public/js,public/js/astriarch,}/*.js\" --print-width 120", - "start": "node app.js", - "test": "./node_modules/.bin/mocha --reporter list" + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] } -} +} \ No newline at end of file diff --git a/packages/astriarch-engine/.prettierrc b/packages/astriarch-engine/.prettierrc new file mode 100644 index 00000000..727f0a5d --- /dev/null +++ b/packages/astriarch-engine/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/packages/astriarch-engine/eslint.config.mjs b/packages/astriarch-engine/eslint.config.mjs new file mode 100644 index 00000000..4baefae0 --- /dev/null +++ b/packages/astriarch-engine/eslint.config.mjs @@ -0,0 +1,61 @@ +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + // Base configuration for all files + { + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: import.meta.dirname, + sourceType: 'module', + }, + ecmaVersion: 'latest', + globals: { + // Adding globals for Node and Jest environments + 'process': 'readonly', + 'console': 'readonly', + 'module': 'readonly', + 'require': 'readonly', + '__dirname': 'readonly', + '__filename': 'readonly', + // Jest globals + 'describe': 'readonly', + 'it': 'readonly', + 'test': 'readonly', + 'expect': 'readonly', + 'beforeAll': 'readonly', + 'beforeEach': 'readonly', + 'afterAll': 'readonly', + 'afterEach': 'readonly', + 'jest': 'readonly', + } + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + }, + extends: [ + tseslint.configs.recommended, + tseslint.configs.stylistic, + ], + ignores: ['.eslintrc.js'], + linterOptions: { + reportUnusedDisableDirectives: true, + }, + rules: { + 'no-console': "off", + // Temporarily disable rules that are causing many errors, to be fixed gradually + '@typescript-eslint/no-unused-vars': 'warn', + 'prefer-const': 'warn', + }, + }, + // Configuration for test files + { + files: ['**/*.spec.ts', '**/test/**/*.ts'], + rules: { + 'no-console': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-empty-function': 'off' + } + } +); diff --git a/packages/astriarch-engine/jest.config.js b/packages/astriarch-engine/jest.config.js new file mode 100644 index 00000000..3abcbd94 --- /dev/null +++ b/packages/astriarch-engine/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", +}; diff --git a/packages/astriarch-engine/package.json b/packages/astriarch-engine/package.json new file mode 100644 index 00000000..0d4aa210 --- /dev/null +++ b/packages/astriarch-engine/package.json @@ -0,0 +1,36 @@ +{ + "private": true, + "name": "astriarch-engine", + "description": "Astriarch game engine", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "test": "jest --runInBand --forceExit --detectOpenHandles --roots src --testRegex \"\\.spec\\.ts$\" --verbose false", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "clean": "rm -rf dist && rm -rf node_modules", + "build": "tsup", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^24.0.10", + "eslint": "^9.30.1", + "typescript-eslint": "^8.35.1", + "jest": "^30.0.4", + "prettier": "^3.6.2", + "ts-jest": "^29.4.0", + "tslib": "^2.8.1", + "tsup": "^8.5.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/astriarch-engine/src/engine/battleSimulator.spec.ts b/packages/astriarch-engine/src/engine/battleSimulator.spec.ts new file mode 100644 index 00000000..82c72176 --- /dev/null +++ b/packages/astriarch-engine/src/engine/battleSimulator.spec.ts @@ -0,0 +1,417 @@ +import { PlanetById } from '../model/clientModel'; +import { FleetData, StarShipType } from '../model/fleet'; +import { PlayerData } from '../model/player'; +import { ResearchType } from '../model/research'; +import { startNewTestGame, TestGameData } from '../test/testUtils'; +import { BattleSimulator, FleetDamagePending } from './battleSimulator'; +import { ClientGameModel } from './clientGameModel'; +import { Fleet } from './fleet'; +import { Research } from './research'; + +let testGameData: TestGameData; +let player1: PlayerData; +let player2: PlayerData; +let planetById: PlanetById; + +const checkFleetWinCount = ( + fleet1: FleetData, + fleet1Owner: PlayerData, + fleet2: FleetData, + fleet2Owner: PlayerData, + battleTries: number, +) => { + const winLoseDraw = { w: 0, l: 0, d: 0 }; + + //try the battle multiple times to account for randomness/luck + for (let i = 0; i < battleTries; i++) { + const f1 = Fleet.cloneFleet(fleet1); + const f2 = Fleet.cloneFleet(fleet2); + const fleet1Wins = BattleSimulator.simulateFleetBattle(f1, fleet1Owner, f2, fleet2Owner); + if (fleet1Wins === null) { + winLoseDraw.d++; + } else if (fleet1Wins) { + winLoseDraw.w++; + } else { + winLoseDraw.l++; + } + } + return winLoseDraw; +}; + +describe('BattleSimulator', () => { + beforeEach(() => { + testGameData = startNewTestGame(); + player1 = testGameData.gameModel.modelData.players[0]; + player2 = testGameData.gameModel.modelData.players[1]; + planetById = ClientGameModel.getPlanetByIdIndex(testGameData.gameModel.modelData.planets); + }); + + describe('SimulateFleetBattle', () => { + //here are the advantages (-> means has an advantage over): + // battleships -> cruisers -> destroyers -> scouts -> defenders (-> battleships) + it('should allow the fleet with the advantage to win given the fleets have the same strength', (done) => { + const battleTries = 100; + + //scouts over defenders + let f1 = Fleet.generateFleetWithShipCount(0, 1, 0, 0, 0, 0, null); + let f2 = Fleet.generateFleetWithShipCount(2, 0, 0, 0, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + let fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + if (fleet1WinCount < battleTries * 0.9) { + return done("Scout didn't win against two defenders!"); + } + + //destroyers over scouts + f1 = Fleet.generateFleetWithShipCount(0, 0, 1, 0, 0, 0, null); + f2 = Fleet.generateFleetWithShipCount(0, 2, 0, 0, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + if (fleet1WinCount < battleTries * 0.9) { + return done("Destroyer didn't win against two scouts!"); + } + + //cruisers over destroyers + f1 = Fleet.generateFleetWithShipCount(0, 0, 0, 1, 0, 0, null); + f2 = Fleet.generateFleetWithShipCount(0, 0, 2, 0, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + if (fleet1WinCount < battleTries * 0.9) { + return done("Cruiser didn't win against two destroyers!"); + } + + //battleships over cruisers + f1 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 1, 0, null); + f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 2, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + if (fleet1WinCount < battleTries * 0.9) { + return done("Battleship didn't win against two cruisers!"); + } + + //defenders over battleships + f1 = Fleet.generateFleetWithShipCount(16, 0, 0, 0, 0, 0, null); + f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 1, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + if (fleet1WinCount < battleTries * 0.9) { + return done("Sixteen defenders didn't win against one battleship!"); + } + done(); + }); + + it('should allow the fleet with the disadvantage to win given the disadvantaged fleet has sufficiently more power (4x)', (done) => { + const battleTries = 1000; + + //scouts over destroyers + let f1 = Fleet.generateFleetWithShipCount(0, 4, 0, 0, 0, 0, null); + let f2 = Fleet.generateFleetWithShipCount(0, 0, 1, 0, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2) * 2); + let fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + //console.log("f1: ", f1.DetermineFleetStrength(), "f2:", f2.DetermineFleetStrength()); + console.log('Scout fleet Win Count:', fleet1WinCount); + if (fleet1WinCount < battleTries * 0.5) { + return done('Four scouts won less than 50% of the time against a Destroyer!'); + } + + //destroyers over cruisers + f1 = Fleet.generateFleetWithShipCount(0, 0, 4, 0, 0, 0, null); + f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 1, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2) * 2); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + console.log('Destroyer fleet Win Count:', fleet1WinCount); + if (fleet1WinCount < battleTries * 0.5) { + return done('Four Destroyers won less than 50% of the time against a Cruiser!'); + } + + //cruisers over battleships + f1 = Fleet.generateFleetWithShipCount(0, 0, 0, 4, 0, 0, null); + f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 1, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2) * 2); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + console.log('Cruiser fleet Win Count:', fleet1WinCount); + if (fleet1WinCount < battleTries * 0.5) { + return done('Four Cruisers won less than 50% of the time against a Battleship!'); + } + + //battleships over defenders + f1 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 1, 0, null); + f2 = Fleet.generateFleetWithShipCount(8, 0, 0, 0, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2) * 2); + fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + console.log('Battleship fleet Win Count:', fleet1WinCount); + if (fleet1WinCount < battleTries * 0.5) { + return done('A Battleship won less than 50% of the time against 4 Scouts!'); + } + + done(); + }); + + it('StarshipFireWeapons should evenly assign damage between 0, 1, and two for each gun', (done) => { + const battleTries = 10000; + let totalDamage = 0; + + for (let i = 0; i < battleTries; i++) { + const f2 = Fleet.generateFleetWithShipCount(1, 0, 0, 0, 0, 0, null); + const f1 = Fleet.generateFleetWithShipCount(1, 0, 0, 0, 0, 0, null); + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + + const f1BonusChance = { attack: 0, defense: 0 }; + const f2BonusChance = { attack: 0, defense: 0 }; + const f1StarShips = f1.starships; + const f2StarShips = f2.starships; + const fleet1DamagePending: FleetDamagePending = {}; + const s = f2StarShips[0]; + BattleSimulator.starshipFireWeapons( + f2BonusChance.attack, + f1BonusChance.defense, + s, + f1StarShips, + fleet1DamagePending, + ); + if (f1StarShips[0].id in fleet1DamagePending) { + totalDamage += BattleSimulator.getTotalPendingDamage(fleet1DamagePending[f1StarShips[0].id].hits); + } + } + + console.log('StarshipFireWeapons test totalDamage:', totalDamage); + if (totalDamage > battleTries * 1.05) { + return done('totalDamage > 105%!'); + } else if (totalDamage < battleTries * 0.95) { + return done('totalDamage < 95%!'); + } + + done(); + }); + + it('should give even odds to identical fleets independent of if attacking or defending', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 0, 1, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 1, 0, 0, 0, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const winLoseDraw = checkFleetWinCount(f1, player1, f2, player2, battleTries); + console.log('Fleet 1 (identical fleet) Win Count:', winLoseDraw); + if (winLoseDraw.w > battleTries * 0.55) { + return done('Fleet1 won more than 55% of the time!'); + } else if (winLoseDraw.w < battleTries * 0.45) { + return done('Fleet1 won less than 45% of the time!'); + } + done(); + }); + + it('should give a slight home system advantage to ships defending on a planet', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 1, 0, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 1, 0, 0, 0, 0, null); + f2.locationHexMidPoint = { x: 0, y: 0 }; + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const winLoseDraw = checkFleetWinCount(f1, player1, f2, player2, battleTries); + console.log('System Attacking fleet Win Count:', winLoseDraw); + if (winLoseDraw.w > battleTries * 0.45) { + return done('System Attacking Fleet won more than 45% of the time!'); + } + done(); + }); + + it('should allow defenders to win against another fleet with the same power about 50% of the time', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(4, 0, 0, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 1, 0, 0, 0, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + console.log('Defender fleet Win Count:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.56) { + return done('Defender Fleet won more than 56% of the time!'); + } else if (fleet1WinCount < battleTries * 0.44) { + return done('Defender Fleet won less than 44% of the time!'); + } + done(); + }); + + it('should allow more defenders to win against another fleet with the same power about 50% of the time', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(16, 0, 0, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 2, 1, 0, 0, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + //Note: when there are lots of little ships like scouts and defenders without advantages or disadvantages against a fleet with the same power, the fleet with lots of little ships wins about 60% of the time + // This is caused by "overkill" since each gun has 2 power, each time a ship has one strength left it's possible that the gun will damage 2, leaving an extra damage that could have been assigned to another ship + // this side effect actually seems realistic and is a reason to keep tougher ships relative costs the same, even though they can repair + console.log('Defender fleet Win Count:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.66) { + return done('Defender Fleet won more than 66% of the time!'); + } else if (fleet1WinCount < battleTries * 0.4) { + return done('Defender Fleet won less than 40% of the time!'); + } + done(); + }); + + it('should allow spaceplatforms to win against any other fleet, even if the other fleet has slightly more power', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 8, 4, 2, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 0, 1, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2) * 1.5); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + console.log('fleet1WinCount against Space Platform:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.15) { + return done('Fleet won more than 15% of the time against a Space Platform!'); + } + done(); + }); + + it('should allow spaceplatforms to win against a 1.5x strength fleet of battleships most of the time', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 3, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 0, 1, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2) * 1.5); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + console.log('Battleship fleet Win Count:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.15) { + return done('Battleship Fleet won more than 15% of the time against a Space Platform!'); + } + done(); + }); + + it('should not allow the disadvantaged fleet to win', (done) => { + const f1 = Fleet.generateFleetWithShipCount(16, 2, 0, 0, 0, 1, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 1, 4, 1, 0, null); + console.log('f1: ', Fleet.determineFleetStrength(f1), 'f2:', Fleet.determineFleetStrength(f2)); + const fleet1Wins = BattleSimulator.simulateFleetBattle(f1, player1, f2, player2); + console.log('f1: ', Fleet.determineFleetStrength(f1), 'f2:', Fleet.determineFleetStrength(f2)); + if (!fleet1Wins) { + return done('The disadvantaged fleet won!'); + } + done(); + }); + + it('should allow identical fleets to win about 50% of the time', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 2, 5, 4, 1, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 2, 5, 4, 1, 0, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + + console.log('fleet1WinCount:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.53) { + return done('Attacking Fleet won more than 53% of the time!'); + } else if (fleet1WinCount < battleTries * 0.47) { + return done('Attacking Fleet won less than 47% of the time!'); + } + done(); + }); + + it('should allow a mixed fleet to win against another mixed fleet with the same power about 50% of the time', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 0, 4, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 1, 0, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + + console.log('fleet1WinCount:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.58) { + return done('Attacking Fleet won more than 58% of the time!'); + } else if (fleet1WinCount < battleTries * 0.42) { + return done('Attacking Fleet won less than 42% of the time!'); + } + done(); + }); + + it('should properly account for customized starship advantages and disadvantages', (done) => { + const battleTries = 1000; + + const f1 = Fleet.generateFleetWithShipCount(0, 0, 4, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 2, 0, 0, null); + + f1.starships.forEach((s) => { + if (s.type === StarShipType.Destroyer) { + s.customShipData = { + advantageAgainst: StarShipType.Cruiser, + disadvantageAgainst: StarShipType.SystemDefense, + }; + } + }); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + + console.log('fleet1WinCount:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.58) { + return done('Attacking Fleet won more than 58% of the time!'); + } else if (fleet1WinCount < battleTries * 0.42) { + return done('Attacking Fleet won less than 42% of the time!'); + } + done(); + }); + + it('should properly account for starship research bonuses', (done) => { + const battleTries = 1000; + + Research.setResearchPointsCompleted( + player1.research.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_ATTACK], + 1000, + ); + Research.setResearchPointsCompleted( + player1.research.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_DEFENSE], + 1000, + ); + + const f1 = Fleet.generateFleetWithShipCount(0, 0, 4, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 2, 0, 0, null); + + expect(Fleet.determineFleetStrength(f1)).toEqual(Fleet.determineFleetStrength(f2)); + const fleet1WinCount = checkFleetWinCount(f1, player1, f2, player2, battleTries).w; + + console.log('fleet1WinCount:', fleet1WinCount); + if (fleet1WinCount > battleTries * 0.58) { + return done('Attacking Fleet won more than 58% of the time!'); + } else if (fleet1WinCount < battleTries * 0.42) { + return done('Attacking Fleet won less than 42% of the time!'); + } + done(); + }); + + it('should properly assign experience points to the winning fleet', (done) => { + const f1 = Fleet.generateFleetWithShipCount(6, 0, 0, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 2, 0, null); + + const f1Strength = Fleet.determineFleetStrength(f1); + + const fleet1wins = BattleSimulator.simulateFleetBattle(f1, player1, f2, player2); + expect(fleet1wins).toEqual(false); + //check total experience points for fleet2 + const ships = f2.starships; + const experiencePointsTotal = f2.starships.reduce((accum, curr) => accum + curr.experienceAmount, 0); + expect(experiencePointsTotal).toEqual(f1Strength); + done(); + }); + + it('should assign experience points to space platforms', (done) => { + const f1 = Fleet.generateFleetWithShipCount(0, 2, 0, 0, 0, 0, null); + const f2 = Fleet.generateFleetWithShipCount(0, 0, 0, 0, 0, 1, null); + + const f1Strength = Fleet.determineFleetStrength(f1); + + const fleet1wins = BattleSimulator.simulateFleetBattle(f1, player1, f2, player2); + expect(fleet1wins).toEqual(false); + //check total experience points for fleet2 + const experiencePointsTotal = f2.starships.reduce((accum, curr) => accum + curr.experienceAmount, 0); + expect(experiencePointsTotal).toEqual(f1Strength); + done(); + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/battleSimulator.ts b/packages/astriarch-engine/src/engine/battleSimulator.ts new file mode 100644 index 00000000..952a282c --- /dev/null +++ b/packages/astriarch-engine/src/engine/battleSimulator.ts @@ -0,0 +1,255 @@ +import { FleetData, StarshipData, StarShipType } from '../model/fleet'; +import { PlayerData } from '../model/player'; +import { ResearchType } from '../model/research'; +import { Utils } from '../utils/utils'; +import { Fleet } from './fleet'; +import { StarShipAdvantageStrengthComparer } from './starShipAdvantageStrengthComparer'; + +interface FleetBonusChance { + attack: number; + defense: number; +} + +interface PendingHit { + damage: number; + source: StarshipData; +} + +export type FleetDamagePending = Record; +type ExperiencePending = Record; + +//new simulate fleet battle logic: +//this will support choosing a target and allowing ships to have an advantage over other types +//here are the advantages (-> means has an advantage over): +//defenders -> destroyers -> battleships -> cruisers -> spaceplatforms -> scouts (-> defenders) +//target decisions: +//if there is an enemy ship that this ship has an advantage over, choose that one +//otherwise go for the weakest ship to shoot at +export class BattleSimulator { + private static STARSHIP_WEAPON_POWER = 2; //defenders have one gun and battleships have 16 + private static STARSHIP_WEAPON_POWER_HALF = 1; + private static HOME_SYSTEM_ADVANTAGE = 0.05; + private static BATTLE_RANDOMNESS_FACTOR: 4.0; //the amount randomness (chance) when determining fleet conflict outcomes, it is the strength multiplyer where the winner is guaranteed to win + + public static simulateFleetBattle( + f1: FleetData, + f1Owner: PlayerData, + f2: FleetData, + f2Owner?: PlayerData, + ): boolean | null { + let fleet1Wins = null; // null means draw which shouldn't happen unless two fleets meet in mid-space (neither has locationHex) + + const f1BonusChance: FleetBonusChance = { attack: 0, defense: 0 }; + const f2BonusChance: FleetBonusChance = { attack: 0, defense: 0 }; + + //fleet damage pending structures are so we can have both fleets fire simultaneously without damaging each-other till the end of each round + let fleet1DamagePending: FleetDamagePending = {}; + let fleet2DamagePending: FleetDamagePending = {}; + + //We don't award experience points until the end of battle so that ships can't level up in the heat of conflict + const experiencedGainedByStarShipId: ExperiencePending = {}; + + f1BonusChance.attack = + f1Owner.research.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_ATTACK].data.chance! + + (f1.locationHexMidPoint ? this.HOME_SYSTEM_ADVANTAGE : 0); + f1BonusChance.defense = + f1Owner.research.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_DEFENSE].data.chance! + + (f1.locationHexMidPoint ? this.HOME_SYSTEM_ADVANTAGE : 0); + + f2BonusChance.attack = + (f2Owner?.research.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_ATTACK].data.chance || 0) + + (f2.locationHexMidPoint ? this.HOME_SYSTEM_ADVANTAGE : 0); + f2BonusChance.defense = + (f2Owner?.research.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_DEFENSE].data.chance || 0) + + (f2.locationHexMidPoint ? this.HOME_SYSTEM_ADVANTAGE : 0); + + while (Fleet.determineFleetStrength(f1) > 0 && Fleet.determineFleetStrength(f2) > 0) { + // fire weapons + for (const s of f1.starships) { + this.starshipFireWeapons(f1BonusChance.attack, f2BonusChance.defense, s, f2.starships, fleet2DamagePending); + } + + for (const s of f2.starships) { + this.starshipFireWeapons(f2BonusChance.attack, f1BonusChance.defense, s, f1.starships, fleet1DamagePending); + } + + // deal damage + this.dealDamage(f1Owner, fleet1DamagePending, experiencedGainedByStarShipId); + this.dealDamage(f2Owner, fleet2DamagePending, experiencedGainedByStarShipId); + fleet1DamagePending = {}; + fleet2DamagePending = {}; + + Fleet.reduceFleet(f1); + Fleet.reduceFleet(f2); + } + + // if both fleets are destroyed, choose fleet with a planet on the hex + const f1Strength = Fleet.determineFleetStrength(f1); + const f2Strength = Fleet.determineFleetStrength(f2); + if (f1Strength <= 0 && f2Strength <= 0) { + if (f1.locationHexMidPoint) { + fleet1Wins = true; + } + if (f2.locationHexMidPoint) { + fleet1Wins = false; + } + } else if (f1Strength > 0) { + fleet1Wins = true; + } else if (f2Strength > 0) { + fleet1Wins = false; + } + + if (fleet1Wins != null) { + //assign experience + (fleet1Wins ? f1 : f2).starships.forEach((s) => { + if (s.id in experiencedGainedByStarShipId) { + s.experienceAmount += experiencedGainedByStarShipId[s.id].damageDealt; + } + }); + } + + return fleet1Wins; + } + + public static dealDamage( + owner: PlayerData | undefined, + fleetDamagePending: FleetDamagePending, + experiencedGainedByStarShipId: ExperiencePending, + ) { + for (const dp of Object.values(fleetDamagePending)) { + for (const hit of dp.hits) { + const sourceId = hit.source.id; + if (!(sourceId in experiencedGainedByStarShipId)) { + experiencedGainedByStarShipId[sourceId] = { starship: hit.source, damageDealt: 0 }; + } + experiencedGainedByStarShipId[sourceId].damageDealt += Fleet.damageStarship(owner, dp.starship, hit.damage); + } + } + } + + public static starshipFireWeapons( + attackBonusChance: number, + defenseBonusChance: number, + ship: StarshipData, + enemyFleet: StarshipData[], + fleetDamagePending: FleetDamagePending, + ) { + let damage = 0; + let maxDamage = 0; + let workingEnemyFleet: StarshipData[] = []; + workingEnemyFleet = workingEnemyFleet.concat(enemyFleet); + const strengthComparer = new StarShipAdvantageStrengthComparer(ship, fleetDamagePending); + workingEnemyFleet.sort((a, b) => strengthComparer.sortFunction(a, b)); + + for (let iGun = 0; iGun < ship.health; iGun += this.STARSHIP_WEAPON_POWER) { + //remove any in the enemy ship with strength - pending damage <= 0 so that they aren't a target + for (let i = workingEnemyFleet.length - 1; i >= 0; i--) { + const enemy = workingEnemyFleet[i]; //StarShip + let pendingDamage = 0; + if (enemy.id in fleetDamagePending) { + pendingDamage = this.getTotalPendingDamage(fleetDamagePending[enemy.id].hits); + } + if (workingEnemyFleet[i].health - pendingDamage <= 0) { + workingEnemyFleet.splice(i, 1); + } + } + + //calculate starship max damage + maxDamage = this.STARSHIP_WEAPON_POWER; + + //add/remove additional max damage for research advancement + if (attackBonusChance && Math.random() < attackBonusChance) { + maxDamage += this.STARSHIP_WEAPON_POWER_HALF; + } + + if (defenseBonusChance && Math.random() < defenseBonusChance) { + maxDamage -= this.STARSHIP_WEAPON_POWER_HALF; + } + + //choose target + if (workingEnemyFleet.length > 0) { + const target = workingEnemyFleet[0]; //StarShip + + //add/remove additional max damage for advantages/disadvantages + if (this.starshipHasAdvantage(ship, target)) { + maxDamage += this.STARSHIP_WEAPON_POWER_HALF; + } else if (this.starshipHasDisadvantage(ship, target)) { + maxDamage -= this.STARSHIP_WEAPON_POWER_HALF; + } + + damage = Utils.nextRandom(0, maxDamage + 1); + + if (damage != 0) { + if (!(target.id in fleetDamagePending)) { + fleetDamagePending[target.id] = { starship: target, hits: [] }; + } + fleetDamagePending[target.id].hits.push({ damage, source: ship }); + } + } + } + } + + public static starshipHasAdvantage(ssAttacker: StarshipData, ssDefender: StarshipData) { + //space platforms have advantages over everything + const attackerAdvantage = ssAttacker.customShipData + ? ssAttacker.customShipData.advantageAgainst + : Fleet.getStarshipStandardAdvantageByType(ssAttacker.type)?.advantageAgainst; + if (ssAttacker.type == StarShipType.SpacePlatform) { + return true; + } else if (ssDefender.type == StarShipType.SpacePlatform) { + return false; + } else if (attackerAdvantage == ssDefender.type) { + return true; + } + return false; + } + + public static starshipHasDisadvantage(ssAttacker: StarshipData, ssDefender: StarshipData) { + const attackerDisadvantage = ssAttacker.customShipData + ? ssAttacker.customShipData.disadvantageAgainst + : Fleet.getStarshipStandardAdvantageByType(ssAttacker.type)?.disadvantageAgainst; + if (ssAttacker.type == StarShipType.SpacePlatform) { + return false; + } else if (ssDefender.type == StarShipType.SpacePlatform) { + return true; + } else if (attackerDisadvantage == ssDefender.type) { + return true; + } + return false; + } + + public static getTotalPendingDamage(hits: PendingHit[]) { + return hits.reduce((accum, curr) => accum + curr.damage, 0); + } + + public static getAttackingFleetChances(playerFleetStrength: number, enemyFleetStrength: number): number { + let attackingFleetChances = 0; + if (enemyFleetStrength > playerFleetStrength * this.BATTLE_RANDOMNESS_FACTOR) { + attackingFleetChances = 1; + } else if (playerFleetStrength > enemyFleetStrength * this.BATTLE_RANDOMNESS_FACTOR) { + attackingFleetChances = enemyFleetStrength == 0 ? 100 : 99; + } else { + //algorithm for estimated chance: BATTLE_RANDOMNESS_FACTOR here = 4 + // % chance = 50 + LOG base 4(greater fleet strength / lesser fleet strength) + let randomnessUpperBounds = 0; + if (playerFleetStrength > enemyFleetStrength) { + //prefer player + const extraPercentageChance = + (Math.log(playerFleetStrength / (enemyFleetStrength * 1.0)) / + Math.log(Math.pow(this.BATTLE_RANDOMNESS_FACTOR, 2))) * + 100; //((playerFleetStrength - enemyFleetStrength) / (double)enemyFleetStrength) * 50; + randomnessUpperBounds = 50 + Math.round(extraPercentageChance); + attackingFleetChances = randomnessUpperBounds; + } else { + //prefer enemy + const extraPercentageChanceEnemy = + (Math.log(enemyFleetStrength / (playerFleetStrength * 1.0)) / + Math.log(Math.pow(this.BATTLE_RANDOMNESS_FACTOR, 2))) * + 100; //((enemyFleetStrength - playerFleetStrength) / (double)playerFleetStrength) * 50; + randomnessUpperBounds = 50 + Math.round(extraPercentageChanceEnemy); + attackingFleetChances = 100 - randomnessUpperBounds; + } + } + return attackingFleetChances; + } +} diff --git a/packages/astriarch-engine/src/engine/clientGameModel.ts b/packages/astriarch-engine/src/engine/clientGameModel.ts new file mode 100644 index 00000000..a805a093 --- /dev/null +++ b/packages/astriarch-engine/src/engine/clientGameModel.ts @@ -0,0 +1,96 @@ +import { ClientModelData, ClientPlanet, ClientPlayer, ClientTradingCenter, PlanetById } from '../model/clientModel'; +import { ModelData } from '../model/model'; +import { PlanetData } from '../model/planet'; +import { PlayerData } from '../model/player'; +import { TradingCenterData } from '../model/tradingCenter'; +import { TaskNotifications } from './taskNotifications'; + +export class ClientGameModel { + public static constructClientGameModel(model: ModelData, targetPlayerId: string): ClientModelData { + const { gameOptions, gameStartedAtTime, lastSnapshotTime, currentCycle } = model; + const clientTradingCenter = ClientGameModel.constructClientTradingCenter(model.tradingCenter, targetPlayerId); + const mainPlayer = model.players.find((p) => p.id === targetPlayerId); + if (!mainPlayer) { + throw new Error('Unable to find target player in constructClientGameModel!'); + } + const mainPlayerOwnedPlanets = ClientGameModel.getOwnedPlanets(mainPlayer?.ownedPlanetIds, model.planets); + const otherPlayers = model.players.filter((player) => player.id !== targetPlayerId); + const clientPlayers = otherPlayers.map((player) => ClientGameModel.constructClientPlayer(player)); + const mainPlayerExploredPlanetIds = new Set(mainPlayer?.knownPlanetIds); + const clientPlanets = model.planets.map((p) => + ClientGameModel.constructClientPlanet(p, mainPlayerExploredPlanetIds.has(p.id)), + ); + + const taskNotifications = TaskNotifications.constructTaskNotifications(); + return { + gameOptions, + gameStartedAtTime, + lastSnapshotTime, + currentCycle, + clientTradingCenter, + mainPlayer, + mainPlayerOwnedPlanets, + clientPlayers, + clientPlanets, + taskNotifications, + }; + } + + public static constructClientPlayer(player: PlayerData): ClientPlayer { + const { id, type, name, color, points, destroyed, research } = player; + + return { + id, + type, + name, + color, + points, + destroyed, + research, + }; + } + + public static constructClientPlanet(planet: PlanetData, targetPlayerHasExploredPlanet: boolean): ClientPlanet { + const { id, name, originPoint, boundingHexMidPoint } = planet; + return { + id, + name, + originPoint, + boundingHexMidPoint, + type: targetPlayerHasExploredPlanet ? planet.type : null, + }; + } + + public static constructClientTradingCenter( + tradingCender: TradingCenterData, + targetPlayerId: string, + ): ClientTradingCenter { + const { energyAmount, foodResource, oreResource, iridiumResource } = tradingCender; + + const mainPlayerTrades = tradingCender.currentTrades.filter((trade) => trade.playerId === targetPlayerId); + + return { + energyAmount, + foodResource, + oreResource, + iridiumResource, + mainPlayerTrades: mainPlayerTrades, + }; + } + + public static getPlanets(planetIds: number[], planets: PlanetData[]) { + const ownedPlanetIds = new Set(planetIds); + return planets.filter((p) => ownedPlanetIds.has(p.id)); + } + + public static getPlanetByIdIndex(planets: PlanetData[]): PlanetById { + return planets.reduce((accum, curr) => { + accum[curr.id] = curr; + return accum; + }, {} as PlanetById); + } + + public static getOwnedPlanets(planetIds: number[], planets: PlanetData[]) { + return ClientGameModel.getPlanetByIdIndex(ClientGameModel.getPlanets(planetIds, planets)); + } +} diff --git a/packages/astriarch-engine/src/engine/computerPlayer.ts b/packages/astriarch-engine/src/engine/computerPlayer.ts new file mode 100644 index 00000000..990d09ab --- /dev/null +++ b/packages/astriarch-engine/src/engine/computerPlayer.ts @@ -0,0 +1,1143 @@ +import { PlanetById } from '../model/clientModel'; +import { FleetData, StarShipType } from '../model/fleet'; +import { PlanetData, PlanetImprovementType, PlanetResourceType, PlanetType } from '../model/planet'; +import { PlayerData, PlayerType } from '../model/player'; +import { TradeType, TradingCenterResourceType } from '../model/tradingCenter'; +import { Utils } from '../utils/utils'; +import { Fleet } from './fleet'; +import { GameModelData } from './gameModel'; +import { Grid } from './grid'; +import { Planet, PlanetPerTurnResourceGeneration, PopulationAssignments } from './planet'; +import { PlanetDistanceComparer } from './planetDistanceComparer'; +import { PlanetProductionItem } from './planetProductionItem'; +import { PlanetResourcePotentialComparer } from './planetResourcePotentialComparer'; +import { Player } from './player'; +import { Research } from './research'; +import { TradingCenter } from './tradingCenter'; + +export type PlanetResourcesPerTurn = Record; + +export class ComputerPlayer { + private static onComputerSentFleet(fleet: FleetData) { + console.debug('Computer Sent Fleet:', fleet); + } + + public static computerTakeTurn(gameModel: GameModelData, player: PlayerData, ownedPlanets: PlanetById) { + //determine highest priority for resource usage + //early game should be building developments and capturing/exploring planets while keeping up food production + //mid game should be building space-platforms, high-class ships and further upgrading planets + //late game should be strategically engaging the enemy + //check planet production, prefer high-class planets (or even weather strategic points should be developed instead of high-class planets?) + //if the planet has slots available and we have enough resources build (in order when we don't have) + // + + const ownedPlanetsSorted = Player.getOwnedPlanetsListSorted(player, ownedPlanets); + this.computerSetPlanetBuildGoals(gameModel, player, ownedPlanets, ownedPlanetsSorted); + + this.computerSubmitTrades(gameModel, player, ownedPlanets, ownedPlanetsSorted); + + this.computerBuildImprovementsAndShips(gameModel, player, ownedPlanets, ownedPlanetsSorted); + + //adjust population assignments as appropriate based on planet and needs + this.computerAdjustPopulationAssignments(player, ownedPlanets, ownedPlanetsSorted); + + //base strategies on computer-level + //here is the basic strategy: + //if there are unclaimed explored planets + //find the closest one and send a detachment, based on planet class + //for easy level send detachments based only on distance + //normal mode additionally prefers class 2 planets + //hard mode additionally prefers Dead planets and considers enemy force before making an attack + //expert mode additionally prefers asteroid belts late in the game when it needs crystal + + //send scouts to unexplored planets (harder levels of computers know where better planets are?) + + this.computerSendShips(gameModel, player, ownedPlanets, ownedPlanetsSorted); + } + + public static computerAdjustPopulationAssignments( + player: PlayerData, + ownedPlanets: PlanetById, + ownedPlanetsSorted: PlanetData[], + ) { + const planetPopulationWorkerTypes: Record = {}; + const planetResourcesPerTurn: PlanetResourcesPerTurn = {}; + const totalResources = Player.getTotalResourceAmount(player, ownedPlanets); + const allPlanets: PlanetData[] = []; //this list will be for sorting + + for (const planet of ownedPlanetsSorted) { + planetPopulationWorkerTypes[planet.id] = Planet.countPopulationWorkerTypes(planet); + planetResourcesPerTurn[planet.id] = Planet.getPlanetWorkerResourceGeneration(planet, player); + allPlanets.push(planet); + + console.debug( + player.name, + 'Population Assignment for planet:', + planet.name, + planetPopulationWorkerTypes[planet.id], + ); + } + + const totalPopulation = Player.getTotalPopulation(player, ownedPlanets); + let totalFoodProduction = 0; + let totalFoodAmountOnPlanets = 0; + let totalPlanetsWithPopulationGrowthPotential = 0; //this will always give the computers some extra food surplus to avoid starving new population + for (const planet of ownedPlanetsSorted) { + totalFoodAmountOnPlanets += planet.resources.food; + totalFoodProduction += planetResourcesPerTurn[planet.id].amountPerTurn.food; + if (planet.population.length < Planet.maxPopulation(planet)) totalPlanetsWithPopulationGrowthPotential++; + } + totalFoodAmountOnPlanets -= totalPopulation; //this is what we'll eat this turn + + //this is what we'll keep in surplus to avoid starving more-difficult comps + totalFoodAmountOnPlanets -= totalPlanetsWithPopulationGrowthPotential; + + //to make the easier computers even easier we will sometimes have them generate too much food and sometimes generate too little so they starve + let totalFoodAmountOnPlanetsAdjustmentLow = 0; + let totalFoodAmountOnPlanetsAdjustmentHigh = 0; + //add some extra food padding based on player type, this will make the easier computers less agressive + switch (player.type) { + case PlayerType.Computer_Easy: + totalFoodAmountOnPlanetsAdjustmentLow -= totalPopulation * 3; + totalFoodAmountOnPlanetsAdjustmentHigh = Math.floor(totalPopulation * 1.5); + break; + case PlayerType.Computer_Normal: + totalFoodAmountOnPlanetsAdjustmentLow -= Math.floor(totalPopulation * 1.5); + totalFoodAmountOnPlanetsAdjustmentHigh = totalPopulation; + break; + case PlayerType.Computer_Hard: + totalFoodAmountOnPlanetsAdjustmentLow -= totalPopulation / 2; + totalFoodAmountOnPlanetsAdjustmentHigh = 0; + break; + } + + const totalFoodAmountOnPlanetsAdjustment = Utils.nextRandom( + totalFoodAmountOnPlanetsAdjustmentLow, + totalFoodAmountOnPlanetsAdjustmentHigh + 1, + ); + + totalFoodAmountOnPlanets += totalFoodAmountOnPlanetsAdjustment; + + let oreAmountRecommended = 0; + let iridiumAmountRecommended = 0; + //base mineral need on desired production (build goals) + // for each planet with a space platform + // if it is a class 1 or asteroid belt (planets with the most mineral potential), recommended ore and iridium should be for a battleship + // otherwise recommended ore and iridum should be for a cruiser + // for each planet without a space platform but at least one factory + // recommended ore and iridum should be for a destroyer + // for each planet witout a factory + // recommended ore for a scout only + + for (const planet of ownedPlanetsSorted) { + if (planet.id in player.planetBuildGoals) { + const ppi = player.planetBuildGoals[planet.id]; + oreAmountRecommended += ppi.oreCost; + iridiumAmountRecommended += ppi.iridiumCost; + } //this happens when we have placed our build goal into the queue already + else { + continue; + //add a bit more? + //oreAmountRecommended += 2; + //iridiumAmountRecommended += 1; + } + } + + //further stunt the easy computers growth by over estimating ore and iridium amount recommended + let mineralOverestimation = 1.0; + switch (player.type) { + case PlayerType.Computer_Easy: + mineralOverestimation = Utils.nextRandom(20, 41) / 10.0; + break; + case PlayerType.Computer_Normal: + mineralOverestimation = Utils.nextRandom(10, 21) / 10.0; + break; + } + + let oreAmountNeeded = Math.round(oreAmountRecommended * mineralOverestimation) - totalResources.ore; + let iridiumAmountNeeded = Math.round(iridiumAmountRecommended * mineralOverestimation) - totalResources.iridium; + let foodDiff = 0; + console.debug(player.name, 'Mineral Needs:', oreAmountNeeded, iridiumAmountNeeded); + const foodResourcePotentialComparer = new PlanetResourcePotentialComparer( + planetResourcesPerTurn, + PlanetResourceType.FOOD, + ); + + if (totalPopulation > totalFoodProduction + totalFoodAmountOnPlanets) { + //check to see if we can add farmers to class 1 and class 2 planets + foodDiff = totalPopulation - (totalFoodProduction + totalFoodAmountOnPlanets); + //first try to satiate by retasking miners/workers on planets with less food amount than population + console.debug(player.name, 'potential food shortage:', foodDiff); + + //gather potential planets for adding farmers to + //TODO: this should order by planets with farms as well as planets who's population demands more food than it produces (more potential for growth) + allPlanets.sort((a, b) => foodResourcePotentialComparer.sortFunction(a, b)); + + let neededFarmers = foodDiff; + const planetCandidatesForAddingFarmers: PlanetData[] = []; + + if (neededFarmers > 0) { + for (const p of allPlanets) { + const rpt = planetResourcesPerTurn[p.id]; + const pw = planetPopulationWorkerTypes[p.id]; + if (neededFarmers > 0 && rpt.amountNextWorkerPerTurn.food > 0 && (pw.miners > 0 || pw.builders > 0)) { + planetCandidatesForAddingFarmers.push(p); + neededFarmers -= rpt.amountNextWorkerPerTurn.food; + if (neededFarmers <= 0) break; + } + } + } + + while (foodDiff > 0) { + let changedAssignment = false; + for (const p of planetCandidatesForAddingFarmers) { + const pw = planetPopulationWorkerTypes[p.id]; + let maxMiners = pw.builders; //we don't want more miners than builders when we have food shortages + if (p.type == PlanetType.PlanetClass2) { + if (p.builtImprovements[PlanetImprovementType.Mine] == 0) maxMiners = 0; + else maxMiners = 1; + } + if (pw.miners >= maxMiners && pw.miners > 0) { + planetResourcesPerTurn[p.id] = Planet.updatePopulationWorkerTypesByDiff(p, player, 1, -1, 0); + pw.farmers++; + pw.miners--; + foodDiff -= planetResourcesPerTurn[p.id].amountNextWorkerPerTurn.food; + changedAssignment = true; + } else if (pw.builders > 0) { + planetResourcesPerTurn[p.id] = Planet.updatePopulationWorkerTypesByDiff(p, player, 1, 0, -1); + pw.farmers++; + pw.builders--; + foodDiff -= planetResourcesPerTurn[p.id].amountNextWorkerPerTurn.food; + changedAssignment = true; + } + + if (foodDiff <= 0) break; + } + + //if we got here and didn't change anything, break out + if (!changedAssignment) break; + } //while (foodDiff > 0) + + //if we weren't able to satisfy the population's hunger at this point, + // we may just have to starve + } //we can re-task farmers at class 1 and class 2 planets (and maybe dead planets?) + else { + foodDiff = totalFoodProduction + totalFoodAmountOnPlanets - totalPopulation; + console.debug(player.name, 'potential food surplus:', foodDiff); + + //gather potential planets for removing farmers from + //TODO: this should order by planets without farms and planets which have more food production than it's population demands (less potential for growth) + allPlanets.sort((a, b) => foodResourcePotentialComparer.sortFunction(a, b)); + allPlanets.reverse(); + + let unneededFarmers = foodDiff; + const planetCandidatesForRemovingFarmers: PlanetData[] = []; + if (unneededFarmers > 0) { + for (const p of allPlanets) { + const rpt = planetResourcesPerTurn[p.id]; + const pw = planetPopulationWorkerTypes[p.id]; + if (unneededFarmers > 0 && unneededFarmers > rpt.amountPerWorkerPerTurn.food && pw.farmers > 0) { + planetCandidatesForRemovingFarmers.push(p); + unneededFarmers -= rpt.amountPerWorkerPerTurn.food; + if (unneededFarmers <= 0) break; + } + } + } + + while (foodDiff > 0) { + let changedAssignment = false; + for (const p of planetCandidatesForRemovingFarmers) { + const pw = planetPopulationWorkerTypes[p.id]; + if (foodDiff < planetResourcesPerTurn[p.id].amountPerWorkerPerTurn.food) continue; //if removing this farmer would create a shortage, skip this planet + + //check if we need more minerals, otherwise prefer production + //on terrestrial planets, make sure we have a mine before we add a miner + const addMiner = p.type !== PlanetType.PlanetClass2 || p.builtImprovements[PlanetImprovementType.Mine] > 0; + if (addMiner && (oreAmountNeeded > 0 || iridiumAmountNeeded > 0) && pw.farmers > 0) { + planetResourcesPerTurn[p.id] = Planet.updatePopulationWorkerTypesByDiff(p, player, -1, 1, 0); + pw.farmers--; + pw.miners++; + oreAmountNeeded -= planetResourcesPerTurn[p.id].amountNextWorkerPerTurn.ore; + iridiumAmountNeeded -= planetResourcesPerTurn[p.id].amountNextWorkerPerTurn.iridium; + foodDiff -= planetResourcesPerTurn[p.id].amountPerWorkerPerTurn.food; + changedAssignment = true; + } else if (pw.farmers > 0) { + planetResourcesPerTurn[p.id] = Planet.updatePopulationWorkerTypesByDiff(p, player, -1, 0, 1); + pw.farmers--; + pw.builders++; + foodDiff -= planetResourcesPerTurn[p.id].amountPerWorkerPerTurn.food; + changedAssignment = true; + } + + if (foodDiff <= 0) break; + } + + //if we got here and didn't change anything, break out + if (!changedAssignment) break; + } //while (foodDiff > 0) + } + + const mineralResourceNeeded = + oreAmountNeeded > iridiumAmountNeeded ? PlanetResourceType.ORE : PlanetResourceType.IRIDIUM; + const mineralResourcePotentialComparer = new PlanetResourcePotentialComparer( + planetResourcesPerTurn, + mineralResourceNeeded, + ); + let oreAmountNeededWorking = oreAmountNeeded * 1.0; + let iridiumAmountNeededWorking = iridiumAmountNeeded * 1.0; + //next see if we need miners, look for workers to reassign (don't reassign farmers at this point) + if (oreAmountNeeded > 0 || iridiumAmountNeeded > 0) { + const planetCandidatesForRemovingWorkers = []; //List + + allPlanets.sort((a, b) => mineralResourcePotentialComparer.sortFunction(a, b)); + + for (const p of allPlanets) { + if (oreAmountNeededWorking < 0 && iridiumAmountNeededWorking < 0) { + break; + } + const rpt = planetResourcesPerTurn[p.id]; + const pw = planetPopulationWorkerTypes[p.id]; + //leave at least one worker on terrestrial planets, leave 2 workers if we don't have a mine yet + let minBuilders = 0; + let minFarmers = -1; + if (p.type == PlanetType.PlanetClass2) { + minBuilders = p.builtImprovements[PlanetImprovementType.Mine] == 0 ? 2 : 1; + minFarmers = 0; //also make sure we have one farmer before reassigning a worker to be miner + } + + if (pw.builders > minBuilders && pw.farmers > minFarmers) { + planetCandidatesForRemovingWorkers.push(p); + oreAmountNeededWorking -= rpt.amountNextWorkerPerTurn.ore; + iridiumAmountNeededWorking -= rpt.amountNextWorkerPerTurn.iridium; + } + } + + while (oreAmountNeeded > 0 || iridiumAmountNeeded > 0) { + let changedAssignment = false; + for (const p of planetCandidatesForRemovingWorkers) { + const rpt = planetResourcesPerTurn[p.id]; + const pw = planetPopulationWorkerTypes[p.id]; + //double check we have enough workers still + let minBuilders = 1; + if (p.builtImprovements[PlanetImprovementType.Mine] == 0) minBuilders = 2; + + if (pw.builders > minBuilders) { + planetResourcesPerTurn[p.id] = Planet.updatePopulationWorkerTypesByDiff(p, player, 0, 1, -1); + pw.miners++; + pw.builders--; + oreAmountNeeded -= rpt.amountNextWorkerPerTurn.ore; + iridiumAmountNeeded -= rpt.amountNextWorkerPerTurn.iridium; + changedAssignment = true; + } + + if (oreAmountNeeded <= 0 && iridiumAmountNeeded <= 0) break; + } + + //if we got here and didn't change anything, break out + if (!changedAssignment) break; + } //while (oreAmountNeeded > 0 || iridiumAmountNeeded > 0) + } else { + //we have enough minerals, reassign miners to workers + + const planetCandidatesForRemovingMiners = []; //List + + allPlanets.sort((a, b) => mineralResourcePotentialComparer.sortFunction(a, b)); + allPlanets.reverse(); + + for (const p of allPlanets) { + if (oreAmountNeededWorking > 0 || iridiumAmountNeededWorking > 0) { + break; + } + const rpt = planetResourcesPerTurn[p.id]; + const pw = planetPopulationWorkerTypes[p.id]; + if (pw.miners > 0) { + planetCandidatesForRemovingMiners.push(p); + oreAmountNeededWorking += rpt.amountPerWorkerPerTurn.ore; + iridiumAmountNeededWorking += rpt.amountPerWorkerPerTurn.iridium; + } + } + + while (oreAmountNeeded < 0 && iridiumAmountNeeded < 0) { + let changedAssignment = false; + for (const p of planetCandidatesForRemovingMiners) { + const rpt = planetResourcesPerTurn[p.id]; + const pw = planetPopulationWorkerTypes[p.id]; + //double check we still have miners and that we don't over compensate + if ( + pw.miners > 0 && + oreAmountNeeded + rpt.amountPerWorkerPerTurn.ore < 0 && + iridiumAmountNeeded + rpt.amountPerWorkerPerTurn.iridium < 0 + ) { + planetResourcesPerTurn[p.id] = Planet.updatePopulationWorkerTypesByDiff(p, player, 0, -1, 1); + pw.miners--; + pw.builders++; + oreAmountNeeded += rpt.amountPerWorkerPerTurn.ore; + iridiumAmountNeeded += rpt.amountPerWorkerPerTurn.iridium; + changedAssignment = true; + } + + if (oreAmountNeeded > 0 || iridiumAmountNeeded > 0) break; + } + + //if we got here and didn't change anything, break out + if (!changedAssignment) break; + } //while (oreAmountNeeded < 0 || iridiumAmountNeeded < 0) + } + } + + public static computerSetPlanetBuildGoals( + gameModel: GameModelData, + player: PlayerData, + ownedPlanets: PlanetById, + ownedPlanetsSorted: PlanetData[], + ) { + //first look for planets that need build goals set, either for ships or for improvements + + const planetCandidatesForNeedingImprovements: PlanetData[] = []; + const planetCandidatesForNeedingSpacePlatforms: PlanetData[] = []; + const planetCandidatesForNeedingShips: PlanetData[] = []; + + const planetCountNeedingExploration = this.countPlanetsNeedingExploration(gameModel, player, ownedPlanets); + + for (const p of ownedPlanetsSorted) { + //if this planet doesn't already have a build goal in player.planetBuildGoals + if (!(p.id in player.planetBuildGoals)) { + if (p.buildQueue.length) console.debug(player.name, 'build queue on:', p.name, p.buildQueue[0]); + if (p.buildQueue.length <= 1) { + const canBuildSpacePlatform = + Planet.getSpacePlatformCount(p, true) < Research.getMaxSpacePlatformCount(player.research) && + p.builtImprovements[PlanetImprovementType.Factory] > 0; + //even if we have something in queue we might want to set a goal to save up resources? + + //always check for improvements in case we need to destroy some + planetCandidatesForNeedingImprovements.push(p); + if (ownedPlanetsSorted.length > 1) { + if (canBuildSpacePlatform) { + planetCandidatesForNeedingSpacePlatforms.push(p); + } else { + planetCandidatesForNeedingShips.push(p); + } + } else { + if (planetCountNeedingExploration != 0) { + //if we need to explore some planets before building a space platform, do so + planetCandidatesForNeedingShips.push(p); + } else if (canBuildSpacePlatform) { + planetCandidatesForNeedingSpacePlatforms.push(p); + } else { + planetCandidatesForNeedingShips.push(p); + } + } + } + } + } + + //space platforms + for (const p of planetCandidatesForNeedingSpacePlatforms) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.SpacePlatform); + } + + const origRecommendedMines = 2; + //build improvements + for (const p of planetCandidatesForNeedingImprovements) { + //planet class 2 should have 3 farms and 2 mines + //planet class 1 should have 2 farms and 0 mines + //dead planets should have 0 farms and 1 mine + //asteroids should have 0 farms and 1 mine + //otherwise build 1 factory if none exist + //otherwise build 1 colony if none exist + //otherwise build factories to recommended amount + //otherwise build a spaceport space platform is none exist + //otherwise colonies till we're filled up + + let recommendedFarms = 0; + let recommendedMines = 0; + let recommendedFactories = 1; + const recommendedColonies = 1; + + const farmCount = Planet.builtAndBuildQueueImprovementTypeCount(p, PlanetImprovementType.Farm); + const mineCount = Planet.builtAndBuildQueueImprovementTypeCount(p, PlanetImprovementType.Mine); + const factoryCount = Planet.builtAndBuildQueueImprovementTypeCount(p, PlanetImprovementType.Factory); + const colonyCount = Planet.builtAndBuildQueueImprovementTypeCount(p, PlanetImprovementType.Colony); + + //NOTE: we aren't checking gold for the purposes of farms, we'll just build them + if (p.type == PlanetType.PlanetClass2) { + if (ownedPlanetsSorted.length == 1) { + //until we have another planet we need to build some mines to get resources + recommendedFarms = 3; + recommendedMines = origRecommendedMines; + } else { + recommendedFarms = 4; + recommendedMines = 0; + } + } else if (p.type == PlanetType.PlanetClass1) { + recommendedFarms = 2; + } else if (p.type == PlanetType.DeadPlanet) { + recommendedMines = 1; + } else if (p.type == PlanetType.AsteroidBelt) { + recommendedMines = 2; + } + recommendedFactories = p.maxImprovements - recommendedMines - recommendedFarms - recommendedColonies; + + //make sure farms are built before mines + if (farmCount < recommendedFarms) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Farm); + } else if (mineCount < recommendedMines) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Mine); + } else if (factoryCount == 0 && recommendedFactories > 0) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Factory); + } else if (colonyCount < recommendedColonies) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Colony); + } else if (factoryCount < recommendedFactories) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Factory); + } else if (farmCount > recommendedFarms) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovementToDestroy( + PlanetImprovementType.Farm, + ); + } else if (mineCount > recommendedMines) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovementToDestroy( + PlanetImprovementType.Mine, + ); + } else if (factoryCount > recommendedFactories) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovementToDestroy( + PlanetImprovementType.Factory, + ); + } else if (colonyCount > recommendedColonies) { + player.planetBuildGoals[p.id] = PlanetProductionItem.constructPlanetImprovementToDestroy( + PlanetImprovementType.Colony, + ); + } + if (player.planetBuildGoals[p.id]) { + console.debug(player.name, 'Planet:', p.name, 'Improvement Build Goal:', player.planetBuildGoals[p.id]); + } + + //after all that we should be ready to set fleet goals + } + + //build ships + for (const p of planetCandidatesForNeedingShips) { + const mineCount = Planet.builtAndBuildQueueImprovementTypeCount(p, PlanetImprovementType.Mine); + if ( + player.planetBuildGoals[p.id] && + (ownedPlanetsSorted.length > 3 || ownedPlanetsSorted.length == 1) && + mineCount != origRecommendedMines + ) { + //do this for now so that the computer builds improvements before too much scouting, however might want to revisit this so that there is some scouting done before all buildings are built + continue; + } + //defenders and destroyers will be built at random for the easier computers + let buildDefenders = false; + let buildDestroyers = false; + if (player.type == PlayerType.Computer_Easy) { + //50% chance to build defenders, 50% chance for destroyers + buildDefenders = Utils.nextRandom(0, 4) <= 1; + buildDestroyers = !buildDefenders && Utils.nextRandom(0, 4) <= 1; + } else if (player.type == PlayerType.Computer_Normal) { + //25% chance to build defenders, 25% chance for destroyers + buildDefenders = Utils.nextRandom(0, 4) == 0; + buildDestroyers = !buildDefenders && Utils.nextRandom(0, 4) == 0; + } + + if (Planet.getSpacePlatformCount(p, false) > 0 && !buildDefenders) { + const rand = Utils.nextRandom(4); + //build battleships at half the planets with spaceplatforms + if (rand < 2) { + if (rand % 2 == 0) + //1/4 the time we build battleships 1/2 time build destroyers + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.Battleship); + else + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.Destroyer); + } else { + if (rand % 2 == 1) + //1/4 the time we build cruisers 1/2 time build destroyers + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.Cruiser); + else + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.Destroyer); + } + } else if (planetCountNeedingExploration != 0) { + //if there are unexplored planets still, build some scouts + console.debug(player.name, planetCountNeedingExploration, 'Planets needing exploration, building scouts'); + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.Scout); + } else if (p.builtImprovements[PlanetImprovementType.Factory] > 0 && buildDestroyers) { + //NOTE: this actually never gets hit because right now we're always building scouts, then spaceplatforms, then above applies + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.Destroyer); + } else if (gameModel.modelData.currentCycle % 4 == 0 && buildDefenders) { + //else create defender (but only sometimes so we save gold) + player.planetBuildGoals[p.id] = PlanetProductionItem.constructStarShipInProduction(StarShipType.SystemDefense); + } + if (player.planetBuildGoals[p.id]) { + console.debug(player.name, 'Planet:', p.name, 'StarShip Build Goal:', player.planetBuildGoals[p.id]); + } + } + } + + public static computerSubmitTrades( + gameModel: GameModelData, + player: PlayerData, + ownedPlanets: PlanetById, + ownedPlanetsSorted: PlanetData[], + ) { + //first decide if we want to trade based on resource prices and needed resources (based on planet build goals) + const totalPopulation = Player.getTotalPopulation(player, ownedPlanets); + const totalResources = Player.getTotalResourceAmount(player, ownedPlanets); + + let energyDesired = 0; + let oreDesired = 0; + let iridiumDesired = 0; + for (const ppi of Object.values(player.planetBuildGoals)) { + energyDesired += ppi.energyCost; + oreDesired += ppi.oreCost; + iridiumDesired += ppi.iridiumCost; + } + const purchaseMultiplier = 0.25; + const tradesToExecute = []; + const planetId = player.homePlanetId ? player.homePlanetId : ownedPlanetsSorted[0].id; + let amount = 0; + if (totalResources.energy < energyDesired) { + //try to sell resources + //only sell resources when you have far more than you need + if (totalResources.food >= totalPopulation * 4) { + //sell some food + amount = Math.floor(totalResources.food * purchaseMultiplier); + tradesToExecute.push( + TradingCenter.constructTrade(player.id, planetId, TradeType.SELL, TradingCenterResourceType.FOOD, amount), + ); + } + if (totalResources.ore >= oreDesired * 2) { + amount = Math.floor(totalResources.ore * purchaseMultiplier); + tradesToExecute.push( + TradingCenter.constructTrade(player.id, planetId, TradeType.SELL, TradingCenterResourceType.ORE, amount), + ); + } + + if (totalResources.iridium >= iridiumDesired * 2) { + amount = Math.floor(totalResources.iridium * purchaseMultiplier); + tradesToExecute.push( + TradingCenter.constructTrade(player.id, planetId, TradeType.SELL, TradingCenterResourceType.IRIDIUM, amount), + ); + } + } else if (totalResources.energy > energyDesired * 1.2) { + //try to buy resources + if (totalResources.food <= totalPopulation * 1.2) { + //buy some food + const amount = Math.floor(totalPopulation * purchaseMultiplier); + tradesToExecute.push( + TradingCenter.constructTrade(player.id, planetId, TradeType.BUY, TradingCenterResourceType.FOOD, amount), + ); + } + + if (totalResources.ore <= oreDesired * 1.2) { + const amount = Math.floor(oreDesired * purchaseMultiplier); + tradesToExecute.push( + TradingCenter.constructTrade(player.id, planetId, TradeType.BUY, TradingCenterResourceType.ORE, amount), + ); + } + if (totalResources.iridium <= iridiumDesired * 1.2) { + const amount = Math.floor(iridiumDesired * purchaseMultiplier); + tradesToExecute.push( + TradingCenter.constructTrade(player.id, planetId, TradeType.BUY, TradingCenterResourceType.IRIDIUM, amount), + ); + } + } + + for (const trade of tradesToExecute) { + if (trade.amount > 0) { + console.debug(player.name, 'Submitted a Trade: ', trade); + gameModel.modelData.tradingCenter.currentTrades.push(trade); + } else { + console.debug(player.name, 'Trade found with zero amount.', trade); + } + } + } + + public static computerBuildImprovementsAndShips( + gameModel: GameModelData, + player: PlayerData, + ownedPlanets: PlanetById, + ownedPlanetsSorted: PlanetData[], + ) { + const totalResources = Player.getTotalResourceAmount(player, ownedPlanets); + //determine energy surplus needed to ship food + let energySurplus = player.lastTurnFoodNeededToBeShipped; + + //increase recommended energySurplus based on computer difficulty to further make the easier computers a bit less agressive + switch (player.type) { + case PlayerType.Computer_Easy: + energySurplus = Utils.nextRandom(0, (energySurplus + 1) / 4); //this should make the easy computer even easier, because sometimes he should starve himself + break; + case PlayerType.Computer_Normal: + energySurplus = Utils.nextRandom(0, (energySurplus + 1) / 2); //this should make the normal computer easier, because sometimes he should starve himself + break; + case PlayerType.Computer_Hard: + energySurplus += (ownedPlanetsSorted.length - 1) / 2; + break; + case PlayerType.Computer_Expert: + energySurplus += (ownedPlanetsSorted.length - 1) / 4; + break; + } + + //build improvements and ships based on build goals + for (const p of ownedPlanetsSorted) { + if (p.buildQueue.length == 0) { + if (p.id in player.planetBuildGoals) { + const ppi = player.planetBuildGoals[p.id]; + //check resources + if ( + totalResources.energy - ppi.energyCost > energySurplus && + totalResources.ore - ppi.oreCost >= 0 && + totalResources.iridium - ppi.iridiumCost >= 0 + ) { + Planet.enqueueProductionItemAndSpendResources(gameModel.grid, player, ownedPlanets, p, ppi); + delete player.planetBuildGoals[p.id]; + } + } //could this be a problem? + else { + continue; + } + } + } + } + + public static computerSendShips( + gameModel: GameModelData, + player: PlayerData, + ownedPlanets: PlanetById, + ownedPlanetsSorted: PlanetData[], + ) { + //easy computer sends ships to closest planet at random + //normal computers keep detachments of ships as defence as deemed necessary based on scouted enemy forces and planet value + //hard computers also prefer planets based on class, location, and fleet defence + //expert computers also amass fleets at strategic planets, + //when two planets have ship building capabilities (i.e. have at least one factory), + //and a 3rd desired planet is unowned, the further of the two owned planets sends it's ships to the closer as reinforcements + + //all but easy computers will also re-scout enemy planets after a time to re-establish intelligence + + const planetCandidatesForSendingShips = []; + for (const p of ownedPlanetsSorted) { + if (Fleet.countMobileStarships(p.planetaryFleet) > 0) { + if (player.type == PlayerType.Computer_Easy) { + //easy computers can send ships as long as there is somthing to send + planetCandidatesForSendingShips.push(p); + } else { + let strengthToDefend = 0; + + if (this.countPlanetsNeedingExploration(gameModel, player, ownedPlanets) != 0) { + //this is done because of how the goals are set right now, + //we don't want the computer defending with all of it's ships when there is exploring to be done + strengthToDefend = 0; + } else if (p.builtImprovements[PlanetImprovementType.Factory] > 0) { + //if we can build ships it is probably later in the game and we should start defending this planet + strengthToDefend = Math.floor(Math.pow(p.type, 2) * 4); //defense based on planet type + } + + if (player.type == PlayerType.Computer_Hard || player.type == PlayerType.Computer_Expert) { + //base defense upon enemy fleet strength within a certain range of last known planets + // as well as if there are ships in queue and estimated time till production + + //TODO: we should get all enemy planets within a certain range instead of just the closest one + const closestUnownedPlanetResults = this.getClosestUnownedPlanet(gameModel, ownedPlanets, p); + if (closestUnownedPlanetResults.planet) { + if (closestUnownedPlanetResults.planet.id in player.lastKnownPlanetFleetStrength) { + strengthToDefend += Fleet.determineFleetStrength( + player.lastKnownPlanetFleetStrength[closestUnownedPlanetResults.planet.id].fleetData, + true, + ); + } else if (closestUnownedPlanetResults.planet.id in player.knownPlanetIds) { + strengthToDefend += Math.floor(Math.pow(closestUnownedPlanetResults.planet.type, 2) * 4); + } + } + + const turnsToCompleteStarship = Planet.buildQueueContainsMobileStarship(p); + if (turnsToCompleteStarship) { + if (turnsToCompleteStarship.turnsToComplete <= closestUnownedPlanetResults.minDistance + 1) { + //if we can build this before an enemy can get here + strengthToDefend -= turnsToCompleteStarship.starshipStrength; + } + } + } + + if (Fleet.determineFleetStrength(p.planetaryFleet) > strengthToDefend) { + planetCandidatesForSendingShips.push(p); //TODO: for some computer levels below we should also leave a defending detachment based on strength to defend, etc... + } + } + } + } + + const planetCandidatesForInboundScouts = []; + const planetCandidatesForInboundAttackingFleets = []; + if (planetCandidatesForSendingShips.length > 0) { + for (const p of gameModel.modelData.planets) { + if (!(p.id in ownedPlanets) && !Player.planetContainsFriendlyInboundFleet(player, p)) { + //exploring/attacking inbound fleets to unowned planets should be excluded + if (this.planetNeedsExploration(p, gameModel, player, ownedPlanets)) { + planetCandidatesForInboundScouts.push(p); + } else { + //TODO: we might still want to gather fleets strategically + planetCandidatesForInboundAttackingFleets.push(p); + } + } + } + } + + //computer should send one available ship to unexplored planets (TODO: later build scouts/destroyers as appropriate for this) + //computer should gather fleets strategically at fronts close to unowned planets (TODO: later base this on last known force strength) + // + //new send ship logic: + // for each planet that can send ships + // get list of the closest unowned planets + // if it is unexplored (and we don't already have an inbound fleet), send a one ship detachment + // if it is explored and if we have more strength than the last known strength on planet (and we don't already have an inbound fleet), send a detachment + + //first sort planet candidates for inbound fleets by closest to home planet + if (player.homePlanetId && player.homePlanetId in ownedPlanets) { + //just to make sure + const homePlanet = ownedPlanets[player.homePlanetId]; + if (player.type == PlayerType.Computer_Easy || player.type == PlayerType.Computer_Normal) { + const planetDistanceComparer = new PlanetDistanceComparer(gameModel.grid, homePlanet); + planetCandidatesForInboundAttackingFleets.sort((a, b) => planetDistanceComparer.sortFunction(a, b)); + planetCandidatesForInboundScouts.sort((a, b) => planetDistanceComparer.sortFunction(a, b)); + } else { + //hard and expert computer will sort with a bit of complexly (based on value and last known strength as well as distance) + const planetValueDistanceStrengthComparer = new PlanetDistanceComparer( + gameModel.grid, + homePlanet, + player.lastKnownPlanetFleetStrength, + ); + planetCandidatesForInboundAttackingFleets.sort((a, b) => + planetValueDistanceStrengthComparer.sortFunction(a, b), + ); + planetCandidatesForInboundScouts.sort((a, b) => planetValueDistanceStrengthComparer.sortFunction(a, b)); + } + } + + const planetCandidatesForInboundReinforcements = []; + if (player.type == PlayerType.Computer_Expert) { + for (const p of ownedPlanetsSorted) { + if (p.builtImprovements[PlanetImprovementType.Factory] > 0) { + planetCandidatesForInboundReinforcements.push(p); + } + } + } + + console.debug( + player.name, + 'planetCandidatesForSendingShips:', + planetCandidatesForSendingShips.length, + 'planetCandidatesForInboundScouts:', + planetCandidatesForInboundScouts.length, + 'planetCandidatesForInboundAttackingFleets:', + planetCandidatesForInboundAttackingFleets.length, + 'planetCandidatesForInboundReinforcements:', + planetCandidatesForInboundReinforcements.length, + ); + + if (planetCandidatesForSendingShips.length > 0) { + for (let i = planetCandidatesForInboundScouts.length - 1; i >= 0; i--) { + const pEnemyInbound = planetCandidatesForInboundScouts[i]; + + if (player.type == PlayerType.Computer_Easy || player.type == PlayerType.Computer_Normal) { + const planetDistanceComparer = new PlanetDistanceComparer(gameModel.grid, pEnemyInbound); + planetCandidatesForSendingShips.sort((a, b) => planetDistanceComparer.sortFunction(a, b)); + } else { + // harder computers should start with planets with more ships and/or reinforce closer planets from further planets with more ships + const planetValueDistanceStrengthComparer = new PlanetDistanceComparer( + gameModel.grid, + pEnemyInbound, + player.lastKnownPlanetFleetStrength, + ); + planetCandidatesForSendingShips.sort((a, b) => planetValueDistanceStrengthComparer.sortFunction(a, b)); + //because the PlanetValueDistanceStrengthComparer prefers weakest planets, we want the opposite in this case + //so we want to prefer sending from asteroid belts with high strength value + planetCandidatesForSendingShips.reverse(); + } + + for (let j = planetCandidatesForSendingShips.length - 1; j >= 0; j--) { + const pFriendly = planetCandidatesForSendingShips[j]; + + //send smallest detachment possible + const inboundPlanet = planetCandidatesForInboundScouts[i]; + const newFleet = Fleet.splitOffSmallestPossibleFleet(pFriendly.planetaryFleet); + //if we do this right newFleet should never be null + if (newFleet) { + Fleet.setDestination( + newFleet, + gameModel.grid, + pFriendly.boundingHexMidPoint, + inboundPlanet.boundingHexMidPoint, + ); + + pFriendly.outgoingFleets.push(newFleet); + this.onComputerSentFleet(newFleet); + + const mobileStarshipsLeft = Fleet.countMobileStarships(pFriendly.planetaryFleet); + if (mobileStarshipsLeft == 0) { + planetCandidatesForSendingShips.splice(j, 1); + } + + break; + } else { + console.error('splitOffSmallestPossibleFleet returned no newFleet!'); + } + } + + if (planetCandidatesForSendingShips.length == 0) break; + } + } + + //next for each candidate for inbound attacking fleets, sort the candidates for sending ships by closest first + + //look for closest planet to attack first + for (let i = planetCandidatesForInboundAttackingFleets.length - 1; i >= 0; i--) { + const pEnemyInbound = planetCandidatesForInboundAttackingFleets[i]; + + if (player.type == PlayerType.Computer_Easy || player.type == PlayerType.Computer_Normal) { + const planetDistanceComparer = new PlanetDistanceComparer(gameModel.grid, pEnemyInbound); + planetCandidatesForSendingShips.sort((a, b) => planetDistanceComparer.sortFunction(a, b)); + } // harder computers should start with planets with more ships and/or reinforce closer planets from further planets with more ships + else { + const planetValueDistanceStrengthComparer = new PlanetDistanceComparer( + gameModel.grid, + pEnemyInbound, + player.lastKnownPlanetFleetStrength, + ); + planetCandidatesForSendingShips.sort((a, b) => planetValueDistanceStrengthComparer.sortFunction(a, b)); + //because the PlanetValueDistanceStrengthComparer prefers weakest planets, we want the opposite in this case + //so we want to prefer sending from asteroid belts with high strength value + planetCandidatesForSendingShips.reverse(); + } + + //in order to slow the agression of the easier computers we want to only attack when we have a multiple of the enemy fleet + let additionalStrengthMultiplierNeededToAttackLow = 0.5; + let additionalStrengthMultiplierNeededToAttackHigh = 1.0; + switch (player.type) { + case PlayerType.Computer_Easy: + additionalStrengthMultiplierNeededToAttackLow = 3.0; + additionalStrengthMultiplierNeededToAttackHigh = 6.0; + break; + case PlayerType.Computer_Normal: + additionalStrengthMultiplierNeededToAttackLow = 2.0; + additionalStrengthMultiplierNeededToAttackHigh = 4.0; + break; + case PlayerType.Computer_Hard: + additionalStrengthMultiplierNeededToAttackLow = 1.0; + additionalStrengthMultiplierNeededToAttackHigh = 2.0; + break; + } + + const additionalStrengthMultiplierNeededToAttack = + Utils.nextRandom( + Math.floor(additionalStrengthMultiplierNeededToAttackLow * 10), + Math.floor(additionalStrengthMultiplierNeededToAttackHigh * 10) + 1, + ) / 10.0; + + let fleetSent = false; + for (let j = planetCandidatesForSendingShips.length - 1; j >= 0; j--) { + const pFriendly = planetCandidatesForSendingShips[j]; //Planet + + //send attacking fleet + + //rely only on our last known-information + let fleetStrength = Math.floor(Math.pow(pEnemyInbound.type + 1, 2) * 4); //estimate required strength based on planet type + const lkpfs = player.lastKnownPlanetFleetStrength[pEnemyInbound.id]; + if (lkpfs) { + fleetStrength = Fleet.determineFleetStrength(lkpfs.fleetData); + } + + const starshipCounts = Fleet.countStarshipsByType(pFriendly.planetaryFleet); + + //TODO: for some computer levels below we should also leave a defending detachment based on strength to defend, etc... + + //generate this fleet just to ensure strength > destination fleet strength + let newFleet = Fleet.generateFleetWithShipCount( + 0, + starshipCounts.scouts, + starshipCounts.destroyers, + starshipCounts.cruisers, + starshipCounts.battleships, + 0, + pFriendly.boundingHexMidPoint, + ); + if (Fleet.determineFleetStrength(newFleet) > fleetStrength * additionalStrengthMultiplierNeededToAttack) { + newFleet = Fleet.splitFleet( + pFriendly.planetaryFleet, + starshipCounts.scouts, + starshipCounts.destroyers, + starshipCounts.cruisers, + starshipCounts.battleships, + ); + + Fleet.setDestination( + newFleet, + gameModel.grid, + pFriendly.boundingHexMidPoint, + pEnemyInbound.boundingHexMidPoint, + ); + + pFriendly.outgoingFleets.push(newFleet); + + this.onComputerSentFleet(newFleet); + + const mobileStarshipsLeft = Fleet.countMobileStarships(pFriendly.planetaryFleet); + if (mobileStarshipsLeft == 0) { + planetCandidatesForSendingShips.splice(j, 1); + } + + fleetSent = true; + break; + } + } + + if (!fleetSent && planetCandidatesForInboundReinforcements.length > 0) { + //here is where we reinforce close planets for expert computers + + //logic: + // find closest planet capable of building better ships (has at least one factory) to enemy planet + // send a detachment from each planetCandidatesForSendingShips other than closest ship builder to reinforce and amass for later + const planetDistanceComparer = new PlanetDistanceComparer(gameModel.grid, pEnemyInbound); + planetCandidatesForInboundReinforcements.sort((a, b) => planetDistanceComparer.sortFunction(a, b)); + const planetToReinforce = + planetCandidatesForInboundReinforcements[planetCandidatesForInboundReinforcements.length - 1]; + const distanceFromPlanetToReinforceToEnemy = Grid.getHexDistanceForMidPoints( + gameModel.grid, + pEnemyInbound.boundingHexMidPoint, + planetToReinforce.boundingHexMidPoint, + ); + + for (let r = planetCandidatesForSendingShips.length - 1; r >= 0; r--) { + const pFriendly = planetCandidatesForSendingShips[r]; + + if (pFriendly.id == planetToReinforce.id) + //don't reinforce ourselves + break; + + //also make sure the friendly planet is further from our target than the planet to reinforce + if ( + Grid.getHexDistanceForMidPoints( + gameModel.grid, + pEnemyInbound.boundingHexMidPoint, + pFriendly.boundingHexMidPoint, + ) < distanceFromPlanetToReinforceToEnemy + ) + break; + + const starshipCounts = Fleet.countStarshipsByType(pFriendly.planetaryFleet); + + //TODO: for some computer levels below we should also leave a defending detachment based on strength to defend, etc... + + //const newFleet = StarShipFactoryHelper.GenerateFleetWithShipCount(player, 0, scouts, destroyers, cruisers, battleships, pFriendly.BoundingHex);//Fleet + + const newFleet = Fleet.splitFleet( + pFriendly.planetaryFleet, + starshipCounts.scouts, + starshipCounts.destroyers, + starshipCounts.cruisers, + starshipCounts.battleships, + ); + + Fleet.setDestination( + newFleet, + gameModel.grid, + pFriendly.boundingHexMidPoint, + planetToReinforce.boundingHexMidPoint, + ); + + pFriendly.outgoingFleets.push(newFleet); + + this.onComputerSentFleet(newFleet); + + const mobileStarshipsLeft = Fleet.countMobileStarships(pFriendly.planetaryFleet); + if (mobileStarshipsLeft == 0) { + planetCandidatesForSendingShips.splice(r, 1); + } + + fleetSent = true; + break; + } + } + + if (planetCandidatesForSendingShips.length == 0) break; + } //end planetCandidatesForInboundAttackingFleets loop + } + + public static countPlanetsNeedingExploration(gameModel: GameModelData, player: PlayerData, ownedPlanets: PlanetById) { + let planetsNeedingExploration = 0; + for (const p of gameModel.modelData.planets) { + if (!(p.id in ownedPlanets) && !Player.planetContainsFriendlyInboundFleet(player, p)) { + //exploring/attacking inbound fleets to unowned planets should be excluded + if (this.planetNeedsExploration(p, gameModel, player, ownedPlanets)) { + planetsNeedingExploration++; + } + } + } + + return planetsNeedingExploration; + } + + /** + * returns true if it has been enough turns since this planet was explored + */ + public static planetNeedsExploration( + planet: PlanetData, + gameModel: GameModelData, + player: PlayerData, + ownedPlanets: PlanetById, + ) { + if (!player.knownPlanetIds.includes(planet.id)) { + return true; + } else if (player.type === PlayerType.Computer_Easy) { + return false; //easy computers never update intelligence by scouting + } + + const turnsSinceLastExplored = + planet.id in player.lastKnownPlanetFleetStrength + ? gameModel.modelData.currentCycle - player.lastKnownPlanetFleetStrength[planet.id].cycleLastExplored + : 0; + //the more planets and larger the galaxy, the longer the time till new intelligence is needed + const turnLastExploredCutoff = + (gameModel.modelData.planets.length / 2) * gameModel.modelData.gameOptions.galaxySize; //range: 4 - 48 + if (turnsSinceLastExplored > turnLastExploredCutoff) { + //this is just a quick short circuit + return true; + } else { + //get average distance of planet to other owned planets, if distance below threshold, treat as needs exploration + const distanceCutoff = turnLastExploredCutoff / 2; + let totalDistance = player.ownedPlanetIds.length || 1; + for (const ownedPlanet of Object.values(ownedPlanets)) { + totalDistance += Grid.getHexDistanceForMidPoints( + gameModel.grid, + planet.boundingHexMidPoint, + ownedPlanet.boundingHexMidPoint, + ); + } + const averageDistance = totalDistance / (player.ownedPlanetIds.length || 1); + if (averageDistance <= distanceCutoff && turnsSinceLastExplored > averageDistance) { + return true; + } + } + return false; + } + + public static getClosestUnownedPlanet( + gameModel: GameModelData, + ownedPlanets: PlanetById, + ownedPlanet: PlanetData, + ): { minDistance: number; planet: PlanetData | null } { + const returnVal: { minDistance: number; planet: PlanetData | null } = { minDistance: 999, planet: null }; + + for (const p of gameModel.modelData.planets) { + if (!(p.id in ownedPlanets)) { + const distance = Grid.getHexDistanceForMidPoints( + gameModel.grid, + p.boundingHexMidPoint, + ownedPlanet.boundingHexMidPoint, + ); + if (distance < returnVal.minDistance) { + returnVal.minDistance = distance; + returnVal.planet = p; + } + } + } + + return returnVal; + } +} diff --git a/packages/astriarch-engine/src/engine/events.ts b/packages/astriarch-engine/src/engine/events.ts new file mode 100644 index 00000000..f11264df --- /dev/null +++ b/packages/astriarch-engine/src/engine/events.ts @@ -0,0 +1,81 @@ +import { ClientPlayer } from '../model/clientModel'; +import { EventNotification, EventNotificationType, PlanetaryConflictData } from '../model/eventNotification'; +import { FleetData } from '../model/fleet'; +import { PlanetData } from '../model/planet'; +import { Fleet } from './fleet'; +import { PlanetResources } from './planetResources'; + +export type Subscription = (playerId: string, enList: EventNotification[]) => void; + +export class Events { + private static subscribers: Record = {}; // key is player id + + private static eventNotificationQueue: Record = {}; + + public static constructEvent( + playerId: string, + type: EventNotificationType, + message: string, + planet?: PlanetData, + data?: PlanetaryConflictData, + ): EventNotification { + return { + playerId, + type, + message, + planet, + data, + }; + } + + public static enqueueNewEvent( + playerId: string, + type: EventNotificationType, + message: string, + planet?: PlanetData, + data?: PlanetaryConflictData, + ): EventNotification { + return Events.enqueueEvent(Events.constructEvent(playerId, type, message, planet, data)); + } + + public static enqueueEvent(en: EventNotification): EventNotification { + if (!(en.playerId in Events.eventNotificationQueue)) { + Events.eventNotificationQueue[en.playerId] = []; + } + Events.eventNotificationQueue[en.playerId].push(en); + return en; + } + + public static constructPlanetaryConflictData( + defendingClientPlayer: ClientPlayer | null, + defendingFleet: FleetData, + attackingClientPlayer: ClientPlayer, + attackingFleet: FleetData, + ): PlanetaryConflictData { + return { + defendingClientPlayer, + defendingFleet: Fleet.cloneFleet(defendingFleet), + defendingFleetResearchBoost: { attack: 0, defense: 0 }, + attackingClientPlayer, + attackingFleet: Fleet.cloneFleet(attackingFleet), + attackingFleetResearchBoost: { attack: 0, defense: 0 }, + attackingFleetChances: null, + winningFleet: null, + resourcesLooted: PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 0), + }; + } + + public static publish() { + for (const [key, value] of Object.entries(Events.eventNotificationQueue)) { + if (key in Events.subscribers) { + Events.subscribers[key](key, value); + } + // clear out the queue for this player + Events.eventNotificationQueue[key] = []; + } + } + + public static subscribe(playerId: string, callback: Subscription) { + Events.subscribers[playerId] = callback; + } +} diff --git a/packages/astriarch-engine/src/engine/fleet.spec.ts b/packages/astriarch-engine/src/engine/fleet.spec.ts new file mode 100644 index 00000000..352c542e --- /dev/null +++ b/packages/astriarch-engine/src/engine/fleet.spec.ts @@ -0,0 +1,50 @@ +import { PlanetById } from '../model/clientModel'; +import { StarShipType } from '../model/fleet'; +import { PlayerData } from '../model/player'; +import { startNewTestGame, TestGameData } from '../test/testUtils'; +import { ClientGameModel } from './clientGameModel'; +import { Fleet } from './fleet'; + +let testGameData: TestGameData; +let player1: PlayerData; +let player2: PlayerData; +let planetById: PlanetById; + +describe('Fleet', function () { + beforeEach(() => { + testGameData = startNewTestGame(); + player1 = testGameData.gameModel.modelData.players[0]; + player2 = testGameData.gameModel.modelData.players[1]; + planetById = ClientGameModel.getPlanetByIdIndex(testGameData.gameModel.modelData.planets); + }); + + describe('strengthBoostFromLevel()', () => { + it('should not give a boost for level 0', () => { + const ss1 = Fleet.generateStarship(StarShipType.Battleship); + const baseStrength = Fleet.getStarshipTypeBaseStrength(ss1.type); + const boostFromLevel = Fleet.strengthBoostFromLevel(ss1, baseStrength); + expect(boostFromLevel).toEqual(0); + }); + + it('should give an appropriate boost based on levels', () => { + const ss1 = Fleet.generateStarship(StarShipType.Battleship); + const baseStrength = Fleet.getStarshipTypeBaseStrength(ss1.type); + ss1.experienceAmount = baseStrength / 2; + const level = Fleet.starShipLevel(ss1, baseStrength).level; + expect(level).toEqual(1); + let strengthBoost = Fleet.strengthBoostFromLevel(ss1, baseStrength); + console.log('Boost for level 1: ', strengthBoost); + expect(strengthBoost).toEqual(baseStrength / 8); + const levelExpBoosts = [8, 16, 20, 23, 26, 28, 30, 32, 34]; + for (let i = 0; i < levelExpBoosts.length; i++) { + const levelExpBoost = levelExpBoosts[i]; + ss1.experienceAmount += ss1.experienceAmount + Math.round(ss1.experienceAmount / 2); + const level = Fleet.starShipLevel(ss1, baseStrength).level; + expect(level).toEqual(i + 2); + strengthBoost = Fleet.strengthBoostFromLevel(ss1, baseStrength); + console.log('Level: ', level, 'exp:', ss1.experienceAmount, 'boost:', strengthBoost); + expect(strengthBoost).toEqual(levelExpBoost); + } + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/fleet.ts b/packages/astriarch-engine/src/engine/fleet.ts new file mode 100644 index 00000000..6eb0bf8c --- /dev/null +++ b/packages/astriarch-engine/src/engine/fleet.ts @@ -0,0 +1,497 @@ +import { EarnedPointsType } from '../model/earnedPoints'; +import { FleetData, LastKnownFleetData, StarshipAdvantageData, StarshipData, StarShipType } from '../model/fleet'; +import { PlanetData, PlanetImprovementType } from '../model/planet'; +import { PlayerData } from '../model/player'; +import { ResearchType } from '../model/research'; +import { PointData } from '../shapes/shapes'; +import { GameTools } from '../utils/gameTools'; +import { Grid } from './grid'; +import { Player } from './player'; +import { Research } from './research'; + +export type StarshipsByType = Record; + +export interface StarshipTypeCounts { + defenders: number; + scouts: number; + destroyers: number; + cruisers: number; + battleships: number; + spaceplatforms: number; +} + +export class Fleet { + private static NEXT_STARSHIP_ID = 1; + public static generateFleet(starships: StarshipData[], locationHexMidPoint: PointData | null) { + return { + starships, + locationHexMidPoint, + travelingFromHexMidPoint: null, + destinationHexMidPoint: null, + parsecsToDestination: null, + totalTravelDistance: null, + }; + } + + public static generateFleetWithShipCount( + defenders: number, + scouts: number, + destroyers: number, + cruisers: number, + battleships: number, + spaceplatforms: number, + locationHexMidPoint: PointData | null, + ): FleetData { + const starships = [ + ...Fleet.generateStarships(StarShipType.SystemDefense, defenders), + ...Fleet.generateStarships(StarShipType.Scout, scouts), + ...Fleet.generateStarships(StarShipType.Destroyer, destroyers), + ...Fleet.generateStarships(StarShipType.Cruiser, cruisers), + ...Fleet.generateStarships(StarShipType.Battleship, battleships), + ...Fleet.generateStarships(StarShipType.SpacePlatform, spaceplatforms), + ]; + + return this.generateFleet(starships, locationHexMidPoint); + } + + public static generateInitialFleet(defenders: number, locationHexMidPoint: PointData): FleetData { + return Fleet.generateFleetWithShipCount(defenders, 0, 0, 0, 0, 0, locationHexMidPoint); + } + + public static generateStarships(type: StarShipType, count: number): StarshipData[] { + const ships = []; + for (let i = 0; i < count; i++) { + ships.push(Fleet.generateStarship(type)); + } + return ships; + } + + public static getStarshipTypeBaseStrength(type: StarShipType) { + let baseStarShipStrength = 0; + + switch (type) { + case StarShipType.SystemDefense: + baseStarShipStrength = 2; + break; + case StarShipType.Scout: + baseStarShipStrength = 4; + break; + case StarShipType.Destroyer: + baseStarShipStrength = 8; + break; + case StarShipType.Cruiser: + baseStarShipStrength = 16; + break; + case StarShipType.Battleship: + baseStarShipStrength = 32; + break; + case StarShipType.SpacePlatform: + baseStarShipStrength = 64; + break; + } + return baseStarShipStrength; + } + + public static generateStarship(type: StarShipType, customShipData?: StarshipAdvantageData): StarshipData { + //ship strength is based on ship cost + // right now it is double the value of the next lower ship class + //maybe later: + 50% (rounded up) of the next lower ship cost + //each system defender is worth 2 + //each scout is worth 4 points + //each destroyer is worth 8 + //each cruiser is worth 16 + //each battleship is worth 32 + + //here are the advantages (-> means has an advantage over): + //space platforms -> all + //battleships -> cruisers -> destroyers -> scouts -> defenders (-> battleships) + + return { + id: Fleet.NEXT_STARSHIP_ID++, + type, + customShipData, + health: this.getStarshipTypeBaseStrength(type), //starships will heal between turns if the planet has the necessary building and the player has the requisite resources + experienceAmount: 0, //each time a starship damages an opponent the experience amount increases by the damage amount + }; + } + + public static getStarshipStandardAdvantageByType(type: StarShipType): StarshipAdvantageData | undefined { + let advantageAgainst = null; + let disadvantageAgainst = null; + + switch (type) { + case StarShipType.SystemDefense: + advantageAgainst = StarShipType.Battleship; + disadvantageAgainst = StarShipType.Scout; + break; + case StarShipType.Scout: + advantageAgainst = StarShipType.SystemDefense; + disadvantageAgainst = StarShipType.Destroyer; + break; + case StarShipType.Destroyer: + advantageAgainst = StarShipType.Scout; + disadvantageAgainst = StarShipType.Cruiser; + break; + case StarShipType.Cruiser: + advantageAgainst = StarShipType.Destroyer; + disadvantageAgainst = StarShipType.Battleship; + break; + case StarShipType.Battleship: + advantageAgainst = StarShipType.Cruiser; + disadvantageAgainst = StarShipType.SystemDefense; + break; + case StarShipType.SpacePlatform: + return undefined; + break; + } + return { + advantageAgainst, + disadvantageAgainst, + }; + } + + public static starshipTypeIsMobile(type: StarShipType): boolean { + return ![StarShipType.SystemDefense, StarShipType.SpacePlatform].includes(type); + } + + /** + * Calculates the strength of the fleet + */ + public static determineFleetStrength(fleet: FleetData, mobileOnly?: boolean) { + let starships = fleet.starships; + if (mobileOnly) { + starships = starships.filter((s) => this.starshipTypeIsMobile(s.type)); + } + + const strength = starships.reduce((accum, curr) => accum + curr.health, 0); + + return strength; + } + + public static getStarshipsByType(fleet: FleetData): StarshipsByType { + const starshipsByType = Object.values(StarShipType).reduce((accum, curr) => { + accum[curr as StarShipType] = []; + return accum; + }, {} as StarshipsByType); + return fleet.starships.reduce((accum, curr) => { + accum[curr.type].push(curr); + return accum; + }, starshipsByType); + } + + public static countStarshipsByType(fleet: FleetData): StarshipTypeCounts { + const ships = Fleet.getStarshipsByType(fleet); + return { + defenders: ships[StarShipType.SystemDefense].length, + scouts: ships[StarShipType.Scout].length, + destroyers: ships[StarShipType.Destroyer].length, + cruisers: ships[StarShipType.Cruiser].length, + battleships: ships[StarShipType.Battleship].length, + spaceplatforms: ships[StarShipType.SpacePlatform].length, + }; + } + + public static countMobileStarships(fleet: FleetData): number { + const counts = this.countStarshipsByType(fleet); + return counts.scouts + counts.destroyers + counts.cruisers + counts.battleships; + } + + /** + * Creates a new fleet with the number of ships specified, removing the ships from the fleet passed in + */ + public static splitFleet( + fleet: FleetData, + scouts: number, + destoyers: number, + cruisers: number, + battleships: number, + ): FleetData { + const newFleet = this.generateFleet([], fleet.locationHexMidPoint); + const starshipsByType = this.getStarshipsByType(fleet); + + for (let i = 0; i < Math.min(scouts, starshipsByType[StarShipType.Scout].length); i++) { + newFleet.starships.push(starshipsByType[StarShipType.Scout].shift()!); + } + for (let i = 0; i < Math.min(destoyers, starshipsByType[StarShipType.Destroyer].length); i++) { + newFleet.starships.push(starshipsByType[StarShipType.Destroyer].shift()!); + } + for (let i = 0; i < Math.min(cruisers, starshipsByType[StarShipType.Cruiser].length); i++) { + newFleet.starships.push(starshipsByType[StarShipType.Cruiser].shift()!); + } + for (let i = 0; i < Math.min(battleships, starshipsByType[StarShipType.Battleship].length); i++) { + newFleet.starships.push(starshipsByType[StarShipType.Battleship].shift()!); + } + + fleet.starships = [ + ...starshipsByType[StarShipType.SystemDefense], + ...starshipsByType[StarShipType.Scout], + ...starshipsByType[StarShipType.Destroyer], + ...starshipsByType[StarShipType.Cruiser], + ...starshipsByType[StarShipType.Battleship], + ...starshipsByType[StarShipType.SpacePlatform], + ]; + + return newFleet; + } + + /** + * Splits this fleet off in a fleet that contains one weak ship + */ + public static splitOffSmallestPossibleFleet(fleet: FleetData): FleetData | undefined { + let newFleet; + let scoutCount = 0; + let destroyerCount = 0; + let cruiserCount = 0; + let battleshipCount = 0; + const starshipCounts = this.countStarshipsByType(fleet); + + if (starshipCounts.scouts !== 0) scoutCount = 1; + else if (starshipCounts.destroyers !== 0) destroyerCount = 1; + else if (starshipCounts.cruisers !== 0) cruiserCount = 1; + else if (starshipCounts.battleships !== 0) battleshipCount = 1; + + if (scoutCount !== 0 || destroyerCount !== 0 || cruiserCount !== 0 || battleshipCount !== 0) + newFleet = this.splitFleet(fleet, scoutCount, destroyerCount, cruiserCount, battleshipCount); + + return newFleet; + } + + /** + * Sets the destimation hex for a fleet + */ + public static setDestination( + fleet: FleetData, + gameGrid: Grid, + locationHexMidPoint: PointData, + destinationHexMidPoint: PointData, + ) { + fleet.locationHexMidPoint = locationHexMidPoint; + fleet.destinationHexMidPoint = destinationHexMidPoint; + fleet.travelingFromHexMidPoint = locationHexMidPoint; + + fleet.totalTravelDistance = Grid.getHexDistanceForMidPoints(gameGrid, locationHexMidPoint, destinationHexMidPoint); + fleet.parsecsToDestination = fleet.totalTravelDistance; + } + + /** + * Simply remove ships with strength <= 0 + */ + public static reduceFleet = function (fleet: FleetData) { + //if a starship's health is at 0 it's destroyed + fleet.starships = fleet.starships.filter((s) => s.health > 0); + }; + + /** + * Reduce a ship's strength / health + */ + public static damageStarship(owner: PlayerData | undefined, starship: StarshipData, damageAmount: number): number { + const damageInflicted = Math.min(starship.health, damageAmount); + starship.health -= damageInflicted; + //assign points + if (owner) { + Player.increasePoints(owner, EarnedPointsType.DAMAGED_STARSHIP_STRENGTH, damageInflicted); + } + return damageInflicted; + } + + /** + * Repairs a fleet + */ + public static repairPlanetaryFleet(planet: PlanetData, maxStrengthToRepair: number, cyclesElapsed: number) { + let totalStrengthRepaired = 0; + let amountRepaired = 0; + let allEligibleStarShips: StarshipData[] = []; + + const starshipsByType = this.getStarshipsByType(planet.planetaryFleet); + + if (planet.builtImprovements[PlanetImprovementType.Factory] > 0) { + allEligibleStarShips = allEligibleStarShips.concat(starshipsByType[StarShipType.Destroyer]); + //cruisers and battleships need a factory and a spaceplatform for repairs + if (starshipsByType[StarShipType.SpacePlatform].length > 0) { + allEligibleStarShips = allEligibleStarShips.concat(starshipsByType[StarShipType.Battleship]); + allEligibleStarShips = allEligibleStarShips.concat(starshipsByType[StarShipType.Cruiser]); + } + allEligibleStarShips = allEligibleStarShips.concat(starshipsByType[StarShipType.SpacePlatform]); + } + allEligibleStarShips = allEligibleStarShips.concat(starshipsByType[StarShipType.Scout]); + allEligibleStarShips = allEligibleStarShips.concat(starshipsByType[StarShipType.SystemDefense]); + + for (const s of allEligibleStarShips) { + // ships should only be able to repair at most 50% of their base strength per turn + const maxToRepairForShip = this.getStarshipTypeBaseStrength(s.type) * 0.5 * cyclesElapsed; + amountRepaired = this.repairStarShip(s, Math.min(maxToRepairForShip, maxStrengthToRepair)); + totalStrengthRepaired += amountRepaired; + maxStrengthToRepair -= amountRepaired; + if (maxStrengthToRepair <= 0) { + break; + } + } + + return totalStrengthRepaired; + } + + /** + * Repairs a starship up to max strength to repair + */ + public static repairStarShip(ship: StarshipData, maxStrengthToRepair: number) { + const damageAmount = this.maxStrength(ship) - ship.health; + const amountRepaired = Math.min(maxStrengthToRepair, damageAmount); + ship.health += amountRepaired; + return amountRepaired; + } + + /** + * Get a starships's maximum Strength (not including damage) + */ + public static maxStrength(ship: StarshipData) { + const baseStrength = this.getStarshipTypeBaseStrength(ship.type); + return baseStrength + this.strengthBoostFromLevel(ship, baseStrength); + } + + /** + * Get a starships's level strength boost + */ + public static strengthBoostFromLevel(ship: StarshipData, baseStrength: number) { + const level = this.starShipLevel(ship, baseStrength).level; + if (level <= 2) { + return Math.round(level * (baseStrength / 8.0)); + } + //at some level the ship's strength should be about 2 times it's base strength + //well use the log function to figure out at a given level for a certain ship what the proper boost should be + const x = 9; //the target level (which is somewhat difficult to achieve) + const y = baseStrength * 1.0; + const b = Math.pow(x, 1 / y); + return Math.round(Math.log(level) / Math.log(b)); //b^y=x + } + + /** + * Get a starships's level based on experience points + */ + public static starShipLevel(ship: StarshipData, baseStrength: number) { + let level = -1; + let levelExpRequirement = baseStrength / 2; + //for the ship to make it to level 1 it must have 1/2 the base strength in experience points + // after that the experience needed for each level = previous level exp + round((previous level exp)/2); + let foundLevel = false; + while (!foundLevel) { + if (ship.experienceAmount < levelExpRequirement) { + foundLevel = true; + } + levelExpRequirement += levelExpRequirement + Math.round(levelExpRequirement / 2); + level++; + } + return { level, nextLevelExpRequirement: levelExpRequirement }; + } + + /** + * Lands a fleet on a planet + * the caller is responsible for removing the landing fleet + */ + public static landFleet = function (planetaryFleet: FleetData, landingFleet: FleetData) { + // NOTE: this isn't technically necessary since we're going to merge these fleets + landingFleet.travelingFromHexMidPoint = null; + landingFleet.destinationHexMidPoint = null; + landingFleet.parsecsToDestination = 0; + + // merge fleet + planetaryFleet.starships = planetaryFleet.starships.concat(landingFleet.starships); + }; + + /** + * Gets parsec / cycle speed based on research + */ + public static getSpeed(owner: PlayerData) { + return Research.getResearchBoostForEfficiencyImprovement(ResearchType.PROPULSION_IMPROVEMENT, owner); + } + + public static getTurnsToDestination(fleet: FleetData, owner: PlayerData) { + return (fleet.parsecsToDestination || 0) / this.getSpeed(owner); + } + + /** + * Moves a fleet + */ + public static moveFleet(fleet: FleetData, owner: PlayerData, cyclesElapsed: number) { + if (fleet.parsecsToDestination === null || fleet.parsecsToDestination === undefined) { + throw new Error('Unable to move fleet until parsecsToDestination is set'); + } + fleet.locationHexMidPoint = null; + //TODO: should this update the location hex to a closer hex as well? + + fleet.parsecsToDestination -= this.getSpeed(owner) * cyclesElapsed; + } + + /** + * Copies this fleet + */ + public static cloneFleet(fleet: FleetData): FleetData { + const f = this.generateFleet([], fleet.locationHexMidPoint); + + for (const s of fleet.starships) { + f.starships.push(this.cloneStarship(s)); + } + + return f; + } + + /** + * A printable version of the fleet + */ + public static toString(fleet: FleetData) { + const shipsByType = Fleet.getStarshipsByType(fleet); + const customCountsByType = Object.entries(shipsByType).reduce( + (accum, [currType, currShips]) => { + const key = currType as unknown as StarShipType; + if (!(key in accum)) { + accum[key] = { standard: 0, custom: 0 }; + } + accum[key].custom = currShips.filter((s) => !!s.customShipData).length; + accum[key].standard = currShips.length - accum[key].custom; + return accum; + }, + {} as Record, + ); + + const fleetSummary = Object.entries(customCountsByType).reduce((accum, [currType, currCounts]) => { + const key = currType as unknown as StarShipType; + if (currCounts.standard) { + if (accum != '') accum += ', '; + accum += `${currCounts.standard} ${GameTools.starShipTypeToFriendlyName(key)}${ + currCounts.standard > 1 ? 's' : '' + }`; + } + if (currCounts.custom) { + if (accum != '') accum += ', '; + accum += `${currCounts.custom} ${GameTools.starShipTypeToFriendlyName(key)}${currCounts.custom > 1 ? 's' : ''}`; + } + return accum; + }, ''); + + return fleetSummary ?? 'No Ships'; + } + + /** + * Copies the properties of this starship + */ + public static cloneStarship(starship: StarshipData): StarshipData { + const s = this.generateStarship(starship.type); + s.health = starship.health; + s.experienceAmount = starship.experienceAmount; + if (starship.customShipData) { + s.customShipData = { ...starship.customShipData }; + } + return s; + } + + public static constructLastKnownFleet( + cycleLastExplored: number, + fleetData: FleetData, + lastKnownOwnerId: string | undefined, + ): LastKnownFleetData { + return { + cycleLastExplored, + fleetData, + lastKnownOwnerId, + }; + } +} diff --git a/packages/astriarch-engine/src/engine/gameController.spec.ts b/packages/astriarch-engine/src/engine/gameController.spec.ts new file mode 100644 index 00000000..0fe14c8b --- /dev/null +++ b/packages/astriarch-engine/src/engine/gameController.spec.ts @@ -0,0 +1,57 @@ +import { PlayerType } from '../model/player'; +import { startNewTestGameWithOptions } from '../test/testUtils'; +import { ClientGameModel } from './clientGameModel'; +import { GameController } from './gameController'; + +const assertPointsBasedOnGameOptions = ( + expectedPoints: number, + turnNumber: number, + points: number, + systemsToGenerate: number, + planetsPerSystem: number, + ownedPlanetCount: number, + playerCount: number, + playerWins: boolean, + playerType: PlayerType = PlayerType.Computer_Expert, +) => { + const testGameData = startNewTestGameWithOptions( + turnNumber, + points, + systemsToGenerate, + planetsPerSystem, + ownedPlanetCount, + playerCount, + playerType, + ); + const { modelData } = testGameData.gameModel; + const [player] = modelData.players; + const ownedPlanets = ClientGameModel.getOwnedPlanets(player.ownedPlanetIds, modelData.planets); + const calculatedPoints = GameController.calculateEndGamePoints(modelData, player, ownedPlanets, playerWins); + expect(calculatedPoints).toEqual(expectedPoints); +}; + +describe('GameController', () => { + beforeEach(() => {}); + + describe('CalculateEndGamePoints()', () => { + it('should base points on total points at the end of the game', () => { + assertPointsBasedOnGameOptions(436, 100, 100, 4, 4, 8, 4, true); + }); + + it('should award less points if the player loses', () => { + assertPointsBasedOnGameOptions(404, 100, 100, 4, 4, 8, 4, false); + }); + + it('should handle awarding points even if the player owns no planets', () => { + assertPointsBasedOnGameOptions(400, 100, 100, 4, 4, 0, 4, false); + }); + + it('should give bonus points for a quick game', () => { + assertPointsBasedOnGameOptions(472, 50, 100, 4, 4, 8, 4, true); + }); + + it('should give less points for an easier game', () => { + assertPointsBasedOnGameOptions(413, 100, 100, 2, 4, 8, 2, true, PlayerType.Human); + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/gameController.ts b/packages/astriarch-engine/src/engine/gameController.ts new file mode 100644 index 00000000..f86a7c42 --- /dev/null +++ b/packages/astriarch-engine/src/engine/gameController.ts @@ -0,0 +1,487 @@ +import { ClientModelData, PlanetById } from '../model/clientModel'; +import { EventNotificationType } from '../model/eventNotification'; +import { FleetData } from '../model/fleet'; +import { GalaxySizeOption, ModelBase, ModelData } from '../model/model'; +import { PlayerData, PlayerType } from '../model/player'; +import { ResearchType } from '../model/research'; +import { Utils } from '../utils/utils'; +import { BattleSimulator } from './battleSimulator'; +import { ClientGameModel } from './clientGameModel'; +import { ComputerPlayer } from './computerPlayer'; +import { Events } from './events'; +import { Fleet } from './fleet'; +import { AdvanceGameClockForPlayerData, GameModel, GameModelData } from './gameModel'; +import { Grid } from './grid'; +import { Planet } from './planet'; +import { Player } from './player'; +import { Research } from './research'; +import { TradingCenter } from './tradingCenter'; + +export class GameController { + public static MS_PER_CYCLE = 30 * 1000; // Time per cycle (or "turn") + + public static startModelSnapshot(modelDataBase: ModelBase) { + const newSnapshotTime = new Date().getTime(); + const lastSnapshotTime = modelDataBase.lastSnapshotTime; + + const elapsedSinceLastSnapshot = newSnapshotTime - lastSnapshotTime; + const cyclesElapsed = elapsedSinceLastSnapshot / GameController.MS_PER_CYCLE; + + const elapsedSinceStart = newSnapshotTime - modelDataBase.gameStartedAtTime; + const advancedCyclesTotal = elapsedSinceStart / GameController.MS_PER_CYCLE; + + return { + newSnapshotTime, + cyclesElapsed, + currentCycle: Math.trunc(advancedCyclesTotal), + }; + } + + public static advanceGameClock(gameModel: GameModelData) { + const { modelData, grid } = gameModel; + const { cyclesElapsed, newSnapshotTime, currentCycle } = GameController.startModelSnapshot(modelData); + const planetById = ClientGameModel.getPlanetByIdIndex(modelData.planets); + + for (const p of modelData.players) { + if (p.type !== PlayerType.Human) { + const ownedPlanets = ClientGameModel.getOwnedPlanets(p.ownedPlanetIds, modelData.planets); + ComputerPlayer.computerTakeTurn(gameModel, p, ownedPlanets); + } + } + + TradingCenter.executeCurrentTrades(gameModel, planetById); + + // NOTE: advanceGameClockForPlayer contains all methods that can run client-side to keep the ui updating w/o trips to the server + const fleetsArrivingOnUnownedPlanetsByPlayerId: Record = {}; + for (const p of modelData.players) { + const clientModel = ClientGameModel.constructClientGameModel(modelData, p.id); + const data: AdvanceGameClockForPlayerData = { + clientModel, + cyclesElapsed, + currentCycle, + grid, + }; + fleetsArrivingOnUnownedPlanetsByPlayerId[p.id] = Player.advanceGameClockForPlayer(data); + } + + // TODO: server side operations after advancing game clock for player + for (const p of modelData.players) { + const arrivingFleets = fleetsArrivingOnUnownedPlanetsByPlayerId[p.id]; + if (arrivingFleets.length) { + this.resolvePlanetaryConflicts(gameModel, p, arrivingFleets); + } + } + + modelData.lastSnapshotTime = newSnapshotTime; + if (currentCycle > modelData.currentCycle) { + this.handleCycleAdvancement(gameModel); + modelData.currentCycle = currentCycle; + } + Events.publish(); + } + + public static handleCycleAdvancement(gameModel: GameModelData) { + // Handle any game logic that needs to occur at the end of a cycle + const { modelData } = gameModel; + for (const p of modelData.players) { + if (p.lastTurnFoodShipped > 0) { + Events.enqueueNewEvent( + p.id, + EventNotificationType.ResourcesAutoSpent, + `${p.lastTurnFoodShipped.toFixed(1)} Energy spent shipping Food`, + ); + p.lastTurnFoodShipped = 0; + } + p.lastTurnFoodNeededToBeShipped = 0; + } + } + + /** + * Reset the snapshot time to the current time to prevent time jumps + * when resuming a paused game. This is called during game resume operations. + */ + public static resetSnapshotTime(modelDataBase: ModelBase): void { + const currentTime = new Date().getTime(); + modelDataBase.lastSnapshotTime = currentTime; + } + + public static advanceClientGameClock(clientModel: ClientModelData, grid: Grid): FleetData[] { + const { cyclesElapsed, newSnapshotTime, currentCycle } = GameController.startModelSnapshot(clientModel); + + const data: AdvanceGameClockForPlayerData = { + clientModel, + cyclesElapsed, + currentCycle, + grid, + }; + const fleetsArrivingOnUnownedPlanets = Player.advanceGameClockForPlayer(data); + + clientModel.lastSnapshotTime = newSnapshotTime; + clientModel.currentCycle = currentCycle; + Events.publish(); + return fleetsArrivingOnUnownedPlanets; + } + + //TODO: this is problematic right now if multiple players show up to battle at a 3rd players planet + // right now one player will attack, then the next one will, which prefers the 2nd player to attack + public static resolvePlanetaryConflicts( + gameModel: GameModelData, + player: PlayerData, + fleetsArrivingOnUnownedPlanets: FleetData[], + ) { + //if any of the player's fleets in transit have reached their destination + // if the destination is not an owned planet, we need to resolve the conflict + // once conflicts are resolved, merge fleets to the fleets of the owned planet + for (const playerFleet of fleetsArrivingOnUnownedPlanets) { + const destinationPlanet = Planet.getPlanetAtMidPoint( + gameModel.modelData.planets, + playerFleet.destinationHexMidPoint!, + ); + if (!destinationPlanet) { + throw new Error('Unable to find fleet destinationPlanet in resolvePlanetaryConflicts'); + } + + //if there are ships in the planet's OutgoingFleets list, recall them since we are being attacked + // this happens when a player has used the waypoint feature and just built a ship + Planet.recallOutgoingFleets(destinationPlanet); + + //battle! + const enemyFleet = destinationPlanet.planetaryFleet; + + const enemyFleetStrength = Fleet.determineFleetStrength(enemyFleet); + + const playerFleetStrength = Fleet.determineFleetStrength(playerFleet); + + //this is for our event message, the PlanetaryConflictData constructor deals with cloning the fleets + const attackingClientPlayer = ClientGameModel.constructClientPlayer(player); + let defendingClientPlayer = null; + const planetOwner = GameModel.findPlanetOwner(gameModel, destinationPlanet.id); + if (planetOwner) { + defendingClientPlayer = ClientGameModel.constructClientPlayer(planetOwner); + } + const planetaryConflictData = Events.constructPlanetaryConflictData( + defendingClientPlayer, + destinationPlanet.planetaryFleet, + attackingClientPlayer, + playerFleet, + ); + + //determine strength differences + // ServerController.BATTLE_RANDOMNESS_FACTOR = 4 in this case + //if one fleet's strength is 4 (log base 16 4 = .5) times as strong or more that fleet automatically wins + // damage done to winning fleet is (strength of loser / strength Multiplier) +- some randomness + //if neither fleet is 4 times as strong as the other or more, we have to roll the dice (preferring the stronger fleet) for who wins + + //if the player's fleet is destroyed the enemy (defender) always wins because you can't land fleets and capture the system without fleets + + //July 21st 2010, changed from pure statistics to BattleSimulator, still have this AttackingFleetChances code to show a percentage (for now as an estimation) + + planetaryConflictData.attackingFleetResearchBoost.attack = Research.getResearchBoostForStarshipCombatImprovement( + ResearchType.COMBAT_IMPROVEMENT_ATTACK, + player, + ); + planetaryConflictData.attackingFleetResearchBoost.defense = Research.getResearchBoostForStarshipCombatImprovement( + ResearchType.COMBAT_IMPROVEMENT_DEFENSE, + player, + ); + + planetaryConflictData.defendingFleetResearchBoost.attack = planetOwner + ? Research.getResearchBoostForStarshipCombatImprovement(ResearchType.COMBAT_IMPROVEMENT_ATTACK, planetOwner) + : 0; + planetaryConflictData.defendingFleetResearchBoost.defense = planetOwner + ? Research.getResearchBoostForStarshipCombatImprovement(ResearchType.COMBAT_IMPROVEMENT_DEFENSE, planetOwner) + : 0; + + planetaryConflictData.attackingFleetChances = BattleSimulator.getAttackingFleetChances( + playerFleetStrength, + enemyFleetStrength, + ); + + //now actually simulate the battle + let playerWins = BattleSimulator.simulateFleetBattle(playerFleet, player, enemyFleet, planetOwner); + //if at this point playerWins doesn't have a value it means that both fleets were destroyed, in that case the enemy should win because they are the defender of the planet + if (playerWins === null || typeof playerWins == 'undefined') playerWins = false; + + if (!playerWins) { + //just kill the fleet + //make sure this planet is now explored + Player.setPlanetExplored(player, destinationPlanet, gameModel.modelData.currentCycle, planetOwner?.id); + + //notify user of fleet loss or defense + if (player.type == PlayerType.Human) { + //the attacking player is a human player and lost + //PlanetaryConflictData summarizes your attacking fleet, the enemy fleet and what was destroyed in the enemy fleet + + let message = 'You lost a fleet attacking planet: ' + destinationPlanet.name; + if (planetOwner) { + message = 'You lost a fleet attacking ' + planetOwner.name + ' at planet: ' + destinationPlanet.name; + } + planetaryConflictData.winningFleet = Fleet.cloneFleet(enemyFleet); + Events.enqueueNewEvent( + player.id, + EventNotificationType.AttackingFleetLost, + message, + destinationPlanet, + planetaryConflictData, + ); + } + + if (planetOwner && planetOwner.type == PlayerType.Human) { + //the defending player is a human player and won + //PlanetaryConflictData summarizes the attacking fleet, your fleet and what was destroyed in your fleet + const message = + 'You successfully defended against ' + player.name + ' attacking planet: ' + destinationPlanet.name; + planetaryConflictData.winningFleet = Fleet.cloneFleet(enemyFleet); + Events.enqueueNewEvent( + planetOwner.id, + EventNotificationType.DefendedAgainstAttackingFleet, + message, + destinationPlanet, + planetaryConflictData, + ); + } + } else { + const defendingPlayer = planetOwner; + + //change planet ownership + GameModel.changePlanetOwner(defendingPlayer, player, destinationPlanet, gameModel.modelData.currentCycle); + + if (defendingPlayer) { + //give the conquering player a chance to loot research from the planet / defending player + //based on how good the planet it is (class) + const researchLootMax = Math.floor(Math.pow(destinationPlanet.type + 1, 2)) * 4; + + //loot research if the defending player has a higher level of research than we do in an area + const potentialResearchToSteal = Research.getResearchProgressListSorted(defendingPlayer.research); + for (const rp of potentialResearchToSteal) { + const attackingPlayerResearch = player.research.researchProgressByType[rp.type]; + //NOTE: currently you can't steal custom ship research + if ( + Research.canResearch(attackingPlayerResearch) && + !Research.researchTypeIndex[rp.type].isCustomShip && + rp.currentResearchLevel > attackingPlayerResearch.currentResearchLevel && + rp.researchPointsCompleted > attackingPlayerResearch.researchPointsCompleted + ) { + planetaryConflictData.resourcesLooted.research = Utils.nextRandom( + 0, + Math.floor( + Math.min( + researchLootMax, + rp.researchPointsCompleted - attackingPlayerResearch.researchPointsCompleted, + ) + 1, + ), + ); + const levelIncrease = Research.setResearchPointsCompleted( + attackingPlayerResearch, + attackingPlayerResearch.researchPointsCompleted + planetaryConflictData.resourcesLooted.research, + ); + if (levelIncrease) { + Events.enqueueNewEvent( + defendingPlayer.id, + EventNotificationType.ResearchStolen, + `${Research.researchProgressToString(attackingPlayerResearch)} was stolen by ${player.name}`, + destinationPlanet, + ); + Events.enqueueNewEvent( + player.id, + EventNotificationType.ResearchStolen, + `You stole ${Research.researchProgressToString(attackingPlayerResearch)} from ${ + defendingPlayer.name + }`, + destinationPlanet, + ); + } + break; + } + } + } + + //add in the other resources looted since the new player is now the owner + planetaryConflictData.resourcesLooted = { ...destinationPlanet.resources }; + + //merge/land fleet + Fleet.landFleet(destinationPlanet.planetaryFleet, playerFleet); + + //notify user of planet capture or loss + if (player.type == PlayerType.Human) { + //the attacking player is a human player and won + //PlanetaryConflictData summarizes your attacking fleet, the enemy fleet and what was destroyed in your fleet + let message = 'Your fleet captured planet: ' + destinationPlanet.name; + if (defendingPlayer != null) { + message = 'Your fleet captured planet: ' + destinationPlanet.name + ', owned by: ' + defendingPlayer.name; + } + planetaryConflictData.winningFleet = Fleet.cloneFleet(playerFleet); + Events.enqueueNewEvent( + player.id, + EventNotificationType.PlanetCaptured, + message, + destinationPlanet, + planetaryConflictData, + ); + } + + if (defendingPlayer && defendingPlayer.type == PlayerType.Human) { + //the defending player is a human player and lost + //planetaryConflictData summarizes your defending fleet, the enemy fleet and what was destroyed in the enemy fleet + const message = player.name + ' captured your planet: ' + destinationPlanet.name; + planetaryConflictData.winningFleet = Fleet.cloneFleet(playerFleet); + Events.enqueueNewEvent( + defendingPlayer.id, + EventNotificationType.PlanetLost, + message, + destinationPlanet, + planetaryConflictData, + ); + } + } + } + } + + public static calculateEndGamePoints( + model: ModelData, + player: PlayerData, + ownedPlanets: PlanetById, + playerWon: boolean, + ) { + return this.getEndGamePlayerPoints(model, player, ownedPlanets, playerWon); + } + + public static getEndGamePlayerPoints( + model: ModelData, + player: PlayerData, + ownedPlanets: PlanetById, + playerWon: boolean, + ) { + let turnsTaken = model.currentCycle; + if (turnsTaken > 1000) { + //some max, nobody should play this long? + turnsTaken = 1000; + } + + const { systemsToGenerate, planetsPerSystem } = model.gameOptions; + let minTurns = systemsToGenerate * planetsPerSystem - 8; + let difficultyRating = minTurns; + + for (const p of model.players) { + if (p.id === player.id) { + continue; + } + switch (p.type) { + case PlayerType.Computer_Easy: + difficultyRating += 1; + break; + case PlayerType.Computer_Normal: + difficultyRating += 2; + break; + case PlayerType.Computer_Hard: + difficultyRating += 3; + break; + case PlayerType.Computer_Expert: + difficultyRating += 4; + break; + case PlayerType.Human: + difficultyRating += 8; + break; + } + } + + let ownedPlanetCount = player.ownedPlanetIds.length; + if (ownedPlanetCount == 0) { + ownedPlanetCount = 1; //so that we have points for loosers too + } + let totalPopulation = Player.getTotalPopulation(player, ownedPlanets); + if (totalPopulation == 0) { + totalPopulation = 1; //so that we have points for loosers too + } + + let maxPopulation = 0; + for (const p of Object.values(ownedPlanets)) { + maxPopulation += p.maxImprovements; //only count max pop w/o colonies + } + //prevent divide by zero + if (maxPopulation == 0) { + maxPopulation = 100; + } + + //max difficulty right now is (4 * 8) - 8 + (3 * 8) = 48 + //min is 1 + difficultyRating = difficultyRating / 48; + + minTurns += playerWon + ? ownedPlanetCount * + (model.gameOptions.galaxySize == GalaxySizeOption.TINY + ? 0.25 + : model.gameOptions.galaxySize == GalaxySizeOption.SMALL + ? 0.5 + : model.gameOptions.galaxySize == GalaxySizeOption.MEDIUM + ? 0.75 + : 1.0) + : 6; + const speedFactor = minTurns / turnsTaken; + + const additionalPoints = this.calculateAdditionalPoints( + player.points, + ownedPlanetCount, + totalPopulation, + maxPopulation, + systemsToGenerate, + planetsPerSystem, + difficultyRating, + speedFactor, + playerWon, + ); + console.log( + 'CalculateEndGamePoints: points:', + player.points, + 'additionalPoints:', + additionalPoints, + 'speedFactor:', + speedFactor, + 'difficultyRating:', + difficultyRating, + 'percentageOwned:', + ownedPlanetCount / (systemsToGenerate * planetsPerSystem), + 'ownedPlanetCount:', + ownedPlanetCount, + 'totalPopulation:', + totalPopulation, + 'maxPopulation:', + maxPopulation, + 'systemsToGenerate:', + systemsToGenerate, + 'planetsPerSystem:', + planetsPerSystem, + 'playerWon:', + playerWon, + 'minTurns:', + minTurns, + 'model.currentCycle:', + model.currentCycle, + ); + + return Math.floor(player.points + additionalPoints); + } + + public static calculateAdditionalPoints( + points: number, + ownedPlanetCount: number, + totalPopulation: number, + maxPopulation: number, + systemsToGenerate: number, + planetsPerSystem: number, + difficultyRating: number, + speedFactor: number, + playerWon: boolean, + ) { + const percentageOwned = ownedPlanetCount / (systemsToGenerate * planetsPerSystem); + const percentagePopulated = totalPopulation / maxPopulation; + + let additionalPoints = points * (playerWon ? 2 : 0.25); + additionalPoints = Math.round( + additionalPoints * + (percentageOwned * difficultyRating * speedFactor + percentagePopulated * difficultyRating * speedFactor), + ); + + return additionalPoints; + } +} diff --git a/packages/astriarch-engine/src/engine/gameModel.ts b/packages/astriarch-engine/src/engine/gameModel.ts new file mode 100644 index 00000000..c975314b --- /dev/null +++ b/packages/astriarch-engine/src/engine/gameModel.ts @@ -0,0 +1,399 @@ +import { ClientModelData, PlanetById } from '../model/clientModel'; +import { StarShipType } from '../model/fleet'; +import { GalaxySizeOption, GameOptions, ModelData, PlanetsPerSystemOption } from '../model/model'; +import { + PlanetData, + PlanetHappinessType, + PlanetImprovementType, + PlanetResourceData, + PlanetType, +} from '../model/planet'; +import { PlayerData } from '../model/player'; +import { Utils } from '../utils/utils'; +import { Fleet } from './fleet'; +import { Grid, GridHex } from './grid'; +import { Planet } from './planet'; +import { PlanetResources } from './planetResources'; +import { Player } from './player'; +import { TradingCenter } from './tradingCenter'; + +export interface GameModelData { + modelData: ModelData; + grid: Grid; +} + +export interface AdvanceGameClockForPlayerData { + clientModel: ClientModelData; + cyclesElapsed: number; + currentCycle: number; + grid: Grid; +} + +export const playerColors = [ + Utils.ColorRgba(0, 255, 0, 255), //Light Green + Utils.ColorRgba(200, 0, 200, 255), //Light Purple + Utils.ColorRgba(0, 128, 255, 255), //Light Blue + Utils.ColorRgba(255, 0, 0, 255), //Light Red +]; + +export const GALAXY_WIDTH = 852.0; //TODO: externalize later +export const GALAXY_HEIGHT = 480.0; //TODO: externalize later + +export class GameModel { + public static constructData(players: PlayerData[], gameOptions: GameOptions): GameModelData { + const gameStartedAtTime = new Date().getTime(); + const lastSnapshotTime = gameStartedAtTime; + const currentCycle = 0; + const tradingCenter = TradingCenter.constructData(players.length); + + const grid = new Grid(GALAXY_WIDTH, GALAXY_HEIGHT, gameOptions); + const planets = GameModel.populatePlanets(grid, players, gameOptions); + + const modelData = { + gameOptions, + gameStartedAtTime, + lastSnapshotTime, + currentCycle, + tradingCenter, + players, + planets, + }; + + return { modelData, grid }; + } + + public static constructGridWithModelData(modelData: ModelData): GameModelData { + const grid = new Grid(GALAXY_WIDTH, GALAXY_HEIGHT, modelData.gameOptions); + return { modelData, grid }; + } + + private static populatePlanets(grid: Grid, players: PlayerData[], gameOptions: GameOptions): PlanetData[] { + const planets: PlanetData[] = []; + const usedPlanetBoundingHexes = new Set(); + for (let q = 0; q < grid.quadrants.length; q++) { + const r = grid.quadrants[q]; + //get a list of hexes inside this quadrant + const subQuadrantHexes: GridHex[][] = []; + + for (let iSQ = 0; iSQ < r.children.length; iSQ++) { + const sub = r.children[iSQ]; + subQuadrantHexes[iSQ] = []; + for (const i in grid.hexes) { + const h = grid.hexes[i]; + //let testPoint = q == 0 ? h.TopLeftPoint : q == 1 ? h.TopRightPoint : q == 3 ? h.BottomLeftPoint : h.BottomRightPoint; + if (sub.contains(h.midPoint)) { + //the hex is inside the quadrant and possibly an outlier on the edge + //and the hex doesn't lie on the outside of the sub-quadrant + subQuadrantHexes[iSQ].push(h); + } + } + } + + if (gameOptions.systemsToGenerate == 2 && (q == 1 || q == 3)) continue; + + const possiblePlanetTypes = [PlanetType.PlanetClass1, PlanetType.DeadPlanet, PlanetType.AsteroidBelt]; + let playerHomePlanetHex: GridHex | undefined; + let planetBoundingHex: GridHex | undefined; + for (let iSQ = 0; iSQ < r.children.length; iSQ++) { + let chosenPlanetSubQuadrant = iSQ; + if (!gameOptions.distributePlanetsEvenly) { + do { + chosenPlanetSubQuadrant = Utils.nextRandom(0, r.children.length); + } while (subQuadrantHexes[chosenPlanetSubQuadrant].length == 0); + } + // pick a planet bounding hex at random from the sub-quadrant (at least for now) + // if we are choosing one of the first 4 planets make sure it is within the min distance from the home planet + let hexFound = false; + while (!hexFound) { + const maxDistanceFromHome = + gameOptions.galaxySize == GalaxySizeOption.LARGE + ? 5 + : gameOptions.galaxySize == GalaxySizeOption.MEDIUM + ? 4 + : gameOptions.galaxySize == GalaxySizeOption.SMALL + ? 3 + : 2; + const hexPos = Utils.nextRandom(0, subQuadrantHexes[chosenPlanetSubQuadrant].length); + planetBoundingHex = subQuadrantHexes[chosenPlanetSubQuadrant][hexPos]; + if (playerHomePlanetHex) { + // get distance from home + const distanceFromHome = Grid.getHexDistance(planetBoundingHex, playerHomePlanetHex); + if (distanceFromHome > maxDistanceFromHome) { + console.log('Chosen location for planet too far from Home, picking again:', iSQ, distanceFromHome); + } else { + hexFound = true; + } + } else { + hexFound = true; + } + if (hexFound) { + subQuadrantHexes[chosenPlanetSubQuadrant].splice(hexPos, 1); //remove this hex as an option + } + } + + //get at least one planet of each type, prefer the highest class planet + let type = 3; + let pt = PlanetType.PlanetClass2; + if (iSQ > 0 && possiblePlanetTypes.length <= 3) { + type = Utils.nextRandom(0, possiblePlanetTypes.length); + pt = possiblePlanetTypes[type]; + possiblePlanetTypes.splice(type, 1); + } + + let initialPlanetOwner: PlayerData | undefined = undefined; + + let assignPlayer = false; + let assignedPlayerIndex = 0; + let assignedPlayerIndexHomeQuadrant = false; + + //it's a home planet, we'll see if we should assign a player + if (q == 0) { + assignedPlayerIndexHomeQuadrant = true; + if (pt == PlanetType.PlanetClass2) { + assignPlayer = true; + } + } else if (players.length == 2) { + if (q == 2) { + assignedPlayerIndexHomeQuadrant = true; + assignedPlayerIndex = 1; + if (pt == PlanetType.PlanetClass2) { + assignPlayer = true; + } + } + } else if (q < players.length) { + assignedPlayerIndexHomeQuadrant = true; + assignedPlayerIndex = q; + if (pt == PlanetType.PlanetClass2) { + assignPlayer = true; + } + } + + if (assignPlayer) { + initialPlanetOwner = players[assignedPlayerIndex]; + initialPlanetOwner.color = playerColors[assignedPlayerIndex]; + } + + const p = Planet.constructPlanet(pt, planetBoundingHex!.data.id, planetBoundingHex!, initialPlanetOwner); + usedPlanetBoundingHexes.add(planetBoundingHex?.data.id); + //if we set an initial owner + if (initialPlanetOwner) { + playerHomePlanetHex = planetBoundingHex; + initialPlanetOwner.homePlanetId = p.id; + initialPlanetOwner.ownedPlanetIds.push(p.id); + } + + if (gameOptions.quickStart && assignedPlayerIndexHomeQuadrant) { + Player.setPlanetExplored(players[assignedPlayerIndex], p, 0, undefined); + if (initialPlanetOwner) { + p.resources.ore *= 2; + p.resources.iridium *= 2; + p.builtImprovements[PlanetImprovementType.Farm] = 3; + p.builtImprovements[PlanetImprovementType.Colony] = 1; + p.builtImprovements[PlanetImprovementType.Factory] = 1; + p.planetaryFleet.starships.push(Fleet.generateStarship(StarShipType.Scout)); + } + } + + planets.push(p); + } + + if (gameOptions.planetsPerSystem > PlanetsPerSystemOption.FOUR) { + const quadrantChances = [25, 25, 25, 25]; + + let chanceToGetAsteroid = 40; + let chanceToGetDead = 34; + let chanceToGetClass1 = 26; + + for (let iPlanet = 4; iPlanet < gameOptions.planetsPerSystem; iPlanet++) { + let hexFound = false; + + while (!hexFound) { + //pick sub quadrant to put the planet in + + let planetBoundingHex: GridHex | undefined = undefined; + for (let iSQ = 0; iSQ < quadrantChances.length; iSQ++) { + if (!subQuadrantHexes[iSQ].length) { + continue; + } + const chance = quadrantChances[iSQ]; + const max = quadrantChances.reduce(function (a, b) { + return Math.max(a, b); + }, 0); + if (Utils.nextRandom(0, max) < chance) { + //pick a planet bounding hex at random from the sub-quadrant (at lest for now) + const hexPos = Utils.nextRandom(0, subQuadrantHexes[iSQ].length); + planetBoundingHex = subQuadrantHexes[iSQ][hexPos]; //Hexagon + subQuadrantHexes[iSQ].splice(hexPos, 1); //remove this hex as an option + //now that we've picked a quadrant, subtract some off the chances for next time + quadrantChances[iSQ] -= 6; + break; + } + } + + if (usedPlanetBoundingHexes.has(planetBoundingHex!.data.id)) continue; + + //hex has been found, now randomly choose planet type + //this logic prefers asteroids then dead then class1 and decreases our chances each time + + let pt = PlanetType.PlanetClass1; + const max = chanceToGetAsteroid + chanceToGetDead + chanceToGetClass1; + if (Utils.nextRandom(0, max) > chanceToGetClass1) { + pt = PlanetType.DeadPlanet; + if (Utils.nextRandom(0, max) > chanceToGetDead) { + pt = PlanetType.AsteroidBelt; + chanceToGetAsteroid -= 15; + } else { + chanceToGetDead -= 15; + } + } else { + chanceToGetClass1 -= 15; + } + + const p = Planet.constructPlanet(pt, planetBoundingHex!.data.id, planetBoundingHex!); + usedPlanetBoundingHexes.add(planetBoundingHex!.data.id); + planets.push(p); + + hexFound = true; + } + } + } + } //foreach quadrant + + return planets; + } + + public static getPlayerTotalResources(player: PlayerData, planetById: PlanetById): PlanetResourceData { + const resources = { + food: 0, + energy: 0, + research: 0, + ore: 0, + iridium: 0, + production: 0, + }; + + return player.ownedPlanetIds.reduce((accum, curr) => { + if (curr in planetById) { + const planet = planetById[curr]; + accum = PlanetResources.addPlanetResources(accum, planet.resources); + } + return accum; + }, resources); + } + + public static getPlayerTotalResourceProductionPerTurn( + player: PlayerData, + planetById: PlanetById, + ): PlanetResourceData { + const resources = { + food: 0, + energy: 0, + research: 0, + ore: 0, + iridium: 0, + production: 0, + }; + + return player.ownedPlanetIds.reduce((accum, curr) => { + if (curr in planetById) { + const planet = planetById[curr]; + const { amountPerTurn } = Planet.getPlanetWorkerResourceGeneration(planet, player); + accum = PlanetResources.addPlanetResources(accum, amountPerTurn); + } + return accum; + }, resources); + } + + public static getPlayerTotalPopulation(player: PlayerData, planetById: PlanetById): number { + return player.ownedPlanetIds.reduce((accum, curr) => { + if (curr in planetById) { + const planet = planetById[curr]; + accum += planet.population.length; + } + return accum; + }, 0); + } + + public static getPlanetById(gameModel: GameModelData, planetId: number): PlanetData | undefined { + return gameModel.modelData.planets.find((p) => p.id === planetId); + } + + public static isPlanetOwnedByPlayer(player: PlayerData, planetId: number): boolean { + return player.ownedPlanetIds.includes(planetId); + } + + public static findPlanetOwner(gameModel: GameModelData, planetId: number): PlayerData | undefined { + for (const player of gameModel.modelData.players) { + if (player.ownedPlanetIds.includes(planetId)) { + return player; + } + } + return undefined; + } + + public static changePlanetOwner( + oldOwner: PlayerData | undefined, + newOwner: PlayerData | undefined, + planet: PlanetData, + currentCycle: number, + ) { + if (newOwner) { + newOwner.ownedPlanetIds.push(planet.id); + newOwner.knownPlanetIds = [...new Set([...newOwner.knownPlanetIds, planet.id])]; + } + + if (oldOwner) { + for (let i = 0; i < oldOwner.ownedPlanetIds.length; i++) { + if (oldOwner.ownedPlanetIds[i] === planet.id) { + oldOwner.ownedPlanetIds.splice(i, 1); + break; + } + } + if (planet.id in oldOwner.planetBuildGoals) { + delete oldOwner.planetBuildGoals[planet.id]; + } + + //set Protest Levels for citizens + let contentCitizenCount = 0; + for (const citizen of planet.population) { + //they were loyal to this player before, they won't be protesting now + //there is a 1 in 3 chance that a citizen won't protest the new ruler + if ((citizen.loyalToPlayerId && citizen.loyalToPlayerId === newOwner?.id) || Utils.nextRandom(0, 3) == 0) { + citizen.protestLevel = 0; + citizen.loyalToPlayerId = newOwner?.id; + contentCitizenCount++; + continue; + } + + const minProtestLevel = citizen.loyalToPlayerId ? 0.5 : 0; //if the planet was run by natives they won't be protesting as much + const maxProtestLevel = citizen.loyalToPlayerId ? 1 : 0.5; //if the planet was run by natives they won't be protesting as much + citizen.protestLevel = Utils.nextRandomFloat(minProtestLevel, maxProtestLevel); + } + + if (contentCitizenCount == 0 && planet.population.length > 0) { + //ensure we have at least one loyal, non-protesting citizen (they were in awe of the new leadership's ability to take the planet) + planet.population[0].protestLevel = 0; + planet.population[0].loyalToPlayerId = newOwner?.id; + } + + //when a planet changes hands it should initially be in unrest + planet.planetHappiness = PlanetHappinessType.Riots; + + //set last known fleet strength + Player.setPlanetLastKnownFleetStrength(oldOwner, planet, currentCycle, newOwner?.id); + } + + planet.waypointBoundingHexMidPoint = null; + planet.starshipTypeLastBuilt = null; + planet.starshipCustomShipLastBuilt = false; + + //if this planet has items in the build queue we should remove them now + for (let i = planet.buildQueue.length - 1; i >= 0; i--) { + Planet.removeBuildQueueItemForRefund(planet, i); + } + + if (planet.population.length === 0) { + planet.population.push(Planet.constructCitizen(planet.type, newOwner?.id)); + } + } +} diff --git a/packages/astriarch-engine/src/engine/grid.ts b/packages/astriarch-engine/src/engine/grid.ts new file mode 100644 index 00000000..b6b033fe --- /dev/null +++ b/packages/astriarch-engine/src/engine/grid.ts @@ -0,0 +1,284 @@ +import { GalaxySizeOption, GameOptions } from '../model/model'; +import { HexagonData, PointData } from '../shapes/shapes'; + +const HexProperties = { HEIGHT: 40.0, WIDTH: 71.0, SIDE: 29.0, X: 21 }; +const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +/** + * Grid is a proper instantiated class since it has deterministic generation + * therefore it doesn't have to be transfered between the database, client and server and can be generated as needed + */ +export class Grid { + quadrants: GridRect[]; + hexes: GridHex[]; + + hexByMidPoint: Record = {}; // indexed by x.y + + constructor(width: number, height: number, options: GameOptions) { + this.quadrants = []; + this.hexes = []; + + let h: GridHex, x: number, y: number; + const quadWidth: number = width / 2; + const quadHeight: number = height / 2; + x = 0; + y = 0; + if (options.systemsToGenerate == 2 || options.systemsToGenerate == 4) { + //[0 , 0] + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + //[0 , 1] + x = quadWidth; + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + //[1 , 1] + y = quadHeight; + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + //[1 , 0] + x = 0; + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + } else { + //we're generating 3 systems in a triangle arrangement + //[0 , 0] + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + //[0 , 1] + x = quadWidth; + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + //[middle] + x = quadWidth / 2; + y = quadHeight; + this.quadrants.push(new GridRect(x, y, quadWidth, quadHeight)); + } + + //build sub-quadrants for each quadrant + for (let q = 0; q < this.quadrants.length; q++) { + const r = this.quadrants[q]; + + const subWidth = r.width / 2.0; + const subHeight = r.height / 2.0; + //insert for the corner sub-quadrants + //this will prefer this sub-quadrant for the home planet + + const r0Sub = new GridRect(r.x, r.y, subWidth, subHeight); + r.children.push(r0Sub); + + const r1Sub = new GridRect(r.x + subWidth, r.y, subWidth, subHeight); + if (q == 1) r.children.splice(0, 0, r1Sub); + else r.children.push(r1Sub); + + const r2Sub = new GridRect(r.x + subWidth, r.y + subHeight, subWidth, subHeight); + if (q == 2) r.children.splice(0, 0, r2Sub); + else r.children.push(r2Sub); + + const r3Sub = new GridRect(r.x, r.y + subHeight, subWidth, subHeight); + if (q == 3) r.children.splice(0, 0, r3Sub); + else r.children.push(r3Sub); + } + + //setup a dictionary for use later for assigning the Y CoOrd + const hexagonsByXCoOrd: Record = {}; + + const galaxySizeMultiplier = Grid.galaxySizeOptionToHexSizeMultiplier(width, options.galaxySize); + + const hexWidth = HexProperties.WIDTH * galaxySizeMultiplier; + const hexHeight = HexProperties.HEIGHT * galaxySizeMultiplier; + const hexSide = HexProperties.SIDE * galaxySizeMultiplier; + + let row = 0; + for (y = 0.0; y + hexHeight <= height; y += hexHeight / 2) { + let colId = 0; + + let offset = 0.0; + if (row % 2 == 1) { + offset = (hexWidth - hexSide) / 2 + hexSide; + colId = 1; + } + + for (x = offset; x + hexWidth <= width; x += hexWidth + hexSide) { + const hexData = { id: LETTERS[row] + (colId + 1), x, y, width: hexWidth, height: hexHeight, side: hexSide }; + h = new GridHex(hexData); + h.pathCoOrdX = colId; //the column is the x coordinate of the hex, for the y coordinate we need to get more fancy + this.hexes.push(h); + + if (!hexagonsByXCoOrd[colId]) hexagonsByXCoOrd[colId] = []; + hexagonsByXCoOrd[colId].push(h); + + colId += 2; + } + row++; + } + + //finally go through our list of hexagons by their x co-ordinate to assign the y co-ordinate + const keys = Object.keys(hexagonsByXCoOrd).map(Number); + for (x of keys) { + const hexagonsByX = hexagonsByXCoOrd[x]; + let yCoOrd = Math.floor(x / 2) + (x % 2); + for (h of hexagonsByX) { + h.pathCoOrdY = yCoOrd++; + } + } + + // build our hexByMidPoint index + for (const h of this.hexes) { + this.hexByMidPoint[this.getHexByMidPointKey(h.midPoint)] = h; + } + } + + private getHexByMidPointKey(p: PointData): string { + return `${p.x}.${p.y}`; + } + + public getHexAt(p: PointData) { + const key = this.getHexByMidPointKey(p); + if (key in this.hexByMidPoint) { + return this.hexByMidPoint[key]; + } + + //find the hex that contains this point + for (const h of this.hexes) { + if (h.contains(p)) { + return h; + } + } + + return null; + } + + public static galaxySizeOptionToHexSizeMultiplier(width: number, galaxySize: GalaxySizeOption) { + if (galaxySize === GalaxySizeOption.LARGE) { + return 1; + } + const hexesInRow = galaxySize == GalaxySizeOption.MEDIUM ? 5 : galaxySize == GalaxySizeOption.SMALL ? 4 : 3; + + //calculate hex multiplier + const multiplier = hexesInRow * HexProperties.WIDTH + hexesInRow * HexProperties.SIDE + HexProperties.X; + return width / multiplier; + } + + public static getHexDistanceForMidPoints(grid: Grid, mp1: PointData, mp2: PointData): number { + const h1 = grid.getHexAt(mp1); + const h2 = grid.getHexAt(mp2); + if (!h1 || !h2) { + throw new Error( + `Cannot find hex in getHexDistanceForMidPoints ${this.pointDataToString(mp1)}, ${this.pointDataToString(mp2)}`, + ); + } + return this.getHexDistance(h1, h2); + } + + public static getHexDistance(h1: GridHex, h2: GridHex) { + //a good explanation of this calc can be found here: + //http://playtechs.blogspot.com/2007/04/hex-grids.html + const deltaX = h1.pathCoOrdX! - h2.pathCoOrdX!; + const deltaY = h1.pathCoOrdY! - h2.pathCoOrdY!; + return (Math.abs(deltaX) + Math.abs(deltaY) + Math.abs(deltaX - deltaY)) / 2; + } + + public static pointDataToString(p: PointData): string { + return `(${p.x},${p.y})`; + } + + public static pointsAreEqual(p1: PointData | null, p2: PointData | null): boolean { + return p1?.x === p2?.x && p1?.y === p2?.y; + } +} + +export class GridRect { + x: number; + y: number; + width: number; + height: number; + left: number; + right: number; + top: number; + bottom: number; + children: GridRect[]; + constructor(x: number, y: number, width: number, height: number) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + this.left = x; + this.right = x + width; + this.top = y; + this.bottom = y + height; + this.children = []; + } + + public contains(point: PointData) { + return this.left < point.x && this.right > point.x && this.top < point.y && this.bottom > point.y; + } + + public toString() { + return `(x: ${this.x}, y: ${this.y}, w: ${this.width}, h: ${this.height}, children: ${this.children.length})`; + } +} + +export class GridHex { + data: HexagonData; + points: PointData[]; + + topLeftPoint: PointData; + topRightPoint: PointData; + bottomLeftPoint: PointData; + bottomRightPoint: PointData; + midPoint: PointData; + + pathCoOrdX: number | null = null; //x co-ordinate for distance finding + pathCoOrdY: number | null = null; //y co-ordinate for distance finding + + constructor(data: HexagonData) { + this.data = data; + this.points = []; + const { x, y, width, height, side } = data; + const x1 = (width - side) / 2; + const y1 = height / 2; + + this.points.push({ x: x1 + x, y }); + this.points.push({ x: x1 + side + x, y }); + this.points.push({ x: width + x, y: y1 + y }); + this.points.push({ x: x1 + side + x, y: height + y }); + this.points.push({ x: x1 + x, y: height + y }); + this.points.push({ x, y: y1 + y }); + + this.topLeftPoint = { x, y }; + this.topRightPoint = { x: x + width, y }; + this.bottomLeftPoint = { x, y: y + height }; + this.bottomRightPoint = { x: x + width, y: y + height }; + this.midPoint = { x: x + width / 2, y: y + height / 2 }; + } + + public isInHexBounds(p: PointData): boolean { + return ( + this.topLeftPoint.x < p.x && + this.topLeftPoint.y < p.y && + p.x < this.bottomRightPoint.x && + p.y < this.bottomRightPoint.y + ); + } + + //grabbed from: + //http://www.developingfor.net/c-20/testing-to-see-if-a-point-is-within-a-polygon.html + //and + //http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html#The%20C%20Code + public contains(p: PointData): boolean { + let isIn = false; + if (this.isInHexBounds(p)) { + //turn our absolute point into a relative point for comparing with the polygon's points + //let pRel = new Astriarch.Point(p.x - this.x, p.Y - this.y); + let i, + j = 0; + for (i = 0, j = this.points.length - 1; i < this.points.length; j = i++) { + const iP = this.points[i]; + const jP = this.points[j]; + if ( + ((iP.y <= p.y && p.y < jP.y) || (jP.y <= p.y && p.y < iP.y)) && + //((iP.y > p.y) != (jP.y > p.y)) + p.x < ((jP.x - iP.x) * (p.y - iP.y)) / (jP.y - iP.y) + iP.x + ) { + isIn = !isIn; + } + } + } + return isIn; + } +} diff --git a/packages/astriarch-engine/src/engine/index.ts b/packages/astriarch-engine/src/engine/index.ts new file mode 100644 index 00000000..ee42559b --- /dev/null +++ b/packages/astriarch-engine/src/engine/index.ts @@ -0,0 +1,19 @@ +// Export all modules from the engine directory +export * from './research'; +export * from './battleSimulator'; +export * from './clientGameModel'; +export * from './computerPlayer'; +export * from './events'; +export * from './fleet'; +export * from './gameController'; +export * from './gameModel'; +export * from './grid'; +export * from './planet'; +export * from './planetDistanceComparer'; +export * from './planetProductionItem'; +export * from './planetResourcePotentialComparer'; +export * from './planetResources'; +export * from './player'; +export * from './starShipAdvantageStrengthComparer'; +export * from './taskNotifications'; +export * from './tradingCenter'; diff --git a/packages/astriarch-engine/src/engine/planet.spec.ts b/packages/astriarch-engine/src/engine/planet.spec.ts new file mode 100644 index 00000000..d136461a --- /dev/null +++ b/packages/astriarch-engine/src/engine/planet.spec.ts @@ -0,0 +1,507 @@ +import { PlanetById } from '../model/clientModel'; +import { CitizenWorkerType, PlanetData, PlanetType, PlanetProductionItemType } from '../model/planet'; +import { PlayerData } from '../model/player'; +import { startNewTestGame, TestGameData } from '../test/testUtils'; +import { ClientGameModel } from './clientGameModel'; +import { Planet } from './planet'; + +let testGameData: TestGameData; +let player1: PlayerData; +let testPlanet: PlanetData; +let planetById: PlanetById; + +describe('Planet', function () { + beforeEach(() => { + testGameData = startNewTestGame(); + player1 = testGameData.gameModel.modelData.players[0]; + planetById = ClientGameModel.getPlanetByIdIndex(testGameData.gameModel.modelData.planets); + + // Get the player's home planet for testing + testPlanet = testGameData.gameModel.modelData.planets.find((p) => p.id === player1.homePlanetId)!; + + // Set up a predictable population for testing (3 farmers, 2 miners, 1 builder) + testPlanet.population = [ + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Farmer }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Farmer }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Farmer }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Miner }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Miner }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Builder }, + ]; + }); + + describe('countPopulationWorkerTypes()', () => { + it('should correctly count worker types', () => { + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(3); + expect(counts.miners).toBe(2); + expect(counts.builders).toBe(1); + }); + + it('should handle empty population', () => { + testPlanet.population = []; + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(0); + expect(counts.miners).toBe(0); + expect(counts.builders).toBe(0); + }); + + it('should ignore protesting citizens', () => { + // Make one citizen protest + testPlanet.population[0].protestLevel = 1; + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(2); // Should be 2 instead of 3 + expect(counts.miners).toBe(2); + expect(counts.builders).toBe(1); + }); + }); + + describe('updatePopulationWorkerTypesByDiff()', () => { + it('should successfully move workers when diffs sum to zero', () => { + // Move 1 farmer to miner (+1 miner, -1 farmer) + Planet.updatePopulationWorkerTypesByDiff(testPlanet, player1, -1, 1, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(2); + expect(counts.miners).toBe(3); + expect(counts.builders).toBe(1); + }); + + it('should move from most populated type when increasing', () => { + // Start: 3 farmers, 2 miners, 1 builder + // Move 1 worker to builder from farmers (most populated) + Planet.updatePopulationWorkerTypesByDiff(testPlanet, player1, -1, 0, 1); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(2); + expect(counts.miners).toBe(2); + expect(counts.builders).toBe(2); + }); + + it('should handle complex multi-type transfers', () => { + // Start: 3 farmers, 2 miners, 1 builder + // Move 2 farmers to 1 miner and 1 builder + Planet.updatePopulationWorkerTypesByDiff(testPlanet, player1, -2, 1, 1); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(1); + expect(counts.miners).toBe(3); + expect(counts.builders).toBe(2); + }); + + it('should error and return early when diffs do not sum to zero', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + + // This should fail because 1 + 1 + 0 = 2 (not zero) + Planet.updatePopulationWorkerTypesByDiff(testPlanet, player1, 1, 1, 0); + + expect(consoleSpy).toHaveBeenCalledWith( + "Couldn't move workers in Planet.updatePopulationWorkerTypesByDiff!", + 1, + 1, + 0, + ); + + // Population should remain unchanged + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(3); + expect(counts.miners).toBe(2); + expect(counts.builders).toBe(1); + + consoleSpy.mockRestore(); + }); + + it('should handle zero diffs (no change)', () => { + Planet.updatePopulationWorkerTypesByDiff(testPlanet, player1, 0, 0, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(3); + expect(counts.miners).toBe(2); + expect(counts.builders).toBe(1); + }); + }); + + describe('updatePopulationWorkerTypes()', () => { + it('should handle simple single worker type increase', () => { + // Add 1 farmer (should take from miners - highest non-farmer count) + Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, 0, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(4); + expect(counts.miners).toBe(1); // Reduced from 2 + expect(counts.builders).toBe(1); + }); + + it('should handle single worker type decrease', () => { + // Remove 1 farmer (should go to builders - lowest count) + Planet.updatePopulationWorkerTypes(testPlanet, player1, -1, 0, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(2); + expect(counts.miners).toBe(2); + expect(counts.builders).toBe(2); // Increased from 1 + }); + + it('should prioritize keeping requested increases when over population limit', () => { + // Try to add workers that would exceed population + // Start: 3f, 2m, 1b = 6 total + // Request: +2f, +2m, +2b = would need 12 total, but only have 6 + Planet.updatePopulationWorkerTypes(testPlanet, player1, 2, 2, 2); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + const total = counts.farmers + counts.miners + counts.builders; + + // Should still have exactly 6 workers total + expect(total).toBe(6); + + // Given the algorithm, all types will get increases, but with reductions + // The exact distribution depends on the sorting algorithm, but total should be 6 + expect(counts.farmers + counts.miners + counts.builders).toBe(6); + }); + + it('should handle negative values by adjusting assignments appropriately', () => { + // Try to set farmers to negative (should become 0) + // Start with 3f, 2m, 1b = 6 total + Planet.updatePopulationWorkerTypes(testPlanet, player1, -10, 0, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + + // Farmers should be minimized (possibly 0, but algorithm distributes workers) + expect(counts.farmers).toBeGreaterThanOrEqual(0); + + // Total should still be 6 (all population assigned) + expect(counts.farmers + counts.miners + counts.builders).toBe(6); + + // Other workers should get the redistributed population + expect(counts.miners + counts.builders).toBeGreaterThan(3); + }); + + it('should distribute unassigned population to lowest assignments', () => { + // Start with: 3f, 2m, 1b = 6 total + // Request: -1f, -1m, 0b = would result in 2f, 1m, 1b = 4 total + // Should auto-assign 2 remaining workers to balance (builders has least) + Planet.updatePopulationWorkerTypes(testPlanet, player1, -1, -1, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + const total = counts.farmers + counts.miners + counts.builders; + + // Should still use all 6 workers + expect(total).toBe(6); + + // Builders should have gotten extra workers (lowest starting count) + expect(counts.builders).toBeGreaterThan(1); + }); + + it('should handle mixed increase/decrease operations', () => { + // Start: 3f, 2m, 1b + // Request: +1f, -1m, +0b + // Should result in taking 1 from miner, giving to farmer + Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, -1, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(4); + expect(counts.miners).toBe(1); + expect(counts.builders).toBe(1); + }); + + it('should work with zero population planet', () => { + testPlanet.population = []; + + Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, 0, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(0); + expect(counts.miners).toBe(0); + expect(counts.builders).toBe(0); + }); + + it('should maintain total population count', () => { + const originalTotal = testPlanet.population.length; + + // Try various operations + Planet.updatePopulationWorkerTypes(testPlanet, player1, 2, -1, 1); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + const newTotal = counts.farmers + counts.miners + counts.builders; + + expect(newTotal).toBe(originalTotal); + }); + + it('should handle equal distribution when all assignments are equal', () => { + // Set up equal assignments: 2f, 2m, 2b + testPlanet.population = [ + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Farmer }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Farmer }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Miner }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Miner }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Builder }, + { populationChange: 0, loyalToPlayerId: player1.id, protestLevel: 0, workerType: CitizenWorkerType.Builder }, + ]; + + // Add 1 farmer + Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, 0, 0); + + const counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(3); + expect(counts.miners + counts.builders).toBe(3); // One should have decreased + }); + }); + + describe('integration tests', () => { + it('should handle realistic game scenarios', () => { + // Simulate player clicking +1 farmer button multiple times + const initial = Planet.countPopulationWorkerTypes(testPlanet); + + // Click +1 farmer + Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, 0, 0); + let counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(initial.farmers + 1); + + // Click +1 farmer again + Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, 0, 0); + counts = Planet.countPopulationWorkerTypes(testPlanet); + expect(counts.farmers).toBe(initial.farmers + 2); + + // Click -1 miner + Planet.updatePopulationWorkerTypes(testPlanet, player1, 0, -1, 0); + counts = Planet.countPopulationWorkerTypes(testPlanet); + + // Total should still be the same + const total = counts.farmers + counts.miners + counts.builders; + expect(total).toBe(testPlanet.population.length); + }); + + it('should return resource generation data', () => { + const result = Planet.updatePopulationWorkerTypes(testPlanet, player1, 1, 0, 0); + + expect(result).toBeDefined(); + expect(result.amountPerTurn).toBeDefined(); + expect(result.baseAmountPerWorkerPerTurn).toBeDefined(); + }); + }); + + describe('spendResources()', () => { + let homePlanet: PlanetData; + let remotePlanet: PlanetData; + + beforeEach(() => { + // Use the existing test planet as home planet + homePlanet = testPlanet; + + // Set specific resource amounts for testing + homePlanet.resources.energy = 50; + homePlanet.resources.food = 30; + homePlanet.resources.ore = 20; + homePlanet.resources.iridium = 10; + + // Create a second planet for the player + remotePlanet = { + id: 999, + name: 'Remote Planet', + type: PlanetType.PlanetClass1, + population: [], + buildQueue: [], + builtImprovements: homePlanet.builtImprovements, + maxImprovements: 6, + resources: { + energy: 10, + food: 100, + ore: 80, + iridium: 60, + research: 5, + production: 15, + }, + originPoint: { x: 50, y: 50 }, + boundingHexMidPoint: { x: 60, y: 60 }, + planetaryFleet: homePlanet.planetaryFleet, + outgoingFleets: [], + planetHappiness: homePlanet.planetHappiness, + starshipTypeLastBuilt: null, + starshipCustomShipLastBuilt: false, + buildLastStarship: true, + waypointBoundingHexMidPoint: null, + }; + + // Add the remote planet to the player's owned planets + player1.ownedPlanetIds.push(remotePlanet.id); + planetById[remotePlanet.id] = remotePlanet; + }); + + describe('when spending resources on the same planet', () => { + it('should spend resources from the local planet only', () => { + const initialEnergy = homePlanet.resources.energy; + const initialOre = homePlanet.resources.ore; + const initialIridium = homePlanet.resources.iridium; + + // Spend some resources that are available locally + Planet.spendResources(testGameData.gameModel.grid, player1, planetById, homePlanet, 10, 0, 5, 3); + + // Resources should be deducted from home planet + expect(homePlanet.resources.energy).toBe(initialEnergy - 10); + expect(homePlanet.resources.ore).toBe(initialOre - 5); + expect(homePlanet.resources.iridium).toBe(initialIridium - 3); + + // Remote planet should be unchanged + expect(remotePlanet.resources.energy).toBe(10); + expect(remotePlanet.resources.ore).toBe(80); + expect(remotePlanet.resources.iridium).toBe(60); + }); + }); + + describe('when spending resources that require shipping from other planets', () => { + it('should spend ore from remote planets when local planet lacks ore', () => { + const initialHomePlanetOre = homePlanet.resources.ore; // 20 + const initialRemotePlanetOre = remotePlanet.resources.ore; // 80 + + // Try to spend 30 ore (more than home planet has) + Planet.spendResources(testGameData.gameModel.grid, player1, planetById, homePlanet, 0, 0, 30, 0); + + // Home planet should be drained of ore + expect(homePlanet.resources.ore).toBe(0); + // Remote planet should have ore deducted + expect(remotePlanet.resources.ore).toBe(initialRemotePlanetOre - (30 - initialHomePlanetOre)); + expect(remotePlanet.resources.ore).toBe(70); // 80 - 10 = 70 + }); + + it('should spend iridium from remote planets when local planet lacks iridium', () => { + const initialHomePlanetIridium = homePlanet.resources.iridium; // 10 + const initialRemotePlanetIridium = remotePlanet.resources.iridium; // 60 + + // Try to spend 25 iridium (more than home planet has) + Planet.spendResources(testGameData.gameModel.grid, player1, planetById, homePlanet, 0, 0, 0, 25); + + // Home planet should be drained of iridium + expect(homePlanet.resources.iridium).toBe(0); + // Remote planet should have iridium deducted + expect(remotePlanet.resources.iridium).toBe(initialRemotePlanetIridium - (25 - initialHomePlanetIridium)); + expect(remotePlanet.resources.iridium).toBe(45); // 60 - 15 = 45 + }); + + it('should spend energy from remote planets when local planet lacks energy', () => { + const initialHomePlanetEnergy = homePlanet.resources.energy; // 50 + const initialRemotePlanetEnergy = remotePlanet.resources.energy; // 10 + + // Try to spend 55 energy (more than home planet has) + Planet.spendResources(testGameData.gameModel.grid, player1, planetById, homePlanet, 55, 0, 0, 0); + + // Home planet should be drained of energy + expect(homePlanet.resources.energy).toBe(0); + // Remote planet should have energy deducted + expect(remotePlanet.resources.energy).toBe(initialRemotePlanetEnergy - (55 - initialHomePlanetEnergy)); + expect(remotePlanet.resources.energy).toBe(5); // 10 - 5 = 5 + }); + + it('should spend food from remote planets when local planet lacks food', () => { + const initialHomePlanetFood = homePlanet.resources.food; // 30 + const initialRemotePlanetFood = remotePlanet.resources.food; // 100 + + // Try to spend 80 food (more than home planet has) + Planet.spendResources(testGameData.gameModel.grid, player1, planetById, homePlanet, 0, 80, 0, 0); + + // Home planet should be drained of food + expect(homePlanet.resources.food).toBe(0); + // Remote planet should have food deducted + expect(remotePlanet.resources.food).toBe(initialRemotePlanetFood - (80 - initialHomePlanetFood)); + expect(remotePlanet.resources.food).toBe(50); // 100 - 50 = 50 + }); + }); + + describe('when there are insufficient resources across all planets', () => { + it('should spend what it can and warn about insufficient resources', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + // Try to spend more ore than available across all planets (20 + 80 = 100 available, asking for 150) + Planet.spendResources(testGameData.gameModel.grid, player1, planetById, homePlanet, 0, 0, 150, 0); + + // All ore should be spent + expect(homePlanet.resources.ore).toBe(0); + expect(remotePlanet.resources.ore).toBe(0); + + // Should warn about insufficient resources + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Problem spending energy, food, ore and iridium as necessary!'), + player1.name, + homePlanet.name, + 0, // energyNeeded + 0, // foodNeeded + 50, // oreNeeded (150 - 100 available) + 0, // iridiumNeeded + ); + + consoleSpy.mockRestore(); + }); + }); + }); + + describe('removeBuildQueueItemForRefund()', () => { + beforeEach(() => { + // Add some items to the build queue for testing + testPlanet.buildQueue = [ + { + itemType: PlanetProductionItemType.PlanetImprovement, + baseProductionCost: 100, + productionCostComplete: 50, + energyCost: 10, + oreCost: 5, + iridiumCost: 3, + resourcesSpent: true, + turnsToComplete: 1, + }, + { + itemType: PlanetProductionItemType.PlanetImprovement, + baseProductionCost: 200, + productionCostComplete: 25, + energyCost: 20, + oreCost: 10, + iridiumCost: 6, + resourcesSpent: true, + turnsToComplete: 2, + }, + ]; + }); + + it('should return false for negative index', () => { + const result = Planet.removeBuildQueueItemForRefund(testPlanet, -1); + expect(result).toBe(false); + expect(testPlanet.buildQueue.length).toBe(2); // Should be unchanged + }); + + it('should return false for index equal to array length (out of bounds)', () => { + const result = Planet.removeBuildQueueItemForRefund(testPlanet, 2); // array length is 2, so index 2 is out of bounds + expect(result).toBe(false); + expect(testPlanet.buildQueue.length).toBe(2); // Should be unchanged + }); + + it('should return false for index greater than array length', () => { + const result = Planet.removeBuildQueueItemForRefund(testPlanet, 5); + expect(result).toBe(false); + expect(testPlanet.buildQueue.length).toBe(2); // Should be unchanged + }); + + it('should successfully remove item at valid index and provide refund', () => { + const initialEnergy = testPlanet.resources.energy; + const initialOre = testPlanet.resources.ore; + const initialIridium = testPlanet.resources.iridium; + + const result = Planet.removeBuildQueueItemForRefund(testPlanet, 0); + + expect(result).toBe(true); + expect(testPlanet.buildQueue.length).toBe(1); // Should have one less item + + // Should have received a refund (partial since item was 50% complete) + expect(testPlanet.resources.energy).toBeGreaterThan(initialEnergy); + expect(testPlanet.resources.ore).toBeGreaterThan(initialOre); + expect(testPlanet.resources.iridium).toBeGreaterThan(initialIridium); + }); + + it('should remove the correct item when removing from middle of queue', () => { + const originalSecondItem = testPlanet.buildQueue[1]; + + const result = Planet.removeBuildQueueItemForRefund(testPlanet, 0); + + expect(result).toBe(true); + expect(testPlanet.buildQueue.length).toBe(1); + expect(testPlanet.buildQueue[0]).toBe(originalSecondItem); // Second item should now be first + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/planet.ts b/packages/astriarch-engine/src/engine/planet.ts new file mode 100644 index 00000000..4e3803b4 --- /dev/null +++ b/packages/astriarch-engine/src/engine/planet.ts @@ -0,0 +1,886 @@ +import { ClientPlanet, PlanetById } from '../model/clientModel'; +import { EarnedPointsType } from '../model/earnedPoints'; +import { EventNotificationType } from '../model/eventNotification'; +import { StarShipType } from '../model/fleet'; +import { + Citizen, + CitizenWorkerType, + PlanetData, + PlanetHappinessType, + PlanetImprovementType, + PlanetProductionItemData, + PlanetProductionItemType, + PlanetResourceData, + PlanetType, + ProductionItemResources, +} from '../model/planet'; +import { PlayerData } from '../model/player'; +import { ResearchType } from '../model/research'; +import { PointData } from '../shapes/shapes'; +import { GameTools } from '../utils/gameTools'; +import { Utils } from '../utils/utils'; +import { Events } from './events'; +import { Fleet } from './fleet'; +import { Grid, GridHex } from './grid'; +import { PlanetDistanceComparer } from './planetDistanceComparer'; +import { PlanetProductionItem } from './planetProductionItem'; +import { PlanetResources } from './planetResources'; +import { Player } from './player'; +import { Research } from './research'; + +export interface PlanetPerTurnResourceGeneration { + baseAmountPerWorkerPerTurn: PlanetResourceData; + amountPerTurn: PlanetResourceData; + amountPerWorkerPerTurn: PlanetResourceData; + amountNextWorkerPerTurn: PlanetResourceData; +} + +export interface PopulationAssignments { + farmers: number; + miners: number; + builders: number; +} + +export interface PopulationByContentment { + protesting: Citizen[]; + content: Citizen[]; +} + +export class Planet { + private static NEXT_PLANET_ID = 1; + private static PLANET_SIZE = 20.0; + private static IMPROVEMENT_RATIO = 2.0; + + public static constructPlanet( + type: PlanetType, + name: string, + boundingHex: GridHex, + initialOwner?: PlayerData, + ): PlanetData { + const halfPlanetSize = Planet.PLANET_SIZE / 2; + const originPoint = { x: boundingHex.midPoint.x - halfPlanetSize, y: boundingHex.midPoint.y - halfPlanetSize }; + const builtImprovements = { + [PlanetImprovementType.Colony]: 0, + [PlanetImprovementType.Factory]: 0, + [PlanetImprovementType.Farm]: 0, + [PlanetImprovementType.Mine]: 0, + }; + + const population: Citizen[] = []; + const resources = PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 0); + if (initialOwner) { + population.push(Planet.constructCitizen(type, initialOwner.id)); + population.push(Planet.constructCitizen(type, initialOwner.id)); + resources.energy = 3; + resources.food = 4; + resources.ore = 2; + resources.iridium = 1; + } + + let maxImprovements = 0; + let planetaryFleet; + + //set our max slots for improvements and build an initial defense fleet + switch (type) { + case PlanetType.AsteroidBelt: + maxImprovements = 3; + planetaryFleet = Fleet.generateInitialFleet(0, boundingHex.midPoint); + break; + case PlanetType.DeadPlanet: + maxImprovements = 5; + planetaryFleet = Fleet.generateInitialFleet(Utils.nextRandom(3, 5), boundingHex.midPoint); + break; + case PlanetType.PlanetClass1: + maxImprovements = 6; + planetaryFleet = Fleet.generateInitialFleet(Utils.nextRandom(4, 6), boundingHex.midPoint); + break; + case PlanetType.PlanetClass2: + maxImprovements = 9; + planetaryFleet = Fleet.generateInitialFleet(Utils.nextRandom(10, 15), boundingHex.midPoint); + break; + } + + const planetData = { + id: Planet.NEXT_PLANET_ID++, + name, + type, + population, + buildQueue: [], + builtImprovements, + maxImprovements, + resources, + originPoint, + boundingHexMidPoint: boundingHex.midPoint, + planetaryFleet: planetaryFleet!, + outgoingFleets: [], + planetHappiness: PlanetHappinessType.Normal, + starshipTypeLastBuilt: null, + starshipCustomShipLastBuilt: false, + buildLastStarship: true, + waypointBoundingHexMidPoint: null, + }; + + Planet.generateResources(planetData, 1.0, initialOwner); + + return planetData; + } + + public static generateResources(p: PlanetData, cyclesElapsed: number, owner?: PlayerData) { + const rpt = Planet.getPlanetWorkerResourceGeneration(p, owner); + + if (owner) { + let resourceDivisor = 1.0; + let developmentDivisor = 1.0; + if (p.planetHappiness == PlanetHappinessType.Unrest) { + //unrest causes 1/2 resource production & 1/4 development + resourceDivisor = 2.0; + developmentDivisor = 4.0; + } else if (p.planetHappiness == PlanetHappinessType.Riots) { + //riots cause 1/4 resource production & 1/8 development + resourceDivisor = 4.0; + developmentDivisor = 8.0; + } + p.resources.food += (rpt.amountPerTurn.food * cyclesElapsed) / resourceDivisor; + p.resources.ore += (rpt.amountPerTurn.ore * cyclesElapsed) / resourceDivisor; + p.resources.iridium += (rpt.amountPerTurn.iridium * cyclesElapsed) / resourceDivisor; + p.resources.production += (rpt.amountPerTurn.production * cyclesElapsed) / developmentDivisor; + const maxCredits = Planet.getTaxRevenueAtMaxPercent(p, owner); + const { creditAmountEarnedPerTurn, researchAmountEarnedPerTurn } = + Research.getCreditAndResearchAmountEarnedPerTurn(owner.research, maxCredits); + p.resources.energy += (creditAmountEarnedPerTurn * cyclesElapsed) / resourceDivisor; + //add a bit more research completed based on a random factor + const additionalResearchBreakthrough = Utils.nextRandom(0, researchAmountEarnedPerTurn * 0.1); + p.resources.research += + ((researchAmountEarnedPerTurn + additionalResearchBreakthrough) * cyclesElapsed) / developmentDivisor; + } + } + + public static spendResources( + grid: Grid, + player: PlayerData, + planetById: PlanetById, + planet: PlanetData, + energy: number, + food: number, + ore: number, + iridium: number, + ) { + const { resources } = planet; + //first check for required energy, food, ore and iridium on this planet + let energyNeeded = energy - PlanetResources.spendEnergyAsPossible(resources, energy); + let foodNeeded = food - PlanetResources.spendFoodAsPossible(resources, food); + let oreNeeded = ore - PlanetResources.spendOreAsPossible(resources, ore); + let iridiumNeeded = iridium - PlanetResources.spendIridiumAsPossible(resources, iridium); + + if (energyNeeded !== 0 || foodNeeded !== 0 || oreNeeded !== 0 || iridiumNeeded !== 0) { + const ownedPlanets: PlanetData[] = []; + for (const id of player.ownedPlanetIds) { + const ownedPlanet = planetById[id]; + if (planet.id != ownedPlanet.id) { + ownedPlanets.push(ownedPlanet); + } + } + //get closest planets to source resources for, we don't charge for shipping ore or iridium + const planetDistanceComparer = new PlanetDistanceComparer(grid, planet); + ownedPlanets.sort((a, b) => planetDistanceComparer.sortFunction(a, b)); + + for (const p of ownedPlanets) { + if (energyNeeded !== 0) energyNeeded -= PlanetResources.spendEnergyAsPossible(p.resources, energyNeeded); + if (foodNeeded !== 0) foodNeeded -= PlanetResources.spendFoodAsPossible(p.resources, foodNeeded); + if (oreNeeded !== 0) oreNeeded -= PlanetResources.spendOreAsPossible(p.resources, oreNeeded); + if (iridiumNeeded !== 0) iridiumNeeded -= PlanetResources.spendIridiumAsPossible(p.resources, iridiumNeeded); + + if (energyNeeded === 0 && foodNeeded === 0 && oreNeeded === 0 && iridiumNeeded === 0) break; + } + if (energyNeeded !== 0 || foodNeeded !== 0 || oreNeeded !== 0 || iridiumNeeded !== 0) { + console.warn( + 'Problem spending energy, food, ore and iridium as necessary! ', + player.name, + planet.name, + energyNeeded, + foodNeeded, + oreNeeded, + iridiumNeeded, + ); + } + } + } + + /** + * Adds an item to the build queue and reduces the players resources based on the cost + */ + public static enqueueProductionItemAndSpendResources( + grid: Grid, + player: PlayerData, + planetById: PlanetById, + planet: PlanetData, + item: PlanetProductionItemData, + ) { + planet.buildQueue.push(item); + + this.spendResources(grid, player, planetById, planet, item.energyCost, 0, item.oreCost, item.iridiumCost); + item.resourcesSpent = true; + } + + /** + * Removes an item from the build queue and returns resources to the planet based on how far the item is along in being built + */ + public static removeBuildQueueItemForRefund(planet: PlanetData, index: number): boolean { + if (index < 0 || index >= planet.buildQueue.length) { + return false; + } + const [item] = planet.buildQueue.splice(index, 1); + const refundAmount = this.getRefundAmount(item); + + planet.resources.energy += refundAmount.energyCost; + planet.resources.ore += refundAmount.oreCost; + planet.resources.iridium += refundAmount.iridiumCost; + return true; + } + + /** + * returns how many resources should be refunded when this improvement is canceled + */ + public static getRefundAmount(item: PlanetProductionItemData): ProductionItemResources { + let energyCost = 0; + let oreCost = 0; + let iridiumCost = 0; + if (item.resourcesSpent) { + //give refund + const refundPercent = 1 - item.productionCostComplete / (item.baseProductionCost * 1.0); + energyCost = item.energyCost * refundPercent; + oreCost = item.oreCost * refundPercent; + iridiumCost = item.iridiumCost * refundPercent; + } + + return { energyCost, oreCost, iridiumCost }; + } + + public static getTaxRevenueAtMaxPercent(p: PlanetData, owner: PlayerData) { + //determine tax revenue (energy credits) + const baseAmountPerPopPerTurn = p.id === owner.homePlanetId ? 2.0 : 1.0; + let amountPerTurn = p.population.length * baseAmountPerPopPerTurn; + const colonyCount = p.builtImprovements[PlanetImprovementType.Colony]; + if (colonyCount) { + const colonyBoost = Research.getResearchBoostForEfficiencyImprovement( + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES, + owner, + ); + amountPerTurn += + Math.min(colonyCount, p.population.length) * baseAmountPerPopPerTurn * colonyBoost * Planet.IMPROVEMENT_RATIO; + } + + return amountPerTurn / 1.5; + } + + public static constructCitizen(planetType: PlanetType, loyalToPlayerId: string | undefined): Citizen { + const workerType = [PlanetType.AsteroidBelt, PlanetType.DeadPlanet].includes(planetType) + ? CitizenWorkerType.Miner + : CitizenWorkerType.Farmer; + + return { + populationChange: 0, + loyalToPlayerId, + protestLevel: 0, + workerType, + }; + } + + public static getPlanetWorkerResourceGeneration(p: PlanetData, owner?: PlayerData): PlanetPerTurnResourceGeneration { + const rpt = { + baseAmountPerWorkerPerTurn: PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 2.0), + amountPerTurn: PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 0), + amountPerWorkerPerTurn: PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 0), + amountNextWorkerPerTurn: PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 0), // Potential extra resources if player adds a farmer or miner + }; + + //this is the initial/base planet resource production + //base values by planet type: + switch (p.type) { + case PlanetType.AsteroidBelt: + rpt.baseAmountPerWorkerPerTurn.food = 0.5; + rpt.baseAmountPerWorkerPerTurn.ore = 1.75; + rpt.baseAmountPerWorkerPerTurn.iridium = 1.0; + break; + case PlanetType.DeadPlanet: + rpt.baseAmountPerWorkerPerTurn.food = 1.0; + rpt.baseAmountPerWorkerPerTurn.ore = 1.5; + rpt.baseAmountPerWorkerPerTurn.iridium = 0.5; + break; + case PlanetType.PlanetClass1: + rpt.baseAmountPerWorkerPerTurn.food = 1.5; + rpt.baseAmountPerWorkerPerTurn.ore = 0.5; + rpt.baseAmountPerWorkerPerTurn.iridium = 0.375; + break; + case PlanetType.PlanetClass2: + rpt.baseAmountPerWorkerPerTurn.food = 2.0; + rpt.baseAmountPerWorkerPerTurn.ore = 0.25; + rpt.baseAmountPerWorkerPerTurn.iridium = 0.125; + break; + } + + const { farmers, miners, builders } = Planet.countPopulationWorkerTypes(p); + + if (farmers < p.population.length) { + rpt.amountNextWorkerPerTurn.food = rpt.baseAmountPerWorkerPerTurn.food; + } + + if (miners < p.population.length) { + rpt.amountNextWorkerPerTurn.ore = rpt.baseAmountPerWorkerPerTurn.ore; + rpt.amountNextWorkerPerTurn.iridium = rpt.baseAmountPerWorkerPerTurn.iridium; + } + + if (builders < p.population.length) { + rpt.amountNextWorkerPerTurn.production = rpt.baseAmountPerWorkerPerTurn.production; + } + + //determine production per turn + rpt.amountPerTurn.food = rpt.baseAmountPerWorkerPerTurn.food * farmers; + rpt.amountPerTurn.ore = rpt.baseAmountPerWorkerPerTurn.ore * miners; + rpt.amountPerTurn.iridium = rpt.baseAmountPerWorkerPerTurn.iridium * miners; + rpt.amountPerTurn.production = rpt.baseAmountPerWorkerPerTurn.production * builders; + + if (Planet.builtImprovementCount(p) > 0) { + const farmCount = p.builtImprovements[PlanetImprovementType.Farm]; + const mineCount = p.builtImprovements[PlanetImprovementType.Mine]; + const factoryCount = p.builtImprovements[PlanetImprovementType.Factory]; + + let researchEffectiveness = null; + if (farmCount > 0) { + researchEffectiveness = Research.getResearchBoostForEfficiencyImprovement( + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS, + owner, + ); + if (farmers < farmCount) { + rpt.amountNextWorkerPerTurn.food = + rpt.baseAmountPerWorkerPerTurn.food * Planet.IMPROVEMENT_RATIO * researchEffectiveness; + } + + if (farmers > 0) { + rpt.amountPerTurn.food += + Math.min(farmCount, farmers) * + rpt.baseAmountPerWorkerPerTurn.food * + Planet.IMPROVEMENT_RATIO * + researchEffectiveness; + } + } + if (mineCount > 0) { + researchEffectiveness = Research.getResearchBoostForEfficiencyImprovement( + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES, + owner, + ); + if (miners < mineCount) { + rpt.amountNextWorkerPerTurn.ore = + rpt.baseAmountPerWorkerPerTurn.ore * Planet.IMPROVEMENT_RATIO * researchEffectiveness; + rpt.amountNextWorkerPerTurn.iridium = + rpt.baseAmountPerWorkerPerTurn.iridium * Planet.IMPROVEMENT_RATIO * researchEffectiveness; + } + + if (miners > 0) { + rpt.amountPerTurn.ore += + Math.min(mineCount, miners) * + rpt.baseAmountPerWorkerPerTurn.ore * + Planet.IMPROVEMENT_RATIO * + researchEffectiveness; + rpt.amountPerTurn.iridium += + Math.min(mineCount, miners) * + rpt.baseAmountPerWorkerPerTurn.iridium * + Planet.IMPROVEMENT_RATIO * + researchEffectiveness; + } + } + if (factoryCount > 0) { + researchEffectiveness = Research.getResearchBoostForEfficiencyImprovement( + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES, + owner, + ); + if (builders < factoryCount) { + rpt.amountNextWorkerPerTurn.production = + rpt.baseAmountPerWorkerPerTurn.production * Planet.IMPROVEMENT_RATIO * researchEffectiveness; + } + + if (builders > 0) { + rpt.amountPerTurn.production += + Math.min(factoryCount, builders) * + rpt.baseAmountPerWorkerPerTurn.production * + Planet.IMPROVEMENT_RATIO * + researchEffectiveness; + } + } + } + + rpt.amountPerWorkerPerTurn.food = farmers ? rpt.amountPerTurn.food / farmers : 0; + rpt.amountPerWorkerPerTurn.ore = miners ? rpt.amountPerTurn.ore / miners : 0; + rpt.amountPerWorkerPerTurn.iridium = miners ? rpt.amountPerTurn.iridium / miners : 0; + rpt.amountPerWorkerPerTurn.production = builders ? rpt.amountPerTurn.production / builders : 0; + + return rpt; + } + + public static builtImprovementCount(p: PlanetData): number { + return ( + p.builtImprovements[PlanetImprovementType.Colony] + + p.builtImprovements[PlanetImprovementType.Factory] + + p.builtImprovements[PlanetImprovementType.Farm] + + p.builtImprovements[PlanetImprovementType.Mine] + ); + } + + public static countPopulationWorkerTypes(p: PlanetData): PopulationAssignments { + const pop: PopulationAssignments = { + farmers: 0, + miners: 0, + builders: 0, + }; + + const citizens = Planet.getPopulationByContentment(p); + + return citizens.content.reduce((accum, curr) => { + if (curr.workerType === CitizenWorkerType.Farmer) { + accum.farmers++; + } else if (curr.workerType === CitizenWorkerType.Miner) { + accum.miners++; + } else if (curr.workerType === CitizenWorkerType.Builder) { + accum.builders++; + } + return accum; + }, pop); + } + + public static getPopulationByContentment(p: PlanetData): PopulationByContentment { + const protesting: Citizen[] = []; + const content: Citizen[] = []; + for (const citizen of p.population) { + if (citizen.protestLevel > 0) { + protesting.push(citizen); + } else { + content.push(citizen); + } + } + return { protesting, content }; + } + + /** + * Gets the speed that population is currently growing on this planet + * @returns {number} a decimal value of the fraction added to the population each turn + * @constructor + */ + public static getPopulationGrowthRate(p: PlanetData, owner: PlayerData, cyclesElapsed: number) { + const popCount = p.population.length; + const maxPop = Planet.maxPopulation(p); + let growthRate = 0; + //check if we can grow + if (popCount > 0 && p.planetHappiness != PlanetHappinessType.Riots && popCount < maxPop) { + const openSlots = maxPop - popCount; + let maxProcreation = popCount / 8.0; + const colonyCount = p.builtImprovements[PlanetImprovementType.Colony]; + if (owner && colonyCount) { + maxProcreation *= + Research.getResearchBoostForEfficiencyImprovement(ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES) * + colonyCount; + } + //when there are 2 open slots per pop, then the maximum growth rate is achieved per population + growthRate = maxProcreation * Math.min(openSlots / popCount, 2.0); + if (p.planetHappiness === PlanetHappinessType.Unrest) + //unrest slows pop growth + growthRate = growthRate / 2.0; + } + return growthRate * cyclesElapsed; + } + + public static getCitizenByType(p: PlanetData, desiredType: CitizenWorkerType): Citizen { + const citizens = this.getPopulationByContentment(p); + for (const c of citizens.content) { + if (c.workerType === desiredType) return c; + } + for (const c of citizens.protesting) { + if (c.workerType === desiredType) { + console.warn('No content citizens found in Planet.getCitizenByType! Returning protesting citizen.'); + return c; + } + } + throw new Error('No matching citizens found in Planet.getCitizenByType!'); + } + + public static maxPopulation(p: PlanetData) { + return p.maxImprovements + p.builtImprovements[PlanetImprovementType.Colony]; + } + + public static getPlanetAtMidPoint(planets: PlanetData[], midPoint: PointData): PlanetData | undefined { + return planets.find((p) => Grid.pointsAreEqual(p.boundingHexMidPoint, midPoint)); + } + + public static getClientPlanetAtMidPoint(planets: ClientPlanet[], midPoint: PointData): ClientPlanet | undefined { + return planets.find((p) => Grid.pointsAreEqual(p.boundingHexMidPoint, midPoint)); + } + + public static recallOutgoingFleets(p: PlanetData) { + for (const f of p.outgoingFleets) { + Fleet.landFleet(p.planetaryFleet, f); + } + p.outgoingFleets = []; + } + + /** + * Updates population worker assignments based on diffs that don't need to sum to zero + * Automatically redistributes workers to/from other assignments as needed + */ + public static updatePopulationWorkerTypes( + p: PlanetData, + owner: PlayerData, + farmerDiff: number, + minerDiff: number, + builderDiff: number, + ) { + const current = this.countPopulationWorkerTypes(p); + + // Calculate what we want to achieve + let desiredFarmers = Math.max(0, current.farmers + farmerDiff); + let desiredMiners = Math.max(0, current.miners + minerDiff); + let desiredBuilders = Math.max(0, current.builders + builderDiff); + + const totalPopulation = p.population.length; + const desiredTotal = desiredFarmers + desiredMiners + desiredBuilders; + + // If we're trying to assign more workers than we have population, we need to adjust + if (desiredTotal > totalPopulation) { + // We need to reduce some assignments - prioritize keeping the requested increases + const excess = desiredTotal - totalPopulation; + + // Create array of worker types with their desired amounts and whether they were increased + const workers = [ + { type: 'farmers', desired: desiredFarmers, wasIncreased: farmerDiff > 0, diff: farmerDiff }, + { type: 'miners', desired: desiredMiners, wasIncreased: minerDiff > 0, diff: minerDiff }, + { type: 'builders', desired: desiredBuilders, wasIncreased: builderDiff > 0, diff: builderDiff }, + ]; + + // Sort to reduce from non-increased types first, then by highest count + workers.sort((a, b) => { + if (a.wasIncreased !== b.wasIncreased) { + return a.wasIncreased ? 1 : -1; // Non-increased first + } + return b.desired - a.desired; // Then by highest count + }); + + let remaining = excess; + for (const worker of workers) { + if (remaining <= 0) break; + const canReduce = Math.min(worker.desired, remaining); + worker.desired -= canReduce; + remaining -= canReduce; + } + + // Update our desired values + desiredFarmers = workers.find((w) => w.type === 'farmers')!.desired; + desiredMiners = workers.find((w) => w.type === 'miners')!.desired; + desiredBuilders = workers.find((w) => w.type === 'builders')!.desired; + } + + // If we have unassigned population, assign to the type that needs the most + const assignedTotal = desiredFarmers + desiredMiners + desiredBuilders; + if (assignedTotal < totalPopulation) { + const unassigned = totalPopulation - assignedTotal; + + // Find which assignment has the least workers to balance things out + const assignments = [ + { type: 'farmers', count: desiredFarmers }, + { type: 'miners', count: desiredMiners }, + { type: 'builders', count: desiredBuilders }, + ]; + assignments.sort((a, b) => a.count - b.count); + + // Distribute unassigned workers starting with the lowest assignment + let remaining = unassigned; + for (const assignment of assignments) { + if (remaining <= 0) break; + const toAdd = Math.min(remaining, Math.ceil(remaining / assignments.length)); + if (assignment.type === 'farmers') desiredFarmers += toAdd; + else if (assignment.type === 'miners') desiredMiners += toAdd; + else if (assignment.type === 'builders') desiredBuilders += toAdd; + remaining -= toAdd; + } + } + + // Now calculate the balanced diffs that sum to zero + const balancedFarmerDiff = desiredFarmers - current.farmers; + const balancedMinerDiff = desiredMiners - current.miners; + const balancedBuilderDiff = desiredBuilders - current.builders; + + // Use the existing method that requires diffs to sum to zero + return this.updatePopulationWorkerTypesByDiff(p, owner, balancedFarmerDiff, balancedMinerDiff, balancedBuilderDiff); + } + + /** + * updates the population worker assignments based on the differences passed in + */ + public static updatePopulationWorkerTypesByDiff( + p: PlanetData, + owner: PlayerData, + farmerDiff: number, + minerDiff: number, + builderDiff: number, + ) { + if (farmerDiff + minerDiff + builderDiff !== 0) { + console.error( + "Couldn't move workers in Planet.updatePopulationWorkerTypesByDiff!", + farmerDiff, + minerDiff, + builderDiff, + ); + return this.getPlanetWorkerResourceGeneration(p, owner); + } + + while (farmerDiff !== 0) { + if (farmerDiff > 0) { + //move miners and workers to be farmers + if (minerDiff < 0) { + this.getCitizenByType(p, CitizenWorkerType.Miner).workerType = CitizenWorkerType.Farmer; + farmerDiff--; + minerDiff++; + } + if (farmerDiff !== 0 && builderDiff < 0) { + this.getCitizenByType(p, CitizenWorkerType.Builder).workerType = CitizenWorkerType.Farmer; + farmerDiff--; + builderDiff++; + } + } else { + //make farmers to miners and workers + if (minerDiff > 0) { + this.getCitizenByType(p, CitizenWorkerType.Farmer).workerType = CitizenWorkerType.Miner; + farmerDiff++; + minerDiff--; + } + if (farmerDiff !== 0 && builderDiff > 0) { + this.getCitizenByType(p, CitizenWorkerType.Farmer).workerType = CitizenWorkerType.Builder; + farmerDiff++; + builderDiff--; + } + } + } + + //next check miners, don't touch farmers + while (minerDiff !== 0) { + if (minerDiff > 0) { + //move workers to be miners + if (builderDiff < 0) { + this.getCitizenByType(p, CitizenWorkerType.Builder).workerType = CitizenWorkerType.Miner; + minerDiff--; + builderDiff++; + } + } else { + //make miners to workers + if (builderDiff > 0) { + this.getCitizenByType(p, CitizenWorkerType.Miner).workerType = CitizenWorkerType.Builder; + minerDiff++; + builderDiff--; + } + } + } + + //check for problems + if (farmerDiff !== 0 || minerDiff !== 0 || builderDiff !== 0) { + console.error( + "Couldn't move workers in Planet.updatePopulationWorkerTypesByDiff!", + farmerDiff, + minerDiff, + builderDiff, + ); + } + return this.getPlanetWorkerResourceGeneration(p, owner); + } + + /** + * Counts the SpacePlatforms in the fleet + */ + public static getSpacePlatformCount(p: PlanetData, includeQueue: boolean) { + let count = Fleet.countStarshipsByType(p.planetaryFleet).spaceplatforms; + if (includeQueue) { + for (const ppi of p.buildQueue) { + if ( + ppi.itemType == PlanetProductionItemType.StarShipInProduction && + ppi.starshipData?.type == StarShipType.SpacePlatform + ) { + count++; + } + } + } + return count; + } + + /** + * Counts the improvements built as well as the improvements in the queue by type + */ + public static builtAndBuildQueueImprovementTypeCount(p: PlanetData, type: PlanetImprovementType) { + let count = p.builtImprovements[type]; + for (const ppi of p.buildQueue) { + if (ppi.itemType === PlanetProductionItemType.PlanetImprovement && ppi.improvementData?.type == type) { + count++; + } + } + return count; + } + + /** + * sets the turns to complete based on production + */ + public static estimateTurnsToComplete( + planet: PlanetData, + item: PlanetProductionItemData, + resourceGeneration: PlanetPerTurnResourceGeneration, + ) { + if (resourceGeneration.amountPerTurn.production !== 0) { + const productionCostLeft = item.baseProductionCost - item.productionCostComplete - planet.resources.production; + item.turnsToComplete = Math.ceil(productionCostLeft / resourceGeneration.amountPerTurn.production); + } else { + item.turnsToComplete = 999; //if there are no workers + } + //We at least need one turn to build it, even if we have stockpile greater than the production cost + if (item.turnsToComplete <= 0) { + item.turnsToComplete = 1; + } + } + + /** + * Builds improvements in the queue + */ + public static buildImprovements( + planet: PlanetData, + owner: PlayerData, + gameGrid: Grid, + resourceGeneration: PlanetPerTurnResourceGeneration, + ): { buildQueueEmpty: boolean } { + const returnVal = { buildQueueEmpty: false }; + + if (planet.buildQueue.length > 0) { + const nextItem = planet.buildQueue[0]; + + // This assumes that the production on the planet has already been adjusted for elapsed cycles + nextItem.productionCostComplete += planet.resources.production; + planet.resources.production = 0; + + if (nextItem.productionCostComplete >= nextItem.baseProductionCost) { + //build it + planet.buildQueue.shift(); + nextItem.turnsToComplete = 0; + + //assign points + Player.increasePoints(owner, EarnedPointsType.PRODUCTION_UNIT_BUILT, nextItem.baseProductionCost); + + let nextItemInQueueName = 'Nothing'; + if (planet.buildQueue.length > 0) { + const nextInQueue = planet.buildQueue[0]; + nextItemInQueueName = PlanetProductionItem.toString(nextInQueue); + + //estimate turns left for next item so that we display it correctly on the main screen + this.estimateTurnsToComplete(planet, nextInQueue, resourceGeneration); + } + + if (nextItem.itemType === PlanetProductionItemType.PlanetImprovement) { + planet.builtImprovements[nextItem.improvementData!.type] += 1; + + Events.enqueueNewEvent( + owner.id, + EventNotificationType.ImprovementBuilt, + PlanetProductionItem.toString(nextItem) + + ' built on planet: ' + + planet.name + + ', next in queue: ' + + nextItemInQueueName, + planet, + ); + } else if (nextItem.itemType === PlanetProductionItemType.StarShipInProduction) { + if (!nextItem.starshipData) { + throw new Error('No starshipData in PlanetProductionItem'); + } + const { type, customShipData } = nextItem.starshipData; + const ship = Fleet.generateStarship(type, customShipData); + + //don't set last built option for space platforms + if (type != StarShipType.SpacePlatform) { + planet.starshipTypeLastBuilt = type; + planet.starshipCustomShipLastBuilt = Boolean(customShipData); + } + + //if we have a waypoint set on the planet, send this new starship to the waypoint planet + if ( + planet.waypointBoundingHexMidPoint && + ![StarShipType.SystemDefense, StarShipType.SpacePlatform].includes(type) + ) { + const newFleet = Fleet.generateFleet([], planet.boundingHexMidPoint); + newFleet.starships.push(ship); + Fleet.setDestination(newFleet, gameGrid, planet.boundingHexMidPoint, planet.waypointBoundingHexMidPoint); + + planet.outgoingFleets.push(newFleet); + } else { + planet.planetaryFleet.starships.push(ship); + } + Events.enqueueNewEvent( + owner.id, + EventNotificationType.ShipBuilt, + PlanetProductionItem.toString(nextItem) + + ' built on planet: ' + + planet.name + + ', next in queue: ' + + nextItemInQueueName, + planet, + ); + } else if (nextItem.itemType === PlanetProductionItemType.PlanetImprovementToDestroy) { + if (planet.builtImprovements[nextItem.improvementData!.type] > 0) { + planet.builtImprovements[nextItem.improvementData!.type]--; + Events.enqueueNewEvent( + owner.id, + EventNotificationType.ImprovementDemolished, + GameTools.planetImprovementTypeToFriendlyName(nextItem.improvementData!.type) + + ' demolished on planet: ' + + planet.name + + ', next in queue: ' + + nextItemInQueueName, + planet, + ); + + //TODO: there is probably a better place to handle this check for population overages + //TODO: should we also notify the user he/she lost a pop due to overcrowding or do this slower? + while (Planet.maxPopulation(planet) < planet.population.length) { + //pitd.TypeToDestroy == PlanetImprovementType.Colony) + planet.population.pop(); + } + } + } + + // return remainder production back to the planet + planet.resources.production = nextItem.productionCostComplete - nextItem.baseProductionCost; + } else { + //not done yet, estimate turns to complete + this.estimateTurnsToComplete(planet, nextItem, resourceGeneration); + } + } else { + //notify user of empty build queue + returnVal.buildQueueEmpty = true; + } + return returnVal; + } + + /** + * Returns data if there is a mobile starship in the queue + */ + public static buildQueueContainsMobileStarship( + p: PlanetData, + ): { turnsToComplete: number; starshipStrength: number } | undefined { + for (const ppi of p.buildQueue) { + if (ppi.starshipData && Fleet.starshipTypeIsMobile(ppi.starshipData.type)) { + const starshipStrength = Fleet.getStarshipTypeBaseStrength(ppi.starshipData.type); + return { turnsToComplete: ppi.turnsToComplete, starshipStrength }; + } + } + return undefined; + } + + /** + * A sort function to prefer planets with higher pop and number of improvements + */ + public static planetPopulationImprovementCountComparerSortFunction(a: PlanetData, b: PlanetData) { + let ret = Utils.compareNumbers(b.population.length, a.population.length); + if (ret == 0) ret = Utils.compareNumbers(Planet.builtImprovementCount(b), Planet.builtImprovementCount(a)); + return ret; + } +} diff --git a/packages/astriarch-engine/src/engine/planetDistanceComparer.ts b/packages/astriarch-engine/src/engine/planetDistanceComparer.ts new file mode 100644 index 00000000..8d895045 --- /dev/null +++ b/packages/astriarch-engine/src/engine/planetDistanceComparer.ts @@ -0,0 +1,108 @@ +import { PlanetData, PlanetType } from '../model/planet'; +import { LastKnownPlanetFleetStrength } from '../model/player'; +import { Fleet } from './fleet'; +import { Grid, GridHex } from './grid'; + +/** + * A sort function object to prefer planets with less distance + */ +export class PlanetDistanceComparer { + grid: Grid; + source: PlanetData; + sourceHex: GridHex; + lastKnownPlanetFleetStrength: LastKnownPlanetFleetStrength | undefined; + + constructor(grid: Grid, source: PlanetData, lastKnownPlanetFleetStrength?: LastKnownPlanetFleetStrength) { + this.grid = grid; + this.source = source; + this.lastKnownPlanetFleetStrength = lastKnownPlanetFleetStrength; + const hex = this.grid.getHexAt(source.boundingHexMidPoint); + if (!hex) { + throw new Error(`PlanetDistanceComparer could not find hex for source planet: ${source.id}`); + } + this.sourceHex = hex; + } + + /** + * sort function for planet distances + * @param a + * @param b + * @returns + */ + public sortFunction(a: PlanetData, b: PlanetData): number { + //TODO: this could be slow, we could just have an index for all distances instead of calculating it each time + let ret = 0; + let distanceA = 0; + let distanceB = 0; + const hexA = this.grid.getHexAt(a.boundingHexMidPoint); + if (hexA && a !== this.source) { + //just to be sure + distanceA = Grid.getHexDistance(this.sourceHex, hexA); + ret = 1; + } + const hexB = this.grid.getHexAt(b.boundingHexMidPoint); + if (hexB && b !== this.source) { + //just to be sure + distanceB = Grid.getHexDistance(this.sourceHex, hexB); + ret = -1; + } + + if (ret !== 0) { + //NOTE: this sorts in decending order or distance because we start at the end of the list + if (this.lastKnownPlanetFleetStrength) { + distanceA += this.increasedDistanceBasedOnPlanetValueAndFleetStrength(a); + distanceB += this.increasedDistanceBasedOnPlanetValueAndFleetStrength(b); + } + + if (distanceA === distanceB) ret = 0; + else if (distanceA < distanceB) ret = 1; + else ret = -1; + } + + return ret; + } + + /** + * returns the distance to increase a planet for sorting based on strength + */ + private increasedDistanceBasedOnPlanetValueAndFleetStrength(p: PlanetData) { + //to normalize distance, value and strength we increase the distance as follows + //Based on Value (could eventually base this on what we need so if we need more minerals we prefer asteroids: + // Class 2 planets add +0 distance + // Class 1 planets add +1 distance + // Dead planets add +2 distance + // Asteroids add +3 distance + //Based on last known fleet strength: + // Strength < 20 add + 0 + // Strength 20 to 39 + 1 + // Strength 40 to 79 + 2 + // Strength > 80 + 3 + + let distance = 0; + switch (p.type) { + case PlanetType.AsteroidBelt: + distance += 3; + break; + case PlanetType.DeadPlanet: + distance += 2; + break; + case PlanetType.PlanetClass1: + distance += 1; + break; + } + + if (this.lastKnownPlanetFleetStrength && p.id in this.lastKnownPlanetFleetStrength) { + const strength = Fleet.determineFleetStrength(this.lastKnownPlanetFleetStrength[p.id].fleetData); + + if (strength >= 20 && strength < 40) { + distance += 1; + } else if (strength >= 40 && strength < 80) { + distance += 2; + } else if (strength >= 80) { + distance += 3; + } + } + + return distance; + } +} diff --git a/packages/astriarch-engine/src/engine/planetProductionItem.spec.ts b/packages/astriarch-engine/src/engine/planetProductionItem.spec.ts new file mode 100644 index 00000000..a19260d8 --- /dev/null +++ b/packages/astriarch-engine/src/engine/planetProductionItem.spec.ts @@ -0,0 +1,269 @@ +import { PlanetProductionItem, CanBuildResult } from './planetProductionItem'; +import { Planet } from './planet'; +import { Player } from './player'; +import { Fleet } from './fleet'; +import { Research } from './research'; +import { PlanetType, PlanetImprovementType, PlanetData } from '../model/planet'; +import { StarShipType } from '../model/fleet'; +import { PlayerData, PlayerType } from '../model/player'; +import { Grid } from './grid'; +import { playerColors } from './gameModel'; +import { PlanetById } from '../model/clientModel'; +import { GalaxySizeOption, GameSpeed, PlanetsPerSystemOption } from '../model/model'; + +describe('PlanetProductionItem validation', () => { + let player: PlayerData; + let planet: PlanetData; + let ownedPlanets: PlanetById; + let grid: Grid; + + beforeEach(() => { + // Create a test player + player = Player.constructPlayer('test-player', PlayerType.Human, 'Test Player', playerColors[0]); + + // Create a test grid with proper options + const gameOptions = { + systemsToGenerate: 2, + planetsPerSystem: PlanetsPerSystemOption.FOUR, + galaxySize: GalaxySizeOption.SMALL, + distributePlanetsEvenly: true, + quickStart: false, + gameSpeed: GameSpeed.NORMAL, + version: '2.0', + }; + grid = new Grid(640, 480, gameOptions); + + // Get the first hex from the grid + const testHex = grid.hexes[0]; + + // Create a test planet + planet = Planet.constructPlanet(PlanetType.PlanetClass2, 'Test Planet', testHex, player); + player.ownedPlanetIds = [planet.id]; + player.homePlanetId = planet.id; + + // Set up owned planets map + ownedPlanets = { [planet.id]: planet }; + + // Give player some resources + planet.resources.energy = 100; + planet.resources.ore = 50; + planet.resources.iridium = 25; + planet.resources.food = 50; + planet.resources.production = 20; + }); + + describe('Resource validation', () => { + test('should return true when player has sufficient resources', () => { + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Farm); + const result = PlanetProductionItem.hasSufficientResources(player, ownedPlanets, item); + expect(result).toBe(true); + }); + + test('should return false when player has insufficient energy', () => { + planet.resources.energy = 0; + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Factory); + const result = PlanetProductionItem.hasSufficientResources(player, ownedPlanets, item); + expect(result).toBe(false); + }); + + test('should return false when player has insufficient ore', () => { + planet.resources.ore = 0; + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Factory); + const result = PlanetProductionItem.hasSufficientResources(player, ownedPlanets, item); + expect(result).toBe(false); + }); + }); + + describe('Slot validation', () => { + test('should return false when planet has no available slots', () => { + // Fill up all improvement slots + planet.builtImprovements[PlanetImprovementType.Farm] = planet.maxImprovements; + + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Mine); + const result = PlanetProductionItem.hasSufficientSlots(planet, item); + expect(result).toBe(false); + }); + + test('should return true when planet has available slots', () => { + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Mine); + const result = PlanetProductionItem.hasSufficientSlots(planet, item); + expect(result).toBe(true); + }); + + test('should account for queued improvements', () => { + // Build improvements up to one less than max + planet.builtImprovements[PlanetImprovementType.Farm] = planet.maxImprovements - 1; + + // Add an improvement to the queue + const queuedItem = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Mine); + planet.buildQueue.push(queuedItem); + + // Try to add another improvement + const newItem = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Colony); + const result = PlanetProductionItem.hasSufficientSlots(planet, newItem); + expect(result).toBe(false); + }); + + test('should not check slots for starships', () => { + // Fill up all improvement slots + planet.builtImprovements[PlanetImprovementType.Farm] = planet.maxImprovements; + + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Scout); + const result = PlanetProductionItem.hasSufficientSlots(planet, item); + expect(result).toBe(true); + }); + }); + + describe('Prerequisite validation', () => { + test('should allow scouts without prerequisites', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Scout); + const result = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(result).toBe(true); + }); + + test('should allow defenders without prerequisites', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.SystemDefense); + const result = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(result).toBe(true); + }); + + test('should require factory for destroyers', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Destroyer); + const result = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(result).toBe(false); + + // Add factory and test again + planet.builtImprovements[PlanetImprovementType.Factory] = 1; + const resultWithFactory = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(resultWithFactory).toBe(true); + }); + + test('should require factory for space platforms', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.SpacePlatform); + const result = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(result).toBe(false); + + // Add factory and test again + planet.builtImprovements[PlanetImprovementType.Factory] = 1; + const resultWithFactory = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(resultWithFactory).toBe(true); + }); + + test('should require space platform for cruisers', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Cruiser); + const result = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(result).toBe(false); + + // Add space platform and test again + const spacePlatform = Fleet.generateStarship(StarShipType.SpacePlatform); + planet.planetaryFleet.starships.push(spacePlatform); + const resultWithSpacePlatform = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(resultWithSpacePlatform).toBe(true); + }); + + test('should require space platform for battleships', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Battleship); + const result = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(result).toBe(false); + + // Add space platform and test again + const spacePlatform = Fleet.generateStarship(StarShipType.SpacePlatform); + planet.planetaryFleet.starships.push(spacePlatform); + const resultWithSpacePlatform = PlanetProductionItem.hasPrerequisitesMet(planet, item); + expect(resultWithSpacePlatform).toBe(true); + }); + }); + + describe('Space platform limit validation', () => { + test('should enforce space platform limit', () => { + // Build factory first (prerequisite) + planet.builtImprovements[PlanetImprovementType.Factory] = 1; + + // Add space platforms up to the limit + const maxPlatforms = Research.getMaxSpacePlatformCount(player.research); + for (let i = 0; i < maxPlatforms; i++) { + const spacePlatform = Fleet.generateStarship(StarShipType.SpacePlatform); + planet.planetaryFleet.starships.push(spacePlatform); + } + + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.SpacePlatform); + const result = PlanetProductionItem.hasReachedSpacePlatformLimit(planet, player, item); + expect(result).toBe(true); + }); + + test('should account for queued space platforms', () => { + // Build factory first (prerequisite) + planet.builtImprovements[PlanetImprovementType.Factory] = 1; + + // Add space platforms up to the limit - 1 + const maxPlatforms = Research.getMaxSpacePlatformCount(player.research); + for (let i = 0; i < maxPlatforms - 1; i++) { + const spacePlatform = Fleet.generateStarship(StarShipType.SpacePlatform); + planet.planetaryFleet.starships.push(spacePlatform); + } + + // Add one to the queue + const queuedItem = PlanetProductionItem.constructStarShipInProduction(StarShipType.SpacePlatform); + planet.buildQueue.push(queuedItem); + + // Try to add another + const newItem = PlanetProductionItem.constructStarShipInProduction(StarShipType.SpacePlatform); + const result = PlanetProductionItem.hasReachedSpacePlatformLimit(planet, player, newItem); + expect(result).toBe(true); + }); + + test('should not check limit for other ship types', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Scout); + const result = PlanetProductionItem.hasReachedSpacePlatformLimit(planet, player, item); + expect(result).toBe(false); + }); + }); + + describe('Comprehensive validation', () => { + test('should return CanBuild when all conditions are met', () => { + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Farm); + const result = PlanetProductionItem.canBuild(planet, player, ownedPlanets, item); + expect(result.result).toBe(CanBuildResult.CanBuild); + }); + + test('should return CannotBuildInsufficientResources when resources are lacking', () => { + planet.resources.energy = 0; + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Factory); + const result = PlanetProductionItem.canBuild(planet, player, ownedPlanets, item); + expect(result.result).toBe(CanBuildResult.CannotBuildInsufficientResources); + expect(result.reason).toContain('Insufficient resources'); + }); + + test('should return CannotBuildInsufficientSlots when no slots available', () => { + planet.builtImprovements[PlanetImprovementType.Farm] = planet.maxImprovements; + const item = PlanetProductionItem.constructPlanetImprovement(PlanetImprovementType.Mine); + const result = PlanetProductionItem.canBuild(planet, player, ownedPlanets, item); + expect(result.result).toBe(CanBuildResult.CannotBuildInsufficientSlots); + expect(result.reason).toContain('No available building slots'); + }); + + test('should return CannotBuildPrerequisitesNotMet when prerequisites are missing', () => { + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.Destroyer); + const result = PlanetProductionItem.canBuild(planet, player, ownedPlanets, item); + expect(result.result).toBe(CanBuildResult.CannotBuildPrerequisitesNotMet); + expect(result.reason).toContain('Requires a Factory'); + }); + + test('should return CannotBuildSpacePlatformLimit when space platform limit reached', () => { + // Build factory first (prerequisite) + planet.builtImprovements[PlanetImprovementType.Factory] = 1; + + // Add space platforms up to the limit + const maxPlatforms = Research.getMaxSpacePlatformCount(player.research); + for (let i = 0; i < maxPlatforms; i++) { + const spacePlatform = Fleet.generateStarship(StarShipType.SpacePlatform); + planet.planetaryFleet.starships.push(spacePlatform); + } + + const item = PlanetProductionItem.constructStarShipInProduction(StarShipType.SpacePlatform); + const result = PlanetProductionItem.canBuild(planet, player, ownedPlanets, item); + expect(result.result).toBe(CanBuildResult.CannotBuildSpacePlatformLimit); + expect(result.reason).toContain('Maximum space platforms reached'); + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/planetProductionItem.ts b/packages/astriarch-engine/src/engine/planetProductionItem.ts new file mode 100644 index 00000000..4e60935a --- /dev/null +++ b/packages/astriarch-engine/src/engine/planetProductionItem.ts @@ -0,0 +1,329 @@ +import { StarshipAdvantageData, StarShipType } from '../model/fleet'; +import { PlanetImprovementType, PlanetProductionItemData, PlanetProductionItemType, PlanetData } from '../model/planet'; +import { GameTools } from '../utils/gameTools'; +import { Fleet } from './fleet'; +import { PlayerData } from '../model/player'; +import { PlanetById } from '../model/clientModel'; +import { Research } from './research'; +import { Player } from './player'; + +export enum CanBuildResult { + CanBuild = 'can-build', + CannotBuildInsufficientResources = 'cannot-build-insufficient-resources', + CannotBuildInsufficientSlots = 'cannot-build-insufficient-slots', + CannotBuildPrerequisitesNotMet = 'cannot-build-prerequisites-not-met', + CannotBuildSpacePlatformLimit = 'cannot-build-space-platform-limit', +} + +export interface CanBuildValidationResult { + result: CanBuildResult; + reason?: string; +} + +export class PlanetProductionItem { + private static constructPlanetProductionItem(itemType: PlanetProductionItemType): PlanetProductionItemData { + return { + itemType, + improvementData: undefined, + starshipData: undefined, + turnsToComplete: 99, + productionCostComplete: 0, + baseProductionCost: 0, + energyCost: 0, + oreCost: 0, + iridiumCost: 0, + resourcesSpent: false, + }; + } + + public static constructPlanetImprovement(type: PlanetImprovementType): PlanetProductionItemData { + const base = this.constructPlanetProductionItem(PlanetProductionItemType.PlanetImprovement); + base.improvementData = { type }; + //setup production costs + switch (type) { + case PlanetImprovementType.Colony: + base.baseProductionCost = 16; + base.oreCost = 2; + base.iridiumCost = 1; + base.energyCost = 3; + break; + case PlanetImprovementType.Factory: + base.baseProductionCost = 32; + base.oreCost = 4; + base.iridiumCost = 2; + base.energyCost = 6; + break; + case PlanetImprovementType.Farm: + base.baseProductionCost = 4; + base.energyCost = 1; + break; + case PlanetImprovementType.Mine: + base.baseProductionCost = 8; + base.oreCost = 1; + base.energyCost = 2; + break; + } + return base; + } + + public static constructPlanetImprovementToDestroy(type: PlanetImprovementType): PlanetProductionItemData { + const base = this.constructPlanetProductionItem(PlanetProductionItemType.PlanetImprovementToDestroy); + base.improvementData = { type }; + + base.baseProductionCost = base.baseProductionCost / 4; + return base; + } + + public static constructStarShipInProduction( + type: StarShipType, + customShipData?: StarshipAdvantageData, + ): PlanetProductionItemData { + const base = this.constructPlanetProductionItem(PlanetProductionItemType.StarShipInProduction); + base.starshipData = { type, customShipData }; + + switch (type) { + case StarShipType.SpacePlatform: + base.baseProductionCost = 162; //space platforms should take a while to build + base.oreCost = 12; + base.iridiumCost = 6; + base.energyCost = 18; + break; + case StarShipType.Battleship: + base.baseProductionCost = 104; + base.oreCost = 16; + base.iridiumCost = 8; + base.energyCost = 24; + break; + case StarShipType.Cruiser: + base.baseProductionCost = 41; + base.oreCost = 8; + base.iridiumCost = 4; + base.energyCost = 12; + break; + case StarShipType.Destroyer: + base.baseProductionCost = 16; + base.oreCost = 4; + base.iridiumCost = 2; + base.energyCost = 6; + break; + case StarShipType.Scout: + base.baseProductionCost = 6; + base.oreCost = 2; + base.iridiumCost = 1; + base.energyCost = 3; + break; + case StarShipType.SystemDefense: + base.baseProductionCost = 2; + base.oreCost = 1; + base.energyCost = 1; + break; + } + + const baseShipAdvantage = Fleet.getStarshipStandardAdvantageByType(type); + if (customShipData && baseShipAdvantage) { + const baseShipAdvantageFactor = baseShipAdvantage.advantageAgainst - (baseShipAdvantage.disadvantageAgainst - 1); + //increase cost based on advantages/disadvantages + const advantageFactorDifference = + customShipData.advantageAgainst - (customShipData.disadvantageAgainst - 1) - baseShipAdvantageFactor; + const costMultiplier = Math.max(1.0, 1 + advantageFactorDifference / 10); + + base.baseProductionCost = Math.ceil(base.baseProductionCost * costMultiplier); + base.oreCost = Math.ceil(base.oreCost * costMultiplier); + base.iridiumCost = Math.ceil(base.iridiumCost * costMultiplier); + base.energyCost = Math.ceil(base.energyCost * costMultiplier); + } + return base; + } + + public static toString(item: PlanetProductionItemData) { + if ( + [PlanetProductionItemType.PlanetImprovement, PlanetProductionItemType.PlanetImprovementToDestroy].includes( + item.itemType, + ) + ) { + if (!item.improvementData) { + throw new Error('No improvementData for PlanetProductionItemData'); + } + return item.itemType === PlanetProductionItemType.PlanetImprovementToDestroy + ? 'Demolish ' + : '' + GameTools.planetImprovementTypeToFriendlyName(item.improvementData?.type); + } + if (!item.starshipData) { + throw new Error('No starshipData for PlanetProductionItemData'); + } + return GameTools.starShipTypeToFriendlyName(item.starshipData.type); + } + + /** + * Checks if a player has sufficient resources to build an item + */ + public static hasSufficientResources( + player: PlayerData, + ownedPlanets: PlanetById, + item: PlanetProductionItemData, + ): boolean { + // Get total resources across all owned planets + const totalResources = Player.getTotalResourceAmount(player, ownedPlanets); + + return ( + totalResources.energy >= item.energyCost && + totalResources.ore >= item.oreCost && + totalResources.iridium >= item.iridiumCost + ); + } + + /** + * Checks if a planet has available slots for building improvements + */ + public static hasSufficientSlots(planet: PlanetData, item: PlanetProductionItemData): boolean { + // Only check slots for planet improvements + if (item.itemType !== PlanetProductionItemType.PlanetImprovement) { + return true; + } + + // Count currently built improvements + const currentImprovements = Object.values(planet.builtImprovements).reduce((sum, count) => sum + count, 0); + + // Count improvement items in the build queue + const queuedImprovements = planet.buildQueue.filter( + (queueItem) => queueItem.itemType === PlanetProductionItemType.PlanetImprovement, + ).length; + + const totalImprovements = currentImprovements + queuedImprovements; + + return totalImprovements < planet.maxImprovements; + } + + /** + * Checks if building prerequisites are met + */ + public static hasPrerequisitesMet(planet: PlanetData, item: PlanetProductionItemData): boolean { + if (item.itemType === PlanetProductionItemType.PlanetImprovement) { + // Planet improvements have no prerequisites + return true; + } + + if (item.itemType === PlanetProductionItemType.StarShipInProduction && item.starshipData) { + const shipType = item.starshipData.type; + + switch (shipType) { + case StarShipType.SystemDefense: + case StarShipType.Scout: + // No prerequisites for defenders and scouts + return true; + + case StarShipType.Destroyer: + case StarShipType.SpacePlatform: + // Require at least one factory + return planet.builtImprovements[PlanetImprovementType.Factory] > 0; + + case StarShipType.Cruiser: + case StarShipType.Battleship: + // Require at least one space platform + const spacePlatformCount = Fleet.countStarshipsByType(planet.planetaryFleet).spaceplatforms; + return spacePlatformCount > 0; + + default: + return true; + } + } + + return true; + } + + /** + * Checks if space platform limit has been reached + */ + public static hasReachedSpacePlatformLimit( + planet: PlanetData, + player: PlayerData, + item: PlanetProductionItemData, + ): boolean { + // Only check for space platforms + if ( + item.itemType !== PlanetProductionItemType.StarShipInProduction || + !item.starshipData || + item.starshipData.type !== StarShipType.SpacePlatform + ) { + return false; + } + + // Count current space platforms on this planet + const currentSpacePlatforms = Fleet.countStarshipsByType(planet.planetaryFleet).spaceplatforms; + + // Count space platforms in build queue + const queuedSpacePlatforms = planet.buildQueue.filter( + (queueItem) => + queueItem.itemType === PlanetProductionItemType.StarShipInProduction && + queueItem.starshipData?.type === StarShipType.SpacePlatform, + ).length; + + const totalSpacePlatforms = currentSpacePlatforms + queuedSpacePlatforms + 1; // +1 for the one we're trying to build + + // Get max space platform count from research + const maxSpacePlatforms = Research.getMaxSpacePlatformCount(player.research); + + return totalSpacePlatforms > maxSpacePlatforms; + } + + /** + * Comprehensive validation to check if an item can be built + */ + public static canBuild( + planet: PlanetData, + player: PlayerData, + ownedPlanets: PlanetById, + item: PlanetProductionItemData, + ): CanBuildValidationResult { + // Check resource requirements + if (!this.hasSufficientResources(player, ownedPlanets, item)) { + return { + result: CanBuildResult.CannotBuildInsufficientResources, + reason: `Insufficient resources: need ${item.energyCost} energy, ${item.oreCost} ore, ${item.iridiumCost} iridium`, + }; + } + + // Check slot availability for improvements + if (!this.hasSufficientSlots(planet, item)) { + return { + result: CanBuildResult.CannotBuildInsufficientSlots, + reason: 'No available building slots on this planet', + }; + } + + // Check prerequisites + if (!this.hasPrerequisitesMet(planet, item)) { + if (item.itemType === PlanetProductionItemType.StarShipInProduction && item.starshipData) { + const shipType = item.starshipData.type; + if (shipType === StarShipType.Destroyer || shipType === StarShipType.SpacePlatform) { + return { + result: CanBuildResult.CannotBuildPrerequisitesNotMet, + reason: 'Requires a Factory to build', + }; + } + if (shipType === StarShipType.Cruiser || shipType === StarShipType.Battleship) { + return { + result: CanBuildResult.CannotBuildPrerequisitesNotMet, + reason: 'Requires a Space Platform to build', + }; + } + } + return { + result: CanBuildResult.CannotBuildPrerequisitesNotMet, + reason: 'Prerequisites not met', + }; + } + + // Check space platform limit + if (this.hasReachedSpacePlatformLimit(planet, player, item)) { + return { + result: CanBuildResult.CannotBuildSpacePlatformLimit, + reason: 'Maximum space platforms reached for this planet', + }; + } + + return { + result: CanBuildResult.CanBuild, + }; + } +} diff --git a/packages/astriarch-engine/src/engine/planetResourcePotentialComparer.ts b/packages/astriarch-engine/src/engine/planetResourcePotentialComparer.ts new file mode 100644 index 00000000..7fd7b262 --- /dev/null +++ b/packages/astriarch-engine/src/engine/planetResourcePotentialComparer.ts @@ -0,0 +1,47 @@ +import { PlanetData, PlanetResourceType } from '../model/planet'; +import { Utils } from '../utils/utils'; +import { PlanetResourcesPerTurn } from './computerPlayer'; + +/** + * A sort function object to prefer planets with higher resource generation potential + */ +export class PlanetResourcePotentialComparer { + planetResourcesPerTurn: PlanetResourcesPerTurn; + resourceTypeToPrefer: PlanetResourceType; + + constructor(planetResourcesPerTurn: PlanetResourcesPerTurn, resourceTypeToPrefer: PlanetResourceType) { + this.planetResourcesPerTurn = planetResourcesPerTurn; + this.resourceTypeToPrefer = resourceTypeToPrefer; + } + + /** + * sort function for planet distances + * @param a + * @param b + * @returns + */ + public sortFunction(a: PlanetData, b: PlanetData): number { + const nextWorkerPlanetA = this.planetResourcesPerTurn[a.id].amountNextWorkerPerTurn; + const nextWorkerPlanetB = this.planetResourcesPerTurn[b.id].amountNextWorkerPerTurn; + switch (this.resourceTypeToPrefer) { + case PlanetResourceType.FOOD: + return Utils.compareNumbers(nextWorkerPlanetB.food, nextWorkerPlanetA.food); + break; + case PlanetResourceType.ORE: + return Utils.compareNumbers(nextWorkerPlanetB.ore, nextWorkerPlanetA.ore); + break; + case PlanetResourceType.IRIDIUM: + return Utils.compareNumbers(nextWorkerPlanetB.iridium, nextWorkerPlanetA.iridium); + break; + case PlanetResourceType.ENERGY: + return Utils.compareNumbers(nextWorkerPlanetB.energy, nextWorkerPlanetA.energy); + break; + case PlanetResourceType.PRODUCTION: + return Utils.compareNumbers(nextWorkerPlanetB.production, nextWorkerPlanetA.production); + break; + case PlanetResourceType.RESEARCH: + return Utils.compareNumbers(nextWorkerPlanetB.research, nextWorkerPlanetA.research); + break; + } + } +} diff --git a/packages/astriarch-engine/src/engine/planetResources.ts b/packages/astriarch-engine/src/engine/planetResources.ts new file mode 100644 index 00000000..1d3cfd68 --- /dev/null +++ b/packages/astriarch-engine/src/engine/planetResources.ts @@ -0,0 +1,72 @@ +import { PlanetResourceData } from '../model/planet'; + +export class PlanetResources { + public static constructPlanetResources( + food: number, + energy: number, + research: number, + ore: number, + iridium: number, + production: number, + ): PlanetResourceData { + return { + food, + energy, + research, + ore, + iridium, + production, + }; + } + + public static addPlanetResources(r1: PlanetResourceData, r2: PlanetResourceData): PlanetResourceData { + return { + food: r1.food + r2.food, + energy: r1.energy + r2.energy, + research: r1.research + r2.research, + ore: r1.ore + r2.ore, + iridium: r1.iridium + r2.iridium, + production: r1.production + r2.production, + }; + } + + public static spendEnergyAsPossible(r: PlanetResourceData, amount: number) { + if (r.energy >= amount) { + r.energy = r.energy - amount; + return amount; + } + const spent = r.energy; + r.energy = 0; + return spent; + } + + public static spendFoodAsPossible(r: PlanetResourceData, amount: number) { + if (r.food >= amount) { + r.food = r.food - amount; + return amount; + } + const spent = r.food; + r.food = 0; + return spent; + } + + public static spendOreAsPossible(r: PlanetResourceData, amount: number) { + if (r.ore >= amount) { + r.ore = r.ore - amount; + return amount; + } + const spent = r.ore; + r.ore = 0; + return spent; + } + + public static spendIridiumAsPossible(r: PlanetResourceData, amount: number) { + if (r.iridium >= amount) { + r.iridium = r.iridium - amount; + return amount; + } + const spent = r.iridium; + r.iridium = 0; + return spent; + } +} diff --git a/packages/astriarch-engine/src/engine/player.spec.ts b/packages/astriarch-engine/src/engine/player.spec.ts new file mode 100644 index 00000000..eec65292 --- /dev/null +++ b/packages/astriarch-engine/src/engine/player.spec.ts @@ -0,0 +1,139 @@ +import { Player } from './player'; +import { PlanetData, CitizenWorkerType, Citizen } from '../model/planet'; +import { PlayerData } from '../model/player'; +import { startNewTestGame, TestGameData } from '../test/testUtils'; +import { ClientGameModel } from './clientGameModel'; +import { AdvanceGameClockForPlayerData } from './gameModel'; +import { Grid } from './grid'; + +describe('Player', () => { + describe('food shipping logic', () => { + let testGameData: TestGameData; + let player: PlayerData; + let planet1: PlanetData; + let planet2: PlanetData; + + beforeEach(() => { + testGameData = startNewTestGame(); + player = testGameData.gameModel.modelData.players[0]; + + // Get two planets and add them to player's ownership + const planets = testGameData.gameModel.modelData.planets; + planet1 = planets[0]; + planet2 = planets[1]; + + // Make sure both planets are owned by player + if (!player.ownedPlanetIds.includes(planet1.id)) { + player.ownedPlanetIds.push(planet1.id); + } + if (!player.ownedPlanetIds.includes(planet2.id)) { + player.ownedPlanetIds.push(planet2.id); + } + + // Set up basic population + const createCitizen = (): Citizen => ({ + populationChange: 0, + loyalToPlayerId: player.id, + protestLevel: 0, + workerType: CitizenWorkerType.Farmer, + }); + + planet1.population = [createCitizen()]; // 1 population + planet2.population = [createCitizen()]; // 1 population + }); + + it('should spend food from surplus planet and energy for shipping when moving food to deficit planet', () => { + // Set up food shortage scenario: + // Planet1 has deficit (negative food after eating) + // Planet2 has surplus food and energy for shipping + planet1.resources.food = 0; // Will become -1 after eating (1 pop * 1 cycle) + planet1.resources.energy = 0; + + planet2.resources.food = 10; // Surplus + planet2.resources.energy = 5; // Energy for shipping + + const initialPlanet2Food = planet2.resources.food; + const initialPlanet2Energy = planet2.resources.energy; + + // Create proper AdvanceGameClockForPlayerData + const clientModel = ClientGameModel.constructClientGameModel(testGameData.gameModel.modelData, player.id); + const grid = new Grid(640, 480, testGameData.gameModel.modelData.gameOptions); + const data: AdvanceGameClockForPlayerData = { + clientModel, + cyclesElapsed: 1, + currentCycle: 1, + grid, + }; + + // Run the eat and starve logic + Player.eatAndStarve(data); + + // After eating, planet1 should have had negative food + // Food should be shipped from planet2 to cover the deficit + // Planet2 eats 1 food first (1 pop * 1 cycle), then ships 1 food to planet1 and spends 1 energy for shipping + + expect(planet2.resources.food).toBe(initialPlanet2Food - 1 - 1); // 1 food eaten + 1 food shipped = 8 + expect(planet2.resources.energy).toBe(initialPlanet2Energy - 1); // 1 energy spent for shipping cost + expect(player.lastTurnFoodNeededToBeShipped).toBe(1); // Ongoing food shipping need per turn (1 food shortage / 1 cycle) + }); + + it('should not ship food when there is no energy for shipping', () => { + // Set up scenario where there's surplus food but no energy for shipping + planet1.resources.food = 0; // Will become -2 after eating + planet1.resources.energy = 0; + + planet2.resources.food = 10; // Surplus + planet2.resources.energy = 0; // No energy for shipping + + const initialPlanet2Food = planet2.resources.food; + + // Create proper AdvanceGameClockForPlayerData + const clientModel = ClientGameModel.constructClientGameModel(testGameData.gameModel.modelData, player.id); + const grid = new Grid(640, 480, testGameData.gameModel.modelData.gameOptions); + const data: AdvanceGameClockForPlayerData = { + clientModel, + cyclesElapsed: 2, // 2 cycles = 2 food needed per pop + currentCycle: 1, + grid, + }; + + Player.eatAndStarve(data); + + // No food should be shipped because there's no energy + // Planet2 eats 2 food (1 pop * 2 cycles), leaving it with 8 food but no shipping occurs + expect(planet2.resources.food).toBe(initialPlanet2Food - 2); // 2 food eaten, no shipping + expect(player.lastTurnFoodNeededToBeShipped).toBe(1); // 1 food per turn deficit (2 food shortage / 2 cycles) + }); + + it('should ship partial food when energy is limited', () => { + // Set up scenario where energy limits how much food can be shipped + planet1.resources.food = 0; // Will become -3 after eating + planet1.resources.energy = 0; + + planet2.resources.food = 10; // Surplus + planet2.resources.energy = 2; // Only enough energy to ship 2 food + + const initialPlanet2Food = planet2.resources.food; + const initialPlanet2Energy = planet2.resources.energy; + + // Create proper AdvanceGameClockForPlayerData + const clientModel = ClientGameModel.constructClientGameModel(testGameData.gameModel.modelData, player.id); + const grid = new Grid(640, 480, testGameData.gameModel.modelData.gameOptions); + const data: AdvanceGameClockForPlayerData = { + clientModel, + cyclesElapsed: 3, // 3 cycles = 3 food needed per pop + currentCycle: 1, + grid, + }; + + Player.eatAndStarve(data); + + // Should ship 2 food (limited by energy) out of 3 needed + // Planet2 eats 3 food first (1 pop * 3 cycles), then ships 2 food and spends 2 energy for shipping + expect(planet2.resources.food).toBe(initialPlanet2Food - 3 - 2); // 3 food eaten + 2 food shipped = 5 + expect(planet2.resources.energy).toBe(initialPlanet2Energy - 2); // 2 energy spent for shipping cost + // lastTurnFoodNeededToBeShipped: ongoing need is 1 food per turn (3 shortage / 3 cycles) + expect(player.lastTurnFoodNeededToBeShipped).toBe(1); // 1 food per turn ongoing shipping need + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/player.ts b/packages/astriarch-engine/src/engine/player.ts new file mode 100644 index 00000000..8228e474 --- /dev/null +++ b/packages/astriarch-engine/src/engine/player.ts @@ -0,0 +1,622 @@ +import { ClientModelData, PlanetById, TaskNotificationType } from '../model/clientModel'; +import { EarnedPointsType, earnedPointsConfigByType } from '../model/earnedPoints'; +import { EventNotificationType } from '../model/eventNotification'; +import { FleetData, StarshipAdvantageData } from '../model/fleet'; +import { PlanetData, PlanetHappinessType, PlanetImprovementType, PlanetProductionItemData } from '../model/planet'; +import { ColorRgbaData, EarnedPointsByType, PlayerData, PlayerType } from '../model/player'; +import { Utils } from '../utils/utils'; +import { Events } from './events'; +import { Fleet } from './fleet'; +import { AdvanceGameClockForPlayerData, GameModel } from './gameModel'; +import { Grid } from './grid'; +import { Planet } from './planet'; +import { PlanetProductionItem } from './planetProductionItem'; +import { PlanetResources } from './planetResources'; +import { Research } from './research'; +import { TaskNotifications } from './taskNotifications'; + +export class Player { + public static constructPlayer(id: string, type: PlayerType, name: string, color: ColorRgbaData): PlayerData { + const research = Research.constructResearch(); + const earnedPointsByType = Object.values(EarnedPointsType).reduce((accum, curr) => { + accum[curr as EarnedPointsType] = 0; + return accum; + }, {} as EarnedPointsByType); + return { + id, + type, + name, + research, + color, + lastTurnFoodNeededToBeShipped: 0, + lastTurnFoodShipped: 0, + options: { showPlanetaryConflictPopups: false }, + ownedPlanetIds: [], + knownPlanetIds: [], + lastKnownPlanetFleetStrength: {}, + planetBuildGoals: {}, + homePlanetId: null, + earnedPointsByType, + points: 0, + fleetsInTransit: [], + destroyed: false, + }; + } + + public static setPlanetExplored( + p: PlayerData, + planet: PlanetData, + cycle: number, + lastKnownOwnerId: string | undefined, + ) { + p.knownPlanetIds.push(planet.id); + p.knownPlanetIds = [...new Set(p.knownPlanetIds)]; + Player.setPlanetLastKnownFleetStrength(p, planet, cycle, lastKnownOwnerId); + } + + public static setPlanetLastKnownFleetStrength( + p: PlayerData, + planet: PlanetData, + cycle: number, + lastKnownOwnerId: string | undefined, + ) { + const { defenders, scouts, destroyers, cruisers, battleships, spaceplatforms } = Fleet.countStarshipsByType( + planet.planetaryFleet, + ); + const lastKnownFleet = Fleet.generateFleetWithShipCount( + defenders, + scouts, + destroyers, + cruisers, + battleships, + spaceplatforms, + planet.boundingHexMidPoint, + ); + const lastKnownFleetData = Fleet.constructLastKnownFleet(cycle, lastKnownFleet, lastKnownOwnerId); + p.lastKnownPlanetFleetStrength[planet.id] = lastKnownFleetData; + } + + public static advanceGameClockForPlayer(data: AdvanceGameClockForPlayerData): FleetData[] { + // addLastStarShipToQueueOnPlanets + + Player.generatePlayerResources(data); + + Research.advanceResearchForPlayer(data); + + this.eatAndStarve(data); + this.adjustPlayerPlanetProtestLevels(data); // has randomness can't use client side, probably should change this + this.buildPlayerPlanetImprovements(data); // deterministic + this.growPlayerPlanetPopulation(data); // deterministic + this.repairPlanetaryFleets(data); // deterministic + + // moveShips (client must notify the server when it thinks fleets should land on unowned planets) + const fleetsArrivingOnUnownedPlanets = this.moveShips(data); + return fleetsArrivingOnUnownedPlanets; + } + + public static generatePlayerResources(data: AdvanceGameClockForPlayerData) { + const { clientModel, cyclesElapsed } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + for (const planet of Object.values(mainPlayerOwnedPlanets)) { + Planet.generateResources(planet, cyclesElapsed, mainPlayer); + } + } + + public static getTotalPopulation(p: PlayerData, ownedPlanets: PlanetById) { + let totalPop = 0; + + for (const planetId of p.ownedPlanetIds) { + totalPop += ownedPlanets[planetId].population.length; + } + + return totalPop; + } + + public static getTotalResourceAmount(p: PlayerData, ownedPlanets: PlanetById) { + const totalResources = p.ownedPlanetIds + .map((planetId) => ownedPlanets[planetId]) + .reduce( + (accum, curr) => { + return PlanetResources.addPlanetResources(accum, curr.resources); + }, + PlanetResources.constructPlanetResources(0, 0, 0, 0, 0, 0), + ); + return totalResources; + } + + /** + * returns true if the planet already has reinforcements arriving + */ + public static planetContainsFriendlyInboundFleet(p: PlayerData, planet: PlanetData) { + for (const f of p.fleetsInTransit) { + if (Grid.pointsAreEqual(f.destinationHexMidPoint, planet.boundingHexMidPoint)) { + return true; + } + } + + return false; + } + + /** + * returns an array of the Owned Planets sorted by population and improvement count descending + */ + public static getOwnedPlanetsListSorted(p: PlayerData, ownedPlanets: PlanetById): PlanetData[] { + const sortedOwnedPlanets = p.ownedPlanetIds.map((planetId) => ownedPlanets[planetId]); + sortedOwnedPlanets.sort(Planet.planetPopulationImprovementCountComparerSortFunction); + return sortedOwnedPlanets; + } + + /** + * Increases the players points + */ + public static increasePoints(p: PlayerData, type: EarnedPointsType, amount: number) { + const config = earnedPointsConfigByType[type]; + + const additionalPoints = config.pointsPer * amount; + p.earnedPointsByType[type] = Math.min(p.earnedPointsByType[type] + additionalPoints, config.maxPoints); + + p.points = Object.values(p.earnedPointsByType).reduce((accum, curr) => accum + curr, 0); + return p.points; + } + + public static enqueueProductionItemAndSpendResourcesIfPossible( + clientModel: ClientModelData, + grid: Grid, + planet: PlanetData, + item: PlanetProductionItemData, + ): boolean { + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + + // Use the new comprehensive validation + const validationResult = PlanetProductionItem.canBuild(planet, mainPlayer, mainPlayerOwnedPlanets, item); + const canBuild = validationResult.result === 'can-build'; + + if (canBuild) { + Planet.enqueueProductionItemAndSpendResources(grid, mainPlayer, mainPlayerOwnedPlanets, planet, item); + } + return canBuild; + } + + /** + * Checks if a production item can be built, returning only true/false for resource availability + * This is the original simpler check for backwards compatibility + */ + public static canBuildBasedOnResources(clientModel: ClientModelData, item: PlanetProductionItemData): boolean { + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + return PlanetProductionItem.hasSufficientResources(mainPlayer, mainPlayerOwnedPlanets, item); + } + + public static addLastStarShipToQueueOnPlanets(data: AdvanceGameClockForPlayerData) { + const { clientModel, grid } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + const totalResources = this.getTotalResourceAmount(mainPlayer, mainPlayerOwnedPlanets); + let autoQueuedCount = 0; + const autoQueuedFleet = Fleet.generateFleet([], null); + let focusPlanet; + for (const p of Object.values(mainPlayerOwnedPlanets)) { + if (p.buildLastStarship && p.buildQueue.length == 0 && p.starshipTypeLastBuilt != null) { + focusPlanet = p; + //check resources + let customShipData: StarshipAdvantageData | undefined; + if (p.starshipCustomShipLastBuilt) { + customShipData = Research.getResearchDataByStarshipHullType(p.starshipTypeLastBuilt, mainPlayer)!; + } + const s = PlanetProductionItem.constructStarShipInProduction(p.starshipTypeLastBuilt, customShipData); + + // Use basic resource check but add special logic for food shipping + let canBuild = PlanetProductionItem.hasSufficientResources(mainPlayer, mainPlayerOwnedPlanets, s); + + // Additional check: ensure we have surplus energy after food shipping needs + if (canBuild && totalResources.energy - s.energyCost <= mainPlayer.lastTurnFoodNeededToBeShipped) { + canBuild = false; + } + + if (canBuild) { + autoQueuedCount++; + autoQueuedFleet.starships.push(Fleet.generateStarship(p.starshipTypeLastBuilt, customShipData)); + Planet.enqueueProductionItemAndSpendResources(grid, mainPlayer, mainPlayerOwnedPlanets, p, s); + } + } + } + + if (autoQueuedCount) { + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.ResourcesAutoSpent, + 'Auto-queued: ' + Fleet.toString(autoQueuedFleet), + focusPlanet, + ); + } + } + + public static eatAndStarve(data: AdvanceGameClockForPlayerData) { + const { clientModel, cyclesElapsed, currentCycle } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + const totalPop = this.getTotalPopulation(mainPlayer, mainPlayerOwnedPlanets); + const totalResources = this.getTotalResourceAmount(mainPlayer, mainPlayerOwnedPlanets); + //for each planet player controls + + //if one planet has a shortage and another a surplus, gold will be spent (if possible) for shipping + + //determine food surplus and shortages + //shortages will kill off a percentage of the population due to starvation depending on the amount of shortage + + const foodDeficitByPlanet: Record = {}; //for calculating starvation later + const foodSurplusPlanets: PlanetData[] = []; //for costing shipments and starvation later + + //calculate surpluses and deficits + for (const p of Object.values(mainPlayerOwnedPlanets)) { + p.planetHappiness = PlanetHappinessType.Normal; //reset our happiness + + p.resources.food = p.resources.food - p.population.length * cyclesElapsed; //eat + + if (p.resources.food < 0) { + const deficit = Math.abs(p.resources.food); + foodDeficitByPlanet[p.id] = deficit; //signify deficit for starvation + p.resources.food = 0; + + mainPlayer.lastTurnFoodNeededToBeShipped += deficit / cyclesElapsed; //increment our food shipments needed so next turn we can ensure we have surplus gold + } else if (p.resources.food > 0) { + //signify surplus for this planet for shipments + foodSurplusPlanets.push(p); + } + } + + let totalFoodShipped = 0; + + let protestingPlanetNames = ''; + let protestingPlanetCount = 0; + let lastProtestingPlanet; + + //starve if we don't have surplus at other planets or can't afford to pay for shipments + for (const planetId in foodDeficitByPlanet) { + const planet = mainPlayerOwnedPlanets[planetId]; + const deficit = foodDeficitByPlanet[planetId]; + let foodShortageTotal = deficit; + let shippedAllResources = false; //if we shipped enough food to prevent starvation + //first see if we can pay for any shipping and there are planets with surplus + if (totalResources.energy > 0 && foodSurplusPlanets.length > 0) { + //it will cost one energy per resource shipped + //look for a planet to send food from + for (const pSurplus of foodSurplusPlanets) { + if (pSurplus.resources.food === 0) { + continue; + } + + // Calculate how much food we can actually ship (limited by available food and energy for shipping cost) + const maxFoodCanShip = Math.min(foodShortageTotal, pSurplus.resources.food); + const energyAvailableForShipping = pSurplus.resources.energy; + const actualFoodToShip = Math.min(maxFoodCanShip, energyAvailableForShipping); + + if (actualFoodToShip === 0) { + continue; // No energy available for shipping or no food to ship + } + + // Spend the actual food from surplus planet + const actualFoodSpent = PlanetResources.spendFoodAsPossible(pSurplus.resources, actualFoodToShip); + + // Spend energy for shipping cost (1 energy per food shipped) + const shippingCostSpent = PlanetResources.spendEnergyAsPossible(pSurplus.resources, actualFoodSpent); + + totalFoodShipped += actualFoodSpent; + + foodShortageTotal = foodShortageTotal - actualFoodSpent; + totalResources.energy = totalResources.energy - shippingCostSpent; + + if (foodShortageTotal === 0) { + //we shipped enough food + shippedAllResources = true; + break; + } + } + } + + if (!shippedAllResources) { + const foodShortageRatio = (foodShortageTotal / (totalPop * 1.0)) * cyclesElapsed; + //starvation + //there is a food shortage ratio chance of loosing one population, + //if you have 4 pop and 2 food you have a 1 in 2 chance of loosing one + //otherwise people just slowly starve + const looseOne = Utils.nextRandom(0, 100) < Math.round(foodShortageRatio * 100); + if (looseOne) { + let riotReason = '.'; //for shortages + if (totalResources.energy <= 0 && foodSurplusPlanets.length != 0) + riotReason = ', insufficient Energy to ship Food.'; + //notify user of starvation + planet.planetHappiness = PlanetHappinessType.Riots; + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.FoodShortageRiots, + 'Riots over food shortages killed one population on planet: ' + planet.name + riotReason, + planet, + ); + if (planet.population.length > 0) { + planet.population.pop(); + } + } //reduce the population a bit + else { + if (planet.population.length > 0) { + planet.planetHappiness = PlanetHappinessType.Unrest; + + const c = planet.population[planet.population.length - 1]; + c.populationChange -= foodShortageRatio; + if (c.populationChange <= -1.0) { + //notify user of starvation + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.PopulationStarvation, + 'You lost one population due to food shortages on planet: ' + planet.name, + planet, + ); + planet.population.pop(); + } else { + protestingPlanetCount++; + + if (protestingPlanetNames != '') protestingPlanetNames += ', '; + protestingPlanetNames += planet.name; + + lastProtestingPlanet = planet; + } + } + } + + //citizens will further protest depending on the amount of food shortages + for (const citizen of planet.population) { + //citizens on the home planet are more forgiving + const protestDenominator = planet.id == mainPlayer.homePlanetId ? 4 : 2; + if (Utils.nextRandom(0, protestDenominator) === 0) { + //only have 1/2 the population protest so the planet isn't totally screwed + citizen.protestLevel += Utils.nextRandomFloat(0, foodShortageRatio); + if (citizen.protestLevel > 1) { + citizen.protestLevel = 1; + } + } + } + + //have to check to see if we removed the last pop and loose this planet from owned planets if so + if (planet.population.length == 0) { + //notify user of planet loss + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.PlanetLostDueToStarvation, + 'You have lost control of ' + planet.name + ' due to starvation', + planet, + ); + + GameModel.changePlanetOwner(mainPlayer, undefined, planet, currentCycle); + } else if (planet.id == mainPlayer.homePlanetId) { + //if this planet is the player's home planet we need to ensure that there is at least one non-protesting citizen because otherwise there is no way to get out of starvation spiral + const citizens = Planet.getPopulationByContentment(planet); + if (citizens.content.length == 0) { + planet.population[0].protestLevel = 0; + } + } + } + } + + if (protestingPlanetCount > 0) { + let protestingPlanetReason = '.'; + if (totalResources.energy <= 0 && foodSurplusPlanets.length != 0) + protestingPlanetReason = ', insufficient Gold to ship Food.'; + let planetPluralized = 'planet: '; + if (protestingPlanetCount > 1) planetPluralized = 'planets: '; + //notify user of population unrest + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.InsufficientFood, + 'Population unrest over lack of Food on ' + planetPluralized + protestingPlanetNames + protestingPlanetReason, + lastProtestingPlanet, + ); + } + + if (totalFoodShipped != 0) { + mainPlayer.lastTurnFoodShipped += totalFoodShipped; + } + } + + public static buildPlayerPlanetImprovements(data: AdvanceGameClockForPlayerData) { + const { clientModel, grid } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + //build planet improvements + const planetNameBuildQueueEmptyList = []; + for (const p of Object.values(mainPlayerOwnedPlanets)) { + const resourceGeneration = Planet.getPlanetWorkerResourceGeneration(p, mainPlayer); + const results = Planet.buildImprovements(p, mainPlayer, grid, resourceGeneration); + + if (results.buildQueueEmpty) { + //if the build queue was empty we'll convert planet production to energy + planetNameBuildQueueEmptyList.push(p.name); + + const energyProduced = p.resources.production / 4.0; + p.resources.energy += energyProduced; + p.resources.production = 0; + TaskNotifications.upsertTask(data.clientModel.taskNotifications, { + type: TaskNotificationType.BuildQueueEmpty, + planetId: p.id, + planetName: p.name, + data: { energyGenerated: energyProduced }, + }); + } + } + } + + public static adjustPlayerPlanetProtestLevels(data: AdvanceGameClockForPlayerData) { + const { clientModel } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + //if we have a normal PlanetHappiness (meaning we didn't cause unrest from starvation) + // we'll slowly reduce the amount of protest on the planet + + //if half the population or more is protesting, we'll set the planet happiness to unrest + for (const p of Object.values(mainPlayerOwnedPlanets)) { + if (p.planetHappiness === PlanetHappinessType.Normal) { + const citizens = Planet.getPopulationByContentment(p); + let protestingCitizenCount = 0; + const contentCitizenRatio = citizens.content.length / p.population.length; + + for (const citizen of citizens.protesting) { + //protest reduction algorithm: + // each turn reduce a random amount of protest for each citizen + // the amount of reduction is based on the percentage of the total population protesting (to a limit) + // (the more people protesting the more likely others will keep protesting) + let protestReduction = Utils.nextRandomFloat(0, Math.max(0.25, contentCitizenRatio)); + //citizens are more quickly content on the home planet + if (p.id == mainPlayer.homePlanetId) { + protestReduction *= 2.0; + } + citizen.protestLevel -= protestReduction; + + if (citizen.protestLevel <= 0) { + citizen.protestLevel = 0; + citizen.loyalToPlayerId = mainPlayer.id; + } else { + protestingCitizenCount++; + //console.log("Planet: ", p.Name, "citizen["+c+"]", citizen); + } + } + const citizenText = protestingCitizenCount > 1 ? ' Citizens' : ' Citizen'; + if (protestingCitizenCount >= p.population.length / 2) { + p.planetHappiness = PlanetHappinessType.Unrest; + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.CitizensProtesting, + 'Population unrest on ' + p.name + ' due to ' + protestingCitizenCount + citizenText + ' protesting', + p, + ); + } else if (protestingCitizenCount > 0) { + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.CitizensProtesting, + protestingCitizenCount + citizenText + ' protesting your rule on ' + p.name, + p, + ); + } + } + } + } + + public static growPlayerPlanetPopulation(data: AdvanceGameClockForPlayerData) { + const { clientModel, cyclesElapsed } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + //population growth rate is based on available space at the planet and the amount currently there + //as we fill up the planet, growth rate slows + for (const p of Object.values(mainPlayerOwnedPlanets)) { + const growthRate = Planet.getPopulationGrowthRate(p, mainPlayer, cyclesElapsed); + //check if we are growing + if (growthRate > 0) { + const lastCitizen = p.population[p.population.length - 1]; + lastCitizen.populationChange += growthRate; + if (lastCitizen.populationChange >= 1.0) { + //notify user of growth + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.PopulationGrowth, + 'Population growth on planet: ' + p.name, + p, + ); + //grow + lastCitizen.populationChange = 0; + p.population.push(Planet.constructCitizen(p.type, mainPlayer.id)); + + //assign points + Player.increasePoints(mainPlayer, EarnedPointsType.POPULATION_GROWTH, 1); + } + } + } + } + + public static repairPlanetaryFleets(data: AdvanceGameClockForPlayerData) { + const { clientModel, cyclesElapsed, grid } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + const resourcesAutoSpent = { energy: 0, ore: 0, iridium: 0 }; + const totalResources = this.getTotalResourceAmount(mainPlayer, mainPlayerOwnedPlanets); + let planetTarget; + + //repair fleets on planets + for (const p of Object.values(mainPlayerOwnedPlanets)) { + //Require a colony and a happy population to even consider repairing a fleet + if (p.planetHappiness === PlanetHappinessType.Normal && p.builtImprovements[PlanetImprovementType.Colony] > 0) { + //charge the user a bit for repairing the fleets + //i.e. 2 gold per 3 damage, 1 ore per 2 damage, 1 iridium per 4 damage + const maxStrengthToRepair = Math.min( + Math.floor((totalResources.energy / 2) * 3), + Math.floor(totalResources.ore * 2), + Math.floor(totalResources.iridium * 4), + ); + + const totalStrengthRepaired = Fleet.repairPlanetaryFleet(p, maxStrengthToRepair, cyclesElapsed); + if (totalStrengthRepaired > 0) { + const energyCost = Math.floor((totalStrengthRepaired * 2) / 3); + const oreCost = Math.floor(totalStrengthRepaired / 2); + const iridiumCost = Math.floor(totalStrengthRepaired / 4); + if (energyCost || oreCost || iridiumCost) { + Planet.spendResources(grid, mainPlayer, mainPlayerOwnedPlanets, p, energyCost, 0, oreCost, iridiumCost); + resourcesAutoSpent.energy += energyCost; + resourcesAutoSpent.ore += oreCost; + resourcesAutoSpent.iridium += iridiumCost; + planetTarget = p; + } + + //assign points + Player.increasePoints(mainPlayer, EarnedPointsType.REPAIRED_STARSHIP_STRENGTH, totalStrengthRepaired); + } + } + } + + if (planetTarget) { + Events.enqueueNewEvent( + mainPlayer.id, + EventNotificationType.ResourcesAutoSpent, + resourcesAutoSpent.energy + + ' Energy, ' + + resourcesAutoSpent.ore + + ' Ore, ' + + resourcesAutoSpent.iridium + + ' Iridium spent repairing fleets.', + planetTarget, + ); + } + return resourcesAutoSpent; + } + + public static moveShips(data: AdvanceGameClockForPlayerData) { + const { clientModel, cyclesElapsed } = data; + const { mainPlayer, mainPlayerOwnedPlanets } = clientModel; + const fleetsArrivingOnUnownedPlanets: FleetData[] = []; + //for each planet and each outgoing fleet on that planet, move the fleet to the player's fleets in transit + for (const p of Object.values(mainPlayerOwnedPlanets)) { + mainPlayer.fleetsInTransit = mainPlayer.fleetsInTransit.concat(p.outgoingFleets); + p.outgoingFleets = []; + } + + //for each fleet in transit, move the fleet towards it's destination + for (const f of mainPlayer.fleetsInTransit) { + Fleet.moveFleet(f, mainPlayer, cyclesElapsed); + } + + //land fleets arriving on owned planets + //merge multiple friendly fleets arriving on unowned planets (before conflicts are resolved) + for (let i = mainPlayer.fleetsInTransit.length - 1; i >= 0; i--) { + const playerFleet = mainPlayer.fleetsInTransit[i]; + if (playerFleet.parsecsToDestination! <= 0) { + const destinationPlanet = Planet.getClientPlanetAtMidPoint( + clientModel.clientPlanets, + playerFleet.destinationHexMidPoint!, + ); + if (!destinationPlanet) { + throw new Error( + `Unable to find client planet at midpoint: ${Grid.pointDataToString(playerFleet.destinationHexMidPoint!)}`, + ); + } + + if (destinationPlanet.id in mainPlayerOwnedPlanets) { + //merge/land fleet + Fleet.landFleet(mainPlayerOwnedPlanets[destinationPlanet.id].planetaryFleet, playerFleet); + } else { + fleetsArrivingOnUnownedPlanets.push(playerFleet); + } + mainPlayer.fleetsInTransit.splice(i, 1); + } + } + return fleetsArrivingOnUnownedPlanets; + } +} diff --git a/packages/astriarch-engine/src/engine/research.spec.ts b/packages/astriarch-engine/src/engine/research.spec.ts new file mode 100644 index 00000000..34a838e3 --- /dev/null +++ b/packages/astriarch-engine/src/engine/research.spec.ts @@ -0,0 +1,194 @@ +import { ResearchData, ResearchType } from '../model/research'; +import { PlayerData } from '../model/player'; +import { startNewTestGame, TestGameData } from '../test/testUtils'; +import { Research } from './research'; + +let testGameData: TestGameData; +let player1: PlayerData; +let researchData: ResearchData; + +describe('Research', function () { + beforeEach(() => { + testGameData = startNewTestGame(); + player1 = testGameData.gameModel.modelData.players[0]; + researchData = player1.research; + }); + + describe('estimateTurnsRemainingInQueue', function () { + it('should return 999 when no research is in queue', () => { + researchData.researchTypeInQueue = null; + researchData.researchPercent = 0.5; + + const result = Research.estimateTurnsRemainingInQueue(researchData, 100); + expect(result).toBe(999); + }); + + it('should return 999 when research percent is 0', () => { + researchData.researchTypeInQueue = ResearchType.COMBAT_IMPROVEMENT_ATTACK; + researchData.researchPercent = 0; + + const result = Research.estimateTurnsRemainingInQueue(researchData, 100); + expect(result).toBe(999); + }); + + it('should calculate correct turns for simple research scenario', () => { + // Set up research in queue + researchData.researchTypeInQueue = ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES; + researchData.researchPercent = 0.5; // 50% of income goes to research + + // Get the research progress for mines (should be at level -1 initially) + const minesResearch = researchData.researchProgressByType[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES]; + expect(minesResearch.currentResearchLevel).toBe(-1); + expect(minesResearch.researchPointsCompleted).toBe(0); + + // Research type data for mines has base value 1 + // Level costs should be [1, 2, 3, 5, 8, 13, 21, 34, 55, 89] + // Total costs should be [1, 3, 6, 11, 19, 32, 53, 87, 142, 231] + const researchTypeData = Research.researchTypeIndex[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES]; + expect(researchTypeData.researchPointsBase).toBe(1); + expect(researchTypeData.researchLevelCosts[0]).toBe(1); // First level costs 1 total + + // With 100 credits max, 50% research = 50 research per turn + // Need 1 research point to reach level 0, so should take 1 turn (Math.ceil(1/50) = 1) + const result = Research.estimateTurnsRemainingInQueue(researchData, 100); + expect(result).toBe(1); + }); + + it('should calculate correct turns for partially completed research', () => { + // Set up research in queue with some progress + researchData.researchTypeInQueue = ResearchType.COMBAT_IMPROVEMENT_ATTACK; + researchData.researchPercent = 0.25; // 25% of income goes to research + + // Get the research progress for attack (base value 4) + const attackResearch = researchData.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_ATTACK]; + + // Manually set some progress (2 out of 4 points completed for level 0) + Research.setResearchPointsCompleted(attackResearch, 2); + expect(attackResearch.currentResearchLevel).toBe(-1); // Still at level -1 + expect(attackResearch.researchPointsCompleted).toBe(2); + + // Research type data for attack has base value 4 + // Level costs should be [4, 8, 12, 20, 32, 52, 84, 136, 220, 356] + // Total costs should be [4, 12, 24, 44, 76, 128, 212, 348, 568, 924] + const researchTypeData = Research.researchTypeIndex[ResearchType.COMBAT_IMPROVEMENT_ATTACK]; + expect(researchTypeData.researchLevelCosts[0]).toBe(4); // First level costs 4 total + + // With 200 credits max, 25% research = 50 research per turn + // Need 2 more research points (4 - 2), so should take 1 turn (Math.ceil(2/50) = 1) + const result = Research.estimateTurnsRemainingInQueue(researchData, 200); + expect(result).toBe(1); + }); + + it('should handle very slow research progress', () => { + // Set up research in queue + researchData.researchTypeInQueue = ResearchType.PROPULSION_IMPROVEMENT; + researchData.researchPercent = 0.01; // Only 1% goes to research + + const propulsionResearch = researchData.researchProgressByType[ResearchType.PROPULSION_IMPROVEMENT]; + expect(propulsionResearch.currentResearchLevel).toBe(-1); + + // Research type data for propulsion has base value 8 + // First level costs 8 total + const researchTypeData = Research.researchTypeIndex[ResearchType.PROPULSION_IMPROVEMENT]; + expect(researchTypeData.researchLevelCosts[0]).toBe(8); + + // With 100 credits max, 1% research = 1 research per turn + // Need 8 research points, so should take 8 turns + const result = Research.estimateTurnsRemainingInQueue(researchData, 100); + expect(result).toBe(8); + }); + + it('should return 0 when research is already at max level', () => { + // Set up research in queue + researchData.researchTypeInQueue = ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS; + researchData.researchPercent = 0.5; + + const farmsResearch = researchData.researchProgressByType[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS]; + + // Set research to max level (level 9) + const researchTypeData = Research.researchTypeIndex[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS]; + const maxLevelCost = researchTypeData.researchLevelCosts[researchTypeData.researchLevelCosts.length - 1]; + Research.setResearchPointsCompleted(farmsResearch, maxLevelCost); + + expect(farmsResearch.currentResearchLevel).toBe(9); // Max level + + const result = Research.estimateTurnsRemainingInQueue(researchData, 100); + expect(result).toBe(0); + }); + + it('should handle fractional research per turn correctly', () => { + // Set up research in queue + researchData.researchTypeInQueue = ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES; + researchData.researchPercent = 0.33; // 33% goes to research + + const coloniesResearch = + researchData.researchProgressByType[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES]; + + // Research type data for colonies has base value 3 + // First level costs 3 total + const researchTypeData = Research.researchTypeIndex[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES]; + expect(researchTypeData.researchLevelCosts[0]).toBe(3); + + // With 10 credits max, 33% research = 3.3 research per turn + // Need 3 research points, so should take 1 turn (Math.ceil(3/3.3) = 1) + const result = Research.estimateTurnsRemainingInQueue(researchData, 10); + expect(result).toBe(1); + }); + }); + + describe('getResearchLevelData', function () { + it('should return correct data for research at level -1', () => { + const minesResearch = researchData.researchProgressByType[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES]; + expect(minesResearch.currentResearchLevel).toBe(-1); + expect(minesResearch.researchPointsCompleted).toBe(0); + + const levelData = Research.getResearchLevelData(minesResearch); + + expect(levelData.currentResearchLevel).toBe(0); // Display level (1-based from 0-based) + expect(levelData.researchCostToNextLevel).toBe(1); // Need 1 point to reach level 0 + expect(levelData.percentComplete).toBe(0); // 0% complete + }); + + it('should return correct data for partially completed research', () => { + const attackResearch = researchData.researchProgressByType[ResearchType.COMBAT_IMPROVEMENT_ATTACK]; + + // Set 2 out of 4 points completed for level 0 + Research.setResearchPointsCompleted(attackResearch, 2); + + const levelData = Research.getResearchLevelData(attackResearch); + + expect(levelData.currentResearchLevel).toBe(0); // Still at display level 0 + expect(levelData.researchCostToNextLevel).toBe(2); // Need 2 more points + expect(levelData.percentComplete).toBe(0.5); // 50% complete (2/4) + }); + + it('should return correct data for completed level transitioning to next', () => { + const propulsionResearch = researchData.researchProgressByType[ResearchType.PROPULSION_IMPROVEMENT]; + + // Complete level 0 (8 points) and add some progress toward level 1 (which costs 24 total) + // With 10 points, we're at level 0 with 2 points toward level 1 + Research.setResearchPointsCompleted(propulsionResearch, 10); + + const levelData = Research.getResearchLevelData(propulsionResearch); + + expect(levelData.currentResearchLevel).toBe(1); // Display level 1 (0-based level 0) + expect(levelData.researchCostToNextLevel).toBe(14); // Need 14 more points (24 - 10) + expect(levelData.percentComplete).toBe(0.125); // 12.5% complete toward level 1 (2/16) + }); + + it('should return 100% complete for max level research', () => { + const farmsResearch = researchData.researchProgressByType[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS]; + + // Set research to max level + const researchTypeData = Research.researchTypeIndex[ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS]; + const maxLevelCost = researchTypeData.researchLevelCosts[researchTypeData.researchLevelCosts.length - 1]; + Research.setResearchPointsCompleted(farmsResearch, maxLevelCost); + + const levelData = Research.getResearchLevelData(farmsResearch); + + expect(levelData.currentResearchLevel).toBe(10); // Display level 10 (0-based level 9) + expect(levelData.researchCostToNextLevel).toBe(0); + expect(levelData.percentComplete).toBe(1.0); // 100% complete + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/research.ts b/packages/astriarch-engine/src/engine/research.ts new file mode 100644 index 00000000..1022ab60 --- /dev/null +++ b/packages/astriarch-engine/src/engine/research.ts @@ -0,0 +1,473 @@ +import { EventNotificationType } from '../model/eventNotification'; +import { StarshipAdvantageData, StarShipType } from '../model/fleet'; +import { PlayerData } from '../model/player'; +import { + ResearchData, + ResearchProgressByType, + ResearchType, + ResearchTypeProgress, + ResearchTypeProgressData, +} from '../model/research'; +import { GameTools } from '../utils/gameTools'; +import { Events } from './events'; +import { AdvanceGameClockForPlayerData } from './gameModel'; + +const MAX_RESEARCH_LEVEL = 9; + +export interface ResearchTypeData { + type: ResearchType; + researchPointsBase: number; //based on the type and data, some types are intrinsically more expensive + isCustomShip: boolean; + researchLevelCosts: number[]; +} + +export type ResearchTypeIndex = Record; + +export class Research { + public static researchTypeIndex: ResearchTypeIndex = Research.constructResearchTypeIndex(); + + public static constructResearch(): ResearchData { + const researchProgressByType = Object.values(ResearchType).reduce((accum, curr) => { + const type = curr as ResearchType; + accum[type] = Research.constructResearchTypeProgress(type); + return accum; + }, {} as ResearchProgressByType); + + return { + researchProgressByType, + researchTypeInQueue: null, + researchPercent: 0, + }; + } + + public static constructResearchTypeProgress(type: ResearchType): ResearchTypeProgress { + const researchTypeData = Research.researchTypeIndex[type]; + const maxResearchLevel = researchTypeData.isCustomShip ? 0 : MAX_RESEARCH_LEVEL; + const researchPointsBase = researchTypeData.researchPointsBase; + const data: ResearchTypeProgressData = {}; + if ([ResearchType.COMBAT_IMPROVEMENT_ATTACK, ResearchType.COMBAT_IMPROVEMENT_DEFENSE].includes(type)) { + data.chance = 0; + } else if ( + [ + ResearchType.PROPULSION_IMPROVEMENT, + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS, + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES, + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES, + ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES, + ].includes(type) + ) { + data.percent = 1.0; + } else if (type === ResearchType.SPACE_PLATFORM_IMPROVEMENT) { + data.max = 1; + } + return { + type, + maxResearchLevel, + currentResearchLevel: -1, + researchPointsBase, + researchPointsCompleted: 0, + data, + }; + } + + public static updateResearchTypeProgressData(rtp: ResearchTypeProgress, data: ResearchTypeProgressData) { + const researchTypeData = Research.researchTypeIndex[rtp.type]; + if (researchTypeData.isCustomShip) { + rtp.researchPointsBase = researchTypeData.researchPointsBase; + let extraResearchAdvantage = 0; + let extraResearchDisadvantage = 0; + switch (data.advantageAgainst) { + case StarShipType.SystemDefense: + extraResearchAdvantage = rtp.researchPointsBase * 0.1; + break; + case StarShipType.Scout: + extraResearchAdvantage = rtp.researchPointsBase * 0.2; + break; + case StarShipType.Destroyer: + extraResearchAdvantage = rtp.researchPointsBase * 0.3; + break; + case StarShipType.Cruiser: + extraResearchAdvantage = rtp.researchPointsBase * 0.5; + break; + case StarShipType.Battleship: + extraResearchAdvantage = rtp.researchPointsBase * 0.8; + break; + } + switch (data.disadvantageAgainst) { + case StarShipType.SystemDefense: + extraResearchDisadvantage = rtp.researchPointsBase * 0.8; + break; + case StarShipType.Scout: + extraResearchDisadvantage = rtp.researchPointsBase * 0.5; + break; + case StarShipType.Destroyer: + extraResearchDisadvantage = rtp.researchPointsBase * 0.3; + break; + case StarShipType.Cruiser: + extraResearchDisadvantage = rtp.researchPointsBase * 0.2; + break; + case StarShipType.Battleship: + extraResearchDisadvantage = rtp.researchPointsBase * 0.1; + break; + } + rtp.researchPointsBase += extraResearchAdvantage + extraResearchDisadvantage; + } + rtp.data = data; + } + + public static getResearchData(player: PlayerData, researchType: ResearchType) { + return player.research.researchProgressByType[researchType].data; + } + + public static getResearchBoostForEfficiencyImprovement(researchType: ResearchType, player?: PlayerData): number { + if (!player) { + return 1.0; + } + return this.getResearchData(player, researchType).percent ?? 1.0; + } + + public static getResearchBoostForStarshipCombatImprovement(researchType: ResearchType, player: PlayerData): number { + return this.getResearchData(player, researchType).chance ?? 0; + } + + public static getResearchDataByStarshipHullType(hullType: StarShipType, player: PlayerData) { + let researchData; + switch (hullType) { + case StarShipType.SystemDefense: + researchData = this.getResearchData(player, ResearchType.NEW_SHIP_TYPE_DEFENDER); + break; + case StarShipType.Scout: + researchData = this.getResearchData(player, ResearchType.NEW_SHIP_TYPE_SCOUT); + break; + case StarShipType.Destroyer: + researchData = this.getResearchData(player, ResearchType.NEW_SHIP_TYPE_DESTROYER); + break; + case StarShipType.Cruiser: + researchData = this.getResearchData(player, ResearchType.NEW_SHIP_TYPE_CRUISER); + break; + case StarShipType.Battleship: + researchData = this.getResearchData(player, ResearchType.NEW_SHIP_TYPE_BATTLESHIP); + break; + } + return researchData ? (researchData as StarshipAdvantageData) : undefined; + } + + public static getCreditAndResearchAmountEarnedPerTurn(researchData: ResearchData, creditAmountAtMaxPercent: number) { + const researchAmountEarnedPerTurn = creditAmountAtMaxPercent * researchData.researchPercent; + const creditAmountEarnedPerTurn = creditAmountAtMaxPercent - researchAmountEarnedPerTurn; + return { researchAmountEarnedPerTurn, creditAmountEarnedPerTurn }; + } + + public static estimateTurnsRemainingInQueue(researchData: ResearchData, creditAmountAtMaxPercent: number): number { + if (!researchData.researchTypeInQueue) { + return 999; // No research in queue + } + + const { researchAmountEarnedPerTurn } = Research.getCreditAndResearchAmountEarnedPerTurn( + researchData, + creditAmountAtMaxPercent, + ); + + if (researchAmountEarnedPerTurn <= 0) { + return 999; // No research progress possible + } + + const researchProgress = researchData.researchProgressByType[researchData.researchTypeInQueue]; + const researchTypeData = Research.researchTypeIndex[researchProgress.type]; + + // Calculate cost to next level + const currentLevel = researchProgress.currentResearchLevel; + const nextLevelIndex = currentLevel + 1; + + if (nextLevelIndex >= researchTypeData.researchLevelCosts.length) { + return 0; // Already at max level + } + + const totalCostToNextLevel = researchTypeData.researchLevelCosts[nextLevelIndex]; + const researchCostToNextLevel = totalCostToNextLevel - researchProgress.researchPointsCompleted; + + return Math.ceil(researchCostToNextLevel / researchAmountEarnedPerTurn); + } + + public static getResearchLevelData(researchProgress: ResearchTypeProgress) { + const researchTypeData = Research.researchTypeIndex[researchProgress.type]; + const currentLevel = researchProgress.currentResearchLevel; + const nextLevelIndex = currentLevel + 1; + + const researchLevelData = { + currentResearchLevel: currentLevel + 1, // Display level (0-based becomes 1-based) + researchCostToNextLevel: 0, + percentComplete: 0, + }; + + if (nextLevelIndex >= researchTypeData.researchLevelCosts.length) { + // Already at max level + researchLevelData.percentComplete = 1.0; + return researchLevelData; + } + + const totalCostAtCurrentLevel = currentLevel === -1 ? 0 : researchTypeData.researchLevelCosts[currentLevel]; + const totalCostToNextLevel = researchTypeData.researchLevelCosts[nextLevelIndex]; + + researchLevelData.researchCostToNextLevel = totalCostToNextLevel - researchProgress.researchPointsCompleted; + researchLevelData.percentComplete = + (researchProgress.researchPointsCompleted - totalCostAtCurrentLevel) / + (totalCostToNextLevel - totalCostAtCurrentLevel); + + return researchLevelData; + } + + public static canResearch(researchProgress: ResearchTypeProgress): boolean { + return researchProgress.currentResearchLevel < researchProgress.maxResearchLevel; + } + + public static advanceResearchForPlayer(data: AdvanceGameClockForPlayerData) { + const { mainPlayer, mainPlayerOwnedPlanets } = data.clientModel; + if (mainPlayer.research.researchTypeInQueue) { + let totalResearch = 0; + Object.values(mainPlayerOwnedPlanets).forEach((planet) => { + totalResearch += planet.resources.research; + planet.resources.research = 0; + }); + const rtpInQueue = mainPlayer.research.researchProgressByType[mainPlayer.research.researchTypeInQueue]; + const levelIncrease = Research.setResearchPointsCompleted( + rtpInQueue, + rtpInQueue.researchPointsCompleted + totalResearch, + ); + if (levelIncrease) { + //we've gained a level + const message = + 'Our Scientists and Engineers have finished researching and developing: ' + + Research.researchProgressToString(rtpInQueue); + Events.enqueueNewEvent(mainPlayer.id, EventNotificationType.ResearchComplete, message); + + if (!Research.canResearch(rtpInQueue)) { + mainPlayer.research.researchTypeInQueue = null; + } + } + } + // else notifiy the player at some point? + } + + public static researchProgressToString(researchProgress: ResearchTypeProgress, nextLevel?: number) { + const level = researchProgress.currentResearchLevel + (nextLevel ? 2 : 1); + let defaultName = Research.researchProgressFriendlyName(researchProgress); + switch (researchProgress.type) { + case ResearchType.COMBAT_IMPROVEMENT_ATTACK: + defaultName += ' Level ' + level; + break; + case ResearchType.COMBAT_IMPROVEMENT_DEFENSE: + defaultName += ' Level ' + level; + break; + case ResearchType.PROPULSION_IMPROVEMENT: + defaultName += ' Level ' + level; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS: + defaultName = 'Level ' + level + ' ' + defaultName; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES: + defaultName = 'Level ' + level + ' ' + defaultName; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES: + defaultName = 'Level ' + level + ' ' + defaultName; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES: + defaultName = 'Level ' + level + ' ' + defaultName; + break; + case ResearchType.SPACE_PLATFORM_IMPROVEMENT: + defaultName = 'Level ' + level + ' ' + defaultName; + break; + } + return defaultName; + } + + public static researchProgressFriendlyName(researchProgress: ResearchTypeProgress) { + let defaultName = 'Unknown'; + switch (researchProgress.type) { + case ResearchType.NEW_SHIP_TYPE_DEFENDER: + defaultName = 'Custom ' + GameTools.starShipTypeToFriendlyName(StarShipType.SystemDefense); + break; + case ResearchType.NEW_SHIP_TYPE_SCOUT: + defaultName = 'Custom ' + GameTools.starShipTypeToFriendlyName(StarShipType.Scout); + break; + case ResearchType.NEW_SHIP_TYPE_DESTROYER: + defaultName = 'Custom ' + GameTools.starShipTypeToFriendlyName(StarShipType.Destroyer); + break; + case ResearchType.NEW_SHIP_TYPE_CRUISER: + defaultName = 'Custom ' + GameTools.starShipTypeToFriendlyName(StarShipType.Cruiser); + break; + case ResearchType.NEW_SHIP_TYPE_BATTLESHIP: + defaultName = 'Custom ' + GameTools.starShipTypeToFriendlyName(StarShipType.Battleship); + break; + case ResearchType.COMBAT_IMPROVEMENT_ATTACK: + defaultName = 'Ship Attack'; + break; + case ResearchType.COMBAT_IMPROVEMENT_DEFENSE: + defaultName = 'Ship Defense'; + break; + case ResearchType.PROPULSION_IMPROVEMENT: + defaultName = 'Ship Propulsion'; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS: + defaultName = 'Farms'; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES: + defaultName = 'Mines'; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES: + defaultName = 'Colonies'; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES: + defaultName = 'Factories'; + break; + case ResearchType.SPACE_PLATFORM_IMPROVEMENT: + defaultName = 'Space Platforms'; + break; + } + return defaultName; + } + + public static setResearchPointsCompleted(researchProgress: ResearchTypeProgress, pointsCompleted: number): number { + const originalLevel = researchProgress.currentResearchLevel; + researchProgress.researchPointsCompleted = pointsCompleted; + const { researchLevelCosts } = Research.researchTypeIndex[researchProgress.type]; + //set current level based on completed points + for (let i = 0; i < researchLevelCosts.length; i++) { + const researchCost = researchLevelCosts[i]; + if (researchProgress.researchPointsCompleted >= researchCost) { + researchProgress.currentResearchLevel = i; + } else { + break; + } + } + Research.setDataBasedOnLevel(researchProgress); + return researchProgress.currentResearchLevel - originalLevel; //returns level increase + } + + private static setDataBasedOnLevel(researchProgress: ResearchTypeProgress) { + switch (researchProgress.type) { + case ResearchType.COMBAT_IMPROVEMENT_ATTACK: + case ResearchType.COMBAT_IMPROVEMENT_DEFENSE: + researchProgress.data.chance = (researchProgress.currentResearchLevel + 1) / 10; + break; + case ResearchType.PROPULSION_IMPROVEMENT: + researchProgress.data.percent = 1.0 + (researchProgress.currentResearchLevel + 1) * 0.5; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS: + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES: + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES: + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES: + researchProgress.data.percent = 1.0 + (researchProgress.currentResearchLevel + 1) / 10; + break; + case ResearchType.SPACE_PLATFORM_IMPROVEMENT: + researchProgress.data.max = Math.max(Math.floor((researchProgress.currentResearchLevel + 1) / 2), 1); + break; + } + } + + private static constructResearchTypeIndex(): ResearchTypeIndex { + return Object.values(ResearchType).reduce((accum, curr) => { + const type = curr as ResearchType; + accum[type] = Research.constructResearchTypeData(type); + return accum; + }, {} as ResearchTypeIndex); + } + + private static constructResearchTypeData(type: ResearchType): ResearchTypeData { + let researchPointsBase = 0; + let isCustomShip = false; + + switch (type) { + case ResearchType.NEW_SHIP_TYPE_DEFENDER: + isCustomShip = true; + researchPointsBase = 10; + break; + case ResearchType.NEW_SHIP_TYPE_SCOUT: + isCustomShip = true; + researchPointsBase = 20; + break; + case ResearchType.NEW_SHIP_TYPE_DESTROYER: + isCustomShip = true; + researchPointsBase = 30; + break; + case ResearchType.NEW_SHIP_TYPE_CRUISER: + isCustomShip = true; + researchPointsBase = 50; + break; + case ResearchType.NEW_SHIP_TYPE_BATTLESHIP: + isCustomShip = true; + researchPointsBase = 80; + break; + case ResearchType.COMBAT_IMPROVEMENT_ATTACK: + researchPointsBase = 4; + break; + case ResearchType.COMBAT_IMPROVEMENT_DEFENSE: + researchPointsBase = 3; + break; + case ResearchType.PROPULSION_IMPROVEMENT: + researchPointsBase = 8; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FARMS: + researchPointsBase = 2; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_MINES: + researchPointsBase = 1; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES: + researchPointsBase = 3; + break; + case ResearchType.BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES: + researchPointsBase = 5; + break; + case ResearchType.SPACE_PLATFORM_IMPROVEMENT: + researchPointsBase = 10; + break; + } + + const researchLevelCosts = Research.getTotalResearchLevelCosts(researchPointsBase); + return { + type, + researchPointsBase, + isCustomShip, + researchLevelCosts, + }; + } + + private static getTotalResearchLevelCosts(baseValue: number) { + const totalResearchLevelCosts = []; + const researchLevelCosts = Research.getResearchLevelCosts(baseValue); + let accum = 0; + for (let i = 0; i <= MAX_RESEARCH_LEVEL; i++) { + accum += researchLevelCosts[i]; + totalResearchLevelCosts.push(accum); + } + return totalResearchLevelCosts; + } + + private static getResearchLevelCosts(baseValue: number) { + baseValue = baseValue || 1; + let curr = baseValue; + let prev = 0; + const levelCosts = []; + for (let i = 0; i <= MAX_RESEARCH_LEVEL; i++) { + const temp = curr; + curr += prev; + levelCosts.push(curr); + prev = temp; + } + return levelCosts; + } + + public static getMaxSpacePlatformCount(data: ResearchData): number { + return data.researchProgressByType[ResearchType.SPACE_PLATFORM_IMPROVEMENT].data.max || 0; + } + + public static getResearchProgressListSorted(data: ResearchData) { + const sortedList = Object.values(data.researchProgressByType); + + sortedList.sort((rptA, rptB) => { + return rptA.currentResearchLevel - rptB.currentResearchLevel; + }); + return sortedList; + } +} diff --git a/packages/astriarch-engine/src/engine/starShipAdvantageStrengthComparer.ts b/packages/astriarch-engine/src/engine/starShipAdvantageStrengthComparer.ts new file mode 100644 index 00000000..40bf509c --- /dev/null +++ b/packages/astriarch-engine/src/engine/starShipAdvantageStrengthComparer.ts @@ -0,0 +1,44 @@ +import { StarshipData } from '../model/fleet'; +import { BattleSimulator, FleetDamagePending } from './battleSimulator'; + +/** + * StarShipAdvantageStrengthComparer is an object that allows array sorting of starships + * @constructor + */ +export class StarShipAdvantageStrengthComparer { + ship: StarshipData; + fleetDamagePending: FleetDamagePending; + constructor(ship: StarshipData, fleetDamagePending: FleetDamagePending) { + this.ship = ship; + this.fleetDamagePending = fleetDamagePending; + } + + public sortFunction(a: StarshipData, b: StarshipData): number { + let ret = 0; + const strengthA = this.getStarShipAdvantageDisadvantageAdjustedStrength(a); + const strengthB = this.getStarShipAdvantageDisadvantageAdjustedStrength(b); + + if (strengthA === strengthB) ret = 0; + else if (strengthA < strengthB) ret = -1; + else ret = 1; + + return ret; + } + + /** + * Adjusts the starships relative strength for sorting based on starship advantages + */ + public getStarShipAdvantageDisadvantageAdjustedStrength(enemy: StarshipData) { + let adjustedStrength = enemy.health; + if (enemy.id in this.fleetDamagePending) { + adjustedStrength -= BattleSimulator.getTotalPendingDamage(this.fleetDamagePending[enemy.id].hits); + } + + if (BattleSimulator.starshipHasAdvantage(this.ship, enemy)) adjustedStrength *= 1; + //this just ensures we're always attaking the enemy we have an advantage over + else if (BattleSimulator.starshipHasDisadvantage(this.ship, enemy)) adjustedStrength *= 10000; + else adjustedStrength *= 100; + + return adjustedStrength; + } +} diff --git a/packages/astriarch-engine/src/engine/taskNotifications.ts b/packages/astriarch-engine/src/engine/taskNotifications.ts new file mode 100644 index 00000000..c65dfafe --- /dev/null +++ b/packages/astriarch-engine/src/engine/taskNotifications.ts @@ -0,0 +1,24 @@ +import { TaskNotification, TaskNotificationIndex, TaskNotificationType } from '../model/clientModel'; + +export class TaskNotifications { + public static constructTaskNotifications(): TaskNotificationIndex { + return { [TaskNotificationType.BuildQueueEmpty]: {} }; + } + + public static upsertTask(taskNotifications: TaskNotificationIndex, task: TaskNotification) { + const taskNotification = taskNotifications[task.type]; + if (!(task.planetId in taskNotification)) { + taskNotification[task.planetId] = task; + } else { + taskNotification[task.planetId].data.energyGenerated += task.data.energyGenerated; + } + } + + public static getTaskNotificationMessage(task: TaskNotification) { + let message = `Build queue empty on ${task.planetName}`; + if (task.data.energyGenerated) { + message += `, ${task.data.energyGenerated} Energy generated`; + } + return message; + } +} diff --git a/packages/astriarch-engine/src/engine/tradingCenter.spec.ts b/packages/astriarch-engine/src/engine/tradingCenter.spec.ts new file mode 100644 index 00000000..a8ac60a9 --- /dev/null +++ b/packages/astriarch-engine/src/engine/tradingCenter.spec.ts @@ -0,0 +1,207 @@ +import { ClientModelData, PlanetById } from '../model/clientModel'; +import { PlayerData } from '../model/player'; +import { TradeType, TradingCenterResourceType } from '../model/tradingCenter'; +import { startNewTestGame, TestGameData } from '../test/testUtils'; +import { ClientGameModel } from './clientGameModel'; +import { GameModel, GameModelData } from './gameModel'; +import { TradingCenter } from './tradingCenter'; + +let testGameData: TestGameData; +let player1: PlayerData; +let player2: PlayerData; +let planetById: PlanetById; + +const checkTrade = function ( + gameModel: GameModelData, + planetById: PlanetById, + clientGameModel: ClientModelData, + player: PlayerData, + tradeType: TradeType, + resourceType: TradingCenterResourceType, + tradeAmount: number, +) { + const { tradingCenter } = gameModel.modelData; + const totalResources = GameModel.getPlayerTotalResources(player, clientGameModel.mainPlayerOwnedPlanets); + const origEnergyAmount = totalResources.energy; + const origResourceAmount = + resourceType === TradingCenterResourceType.FOOD + ? totalResources.food + : resourceType === TradingCenterResourceType.ORE + ? totalResources.ore + : totalResources.iridium; + const origResourcePrice = + resourceType === TradingCenterResourceType.FOOD + ? tradingCenter.foodResource.currentPrice + : resourceType === TradingCenterResourceType.ORE + ? tradingCenter.foodResource.currentPrice + : tradingCenter.iridiumResource.currentPrice; + const trade = TradingCenter.constructTrade(player.id, player.homePlanetId!, tradeType, resourceType, tradeAmount); + //{executed:false, foodAmount:0, oreAmount:0, iridiumAmount:0, tradeGoldAmount:0} + const homePlanet = GameModel.getPlanetById(gameModel, player.homePlanetId!); + const executedStatus = TradingCenter.executeTrade(gameModel, planetById, player, homePlanet!, trade); + if (executedStatus.executed) { + //recalculate current prices in trading center for each trade executed + TradingCenter.recalculatePrices(tradingCenter); + } else { + throw new Error('Trade was not executed successfully'); + } + let tradeEnergyAmount = origResourcePrice * tradeAmount; + //subtract fee + tradeEnergyAmount -= tradeEnergyAmount * tradingCenter.transactionFeePercentage; + const totalResourcesNew = GameModel.getPlayerTotalResources(player, clientGameModel.mainPlayerOwnedPlanets); + expect(executedStatus.tradeEnergyAmount).toEqual(tradeEnergyAmount); + expect( + resourceType === TradingCenterResourceType.FOOD + ? executedStatus.foodAmount + : resourceType === TradingCenterResourceType.ORE + ? executedStatus.oreAmount + : executedStatus.iridiumAmount, + ).toEqual(tradeAmount); + expect(origEnergyAmount + tradeEnergyAmount).toEqual(totalResourcesNew.energy); + expect(origResourceAmount - tradeAmount).toEqual(totalResourcesNew.food); +}; + +describe('tradingCenter', () => { + beforeEach(() => { + testGameData = startNewTestGame(); + player1 = testGameData.gameModel.modelData.players[0]; + player2 = testGameData.gameModel.modelData.players[1]; + planetById = ClientGameModel.getPlanetByIdIndex(testGameData.gameModel.modelData.planets); + }); + + describe('executeTrade', () => { + it('should execute a sell food trade for player1 given that player has sufficient resources', function () { + const tradeAmount = 1; + const clientGameModel = ClientGameModel.constructClientGameModel(testGameData.gameModel.modelData, player1.id); + checkTrade( + testGameData.gameModel, + planetById, + clientGameModel, + player1, + TradeType.SELL, + TradingCenterResourceType.FOOD, + tradeAmount, + ); + }); + + it('should execute a sell food trade for player2 given that player has sufficient resources', () => { + const tradeAmount = 2; + const clientGameModel = ClientGameModel.constructClientGameModel(testGameData.gameModel.modelData, player2.id); + checkTrade( + testGameData.gameModel, + planetById, + clientGameModel, + player2, + TradeType.SELL, + TradingCenterResourceType.FOOD, + tradeAmount, + ); + }); + + it('should not execute a sell food trade for player1 given that player has insufficient resources', () => { + const tradeAmount = 20; + const trade = TradingCenter.constructTrade( + player1.id, + player1.homePlanetId!, + TradeType.SELL, + TradingCenterResourceType.FOOD, + tradeAmount, + ); + const homePlanet = GameModel.getPlanetById(testGameData.gameModel, player1.homePlanetId!); + const executedStatus = TradingCenter.executeTrade( + testGameData.gameModel, + planetById, + player1, + homePlanet!, + trade, + ); + expect(executedStatus.executed).toEqual(false); + }); + + it('should not execute a buy food trade for player1 given that player has insufficient gold', () => { + const tradeAmount = 30; + const trade = TradingCenter.constructTrade( + player1.id, + player1.homePlanetId!, + TradeType.BUY, + TradingCenterResourceType.FOOD, + tradeAmount, + ); + const homePlanet = GameModel.getPlanetById(testGameData.gameModel, player1.homePlanetId!); + const executedStatus = TradingCenter.executeTrade( + testGameData.gameModel, + planetById, + player1, + homePlanet!, + trade, + ); + expect(executedStatus.executed).toEqual(false); + }); + }); + + describe('executeCurrentTrades', () => { + it('should execute all current trades when trades are valid', () => { + const newTrades = []; + newTrades.push( + TradingCenter.constructTrade( + player1.id, + player1.homePlanetId!, + TradeType.SELL, + TradingCenterResourceType.FOOD, + 1, + ), + ); + newTrades.push( + TradingCenter.constructTrade( + player2.id, + player2.homePlanetId!, + TradeType.BUY, + TradingCenterResourceType.FOOD, + 1, + ), + ); + + testGameData.gameModel.modelData.tradingCenter.currentTrades = + testGameData.gameModel.modelData.tradingCenter.currentTrades.concat(newTrades); + const executedStatusListByPlayerId = TradingCenter.executeCurrentTrades(testGameData.gameModel, planetById); + + const executed = Object.values(executedStatusListByPlayerId).reduce( + (accum, curr) => accum.concat(curr.filter((t) => t.results.executed)), + [], + ); + expect(executed.length).toEqual(newTrades.length); + }); + + it('should not execute all current trades when some trades are invalid', () => { + const newTrades = []; + newTrades.push( + TradingCenter.constructTrade( + player1.id, + player1.homePlanetId!, + TradeType.BUY, + TradingCenterResourceType.FOOD, + 15, + ), + ); + newTrades.push( + TradingCenter.constructTrade( + player1.id, + player1.homePlanetId!, + TradeType.BUY, + TradingCenterResourceType.FOOD, + 15, + ), + ); + + testGameData.gameModel.modelData.tradingCenter.currentTrades = + testGameData.gameModel.modelData.tradingCenter.currentTrades.concat(newTrades); + const executedStatusListByPlayerId = TradingCenter.executeCurrentTrades(testGameData.gameModel, planetById); + + const executed = Object.values(executedStatusListByPlayerId).reduce( + (accum, curr) => accum.concat(curr.filter((t) => t.results.executed)), + [], + ); + expect(executed.length).toEqual(1); + }); + }); +}); diff --git a/packages/astriarch-engine/src/engine/tradingCenter.ts b/packages/astriarch-engine/src/engine/tradingCenter.ts new file mode 100644 index 00000000..c69b056e --- /dev/null +++ b/packages/astriarch-engine/src/engine/tradingCenter.ts @@ -0,0 +1,378 @@ +import { PlanetById } from '../model/clientModel'; +import { PlanetData } from '../model/planet'; +import { PlayerData } from '../model/player'; +import { + TradeData, + TradeType, + TradingCenterData, + TradingCenterResource, + TradingCenterResourceType, +} from '../model/tradingCenter'; +import { Events } from './events'; +import { GameModel, GameModelData } from './gameModel'; +import { Planet } from './planet'; +import { Utils } from '../utils/utils'; +import { EventNotificationType } from '../model/eventNotification'; + +export interface ExecuteTradeResults { + executed: boolean; + foodAmount: number; + oreAmount: number; + iridiumAmount: number; + tradeEnergyAmount: number; +} + +export class TradingCenter { + public static constructData(playerCount: number): TradingCenterData { + const energyAmount = playerCount ? playerCount * 5 : 10; + const foodResource = TradingCenter.constructResource( + TradingCenterResourceType.FOOD, + (playerCount || 2) * 200, + 400, + 0.1, + 1.5, + 40, + ); + const oreResource = TradingCenter.constructResource( + TradingCenterResourceType.ORE, + (playerCount || 2) * 10, + 200, + 0.2, + 3.0, + 20, + ); + const iridiumResource = TradingCenter.constructResource( + TradingCenterResourceType.IRIDIUM, + (playerCount || 2) * 5, + 100, + 0.4, + 6.0, + 10, + ); + return { + energyAmount, + foodResource, + oreResource, + iridiumResource, + currentTrades: [], + transactionFeePercentage: 0.1, + interestPercentage: 0.01, + }; + } + + public static constructResource( + type: TradingCenterResourceType, + amount: number, + desiredAmount: number, + priceMin: number, + priceMax: number, + tradeAmountMax: number, + ): TradingCenterResource { + const resource = { + type, + amount, + desiredAmount, + priceMin, + priceMax, + tradeAmountMax, + currentPrice: priceMax, + }; + TradingCenter.adjustCurrentPrice(resource); + return resource; + } + + public static constructTrade( + playerId: string, + planetId: number, + tradeType: TradeType, + resourceType: TradingCenterResourceType, + amount: number, + delaySeconds = 5, + ): TradeData { + const now = Date.now(); + return { + id: Utils.generateUniqueId(), + playerId, + planetId, + tradeType, + resourceType, + amount, + submittedAt: now, + executeAfter: now + delaySeconds * 1000, + }; + } + + public static cancelTrade(tradingCenterData: TradingCenterData, tradeId: string, playerId: string): boolean { + const tradeIndex = tradingCenterData.currentTrades.findIndex( + (trade) => trade.id === tradeId && trade.playerId === playerId, + ); + + if (tradeIndex === -1) { + return false; // Trade not found or not owned by player + } + + const trade = tradingCenterData.currentTrades[tradeIndex]; + const now = Date.now(); + + // Allow cancellation only if trade hasn't been executed yet + if (now < trade.executeAfter) { + tradingCenterData.currentTrades.splice(tradeIndex, 1); + return true; // Successfully cancelled + } + + return false; // Too late to cancel + } + + public static adjustCurrentPrice(resource: TradingCenterResource): TradingCenterResource { + if (resource.amount >= resource.desiredAmount) { + resource.currentPrice = resource.priceMin; + } else if (resource.amount <= 0) { + resource.currentPrice = resource.priceMax; + } else { + const priceChangePerUnit = (resource.priceMax - resource.priceMin) / resource.desiredAmount; + resource.currentPrice = resource.priceMin + priceChangePerUnit * (resource.desiredAmount - resource.amount); + } + return resource; + } + + public static recalculatePrices(tradingCenterData: TradingCenterData) { + this.adjustCurrentPrice(tradingCenterData.foodResource); + this.adjustCurrentPrice(tradingCenterData.oreResource); + this.adjustCurrentPrice(tradingCenterData.iridiumResource); + } + + public static getResourceByType(tradingCenterData: TradingCenterData, resourceType: TradingCenterResourceType) { + return resourceType === TradingCenterResourceType.FOOD + ? tradingCenterData.foodResource + : resourceType === TradingCenterResourceType.ORE + ? tradingCenterData.oreResource + : tradingCenterData.iridiumResource; + } + + public static executeTrade( + gameModel: GameModelData, + planetById: PlanetById, + player: PlayerData, + planet: PlanetData, + trade: TradeData, + ): ExecuteTradeResults { + const executedStatus = { executed: false, foodAmount: 0, oreAmount: 0, iridiumAmount: 0, tradeEnergyAmount: 0 }; + const { tradingCenter } = gameModel.modelData; + const playerTotalResources = GameModel.getPlayerTotalResources(player, planetById); + const marketResource = this.getResourceByType(tradingCenter, trade.resourceType); + let playerResourceAmount = 0; + + if (trade.resourceType == TradingCenterResourceType.FOOD) { + trade.amount = Math.min(trade.amount, tradingCenter.foodResource.tradeAmountMax); //limit trade to trading max for resource + playerResourceAmount = playerTotalResources.food; + executedStatus.foodAmount = trade.amount; + } else if (trade.resourceType == TradingCenterResourceType.ORE) { + trade.amount = Math.min(trade.amount, tradingCenter.oreResource.tradeAmountMax); //limit trade to trading max for resource + playerResourceAmount = playerTotalResources.ore; + executedStatus.oreAmount = trade.amount; + } else { + trade.amount = Math.min(trade.amount, tradingCenter.iridiumResource.tradeAmountMax); //limit trade to trading max for resource + playerResourceAmount = playerTotalResources.iridium; + executedStatus.iridiumAmount = trade.amount; + } + executedStatus.tradeEnergyAmount = trade.amount * marketResource.currentPrice; + + if (trade.tradeType == TradeType.SELL) { + //ensure the player has enough resources and the market has enough gold + + executedStatus.tradeEnergyAmount -= executedStatus.tradeEnergyAmount * tradingCenter.transactionFeePercentage; + if (tradingCenter.energyAmount >= executedStatus.tradeEnergyAmount && playerResourceAmount >= trade.amount) { + //execute trade + tradingCenter.energyAmount -= executedStatus.tradeEnergyAmount; + planet.resources.energy += executedStatus.tradeEnergyAmount; + marketResource.amount += trade.amount; + + Planet.spendResources( + gameModel.grid, + player, + planetById, + planet, + 0, + executedStatus.foodAmount, + executedStatus.oreAmount, + executedStatus.iridiumAmount, + ); + + executedStatus.executed = true; + } + } else { + //BUY + //ensure the player has enough energy and the market has enough resources + + executedStatus.tradeEnergyAmount += executedStatus.tradeEnergyAmount * tradingCenter.transactionFeePercentage; + if (playerTotalResources.energy >= executedStatus.tradeEnergyAmount && marketResource.amount >= trade.amount) { + //execute trade + tradingCenter.energyAmount += executedStatus.tradeEnergyAmount; + Planet.spendResources(gameModel.grid, player, planetById, planet, executedStatus.tradeEnergyAmount, 0, 0, 0); + + marketResource.amount -= trade.amount; + if (trade.resourceType === TradingCenterResourceType.FOOD) { + planet.resources.food += trade.amount; + } else if (trade.resourceType == TradingCenterResourceType.ORE) { + planet.resources.ore += trade.amount; + } else { + planet.resources.iridium += trade.amount; + } + + executedStatus.executed = true; + } + } + + return executedStatus; + } + + public static executeCurrentTrades(gameModel: GameModelData, planetById: PlanetById) { + //go through the current trades and deduct from the stockpile for buy orders and add to the stockpile for sell orders + const tc = gameModel.modelData.tradingCenter; + const now = Date.now(); + + // Separate trades into executable and pending based on timing + const executableTrades = tc.currentTrades.filter((trade) => now >= trade.executeAfter); + const pendingTrades = tc.currentTrades.filter((trade) => now < trade.executeAfter); + + // Update currentTrades to only contain pending trades + tc.currentTrades = pendingTrades; + + const playersById: Record = {}; + const executedTradeResultsByPlayerId: Record = {}; + for (const p of gameModel.modelData.players) { + playersById[p.id] = p; + executedTradeResultsByPlayerId[p.id] = []; + } + + // Execute only the trades that are ready + for (const trade of executableTrades) { + const player = playersById[trade.playerId]; + if (player) { + const planet = planetById[trade.planetId]; + if (planet) { + //{executed:false, foodAmount:0, oreAmount:0, iridiumAmount:0, tradeGoldAmount:0} + const results = TradingCenter.executeTrade(gameModel, planetById, player, planet, trade); + if (results.executed) { + //recalculate current prices in trading center for each trade executed + TradingCenter.recalculatePrices(tc); + } + executedTradeResultsByPlayerId[trade.playerId].push({ trade, results }); + } else { + console.warn('Unable to find planet in executeCurrentTrades:', trade); + } + } else { + console.warn('Unable to find player in executeCurrentTrades:', trade); + } + } + + //create summary endOfTurnMessages + for (const [playerId, results] of Object.entries(executedTradeResultsByPlayerId)) { + const resourcesBought = { food: 0, ore: 0, iridium: 0, energySpent: 0, tradeCount: 0 }; + const resourcesSold = { food: 0, ore: 0, iridium: 0, energyEarned: 0, tradeCount: 0 }; + const resourcesNotBought = { food: 0, ore: 0, iridium: 0, energySpent: 0, tradeCount: 0 }; + const resourcesNotSold = { food: 0, ore: 0, iridium: 0, energyEarned: 0, tradeCount: 0 }; + for (const r of results) { + let rbTarget = resourcesNotBought; + let rsTarget = resourcesNotSold; + if (r.results.executed) { + rbTarget = resourcesBought; + rsTarget = resourcesSold; + } + if (r.trade.tradeType === TradeType.BUY) { + rbTarget.food += r.results.foodAmount; + rbTarget.ore += r.results.oreAmount; + rbTarget.iridium += r.results.iridiumAmount; + rbTarget.energySpent += r.results.tradeEnergyAmount; + rbTarget.tradeCount++; + } else { + rsTarget.food += r.results.foodAmount; + rsTarget.ore += r.results.oreAmount; + rsTarget.iridium += r.results.iridiumAmount; + rsTarget.energyEarned += r.results.tradeEnergyAmount; + rsTarget.tradeCount++; + } + } + if (resourcesBought.tradeCount || resourcesSold.tradeCount) { + const tradeCount = resourcesBought.tradeCount + resourcesSold.tradeCount; + let message = tradeCount > 1 ? tradeCount + ' Trades Executed,' : tradeCount + ' Trade Executed,'; + if (resourcesBought.energySpent) { + message += + ' Spent ' + + Utils.decimalToFixed(resourcesBought.energySpent, 2) + + ' gold buying: ' + + (resourcesBought.food ? resourcesBought.food + ' food, ' : '') + + (resourcesBought.ore ? resourcesBought.ore + ' ore, ' : '') + + (resourcesBought.iridium ? resourcesBought.iridium + ' iridium, ' : ''); + } + if (resourcesSold.energyEarned) { + message += + ' Earned ' + + Utils.decimalToFixed(resourcesSold.energyEarned, 2) + + ' gold selling: ' + + (resourcesSold.food ? resourcesSold.food + ' food, ' : '') + + (resourcesSold.ore ? resourcesSold.ore + ' ore, ' : '') + + (resourcesSold.iridium ? resourcesSold.iridium + ' iridium, ' : ''); + } + message = message.substring(0, message.length - 2); + + Events.enqueueNewEvent(playerId, EventNotificationType.TradesExecuted, message); + } + + if (resourcesNotBought.tradeCount || resourcesNotSold.tradeCount) { + const tradeCount = resourcesNotBought.tradeCount + resourcesNotSold.tradeCount; + let message = tradeCount > 1 ? tradeCount + ' Trades Not Executed,' : tradeCount + ' Trade Not Executed,'; + if (resourcesNotBought.energySpent) { + message += + ' Could not Buy: ' + + (resourcesNotBought.food ? resourcesNotBought.food + ' food, ' : '') + + (resourcesNotBought.ore ? resourcesNotBought.ore + ' ore, ' : '') + + (resourcesNotBought.iridium ? resourcesNotBought.iridium + ' iridium, ' : ''); + } + if (resourcesNotSold.energyEarned) { + message += + ' Could not Sell: ' + + (resourcesNotSold.food ? resourcesNotSold.food + ' food, ' : '') + + (resourcesNotSold.ore ? resourcesNotSold.ore + ' ore, ' : '') + + (resourcesNotSold.iridium ? resourcesNotSold.iridium + ' iridium, ' : ''); + } + message = message.substring(0, message.length - 2); + + Events.enqueueNewEvent(playerId, EventNotificationType.TradesNotExecuted, message); + } + } + + // Note: tc.currentTrades now only contains pending trades (not yet executable) + // We no longer clear all trades here since pending trades should remain + this.earnInterest(tc); + + return executedTradeResultsByPlayerId; + } + + public static earnInterest(tradingCenter: TradingCenterData) { + tradingCenter.energyAmount += this.getResourcesTotalValue(tradingCenter) * tradingCenter.interestPercentage; + } + + public static getResourcesTotalValue(tradingCenter: TradingCenterData) { + let totalValue = tradingCenter.energyAmount; + totalValue += tradingCenter.foodResource.amount * tradingCenter.foodResource.currentPrice; + totalValue += tradingCenter.oreResource.amount * tradingCenter.oreResource.currentPrice; + totalValue += tradingCenter.iridiumResource.amount * tradingCenter.iridiumResource.currentPrice; + return totalValue; + } + + public static getPendingTrades(tradingCenterData: TradingCenterData, playerId: string): TradeData[] { + return tradingCenterData.currentTrades.filter((trade) => trade.playerId === playerId); + } + + public static getTradeTimeRemaining(trade: TradeData): number { + const now = Date.now(); + const remaining = trade.executeAfter - now; + return Math.max(0, remaining); + } + + public static canCancelTrade(trade: TradeData): boolean { + const now = Date.now(); + return now < trade.executeAfter; + } +} diff --git a/packages/astriarch-engine/src/index.ts b/packages/astriarch-engine/src/index.ts new file mode 100644 index 00000000..bee827f4 --- /dev/null +++ b/packages/astriarch-engine/src/index.ts @@ -0,0 +1,199 @@ +import { ClientGameModel } from './engine/clientGameModel'; +import { Events, Subscription } from './engine/events'; +import { Fleet } from './engine/fleet'; +import { GameController } from './engine/gameController'; +import { GameModel, GameModelData, playerColors, GALAXY_WIDTH, GALAXY_HEIGHT } from './engine/gameModel'; +import { Player } from './engine/player'; +import { Planet } from './engine/planet'; +import { PlanetProductionItem, CanBuildResult, CanBuildValidationResult } from './engine/planetProductionItem'; +import { Research } from './engine/research'; +import { TradingCenter } from './engine/tradingCenter'; +import { ClientModelData, PlanetById, ClientPlayer } from './model/clientModel'; +import { StarShipType, FleetData, StarshipData } from './model/fleet'; +import { + GalaxySizeOption, + GameOptions, + GameSpeed, + ModelData, + OpponentOptionType, + PlanetsPerSystemOption, + ServerGameOptions, +} from './model/model'; +import { + PlanetProductionItemData, + PlanetImprovementType, + CitizenWorkerType, + PlanetType, + PlanetHappinessType, + PlanetData, +} from './model/planet'; +import { PlayerData, PlayerType } from './model/player'; +import { ResearchType } from './model/research'; +import { TradeType, TradingCenterResourceType, TradeData, TradingCenterData } from './model/tradingCenter'; +import { Grid } from './engine/grid'; +import { EventNotification, EventNotificationType, PlanetaryConflictData } from './model/eventNotification'; +import { GameTools } from './utils/gameTools'; + +// Export messaging types +export * from './messaging/MessageTypes'; + +// Export engine classes +export { Player }; +export { Planet }; +export { PlanetProductionItem }; +export { Fleet }; +export { GameModel }; +export { ClientGameModel }; +export { GameController }; +export { Events }; +export { Grid }; +export { Research }; +export { TradingCenter }; +export { GameTools }; + +// Export model types and enums +export { PlayerType }; +export { StarShipType }; +export { GalaxySizeOption }; +export { GameSpeed }; +export { PlanetsPerSystemOption }; +export { ResearchType }; +export { CitizenWorkerType }; +export { PlanetType }; +export { PlanetImprovementType }; +export { PlanetHappinessType }; +export { EventNotificationType }; +export { CanBuildResult }; +export { playerColors }; +export { GALAXY_WIDTH }; +export { GALAXY_HEIGHT }; +export { TradeType }; +export { TradingCenterResourceType }; + +// Export interfaces +export type { PlayerData }; +export type { GameOptions }; +export type { ServerGameOptions }; +export type { ModelData }; +export type { GameModelData }; +export type { ClientModelData }; +export type { PlanetById }; +export type { ClientPlayer }; +export type { PlanetProductionItemData }; +export type { CanBuildValidationResult }; +export type { Subscription }; +export type { FleetData }; +export type { StarshipData }; +export type { EventNotification }; +export type { PlanetaryConflictData }; +export type { PlanetData }; +export type { TradeData }; +export type { TradingCenterData }; + +export const MS_PER_TICK = 200; // Time for client side refreshes +export const MS_PER_CYCLE = 30 * 1000; // Time per "turn" + +export const createGame = (hostPlayerId: string, hostPlayerName: string, gameOptions: GameOptions) => { + const players = [] as PlayerData[]; + players.push(Player.constructPlayer(hostPlayerId, PlayerType.Human, hostPlayerName, playerColors[0])); + const gameModel = GameModel.constructData(players, gameOptions); + return gameModel; +}; + +export const getDefaultServerGameOptions = (userPreferences: Partial): ServerGameOptions => { + const defaults: ServerGameOptions = { + name: 'New Game', + mainPlayerName: 'Player', + systemsToGenerate: 4, + planetsPerSystem: PlanetsPerSystemOption.FOUR, + galaxySize: GalaxySizeOption.SMALL, + gameSpeed: GameSpeed.NORMAL, + distributePlanetsEvenly: true, + quickStart: false, + opponentOptions: [ + { name: '', type: OpponentOptionType.OPEN }, // Player 2: Open + { name: '', type: OpponentOptionType.HARD_COMPUTER }, // Player 3: Hard Computer + { name: '', type: OpponentOptionType.EXPERT_COMPUTER }, // Player 4: Expert Computer + ], + version: '2.0', + }; + + return { + ...defaults, + ...userPreferences, + }; +}; + +export const constructClientGameModel = (gameModel: ModelData, playerId: string) => { + const clientModel = ClientGameModel.constructClientGameModel(gameModel, playerId); + return clientModel; +}; + +export const startNewGame = () => { + // NOTE: just for testing right now + const players = [] as PlayerData[]; + players.push(Player.constructPlayer('me', PlayerType.Human, 'Matt', playerColors[0])); + players.push(Player.constructPlayer('c1', PlayerType.Computer_Hard, 'Computer1', playerColors[1])); + + players[0].research.researchPercent = 0.9; + players[0].research.researchTypeInQueue = ResearchType.PROPULSION_IMPROVEMENT; + + const gameOptions = { + systemsToGenerate: 2, + planetsPerSystem: PlanetsPerSystemOption.FIVE, + galaxySize: GalaxySizeOption.SMALL, + distributePlanetsEvenly: true, + quickStart: true, + gameSpeed: GameSpeed.NORMAL, + version: '2.0', + }; + + const gameModel = GameModel.constructData(players, gameOptions); + + const homePlanet = gameModel.modelData.planets.find((p) => p.id === players[0].homePlanetId); + homePlanet!.population[0].workerType = CitizenWorkerType.Miner; + + const clientGameModel = ClientGameModel.constructClientGameModel(gameModel.modelData, 'me'); + + return { gameModel, clientGameModel }; +}; + +export const advanceGameModelTime = (gameModel: GameModelData) => { + GameController.advanceGameClock(gameModel); + + return gameModel; +}; + +export const advanceClientGameModelTime = ( + clientGameModel: ClientModelData, + grid: Grid, +): { clientGameModel: ClientModelData; fleetsArrivingOnUnownedPlanets: FleetData[] } => { + const fleetsArrivingOnUnownedPlanets = GameController.advanceClientGameClock(clientGameModel, grid); + + return { clientGameModel, fleetsArrivingOnUnownedPlanets }; +}; + +export const resetGameSnapshotTime = (gameModel: GameModelData) => { + GameController.resetSnapshotTime(gameModel.modelData); + return gameModel; +}; + +export const subscribeToEvents = (playerId: string, callback: Subscription) => { + Events.subscribe(playerId, callback); +}; + +export const getPlayerTotalResources = (player: PlayerData, planetById: PlanetById) => { + return GameModel.getPlayerTotalResources(player, planetById); +}; + +export const getPlayerTotalResourceProductionPerTurn = (player: PlayerData, planetById: PlanetById) => { + return GameModel.getPlayerTotalResourceProductionPerTurn(player, planetById); +}; + +export const getPlayerTotalPopulation = (player: PlayerData, planetById: PlanetById) => { + return GameModel.getPlayerTotalPopulation(player, planetById); +}; + +export const getEngineVersion = () => { + return '2.0.0'; +}; diff --git a/packages/astriarch-engine/src/messaging/MessageTypes.ts b/packages/astriarch-engine/src/messaging/MessageTypes.ts new file mode 100644 index 00000000..f5de87a4 --- /dev/null +++ b/packages/astriarch-engine/src/messaging/MessageTypes.ts @@ -0,0 +1,482 @@ +/** + * WebSocket Message Types - Shared between client and server + * Based on the original Astriarch architecture from app.js + */ + +import { ServerGameOptions } from '../model'; + +export enum MESSAGE_TYPE { + // Connection & System + NOOP = 'NOOP', + PING = 'PING', + PONG = 'PONG', + ERROR = 'ERROR', + + // Game Management + CREATE_GAME = 'CREATE_GAME', + JOIN_GAME = 'JOIN_GAME', + LIST_GAMES = 'LIST_GAMES', + GAME_LIST_UPDATED = 'GAME_LIST_UPDATED', + START_GAME = 'START_GAME', + RESUME_GAME = 'RESUME_GAME', + CHANGE_GAME_OPTIONS = 'CHANGE_GAME_OPTIONS', + CHANGE_PLAYER_NAME = 'CHANGE_PLAYER_NAME', + + // Game Actions + SYNC_STATE = 'SYNC_STATE', + SEND_SHIPS = 'SEND_SHIPS', + UPDATE_PLANET_OPTIONS = 'UPDATE_PLANET_OPTIONS', + UPDATE_PLANET_BUILD_QUEUE = 'UPDATE_PLANET_BUILD_QUEUE', + CLEAR_WAYPOINT = 'CLEAR_WAYPOINT', + + // Research & Trading + ADJUST_RESEARCH_PERCENT = 'ADJUST_RESEARCH_PERCENT', + SUBMIT_RESEARCH_ITEM = 'SUBMIT_RESEARCH_ITEM', + CANCEL_RESEARCH_ITEM = 'CANCEL_RESEARCH_ITEM', + SUBMIT_TRADE = 'SUBMIT_TRADE', + CANCEL_TRADE = 'CANCEL_TRADE', + + // Chat & Communication + CHAT_MESSAGE = 'CHAT_MESSAGE', + CHAT_ROOM_SESSIONS_UPDATED = 'CHAT_ROOM_SESSIONS_UPDATED', + + // Game State + GAME_STATE_UPDATE = 'GAME_STATE_UPDATE', + EVENT_NOTIFICATIONS = 'EVENT_NOTIFICATIONS', + GAME_OVER = 'GAME_OVER', + GAME_PAUSED = 'GAME_PAUSED', + GAME_RESUMED = 'GAME_RESUMED', + PLAYER_DISCONNECTED = 'PLAYER_DISCONNECTED', + PLAYER_RECONNECTED = 'PLAYER_RECONNECTED', + + // Player Actions + EXIT_RESIGN = 'EXIT_RESIGN', + LOGOUT = 'LOGOUT', +} + +export enum ERROR_TYPE { + INVALID_GAME_OPTIONS = 'INVALID_GAME_OPTIONS', + GAME_NOT_FOUND = 'GAME_NOT_FOUND', + PLAYER_NOT_FOUND = 'PLAYER_NOT_FOUND', + UNAUTHORIZED = 'UNAUTHORIZED', + INVALID_ACTION = 'INVALID_ACTION', +} + +export enum CHAT_MESSAGE_TYPE { + TEXT_MESSAGE = 'TEXT_MESSAGE', + PLAYER_ENTER = 'PLAYER_ENTER', + PLAYER_LEAVE = 'PLAYER_LEAVE', + SYSTEM_MESSAGE = 'SYSTEM_MESSAGE', +} + +// ============================================= +// PAYLOAD TYPE DEFINITIONS +// ============================================= + +// Game data interfaces +export interface IGame { + _id: string; + name: string; + players: IPlayer[]; + hostPlayerName?: string; // Optional to maintain compatibility + gameOptions?: ServerGameOptions; + status: 'waiting' | 'in_progress' | 'completed'; + createdAt: Date; + lastActivity: Date; +} + +export interface IPlayer { + position: number; + name: string; + connected?: boolean; +} + +// Payload interfaces for each message type +export interface INoopPayload { + counter?: number; + message?: string; +} + +export interface IPingPayload { + timestamp?: number; +} + +export interface IPongPayload { + timestamp?: number; +} + +export interface IErrorPayload { + error: string; + message?: string; + errorType?: ERROR_TYPE; +} + +// Game Management Payloads +export interface ICreateGameRequestPayload { + name: string; + playerName: string; + gameOptions: ServerGameOptions; +} + +export interface ICreateGameResponsePayload { + gameId: string; + gameName?: string; +} + +export interface IJoinGameRequestPayload { + gameId: string; + playerName: string; +} + +export interface IJoinGameResponsePayload { + success: boolean; + gameId?: string; + error?: string; +} + +export type IListGamesRequestPayload = Record; + +export interface IListGamesResponsePayload { + games: IGame[]; +} + +export interface IStartGameRequestPayload { + gameId: string; + gameOptions?: ServerGameOptions; +} + +export interface IStartGameResponsePayload { + success: boolean; + gameState?: unknown; + error?: string; +} + +export interface IChangeGameOptionsPayload { + gameId: string; + gameOptions: ServerGameOptions; + playerName?: string; +} + +export interface IChangePlayerNamePayload { + gameId: string; + playerName: string; +} + +export interface IGameStateUpdatePayload { + gameState?: unknown; + changes?: unknown; + clientGameModel?: unknown; + currentCycle?: number; +} + +export interface IEventNotificationsPayload { + events: { + playerId: string; + type: number; // EventNotificationType + message: string; + planet?: unknown; // PlanetData + data?: unknown; // PlanetaryConflictData + }[]; +} + +export interface IChatMessagePayload { + id: string; + playerId: string; + playerName: string; + message: string; + timestamp: number; + messageType?: CHAT_MESSAGE_TYPE; +} + +// ============================================= +// GENERIC MESSAGE INTERFACE AND BASE CLASS +// ============================================= + +export interface IMessage> { + type: MESSAGE_TYPE; + payload: T; + sessionId?: string; + gameId?: string; + timestamp?: Date; +} + +export class Message> implements IMessage { + public type: MESSAGE_TYPE; + public payload: T; + public sessionId?: string; + public gameId?: string; + public timestamp: Date; + + constructor(type: MESSAGE_TYPE, payload: T, sessionId?: string, gameId?: string) { + this.type = type; + this.payload = payload; + this.sessionId = sessionId; + this.gameId = gameId; + this.timestamp = new Date(); + } +} + +// ============================================= +// TYPED MESSAGE CLASSES +// ============================================= + +// System Messages +export class NoopMessage extends Message { + constructor(payload: INoopPayload, sessionId?: string) { + super(MESSAGE_TYPE.NOOP, payload, sessionId); + } +} + +export class PingMessage extends Message { + constructor(payload: IPingPayload = {}, sessionId?: string) { + super(MESSAGE_TYPE.PING, payload, sessionId); + } +} + +export class PongMessage extends Message { + constructor(payload: IPongPayload = {}, sessionId?: string) { + super(MESSAGE_TYPE.PONG, payload, sessionId); + } +} + +export class ErrorMessage extends Message { + constructor(payload: IErrorPayload, sessionId?: string) { + super(MESSAGE_TYPE.ERROR, payload, sessionId); + } +} + +// Game Management Messages - Client to Server +export class CreateGameRequestMessage extends Message { + constructor(payload: ICreateGameRequestPayload, sessionId?: string) { + super(MESSAGE_TYPE.CREATE_GAME, payload, sessionId); + } +} + +export class JoinGameRequestMessage extends Message { + constructor(payload: IJoinGameRequestPayload, sessionId?: string) { + super(MESSAGE_TYPE.JOIN_GAME, payload, sessionId); + } +} + +export class ListGamesRequestMessage extends Message { + constructor(sessionId?: string) { + super(MESSAGE_TYPE.LIST_GAMES, {} as IListGamesRequestPayload, sessionId); + } +} + +export class StartGameRequestMessage extends Message { + constructor(payload: IStartGameRequestPayload, sessionId?: string) { + super(MESSAGE_TYPE.START_GAME, payload, sessionId); + } +} + +// Game Management Messages - Server to Client +export class CreateGameResponseMessage extends Message { + constructor(payload: ICreateGameResponsePayload, sessionId?: string) { + super(MESSAGE_TYPE.CREATE_GAME, payload, sessionId); + } +} + +export class JoinGameResponseMessage extends Message { + constructor(payload: IJoinGameResponsePayload, sessionId?: string) { + super(MESSAGE_TYPE.JOIN_GAME, payload, sessionId); + } +} + +export class ListGamesResponseMessage extends Message { + constructor(payload: IListGamesResponsePayload, sessionId?: string) { + super(MESSAGE_TYPE.LIST_GAMES, payload, sessionId); + } +} + +export class StartGameResponseMessage extends Message { + constructor(payload: IStartGameResponsePayload, sessionId?: string) { + super(MESSAGE_TYPE.START_GAME, payload, sessionId); + } +} + +export class GameStateUpdateMessage extends Message { + constructor(payload: IGameStateUpdatePayload, sessionId?: string) { + super(MESSAGE_TYPE.GAME_STATE_UPDATE, payload, sessionId); + } +} + +export class ChatMessage extends Message { + constructor(payload: IChatMessagePayload, sessionId?: string) { + super(MESSAGE_TYPE.CHAT_MESSAGE, payload, sessionId); + } +} + +// ============================================= +// TYPE GUARDS FOR RUNTIME TYPE CHECKING +// ============================================= + +export function isCreateGameRequest(message: IMessage): message is Message { + return ( + message.type === MESSAGE_TYPE.CREATE_GAME && + typeof message.payload === 'object' && + message.payload !== null && + 'name' in message.payload && + 'playerName' in message.payload + ); +} + +export function isCreateGameResponse(message: IMessage): message is Message { + return ( + message.type === MESSAGE_TYPE.CREATE_GAME && + typeof message.payload === 'object' && + message.payload !== null && + 'gameId' in message.payload + ); +} + +export function isJoinGameRequest(message: IMessage): message is Message { + return ( + message.type === MESSAGE_TYPE.JOIN_GAME && + typeof message.payload === 'object' && + message.payload !== null && + 'gameId' in message.payload && + 'playerName' in message.payload + ); +} + +export function isJoinGameResponse(message: IMessage): message is Message { + return message.type === MESSAGE_TYPE.JOIN_GAME && typeof message.payload === 'object' && message.payload !== null; +} + +export function isListGamesResponse(message: IMessage): message is Message { + return ( + (message.type === MESSAGE_TYPE.LIST_GAMES || message.type === MESSAGE_TYPE.GAME_LIST_UPDATED) && + typeof message.payload === 'object' && + message.payload !== null && + 'games' in message.payload + ); +} + +export function isStartGameRequest(message: IMessage): message is Message { + return message.type === MESSAGE_TYPE.START_GAME; +} + +export function isStartGameResponse(message: IMessage): message is Message { + return ( + message.type === MESSAGE_TYPE.START_GAME && + typeof message.payload === 'object' && + message.payload !== null && + 'success' in message.payload + ); +} + +export function isGameStateUpdate(message: IMessage): message is Message { + return message.type === MESSAGE_TYPE.GAME_STATE_UPDATE; +} + +export function isChatMessage(message: IMessage): message is Message { + return ( + message.type === MESSAGE_TYPE.CHAT_MESSAGE && + typeof message.payload === 'object' && + message.payload !== null && + 'message' in message.payload + ); +} + +export function isErrorMessage(message: IMessage): message is Message { + return ( + message.type === MESSAGE_TYPE.ERROR && + typeof message.payload === 'object' && + message.payload !== null && + 'error' in message.payload + ); +} + +export function getMessageTypeName(type: MESSAGE_TYPE): string { + return MESSAGE_TYPE[type] || 'UNKNOWN'; +} + +/** + * Helper function to get message type numeric values for compatibility + * with the original app.js numeric message system + */ +export function getMessageTypeNumeric(type: MESSAGE_TYPE): number { + const typeMap: Record = { + [MESSAGE_TYPE.NOOP]: 0, + [MESSAGE_TYPE.PING]: 1, + [MESSAGE_TYPE.PONG]: 2, + [MESSAGE_TYPE.LIST_GAMES]: 10, + [MESSAGE_TYPE.CREATE_GAME]: 11, + [MESSAGE_TYPE.JOIN_GAME]: 12, + [MESSAGE_TYPE.START_GAME]: 13, + [MESSAGE_TYPE.RESUME_GAME]: 14, + [MESSAGE_TYPE.CHANGE_GAME_OPTIONS]: 15, + [MESSAGE_TYPE.CHANGE_PLAYER_NAME]: 16, + [MESSAGE_TYPE.SYNC_STATE]: 20, + [MESSAGE_TYPE.SEND_SHIPS]: 21, + //[MESSAGE_TYPE.UPDATE_PLANET_START]: 22, + [MESSAGE_TYPE.UPDATE_PLANET_OPTIONS]: 23, + [MESSAGE_TYPE.UPDATE_PLANET_BUILD_QUEUE]: 24, + [MESSAGE_TYPE.CLEAR_WAYPOINT]: 25, + [MESSAGE_TYPE.ADJUST_RESEARCH_PERCENT]: 26, + [MESSAGE_TYPE.SUBMIT_RESEARCH_ITEM]: 27, + [MESSAGE_TYPE.CANCEL_RESEARCH_ITEM]: 28, + [MESSAGE_TYPE.SUBMIT_TRADE]: 29, + [MESSAGE_TYPE.CANCEL_TRADE]: 30, + [MESSAGE_TYPE.CHAT_MESSAGE]: 40, + [MESSAGE_TYPE.EXIT_RESIGN]: 50, + [MESSAGE_TYPE.LOGOUT]: 51, + [MESSAGE_TYPE.ERROR]: 60, + [MESSAGE_TYPE.GAME_LIST_UPDATED]: 61, + [MESSAGE_TYPE.GAME_STATE_UPDATE]: 62, + [MESSAGE_TYPE.EVENT_NOTIFICATIONS]: 63, + [MESSAGE_TYPE.GAME_OVER]: 64, + [MESSAGE_TYPE.GAME_PAUSED]: 65, + [MESSAGE_TYPE.GAME_RESUMED]: 66, + [MESSAGE_TYPE.PLAYER_DISCONNECTED]: 67, + [MESSAGE_TYPE.PLAYER_RECONNECTED]: 68, + [MESSAGE_TYPE.CHAT_ROOM_SESSIONS_UPDATED]: 69, + }; + + return typeMap[type] ?? -1; +} + +/** + * Convert numeric message type back to enum (for compatibility) + */ +export function getMessageTypeFromNumeric(typeNum: number): MESSAGE_TYPE | null { + const reverseMap: Record = { + 0: MESSAGE_TYPE.NOOP, + 1: MESSAGE_TYPE.PING, + 2: MESSAGE_TYPE.PONG, + 10: MESSAGE_TYPE.LIST_GAMES, + 11: MESSAGE_TYPE.CREATE_GAME, + 12: MESSAGE_TYPE.JOIN_GAME, + 13: MESSAGE_TYPE.START_GAME, + 14: MESSAGE_TYPE.RESUME_GAME, + 15: MESSAGE_TYPE.CHANGE_GAME_OPTIONS, + 16: MESSAGE_TYPE.CHANGE_PLAYER_NAME, + 20: MESSAGE_TYPE.SYNC_STATE, + 21: MESSAGE_TYPE.SEND_SHIPS, + //22: MESSAGE_TYPE.UPDATE_PLANET_START, + 23: MESSAGE_TYPE.UPDATE_PLANET_OPTIONS, + 24: MESSAGE_TYPE.UPDATE_PLANET_BUILD_QUEUE, + 25: MESSAGE_TYPE.CLEAR_WAYPOINT, + 26: MESSAGE_TYPE.ADJUST_RESEARCH_PERCENT, + 27: MESSAGE_TYPE.SUBMIT_RESEARCH_ITEM, + 28: MESSAGE_TYPE.CANCEL_RESEARCH_ITEM, + 29: MESSAGE_TYPE.SUBMIT_TRADE, + 30: MESSAGE_TYPE.CANCEL_TRADE, + 40: MESSAGE_TYPE.CHAT_MESSAGE, + 50: MESSAGE_TYPE.EXIT_RESIGN, + 51: MESSAGE_TYPE.LOGOUT, + 60: MESSAGE_TYPE.ERROR, + 61: MESSAGE_TYPE.GAME_LIST_UPDATED, + 62: MESSAGE_TYPE.GAME_STATE_UPDATE, + 63: MESSAGE_TYPE.EVENT_NOTIFICATIONS, + 64: MESSAGE_TYPE.GAME_OVER, + 65: MESSAGE_TYPE.GAME_PAUSED, + 66: MESSAGE_TYPE.GAME_RESUMED, + 67: MESSAGE_TYPE.PLAYER_DISCONNECTED, + 68: MESSAGE_TYPE.PLAYER_RECONNECTED, + 69: MESSAGE_TYPE.CHAT_ROOM_SESSIONS_UPDATED, + }; + + return reverseMap[typeNum] ?? null; +} diff --git a/packages/astriarch-engine/src/model/clientModel.ts b/packages/astriarch-engine/src/model/clientModel.ts new file mode 100644 index 00000000..b1281f3e --- /dev/null +++ b/packages/astriarch-engine/src/model/clientModel.ts @@ -0,0 +1,59 @@ +import { PointData } from '../shapes/shapes'; +import { ModelBase } from './model'; +import { PlanetData, PlanetType } from './planet'; +import { ColorRgbaData, PlayerData, PlayerType } from './player'; +import { ResearchData } from './research'; +import { TradeData, TradingCenterResource } from './tradingCenter'; + +export type PlanetById = Record; + +export interface ClientPlayer { + id: string; + type: PlayerType; + name: string; + color: ColorRgbaData; + points: number; + destroyed: boolean; + research: ResearchData; +} + +export interface ClientPlanet { + id: number; + name: string; + originPoint: PointData; + boundingHexMidPoint: PointData; + type: PlanetType | null; //NOTE: Populated when the main player explores the planet +} + +export interface ClientTradingCenter { + energyAmount: number; + foodResource: TradingCenterResource; + oreResource: TradingCenterResource; + iridiumResource: TradingCenterResource; + mainPlayerTrades: TradeData[]; +} + +export type TaskNotificationByPlanetId = Record; + +export interface TaskNotification { + type: TaskNotificationType; + planetId: number; + planetName: string; + data: { energyGenerated: number }; +} + +export enum TaskNotificationType { + BuildQueueEmpty = 0, +} + +export type TaskNotificationIndex = Record; + +export interface ClientModelData extends ModelBase { + clientTradingCenter: ClientTradingCenter; + mainPlayer: PlayerData; + mainPlayerOwnedPlanets: PlanetById; + clientPlayers: ClientPlayer[]; + clientPlanets: ClientPlanet[]; + + taskNotifications: TaskNotificationIndex; +} diff --git a/packages/astriarch-engine/src/model/earnedPoints.ts b/packages/astriarch-engine/src/model/earnedPoints.ts new file mode 100644 index 00000000..d3bcf541 --- /dev/null +++ b/packages/astriarch-engine/src/model/earnedPoints.ts @@ -0,0 +1,20 @@ +export enum EarnedPointsType { + POPULATION_GROWTH = 0, + PRODUCTION_UNIT_BUILT = 1, + REPAIRED_STARSHIP_STRENGTH = 2, + DAMAGED_STARSHIP_STRENGTH = 3, + CITIZEN_ON_CAPTURED_PLANET = 4, +} + +export interface EarnedPointsConfigData { + pointsPer: number; + maxPoints: number; +} + +export const earnedPointsConfigByType: Record = { + [EarnedPointsType.POPULATION_GROWTH]: { pointsPer: 4, maxPoints: 100000 }, + [EarnedPointsType.PRODUCTION_UNIT_BUILT]: { pointsPer: 0.25, maxPoints: 500 }, + [EarnedPointsType.REPAIRED_STARSHIP_STRENGTH]: { pointsPer: 2, maxPoints: 4000 }, + [EarnedPointsType.DAMAGED_STARSHIP_STRENGTH]: { pointsPer: 0.5, maxPoints: 1000 }, + [EarnedPointsType.CITIZEN_ON_CAPTURED_PLANET]: { pointsPer: 10, maxPoints: 10000 }, +}; diff --git a/packages/astriarch-engine/src/model/eventNotification.ts b/packages/astriarch-engine/src/model/eventNotification.ts new file mode 100644 index 00000000..6999e8c9 --- /dev/null +++ b/packages/astriarch-engine/src/model/eventNotification.ts @@ -0,0 +1,51 @@ +import { ClientPlayer } from './clientModel'; +import { FleetData } from './fleet'; +import { PlanetData, PlanetResourceData } from './planet'; + +export interface ResearchBoost { + attack: number; + defense: number; +} + +export interface PlanetaryConflictData { + defendingClientPlayer: ClientPlayer | null; + defendingFleet: FleetData; + defendingFleetResearchBoost: ResearchBoost; + attackingClientPlayer: ClientPlayer; + attackingFleet: FleetData; + attackingFleetResearchBoost: ResearchBoost; + attackingFleetChances: number | null; + winningFleet: FleetData | null; // Fleet remaining + resourcesLooted: PlanetResourceData; +} + +export enum EventNotificationType { + ResourcesAutoSpent = 0, + PopulationGrowth = 1, + TradesExecuted = 2, + TradesNotExecuted = 3, + ImprovementBuilt = 4, + ShipBuilt = 5, + ImprovementDemolished = 6, + //BuildQueueEmpty = 7, + ResearchComplete = 8, + ResearchQueueEmpty = 9, + ResearchStolen = 10, + CitizensProtesting = 11, + InsufficientFood = 12, //either general food shortage or couldn't ship because of lack of gold, leads to population unrest + DefendedAgainstAttackingFleet = 13, + AttackingFleetLost = 14, + PlanetCaptured = 15, + PopulationStarvation = 16, + FoodShortageRiots = 17, + PlanetLostDueToStarvation = 18, //this is bad but you probably know it's bad + PlanetLost = 19, +} + +export interface EventNotification { + playerId: string; + type: EventNotificationType; + message: string; + planet?: PlanetData; //only populated if applies to message + data?: PlanetaryConflictData; //for planetary conflict type messages +} diff --git a/packages/astriarch-engine/src/model/fleet.ts b/packages/astriarch-engine/src/model/fleet.ts new file mode 100644 index 00000000..ecd8c070 --- /dev/null +++ b/packages/astriarch-engine/src/model/fleet.ts @@ -0,0 +1,38 @@ +import { PointData } from '../shapes/shapes'; + +export enum StarShipType { + SystemDefense = 1, //System defense ships are not equiped with hyperdrive and cannot leave the system they are in + Scout = 2, + Destroyer = 3, + Cruiser = 4, + Battleship = 5, + SpacePlatform = 6, //provides defense for the planet, further speeds ship production and allows for cruiser and battleship production +} + +export interface StarshipAdvantageData { + advantageAgainst: StarShipType; + disadvantageAgainst: StarShipType; +} + +export interface StarshipData { + id: number; + type: StarShipType; + customShipData?: StarshipAdvantageData; + health: number; + experienceAmount: number; +} + +export interface FleetData { + starships: StarshipData[]; + locationHexMidPoint: PointData | null; + travelingFromHexMidPoint: PointData | null; + destinationHexMidPoint: PointData | null; + parsecsToDestination: number | null; + totalTravelDistance: number | null; +} + +export interface LastKnownFleetData { + cycleLastExplored: number; + fleetData: FleetData; + lastKnownOwnerId: string | undefined; +} diff --git a/packages/astriarch-engine/src/model/index.ts b/packages/astriarch-engine/src/model/index.ts new file mode 100644 index 00000000..6652d2c5 --- /dev/null +++ b/packages/astriarch-engine/src/model/index.ts @@ -0,0 +1,10 @@ +// Export all modules from the model directory +export * from './clientModel'; +export * from './earnedPoints'; +export * from './eventNotification'; +export * from './fleet'; +export * from './model'; +export * from './planet'; +export * from './player'; +export * from './research'; +export * from './tradingCenter'; diff --git a/packages/astriarch-engine/src/model/model.ts b/packages/astriarch-engine/src/model/model.ts new file mode 100644 index 00000000..fcb0f666 --- /dev/null +++ b/packages/astriarch-engine/src/model/model.ts @@ -0,0 +1,70 @@ +import { PlanetData } from './planet'; +import { PlayerData } from './player'; +import { TradingCenterData } from './tradingCenter'; + +export enum GameSpeed { + SLOWEST = 1, + SLOW = 2, + NORMAL = 3, + FAST = 4, + FASTEST = 5, +} + +export enum PlanetsPerSystemOption { + FOUR = 4, + FIVE = 5, + SIX = 6, + SEVEN = 7, + EIGHT = 8, +} + +export enum GalaxySizeOption { + TINY = 1, + SMALL = 2, + MEDIUM = 3, + LARGE = 4, +} //multiplier for Astriarch.Hexagon.Static + +export enum OpponentOptionType { + CLOSED = -2, + OPEN = -1, + HUMAN = 0, + EASY_COMPUTER = 1, + NORMAL_COMPUTER = 2, + HARD_COMPUTER = 3, + EXPERT_COMPUTER = 4, +} + +export interface OpponentOption { + name: string; + type: OpponentOptionType; +} + +export interface GameOptions { + systemsToGenerate: number; // Number of systems (2-4 players) + planetsPerSystem: PlanetsPerSystemOption; + galaxySize: GalaxySizeOption; + distributePlanetsEvenly: boolean; + quickStart: boolean; + gameSpeed: GameSpeed; + version: string; +} + +export interface ServerGameOptions extends GameOptions { + name?: string; + mainPlayerName?: string; + opponentOptions: OpponentOption[]; +} + +export interface ModelBase { + gameOptions: GameOptions; + gameStartedAtTime: number; + lastSnapshotTime: number; + currentCycle: number; +} + +export interface ModelData extends ModelBase { + tradingCenter: TradingCenterData; + players: PlayerData[]; + planets: PlanetData[]; +} diff --git a/packages/astriarch-engine/src/model/planet.ts b/packages/astriarch-engine/src/model/planet.ts new file mode 100644 index 00000000..bb0868b0 --- /dev/null +++ b/packages/astriarch-engine/src/model/planet.ts @@ -0,0 +1,92 @@ +import { PointData } from '../shapes/shapes'; +import { ClientPlanet } from './clientModel'; +import { StarshipAdvantageData, FleetData, StarShipType } from './fleet'; + +export interface Citizen { + populationChange: number; //between -1 and 1, when this gets >= -1 then we loose one pop, > 1 we gain one pop + loyalToPlayerId: string | undefined; //this allows us to remove the protest level when ownership of the planet reverts to this player, after protest level hits 0, this should be reset to the current owner + protestLevel: number; //between 0 and 1, 0 means they are able to work, anything above this means they are busy protesting the government rule + workerType: CitizenWorkerType; +} + +export enum PlanetResourceType { + FOOD, + ENERGY, + RESEARCH, + ORE, + IRIDIUM, + PRODUCTION, +} + +export interface PlanetResourceData { + food: number; + energy: number; + research: number; + ore: number; + iridium: number; + production: number; +} + +export enum PlanetImprovementType { + Factory = 1, //increases the speed of building other improvements and ships (and allows for building destroyers and the space platform) + Colony = 2, //increases the max population + Farm = 3, //increases food production + Mine = 4, //increases the rate of raw minerals production +} + +export enum PlanetType { + AsteroidBelt = 1, + DeadPlanet = 2, + PlanetClass1 = 3, + PlanetClass2 = 4, +} + +export enum PlanetHappinessType { + Normal = 1, + Unrest = 2, + Riots = 3, +} + +export enum CitizenWorkerType { + Farmer = 1, + Miner = 2, + Builder = 3, +} + +export enum PlanetProductionItemType { + PlanetImprovement = 1, + StarShipInProduction = 2, + PlanetImprovementToDestroy = 3, +} + +export interface ProductionItemResources { + energyCost: number; + oreCost: number; + iridiumCost: number; +} + +export interface PlanetProductionItemData extends ProductionItemResources { + itemType: PlanetProductionItemType; + improvementData?: { type: PlanetImprovementType }; + starshipData?: { type: StarShipType; customShipData?: StarshipAdvantageData }; + turnsToComplete: number; //once this is built turns to complete will be 0 and will go into the built improvements for the planet + productionCostComplete: number; //this is how much of the BaseProductionCost we've completed + baseProductionCost: number; //this will translate into Turns to Complete based on population, factories, etc... + resourcesSpent: boolean; //once this item is at the front of the queue we spend resources on it +} + +export interface PlanetData extends ClientPlanet { + type: PlanetType; + population: Citizen[]; + buildQueue: PlanetProductionItemData[]; + builtImprovements: Record; + maxImprovements: number; + resources: PlanetResourceData; + planetaryFleet: FleetData; + outgoingFleets: FleetData[]; + planetHappiness: PlanetHappinessType; + starshipTypeLastBuilt: StarShipType | null; + starshipCustomShipLastBuilt: boolean; + buildLastStarship: boolean; + waypointBoundingHexMidPoint: PointData | null; +} diff --git a/packages/astriarch-engine/src/model/player.ts b/packages/astriarch-engine/src/model/player.ts new file mode 100644 index 00000000..39162d1f --- /dev/null +++ b/packages/astriarch-engine/src/model/player.ts @@ -0,0 +1,47 @@ +import { EarnedPointsType } from './earnedPoints'; +import { FleetData, LastKnownFleetData } from './fleet'; +import { PlanetProductionItemData } from './planet'; +import { ResearchData } from './research'; + +export interface PlayerGameOptions { + showPlanetaryConflictPopups: boolean; +} + +export enum PlayerType { + Human = 0, + Computer_Easy = 1, + Computer_Normal = 2, + Computer_Hard = 3, + Computer_Expert = 4, +} + +export interface ColorRgbaData { + r: number; // red + g: number; // green + b: number; // blue + a: number; // alpha +} + +export type EarnedPointsByType = Record; + +export type LastKnownPlanetFleetStrength = Record; + +export interface PlayerData { + id: string; + type: PlayerType; + name: string; + research: ResearchData; + color: ColorRgbaData; + lastTurnFoodNeededToBeShipped: number; //this is to know how much energy to keep in surplus for food shipments + lastTurnFoodShipped: number; //how much food was actually shipped last turn + options: PlayerGameOptions; + ownedPlanetIds: number[]; + knownPlanetIds: number[]; + lastKnownPlanetFleetStrength: LastKnownPlanetFleetStrength; + planetBuildGoals: Record; + homePlanetId: number | null; + earnedPointsByType: EarnedPointsByType; + points: number; + fleetsInTransit: FleetData[]; + destroyed: boolean; +} diff --git a/packages/astriarch-engine/src/model/research.ts b/packages/astriarch-engine/src/model/research.ts new file mode 100644 index 00000000..46792d4e --- /dev/null +++ b/packages/astriarch-engine/src/model/research.ts @@ -0,0 +1,43 @@ +import { StarShipType } from './fleet'; + +export enum ResearchType { + UNKNOWN = 0, + NEW_SHIP_TYPE_DEFENDER = 1, + NEW_SHIP_TYPE_SCOUT = 2, + NEW_SHIP_TYPE_DESTROYER = 3, + NEW_SHIP_TYPE_CRUISER = 4, + NEW_SHIP_TYPE_BATTLESHIP = 5, + COMBAT_IMPROVEMENT_ATTACK = 6, //data key is "chance" 0 - 1 of increasing damage by 50% + COMBAT_IMPROVEMENT_DEFENSE = 7, //data key is "chance" 0 - 1 of decreasing damage by 50% + PROPULSION_IMPROVEMENT = 8, //data key is "percent" 1 - 10 + BUILDING_EFFICIENCY_IMPROVEMENT_FARMS = 9, //data key is "percent" 1 - 2 increased efficiency of farms + BUILDING_EFFICIENCY_IMPROVEMENT_MINES = 10, //data key is "percent" 1 - 2 increased efficiency of mines + BUILDING_EFFICIENCY_IMPROVEMENT_COLONIES = 11, //data key is "percent" 1 - 2 increases population growth on planet + BUILDING_EFFICIENCY_IMPROVEMENT_FACTORIES = 12, //data key is "percent" 1 - 2 increased efficiency of factories + SPACE_PLATFORM_IMPROVEMENT = 13, //data key is "max" 1 - 5 maximum space platforms player can build on a planet +} + +export interface ResearchTypeProgressData { + chance?: number; // applies to combat improvements + percent?: number; // applies to building and propulsion improvements + max?: number; // applies to space platforms + advantageAgainst?: StarShipType; // applies to custom ships + disadvantageAgainst?: StarShipType; // applies to custom ships +} + +export interface ResearchTypeProgress { + type: ResearchType; + maxResearchLevel: number; + currentResearchLevel: number; + researchPointsBase: number; + researchPointsCompleted: number; + data: ResearchTypeProgressData; +} + +export type ResearchProgressByType = Record; + +export interface ResearchData { + researchProgressByType: ResearchProgressByType; + researchTypeInQueue: ResearchType | null; //for now a player can only research one new item at a time + researchPercent: number; // 0 - 1, the rest goes to gold/taxes +} diff --git a/packages/astriarch-engine/src/model/tradingCenter.ts b/packages/astriarch-engine/src/model/tradingCenter.ts new file mode 100644 index 00000000..b4e9e7ad --- /dev/null +++ b/packages/astriarch-engine/src/model/tradingCenter.ts @@ -0,0 +1,41 @@ +export enum TradingCenterResourceType { + FOOD = 1, + ORE = 2, + IRIDIUM = 3, +} + +export enum TradeType { + BUY = 1, + SELL = 2, +} + +export interface TradeData { + id: string; // Unique trade ID for cancellation + playerId: string; + planetId: number; + tradeType: TradeType; + resourceType: TradingCenterResourceType; + amount: number; + submittedAt: number; // Timestamp when trade was submitted + executeAfter: number; // Timestamp when trade becomes executable +} + +export interface TradingCenterResource { + type: TradingCenterResourceType; + amount: number; + desiredAmount: number; + priceMin: number; + priceMax: number; + tradeAmountMax: number; + currentPrice: number; +} + +export interface TradingCenterData { + energyAmount: number; + foodResource: TradingCenterResource; + oreResource: TradingCenterResource; + iridiumResource: TradingCenterResource; + currentTrades: TradeData[]; + transactionFeePercentage: number; + interestPercentage: number; +} diff --git a/packages/astriarch-engine/src/shapes/shapes.ts b/packages/astriarch-engine/src/shapes/shapes.ts new file mode 100644 index 00000000..1f781066 --- /dev/null +++ b/packages/astriarch-engine/src/shapes/shapes.ts @@ -0,0 +1,27 @@ +export interface PointData { + x: number; + y: number; +} + +export interface RectangleData { + x: number; + y: number; + width: number; + height: number; +} + +export interface LineData { + x1: number; + y1: number; + x2: number; + y2: number; +} + +export interface HexagonData { + id: string; + x: number; + y: number; + width: number; + height: number; + side: number; +} diff --git a/packages/astriarch-engine/src/test/testUtils.ts b/packages/astriarch-engine/src/test/testUtils.ts new file mode 100644 index 00000000..50f72822 --- /dev/null +++ b/packages/astriarch-engine/src/test/testUtils.ts @@ -0,0 +1,95 @@ +import { ClientGameModel } from '../engine/clientGameModel'; +import { GameModel, GameModelData, playerColors } from '../engine/gameModel'; +import { Player } from '../engine/player'; +import { GalaxySizeOption, GameSpeed, PlanetsPerSystemOption } from '../model/model'; +import { PlayerData, PlayerType } from '../model/player'; +import { ResearchType } from '../model/research'; +import { CitizenWorkerType } from '../model/planet'; +import { ClientModelData } from '../model/clientModel'; +import { EarnedPointsType } from '../model/earnedPoints'; + +export interface TestGameData { + gameModel: GameModelData; +} + +export const startNewTestGame = (): TestGameData => { + const players = [] as PlayerData[]; + players.push(Player.constructPlayer('me', PlayerType.Human, 'Matt', playerColors[0])); + players.push(Player.constructPlayer('c1', PlayerType.Computer_Hard, 'Computer1', playerColors[1])); + + players[0].research.researchPercent = 0.9; + players[0].research.researchTypeInQueue = ResearchType.PROPULSION_IMPROVEMENT; + + const gameOptions = { + systemsToGenerate: 2, + planetsPerSystem: PlanetsPerSystemOption.FIVE, + galaxySize: GalaxySizeOption.SMALL, + distributePlanetsEvenly: true, + quickStart: true, + gameSpeed: GameSpeed.NORMAL, + version: '2.0', + }; + + const gameModel = GameModel.constructData(players, gameOptions); + + const homePlanet = gameModel.modelData.planets.find((p) => p.id === players[0].homePlanetId); + homePlanet!.population[0].workerType = CitizenWorkerType.Miner; + + return { gameModel }; +}; + +export const startNewTestGameWithOptions = ( + turnNumber: number, + points: number, + systemsToGenerate: number, + planetsPerSystem: number, + ownedPlanetCount: number, + playerCount: number, + playerType: PlayerType, +): TestGameData => { + const players = [] as PlayerData[]; + for (let i = 0; i < playerCount; i++) { + const player = Player.constructPlayer(`p${i}`, playerType, `Player${i + 1}`, playerColors[i]); + players.push(player); + Player.increasePoints(player, EarnedPointsType.POPULATION_GROWTH, points); + } + + const gameOptions = { + systemsToGenerate, + planetsPerSystem, + galaxySize: GalaxySizeOption.LARGE, + distributePlanetsEvenly: true, + quickStart: true, + gameSpeed: GameSpeed.NORMAL, + version: '2.0', + }; + + const gameModel = GameModel.constructData(players, gameOptions); + gameModel.modelData.currentCycle = turnNumber; + + const [firstPlayer] = players; + const ownedPlanetsPlayer1 = ClientGameModel.getOwnedPlanets(firstPlayer.ownedPlanetIds, gameModel.modelData.planets); + + if (ownedPlanetCount === 0) { + GameModel.changePlanetOwner( + firstPlayer, + undefined, + ownedPlanetsPlayer1[firstPlayer.ownedPlanetIds[0]], + gameModel.modelData.currentCycle, + ); + } + + let assignedPlanets = 1; + for (const p of gameModel.modelData.planets) { + if (assignedPlanets >= ownedPlanetCount) { + break; + } + if (!(p.id in ownedPlanetsPlayer1)) { + const oldOwner = GameModel.findPlanetOwner(gameModel, p.id); + GameModel.changePlanetOwner(oldOwner, firstPlayer, p, gameModel.modelData.currentCycle); + assignedPlanets++; + } + } + + return { gameModel }; +}; diff --git a/packages/astriarch-engine/src/utils/gameTools.ts b/packages/astriarch-engine/src/utils/gameTools.ts new file mode 100644 index 00000000..3ce3a17e --- /dev/null +++ b/packages/astriarch-engine/src/utils/gameTools.ts @@ -0,0 +1,67 @@ +import { StarShipType } from '../model/fleet'; +import { PlanetImprovementType, PlanetType } from '../model/planet'; + +export class GameTools { + public static starShipTypeToFriendlyName(t: StarShipType) { + let name = ''; + switch (t) { + case StarShipType.SystemDefense: + name = 'Defender'; + break; + case StarShipType.Scout: + name = 'Scout'; + break; + case StarShipType.Destroyer: + name = 'Destroyer'; + break; + case StarShipType.Cruiser: + name = 'Cruiser'; + break; + case StarShipType.Battleship: + name = 'Battleship'; + break; + case StarShipType.SpacePlatform: + name = 'Space Platform'; + break; + } + return name; + } + + public static planetImprovementTypeToFriendlyName(t: PlanetImprovementType): string { + let name = ''; + switch (t) { + case PlanetImprovementType.Factory: + name = 'Factory'; + break; + case PlanetImprovementType.Colony: + name = 'Colony'; + break; + case PlanetImprovementType.Farm: + name = 'Farm'; + break; + case PlanetImprovementType.Mine: + name = 'Mine'; + break; + } + return name; + } + + public static planetTypeToFriendlyName(t: PlanetType): string { + let name = ''; + switch (t) { + case PlanetType.DeadPlanet: + name = 'Dead'; + break; + case PlanetType.PlanetClass1: + name = 'Arid'; + break; + case PlanetType.PlanetClass2: + name = 'Terrestrial'; + break; + case PlanetType.AsteroidBelt: + name = 'Asteroid Belt'; + break; + } + return name; + } +} diff --git a/packages/astriarch-engine/src/utils/utils.ts b/packages/astriarch-engine/src/utils/utils.ts new file mode 100644 index 00000000..0d30a4c1 --- /dev/null +++ b/packages/astriarch-engine/src/utils/utils.ts @@ -0,0 +1,48 @@ +import { ColorRgbaData } from '../model/player'; + +export class Utils { + public static nextRandom(lowInclusive: number, highExclusive?: number) { + if (highExclusive === null || typeof highExclusive == 'undefined') { + highExclusive = lowInclusive; + lowInclusive = 0; + } + + if (lowInclusive < 0) { + highExclusive += Math.abs(lowInclusive); + } else { + highExclusive -= lowInclusive; + } + + const val = Math.floor(Math.random() * highExclusive) + lowInclusive; + return val; + } + + public static nextRandomFloat(lowInclusive: number, highExclusive: number) { + return Math.random() * (highExclusive - lowInclusive) + lowInclusive; + } + + public static decimalToFixed(number: number, maxDecimalPlaces: number) { + return number + ? Math.round(number) != number + ? number.toFixed(maxDecimalPlaces).replace(/0+$/, '') + : number + : number; + } + + public static ColorRgba(r: number, g: number, b: number, a: number): ColorRgbaData { + return { r, g, b, a }; + } + + public static compareNumbers(a: number, b: number) { + if (b < a) return 1; + else if (b > a) return -1; + else return 0; + } + + public static generateUniqueId(): string { + // Generate a unique ID using timestamp + random string + const timestamp = Date.now().toString(36); + const randomPart = Math.random().toString(36).substring(2, 8); + return `${timestamp}-${randomPart}`; + } +} diff --git a/packages/astriarch-engine/tsconfig.json b/packages/astriarch-engine/tsconfig.json new file mode 100644 index 00000000..a3df7f4e --- /dev/null +++ b/packages/astriarch-engine/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "skipLibCheck": true, + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "importHelpers": true, + "outDir": "./dist", + + /* Linting */ + "strict": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], +} \ No newline at end of file diff --git a/packages/astriarch-engine/tsconfig.prod.json b/packages/astriarch-engine/tsconfig.prod.json new file mode 100644 index 00000000..299686e8 --- /dev/null +++ b/packages/astriarch-engine/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["dist", "node_modules", "**/*.spec.ts"] +} diff --git a/packages/astriarch-engine/tsup.config.ts b/packages/astriarch-engine/tsup.config.ts new file mode 100644 index 00000000..5ea78a49 --- /dev/null +++ b/packages/astriarch-engine/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + external: ['jsdom'], + splitting: false, + sourcemap: true, + clean: true, + dts: true, + format: ['cjs', 'esm'], // generate cjs and esm files + target: 'es2020' +}); \ No newline at end of file diff --git a/packages/astriarch-ui/.babelrc.json b/packages/astriarch-ui/.babelrc.json new file mode 100644 index 00000000..00ca841a --- /dev/null +++ b/packages/astriarch-ui/.babelrc.json @@ -0,0 +1,16 @@ +{ + "sourceType": "unambiguous", + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "chrome": 100 + } + } + ], + "@babel/preset-typescript", + "@babel/preset-react" + ], + "plugins": [] +} diff --git a/packages/astriarch-ui/.storybook/main.ts b/packages/astriarch-ui/.storybook/main.ts new file mode 100644 index 00000000..7a2e3d02 --- /dev/null +++ b/packages/astriarch-ui/.storybook/main.ts @@ -0,0 +1,27 @@ +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import type { StorybookConfig } from "@storybook/react-webpack5"; +const require = createRequire(import.meta.url); +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + + addons: [ + getAbsolutePath("@storybook/addon-links"), + getAbsolutePath("@chakra-ui/storybook-addon"), + getAbsolutePath("@storybook/addon-docs"), + ], + + framework: { + name: getAbsolutePath("@storybook/react-webpack5"), + options: {}, + }, + + features: { + //emotionAlias: false, // https://chakra-ui.com/getting-started/with-storybook + }, +}; +export default config; + +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, "package.json"))); +} diff --git a/packages/astriarch-ui/.storybook/preview-head.html b/packages/astriarch-ui/.storybook/preview-head.html new file mode 100644 index 00000000..40bfc4c0 --- /dev/null +++ b/packages/astriarch-ui/.storybook/preview-head.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/astriarch-ui/.storybook/preview.ts b/packages/astriarch-ui/.storybook/preview.ts new file mode 100644 index 00000000..2b4a1a52 --- /dev/null +++ b/packages/astriarch-ui/.storybook/preview.ts @@ -0,0 +1,34 @@ +import type { Preview } from "@storybook/react-webpack5"; + +import theme from "../src/theme/theme"; +import "../src/styles/global.css"; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + chakra: { + theme, + }, + backgrounds: { + default: "space", + values: [ + { + name: "space", + value: "#000000", + }, + { + name: "light", + value: "#FFFFFF", + }, + ], + }, + }, +}; + +export default preview; diff --git a/packages/astriarch-ui/package.json b/packages/astriarch-ui/package.json new file mode 100644 index 00000000..180a4657 --- /dev/null +++ b/packages/astriarch-ui/package.json @@ -0,0 +1,43 @@ +{ + "name": "astriarch-ui", + "private": true, + "description": "Shared UI Components", + "main": "dist/index.js", + "scripts": { + "tsc": "rm -rf ./dist && tsc", + "build": "tsup src/index.ts --format esm,cjs --dts --external react --inject ./react-shim.js --sourcemap", + "dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react --inject ./react-shim.js --sourcemap", + "test": "echo \"Error: no test specified\" && exit 1", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@chakra-ui/react": "^3.21.1", + "@emotion/react": "^11.14.0", + "next-themes": "^0.4.6", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-icons": "^5.5.0" + }, + "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/preset-env": "^7.28.0", + "@babel/preset-react": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@chakra-ui/storybook-addon": "^5.2.5", + "@storybook/addon-docs": "^9.0.15", + "@storybook/addon-links": "^9.0.15", + "@storybook/addons": "^7.6.17", + "@storybook/api": "^7.6.17", + "@storybook/react-webpack5": "^9.0.15", + "@storybook/testing-library": "^0.2.2", + "@types/react": "^19.1.8", + "prop-types": "^15.8.1", + "storybook": "^9.0.15", + "tsup": "^8.5.0", + "typescript": "^5.8.3" + } +} diff --git a/packages/astriarch-ui/react-shim.js b/packages/astriarch-ui/react-shim.js new file mode 100644 index 00000000..91567f83 --- /dev/null +++ b/packages/astriarch-ui/react-shim.js @@ -0,0 +1,4 @@ +// https://esbuild.github.io/content-types/#auto-import-for-jsx +import * as React from "react"; + +export { React }; diff --git a/packages/astriarch-ui/src/assets/icons/energy.svg b/packages/astriarch-ui/src/assets/icons/energy.svg new file mode 100644 index 00000000..4b518696 --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/energy.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/exit.svg b/packages/astriarch-ui/src/assets/icons/exit.svg new file mode 100644 index 00000000..cc986b20 --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/exit.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/food.svg b/packages/astriarch-ui/src/assets/icons/food.svg new file mode 100644 index 00000000..db815beb --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/food.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/iridium.svg b/packages/astriarch-ui/src/assets/icons/iridium.svg new file mode 100644 index 00000000..f5137968 --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/iridium.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/ore.svg b/packages/astriarch-ui/src/assets/icons/ore.svg new file mode 100644 index 00000000..d8eada0f --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/ore.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/population.svg b/packages/astriarch-ui/src/assets/icons/population.svg new file mode 100644 index 00000000..6c696e97 --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/population.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/research.svg b/packages/astriarch-ui/src/assets/icons/research.svg new file mode 100644 index 00000000..8226925f --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/research.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/icons/volume.svg b/packages/astriarch-ui/src/assets/icons/volume.svg new file mode 100644 index 00000000..c94355b3 --- /dev/null +++ b/packages/astriarch-ui/src/assets/icons/volume.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/test.svg b/packages/astriarch-ui/src/assets/test.svg new file mode 100644 index 00000000..4b4b9da1 --- /dev/null +++ b/packages/astriarch-ui/src/assets/test.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/assets/test2.svg b/packages/astriarch-ui/src/assets/test2.svg new file mode 100644 index 00000000..dd36acd4 --- /dev/null +++ b/packages/astriarch-ui/src/assets/test2.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/astriarch-ui/src/components/Box/index.tsx b/packages/astriarch-ui/src/components/Box/index.tsx new file mode 100644 index 00000000..016e9ce0 --- /dev/null +++ b/packages/astriarch-ui/src/components/Box/index.tsx @@ -0,0 +1,3 @@ +import { Box } from "@chakra-ui/react"; + +//export const Box = styled("div"); diff --git a/packages/astriarch-ui/src/components/Button/Button.stories.ts b/packages/astriarch-ui/src/components/Button/Button.stories.ts new file mode 100644 index 00000000..698a3dce --- /dev/null +++ b/packages/astriarch-ui/src/components/Button/Button.stories.ts @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from "@storybook/react-webpack5"; + +import Button from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/Button", + component: Button, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: { + backgroundColor: { control: "color" }, + }, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + isActive: true, + label: "Button", + }, +}; + +export const Secondary: Story = { + args: { + isActive: false, + label: "Button", + variant: "outline", + }, +}; + +export const Large: Story = { + args: { + size: "lg", + label: "Large Primary", + }, +}; + +export const LargeSecondary: Story = { + args: { + size: "lg", + label: "Secondary", + variant: "outline", + }, +}; + +export const Medium: Story = { + args: { + size: "md", + label: "Medium", + }, +}; + +export const MediumSecondary: Story = { + args: { + size: "md", + label: "Medium", + variant: "outline", + }, +}; + +export const Small: Story = { + args: { + size: "sm", + label: "Small", + }, +}; + +export const SmallSecondary: Story = { + args: { + size: "sm", + label: "Small", + variant: "outline", + }, +}; diff --git a/packages/astriarch-ui/src/components/Button/index.tsx b/packages/astriarch-ui/src/components/Button/index.tsx new file mode 100644 index 00000000..be68b215 --- /dev/null +++ b/packages/astriarch-ui/src/components/Button/index.tsx @@ -0,0 +1,62 @@ +import { Button as ChakraButton, ButtonProps as ChakraButtonProps } from "@chakra-ui/react"; +import React from "react"; +import Text from "../Text"; +import { ButtonSvg } from "./svg/ButtonSvg"; +import { Size } from "../component.types"; + +export interface ButtonProps extends ChakraButtonProps { + label: string; + onClick?: () => void; +} + +const Button: React.FC = ({ children, label, size, variant, ...rest }) => { + const buttonSize = (size || "lg") as Size; + const widthBySize = { + lg: "207px", + md: "157px", + sm: "87px", + xs: "87px", + }; + + const additionalButtonCss = { + width: "207px", + height: "48px", + backgroundColor: "transparent", + // flexShrink: 0, + // fill: '#0FF', + // strokeWidth: '2px', + // stroke: '#000', + // filter: 'drop-shadow(0px 16px 32px rgba(0, 0, 0, 0.60))', + }; + + const additionalTextCss = { + color: "#1B1F25", + fontSize: "14px", + fontWeight: 800, + lineHeight: "20px", + letterSpacing: "2px", + }; + + additionalButtonCss.width = widthBySize[buttonSize]; + if (variant === "outline") { + additionalTextCss.color = "#0FF"; + } + return ( + <> + + + {label} + + + {children} + + + ); +}; + +export default Button; diff --git a/packages/astriarch-ui/src/components/Button/svg/ButtonSvg.tsx b/packages/astriarch-ui/src/components/Button/svg/ButtonSvg.tsx new file mode 100644 index 00000000..4411b3fa --- /dev/null +++ b/packages/astriarch-ui/src/components/Button/svg/ButtonSvg.tsx @@ -0,0 +1,29 @@ +import { Box, ButtonProps as ChakraButtonProps } from "@chakra-ui/react"; +import { ButtonSvgLargePrimary, ButtonSvgMediumPrimary, ButtonSvgSmallPrimary } from "./ButtonSvgPrimary"; +import { ButtonSvgLargeSecondary, ButtonSvgMediumSecondary, ButtonSvgSmallSecondary } from "./ButtonSvgSecondary"; +import { Size } from "../../component.types"; + +export interface ButtonSvgProps { + size: Size; //Pick; + variant: "primary" | "outline"; +} + +const componentBySizePrimary = { + lg: ButtonSvgLargePrimary, + md: ButtonSvgMediumPrimary, + sm: ButtonSvgSmallPrimary, + xs: ButtonSvgSmallPrimary, +}; + +const componentBySizeSecondary = { + lg: ButtonSvgLargeSecondary, + md: ButtonSvgMediumSecondary, + sm: ButtonSvgSmallSecondary, + xs: ButtonSvgSmallSecondary, +}; + +export const ButtonSvg = (props: ButtonSvgProps) => { + const { size, variant } = props; + const component = variant === "primary" ? componentBySizePrimary[size] : componentBySizeSecondary[size]; + return {component()}; +}; diff --git a/packages/astriarch-ui/src/components/Button/svg/ButtonSvgPrimary.tsx b/packages/astriarch-ui/src/components/Button/svg/ButtonSvgPrimary.tsx new file mode 100644 index 00000000..995b86ce --- /dev/null +++ b/packages/astriarch-ui/src/components/Button/svg/ButtonSvgPrimary.tsx @@ -0,0 +1,179 @@ +export const ButtonSvgLargePrimary = () => { + return ( + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + ); +}; + +export const ButtonSvgMediumPrimary = () => { + return ( + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + ); +}; + +export const ButtonSvgSmallPrimary = () => { + return ( + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Button/svg/ButtonSvgSecondary.tsx b/packages/astriarch-ui/src/components/Button/svg/ButtonSvgSecondary.tsx new file mode 100644 index 00000000..7abcb176 --- /dev/null +++ b/packages/astriarch-ui/src/components/Button/svg/ButtonSvgSecondary.tsx @@ -0,0 +1,119 @@ +export const ButtonSvgLargeSecondary = () => { + return ( + + + + + + + + {/* */} + + + + + + + + + + ); +}; + +export const ButtonSvgMediumSecondary = () => { + return ( + + + + + + + + {/* */} + + + + + + + + + + ); +}; + +export const ButtonSvgSmallSecondary = () => { + return ( + + + + + + + + {/* */} + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Card/Card.stories.ts b/packages/astriarch-ui/src/components/Card/Card.stories.ts new file mode 100644 index 00000000..be85efd7 --- /dev/null +++ b/packages/astriarch-ui/src/components/Card/Card.stories.ts @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Card from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/Card", + component: Card, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + label: "Building 1 mine on planet H6", + enabled: true, + }, +}; + +export const Large: Story = { + args: { + size: "lg", + label: "Large", + enabled: true, + }, +}; + +export const LargeDisabled: Story = { + args: { + size: "lg", + label: "Large", + enabled: false, + }, +}; + +export const Medium: Story = { + args: { + size: "md", + label: "Medium", + enabled: true, + }, +}; + +export const MediumDisabled: Story = { + args: { + size: "md", + label: "Medium", + enabled: false, + }, +}; diff --git a/packages/astriarch-ui/src/components/Card/index.tsx b/packages/astriarch-ui/src/components/Card/index.tsx new file mode 100644 index 00000000..db5d116f --- /dev/null +++ b/packages/astriarch-ui/src/components/Card/index.tsx @@ -0,0 +1,40 @@ +import { Box } from "@chakra-ui/react"; +import React from "react"; +import Text from "../Text"; +import { Size } from "../component.types"; +import { CardSvg } from "./svg/CardSvg"; + +export interface CardProps { + children?: React.ReactNode; + label: string; + size?: Size; + enabled: boolean; + onClick?: () => void; +} + +const Card: React.FC = ({ children, label, size, enabled, ...rest }) => { + const additionalCardCss = { + width: size === "lg" ? "109px" : "115px", + height: size === "lg" ? "136px" : "76px", + backgroundColor: "transparent", + }; + + const additionalTextCss = { + color: "#FFF", + fontSize: "14px", + fontWeight: 400, + lineHeight: "28px", + letterSpacing: "0.14px", + }; + return ( + + + {label} + + + {children} + + ); +}; + +export default Card; diff --git a/packages/astriarch-ui/src/components/Card/svg/CardSvg.tsx b/packages/astriarch-ui/src/components/Card/svg/CardSvg.tsx new file mode 100644 index 00000000..8e7bf848 --- /dev/null +++ b/packages/astriarch-ui/src/components/Card/svg/CardSvg.tsx @@ -0,0 +1,15 @@ +import { Box } from "@chakra-ui/react"; +import { Size } from "../../component.types"; +import { CardSvgLarge } from "./CardSvgLarge"; +import { CardSvgMedium } from "./CardSvgMedium"; + +export interface CardSvgProps { + size: Size; + enabled: boolean; +} + +export const CardSvg = (props: CardSvgProps) => { + const { size, enabled } = props; + const component = size === "lg" ? CardSvgLarge : CardSvgMedium; + return {component({ enabled })}; +}; diff --git a/packages/astriarch-ui/src/components/Card/svg/CardSvgLarge.tsx b/packages/astriarch-ui/src/components/Card/svg/CardSvgLarge.tsx new file mode 100644 index 00000000..de7b7fee --- /dev/null +++ b/packages/astriarch-ui/src/components/Card/svg/CardSvgLarge.tsx @@ -0,0 +1,84 @@ +export const CardSvgLarge = ({ enabled }: { enabled: boolean }) => { + if (!enabled) { + return ( + + + + + + + + + + + + + + + + + + ); + } + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Card/svg/CardSvgMedium.tsx b/packages/astriarch-ui/src/components/Card/svg/CardSvgMedium.tsx new file mode 100644 index 00000000..f363986d --- /dev/null +++ b/packages/astriarch-ui/src/components/Card/svg/CardSvgMedium.tsx @@ -0,0 +1,64 @@ +export const CardSvgMedium = ({ enabled }: { enabled: boolean }) => { + if (!enabled) { + return ( + + + + + + + + + + ); + } + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Dialog/index.tsx b/packages/astriarch-ui/src/components/Dialog/index.tsx new file mode 100644 index 00000000..1d4bc29f --- /dev/null +++ b/packages/astriarch-ui/src/components/Dialog/index.tsx @@ -0,0 +1,55 @@ +import { ReactNode } from "react"; +import { Button, CloseButton, Dialog, Portal } from "@chakra-ui/react"; + +export interface DialogProps { + onChange?: (index: number) => void; + placement?: React.ComponentProps["placement"]; + title?: string; + children?: ReactNode; + onCancel?: () => void; + onSave?: () => void; + cancelButtonText?: string; + saveButtonText?: string; +} + +/** + * Dialog component using Chakra UI v3 Dialog component + */ +const DialogComponent: React.FC = ({ + placement = "top", + title = "Dialog Title", + cancelButtonText = "Cancel", + saveButtonText = "Save", + onChange, + onCancel, + onSave, + ...rest +}) => { + return ( + + + + + + + {title} + + {rest.children} + + + + + + + + + + ); +}; + +export default DialogComponent; + +// Export additional Dialog sub-components for more flexibility +export { Dialog }; diff --git a/packages/astriarch-ui/src/components/IconImage/index.tsx b/packages/astriarch-ui/src/components/IconImage/index.tsx new file mode 100644 index 00000000..87b48b45 --- /dev/null +++ b/packages/astriarch-ui/src/components/IconImage/index.tsx @@ -0,0 +1,26 @@ +import { Box } from "@chakra-ui/react"; +import React from "react"; + +export type IconImageType = "population" | "food" | "energy" | "research" | "ore" | "iridium"; + +export interface IconImageProps { + type: IconImageType; + size?: number; + altText?: string; +} + +const IconImage: React.FC = (props) => { + const { type, size, altText, ...rest } = props; + const icon = require(`../../assets/icons/${type}.svg`); + const alt = altText ?? type.charAt(0).toUpperCase() + type.slice(1); + return ( + + {alt} + + ); +}; + +export default IconImage; diff --git a/packages/astriarch-ui/src/components/NavigationController/index.tsx b/packages/astriarch-ui/src/components/NavigationController/index.tsx new file mode 100644 index 00000000..a1f683ef --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationController/index.tsx @@ -0,0 +1,102 @@ +import { Box, Flex, useDisclosure } from "@chakra-ui/react"; +import React, { useState } from "react"; +import Text from "../Text"; + +export interface NavigationItem { + label: string; + content?: React.ReactNode; + onClick?: () => void; +} + +export interface NavigationControllerProps { + items: NavigationItem[]; + initialSelectedIndex?: number; + onChange?: (index: number) => void; +} + +/** + * Navigation controller for bottom tabs as shown in the design + */ +const NavigationController: React.FC = ({ + items, + initialSelectedIndex = 0, + onChange, + ...rest +}) => { + const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex); + + const handleTabClick = (index: number) => { + setSelectedIndex(index); + if (onChange) { + onChange(index); + } + if (items[index].onClick) { + items[index].onClick!(); + } + }; + + return ( + + + {items.map((item, i) => ( + handleTabClick(i)} + cursor="pointer" + > + {/* Tab background */} + + + {/* Tab text */} + + {item.label} + + + ))} + + + {/* Content area */} + {items[selectedIndex]?.content} + + ); +}; + +export default NavigationController; diff --git a/packages/astriarch-ui/src/components/NavigationTab/index.tsx b/packages/astriarch-ui/src/components/NavigationTab/index.tsx new file mode 100644 index 00000000..5f474290 --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationTab/index.tsx @@ -0,0 +1,44 @@ +import { Box } from "@chakra-ui/react"; +import React from "react"; +import Text from "../Text"; +import { NavigationTabSvg } from "./svg/NavigationTabSvg"; + +export interface NavigationTabProps { + label: string; + selected: boolean; + zIndex: number; + onClick?: () => void; +} + +const NavigationTab: React.FC = ({ label, selected, zIndex, onClick, ...rest }) => { + const additionalTextCss = { + fontSize: "14px", + fontWeight: 800, + lineHeight: "48px", + letterSpacing: "2px", + width: 240, + ...(selected + ? { color: "#1B1F25" } + : { + color: "#FFF", + }), + }; + + return ( + + + {label} + + + + ); +}; + +export default NavigationTab; diff --git a/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvg.tsx b/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvg.tsx new file mode 100644 index 00000000..6f506317 --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvg.tsx @@ -0,0 +1,14 @@ +import { Box } from "@chakra-ui/react"; +import { NavigationTabSvgSelected } from "./NavigationTabSvgSelected"; +import { NavigationTabSvgUnselected } from "./NavigationTabSvgUnselected"; + +export interface NavigationTabSvgProps { + selected: boolean; + onClick?: () => void; +} + +export const NavigationTabSvg = (props: NavigationTabSvgProps) => { + const { selected, onClick } = props; + const component = selected ? NavigationTabSvgSelected : NavigationTabSvgUnselected; + return {component({ onClick })}; +}; diff --git a/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvgSelected.tsx b/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvgSelected.tsx new file mode 100644 index 00000000..09d07d93 --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvgSelected.tsx @@ -0,0 +1,69 @@ +export const NavigationTabSvgSelected = (props: { onClick?: () => void }) => { + return ( + + {/* {alert('blaw');props.onClick?.();}} fill="#FF00FF" pointerEvents={'all'} /> */} + + { + props.onClick?.(); + }} + cursor={"pointer"} + d="M188.3 14H15.9625C15.0054 14 14.5951 15.2153 15.3565 15.7954L73.852 60.3635C75.2454 61.4251 76.9487 62 78.7004 62H251.037C251.995 62 252.405 60.7847 251.643 60.2046L193.148 15.6365C191.755 14.5749 190.051 14 188.3 14Z" + fill="#00FFFF" + /> + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvgUnselected.tsx b/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvgUnselected.tsx new file mode 100644 index 00000000..a5b29a1e --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationTab/svg/NavigationTabSvgUnselected.tsx @@ -0,0 +1,41 @@ +export const NavigationTabSvgUnselected = (props: { onClick?: () => void }) => { + return ( + + {/* {alert('blaw');props.onClick?.();}} fill="#00FF00" pointerEvents={'all'} /> */} + { + props.onClick?.(); + }} + cursor={"pointer"} + opacity="0.9" + d="M61.0641 44.7726L4.92509 2H174.3C175.613 2 176.891 2.43121 177.936 3.22741L234.075 46H64.7004C63.3866 46 62.1091 45.5688 61.0641 44.7726ZM236.431 47.7954C236.43 47.7947 236.429 47.7939 236.428 47.7932L236.431 47.7954Z" + fill="#1B1F25" + stroke="url(#paint0_linear_143_69328)" + strokeWidth="4" + /> + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/NavigationTabController/NavigationTabController.stories.tsx b/packages/astriarch-ui/src/components/NavigationTabController/NavigationTabController.stories.tsx new file mode 100644 index 00000000..77ca4593 --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationTabController/NavigationTabController.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import NavigationTabController from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/NavigationTabController", + component: NavigationTabController, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + labels: ["NavigationTabController"], + }, +}; + +export const Default: Story = { + args: { + labels: ["Tab1", "Tab2", "Tab3"], + }, +}; + +export const Alternate: Story = { + args: { + labels: ["Tab1", "Tab2", "Tab3"], + }, +}; diff --git a/packages/astriarch-ui/src/components/NavigationTabController/index.tsx b/packages/astriarch-ui/src/components/NavigationTabController/index.tsx new file mode 100644 index 00000000..1aa29058 --- /dev/null +++ b/packages/astriarch-ui/src/components/NavigationTabController/index.tsx @@ -0,0 +1,33 @@ +import { Box } from "@chakra-ui/react"; +import React, { useState } from "react"; +import NavigationTab from "../NavigationTab"; + +export interface NavigationTabControllerProps { + children?: React.ReactNode; + labels: string[]; + onTabClick?: (i: number) => void; +} + +const NavigationTabController: React.FC = ({ children, labels, onTabClick, ...rest }) => { + const [tabIndex, setTabIndex] = useState(0); + return ( + + {labels.map((label, i) => ( + { + setTabIndex(i); + onTabClick?.(i); + }} + /> + ))} + + {children} + + ); +}; + +export default NavigationTabController; diff --git a/packages/astriarch-ui/src/components/Notification/Notification.stories.ts b/packages/astriarch-ui/src/components/Notification/Notification.stories.ts new file mode 100644 index 00000000..3f16d184 --- /dev/null +++ b/packages/astriarch-ui/src/components/Notification/Notification.stories.ts @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Notification from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/Notification", + component: Notification, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + label: "Building 1 mine on planet H6", + }, +}; + +export const Large: Story = { + args: { + size: "lg", + label: "Large Notification", + }, +}; + +export const Medium: Story = { + args: { + size: "md", + label: "Medium Notification", + }, +}; + +export const Small: Story = { + args: { + size: "sm", + label: "Small Notification", + }, +}; diff --git a/packages/astriarch-ui/src/components/Notification/index.tsx b/packages/astriarch-ui/src/components/Notification/index.tsx new file mode 100644 index 00000000..2c59e2ba --- /dev/null +++ b/packages/astriarch-ui/src/components/Notification/index.tsx @@ -0,0 +1,39 @@ +import { Box } from "@chakra-ui/react"; +import React from "react"; +import Text from "../Text"; +import { NotificationSvg } from "./svg/NotificationSvg"; +import { Size } from "../component.types"; + +export interface NotificationProps { + children?: React.ReactNode; + label: string; + size?: Size; + onClick?: () => void; +} + +const Notification: React.FC = ({ children, label, size, ...rest }) => { + const additionalNotificationCss = { + width: "277px", + height: "64px", + backgroundColor: "transparent", + }; + + const additionalTextCss = { + color: "#FFF", + fontSize: "14px", + fontWeight: 400, + lineHeight: "28px", + letterSpacing: "0.14px", + }; + return ( + + + {label} + + + {children} + + ); +}; + +export default Notification; diff --git a/packages/astriarch-ui/src/components/Notification/svg/NotificationSvg.tsx b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvg.tsx new file mode 100644 index 00000000..46127356 --- /dev/null +++ b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvg.tsx @@ -0,0 +1,15 @@ +import { Box } from "@chakra-ui/react"; +import { NotificationSvgSmall } from "./NotificationSvgSmall"; +import { Size } from "../../component.types"; +import { NotificationSvgMedium } from "./NotificationSvgMedium"; +import { NotificationSvgLarge } from "./NotificationSvgLarge"; + +export interface NotificationSvgProps { + size: Size; +} + +export const NotificationSvg = (props: NotificationSvgProps) => { + const { size } = props; + const component = size === "sm" ? NotificationSvgSmall : size === "md" ? NotificationSvgMedium : NotificationSvgLarge; + return {component()}; +}; diff --git a/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgLarge.tsx b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgLarge.tsx new file mode 100644 index 00000000..1484543b --- /dev/null +++ b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgLarge.tsx @@ -0,0 +1,57 @@ +export const NotificationSvgLarge = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgMedium.tsx b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgMedium.tsx new file mode 100644 index 00000000..00435c83 --- /dev/null +++ b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgMedium.tsx @@ -0,0 +1,57 @@ +export const NotificationSvgMedium = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgSmall.tsx b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgSmall.tsx new file mode 100644 index 00000000..e96a01f9 --- /dev/null +++ b/packages/astriarch-ui/src/components/Notification/svg/NotificationSvgSmall.tsx @@ -0,0 +1,57 @@ +export const NotificationSvgSmall = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Tab/index.tsx b/packages/astriarch-ui/src/components/Tab/index.tsx new file mode 100644 index 00000000..0add84f7 --- /dev/null +++ b/packages/astriarch-ui/src/components/Tab/index.tsx @@ -0,0 +1,38 @@ +import { Tab as ChakraTab } from "@chakra-ui/react"; +import React from "react"; +import Text from "../Text"; +import { TabSvg } from "./svg/TabSvg"; + +export interface TabProps { + label: string; + selected: boolean; + zIndex: number; + onClick?: () => void; +} + +const Tab: React.FC = ({ label, selected, zIndex, ...rest }) => { + const additionalTextCss = { + color: "#FFF", + fontSize: "14px", + fontWeight: 800, + lineHeight: "20px", + letterSpacing: "2px", + ...(selected + ? {} + : { + textShadow: "0px 4px 4px rgba(0, 0, 0, 0.25)", + WebkitTextStroke: "1px #000", + }), + }; + + return ( + + + {label} + + + + ); +}; + +export default Tab; diff --git a/packages/astriarch-ui/src/components/Tab/svg/TabSvg.tsx b/packages/astriarch-ui/src/components/Tab/svg/TabSvg.tsx new file mode 100644 index 00000000..a2a6ae13 --- /dev/null +++ b/packages/astriarch-ui/src/components/Tab/svg/TabSvg.tsx @@ -0,0 +1,13 @@ +import { Box } from "@chakra-ui/react"; +import { TabSvgSelected } from "./TabSvgSelected"; +import { TabSvgUnselected } from "./TabSvgUnselected"; + +export interface TabSvgProps { + selected: boolean; +} + +export const TabSvg = (props: TabSvgProps) => { + const { selected } = props; + const component = selected ? TabSvgSelected : TabSvgUnselected; + return {component()}; +}; diff --git a/packages/astriarch-ui/src/components/Tab/svg/TabSvgSelected.tsx b/packages/astriarch-ui/src/components/Tab/svg/TabSvgSelected.tsx new file mode 100644 index 00000000..71d760a4 --- /dev/null +++ b/packages/astriarch-ui/src/components/Tab/svg/TabSvgSelected.tsx @@ -0,0 +1,42 @@ +export const TabSvgSelected = () => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Tab/svg/TabSvgUnselected.tsx b/packages/astriarch-ui/src/components/Tab/svg/TabSvgUnselected.tsx new file mode 100644 index 00000000..8cb8927d --- /dev/null +++ b/packages/astriarch-ui/src/components/Tab/svg/TabSvgUnselected.tsx @@ -0,0 +1,12 @@ +export const TabSvgUnselected = () => { + return ( + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/TabController/TabController.stories.tsx b/packages/astriarch-ui/src/components/TabController/TabController.stories.tsx new file mode 100644 index 00000000..c93a9a9f --- /dev/null +++ b/packages/astriarch-ui/src/components/TabController/TabController.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import TabController from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/TabController", + component: TabController, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + tabs: [{ label: "TabController" }], + }, +}; + +const children1 =

Test Panel Content 1

; +const children2 =

Test Panel Content 2

; +const children3 =

Test Panel Content 3

; + +export const Large: Story = { + args: { + size: "lg", + tabs: [ + { label: "Large", children: children1 }, + { label: "Tab1", children: children2 }, + { label: "Tab2", children: children3 }, + ], + }, +}; + +export const Medium: Story = { + args: { + size: "md", + tabs: [ + { label: "Medium", children: children1 }, + { label: "Tab", children: children2 }, + ], + }, +}; + +export const Small: Story = { + args: { + size: "sm", + tabs: [ + { label: "Small", children: children1 }, + { label: "Tab", children: children2 }, + ], + }, +}; diff --git a/packages/astriarch-ui/src/components/TabController/index.tsx b/packages/astriarch-ui/src/components/TabController/index.tsx new file mode 100644 index 00000000..73dcac95 --- /dev/null +++ b/packages/astriarch-ui/src/components/TabController/index.tsx @@ -0,0 +1,42 @@ +import { Box, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import React, { useState } from "react"; +import { TabControllerSvg } from "./svg/TabControllerSvg"; +import { Size, TabControllerTab } from "../component.types"; +import Tab from "../Tab"; + +export interface TabControllerProps { + children?: React.ReactNode; + tabs: TabControllerTab[]; + size?: Size; + onClick?: () => void; +} + +const TabController: React.FC = ({ children, tabs, size, ...rest }) => { + const [tabIndex, setTabIndex] = useState(0); + return ( + + setTabIndex(index)}> + + {tabs.map((tab, i) => ( + + ))} + + + + {tabs.map((tab, i) => ( + {tab.children} + ))} + + + + {children} + + ); +}; + +export default TabController; diff --git a/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvg.tsx b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvg.tsx new file mode 100644 index 00000000..754e5ee1 --- /dev/null +++ b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvg.tsx @@ -0,0 +1,20 @@ +import { Box } from "@chakra-ui/react"; +import { TabControllerSvgBaselineSmall } from "./TabControllerSvgBaselineSmall"; +import { Size } from "../../component.types"; +import { TabControllerSvgBaselineMedium } from "./TabControllerSvgBaselineMedium"; +import { TabControllerSvgBaselineLarge } from "./TabControllerSvgBaselineLarge"; + +export interface TabControllerSvgProps { + size: Size; +} + +export const TabControllerSvg = (props: TabControllerSvgProps) => { + const { size } = props; + const component = + size === "sm" + ? TabControllerSvgBaselineSmall + : size === "md" + ? TabControllerSvgBaselineMedium + : TabControllerSvgBaselineLarge; + return {component()}; +}; diff --git a/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineLarge.tsx b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineLarge.tsx new file mode 100644 index 00000000..361b47aa --- /dev/null +++ b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineLarge.tsx @@ -0,0 +1,43 @@ +export const TabControllerSvgBaselineLarge = () => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineMedium.tsx b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineMedium.tsx new file mode 100644 index 00000000..891b5ba8 --- /dev/null +++ b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineMedium.tsx @@ -0,0 +1,43 @@ +export const TabControllerSvgBaselineMedium = () => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineSmall.tsx b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineSmall.tsx new file mode 100644 index 00000000..eac78c45 --- /dev/null +++ b/packages/astriarch-ui/src/components/TabController/svg/TabControllerSvgBaselineSmall.tsx @@ -0,0 +1,43 @@ +export const TabControllerSvgBaselineSmall = () => { + return ( + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/Text/Text.stories.ts b/packages/astriarch-ui/src/components/Text/Text.stories.ts new file mode 100644 index 00000000..5febeb2a --- /dev/null +++ b/packages/astriarch-ui/src/components/Text/Text.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Text from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/Text", + component: Text, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: { + backgroundColor: { control: "color" }, + }, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + children: "Test", + }, +}; diff --git a/packages/astriarch-ui/src/components/Text/index.tsx b/packages/astriarch-ui/src/components/Text/index.tsx new file mode 100644 index 00000000..3f176589 --- /dev/null +++ b/packages/astriarch-ui/src/components/Text/index.tsx @@ -0,0 +1,16 @@ +import { Text as ChakraText, TextProps as ChakraTextProps } from "@chakra-ui/react"; +import React from "react"; + +export interface TextProps extends ChakraTextProps { + // Your additional props go here +} + +const Text: React.FC = ({ children, ...rest }) => { + return ( + + {children} + + ); +}; + +export default Text; diff --git a/packages/astriarch-ui/src/components/TopOverview/TopOverview.stories.tsx b/packages/astriarch-ui/src/components/TopOverview/TopOverview.stories.tsx new file mode 100644 index 00000000..595d3171 --- /dev/null +++ b/packages/astriarch-ui/src/components/TopOverview/TopOverview.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import TopOverview from "./index"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/TopOverview", + component: TopOverview, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs + tags: ["autodocs"], + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: {}, +}; + +export default meta; +type Story = StoryObj; +const resourceData = { + total: { + food: 3, + energy: 5, + research: 1, + ore: 2, + iridium: 1, + production: 42, + }, + perTurn: { + food: -3.5556, + energy: 5.23221, + research: 10, + ore: 12, + iridium: 3, + production: 5.7892, + }, +}; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + population: 42, + resourceData, + }, +}; diff --git a/packages/astriarch-ui/src/components/TopOverview/index.tsx b/packages/astriarch-ui/src/components/TopOverview/index.tsx new file mode 100644 index 00000000..5bc67907 --- /dev/null +++ b/packages/astriarch-ui/src/components/TopOverview/index.tsx @@ -0,0 +1,52 @@ +import { Box } from "@chakra-ui/react"; +import React, { useState } from "react"; +import TopOverviewItem from "../TopOverviewItem"; +import { TopOverviewFrameSvg } from "./svg/TopOverviewFrameSvg"; + +export interface PlanetResourceData { + food: number; + energy: number; + research: number; + ore: number; + iridium: number; + production: number; +} + +export interface ResourceData { + total: PlanetResourceData; + perTurn: PlanetResourceData; +} + +export interface TopOverviewProps { + children?: React.ReactNode; + resourceData: ResourceData; + population: number; +} + +const TopOverview: React.FC = (props) => { + const { children, resourceData, population, ...rest } = props; + return ( + + + + + + + + + + + {children} + + ); +}; + +export default TopOverview; diff --git a/packages/astriarch-ui/src/components/TopOverview/svg/TopOverviewFrameSvg.tsx b/packages/astriarch-ui/src/components/TopOverview/svg/TopOverviewFrameSvg.tsx new file mode 100644 index 00000000..2691bd56 --- /dev/null +++ b/packages/astriarch-ui/src/components/TopOverview/svg/TopOverviewFrameSvg.tsx @@ -0,0 +1,57 @@ +export const TopOverviewFrameSvg = () => { + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/TopOverview/svg/TopOverviewSingleControlFrameSvg.tsx b/packages/astriarch-ui/src/components/TopOverview/svg/TopOverviewSingleControlFrameSvg.tsx new file mode 100644 index 00000000..ea07f836 --- /dev/null +++ b/packages/astriarch-ui/src/components/TopOverview/svg/TopOverviewSingleControlFrameSvg.tsx @@ -0,0 +1,12 @@ +export const TopOverviewFrameSvg = () => { + return ( + + + + ); +}; diff --git a/packages/astriarch-ui/src/components/TopOverviewItem/index.tsx b/packages/astriarch-ui/src/components/TopOverviewItem/index.tsx new file mode 100644 index 00000000..ab7c1d6e --- /dev/null +++ b/packages/astriarch-ui/src/components/TopOverviewItem/index.tsx @@ -0,0 +1,42 @@ +import { Box } from "@chakra-ui/react"; +import React from "react"; +import Text from "../Text"; +import IconImage from "../IconImage"; + +export interface TopOverviewItemProps { + children?: React.ReactNode; + amount: number; + amountPerTurn?: number; + type: "population" | "food" | "energy" | "research" | "ore" | "iridium"; + onClick?: () => void; +} + +const TopOverviewItem: React.FC = (props) => { + const { children, amount, amountPerTurn, type, ...rest } = props; + const amountFormatted = Math.floor(amount); + const amountPerTurnFormatted = amountPerTurn ? amountPerTurn.toFixed(1) : null; + const amountPerTurnSign = amountPerTurn ? (amountPerTurn > 0 ? "+" : "") : ""; + const amountContent = amountPerTurn + ? `${amountFormatted} ${amountPerTurnSign}${amountPerTurnFormatted}` + : amountFormatted; + const additionalTextCss = { + color: "#FFF", + fontSize: "18px", + fontWeight: 600, + lineHeight: "32px", + letterSpacing: "0.09px", + }; + + return ( + + + + {amountContent} + + + {children} + + ); +}; + +export default TopOverviewItem; diff --git a/packages/astriarch-ui/src/components/component.types.ts b/packages/astriarch-ui/src/components/component.types.ts new file mode 100644 index 00000000..c1231263 --- /dev/null +++ b/packages/astriarch-ui/src/components/component.types.ts @@ -0,0 +1,6 @@ +export type Size = "lg" | "md" | "sm" | "xs"; + +export interface TabControllerTab { + label: string; + children?: React.ReactNode; +} diff --git a/packages/astriarch-ui/src/components/tabs-compat.tsx b/packages/astriarch-ui/src/components/tabs-compat.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/astriarch-ui/src/components/ui/color-mode.tsx b/packages/astriarch-ui/src/components/ui/color-mode.tsx new file mode 100644 index 00000000..644a76ca --- /dev/null +++ b/packages/astriarch-ui/src/components/ui/color-mode.tsx @@ -0,0 +1,102 @@ +"use client"; + +import type { IconButtonProps, SpanProps } from "@chakra-ui/react"; +import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"; +import { ThemeProvider, useTheme } from "next-themes"; +import type { ThemeProviderProps } from "next-themes"; +import * as React from "react"; +import { LuMoon, LuSun } from "react-icons/lu"; + +export interface ColorModeProviderProps extends ThemeProviderProps {} + +export function ColorModeProvider(props: ColorModeProviderProps) { + return ; +} + +export type ColorMode = "light" | "dark"; + +export interface UseColorModeReturn { + colorMode: ColorMode; + setColorMode: (colorMode: ColorMode) => void; + toggleColorMode: () => void; +} + +export function useColorMode(): UseColorModeReturn { + const { resolvedTheme, setTheme, forcedTheme } = useTheme(); + const colorMode = forcedTheme || resolvedTheme; + const toggleColorMode = () => { + setTheme(resolvedTheme === "dark" ? "light" : "dark"); + }; + return { + colorMode: colorMode as ColorMode, + setColorMode: setTheme, + toggleColorMode, + }; +} + +export function useColorModeValue(light: T, dark: T) { + const { colorMode } = useColorMode(); + return colorMode === "dark" ? dark : light; +} + +export function ColorModeIcon() { + const { colorMode } = useColorMode(); + return colorMode === "dark" ? : ; +} + +interface ColorModeButtonProps extends Omit {} + +export const ColorModeButton = React.forwardRef(function ColorModeButton( + props, + ref +) { + const { toggleColorMode } = useColorMode(); + return ( + }> + + + + + ); +}); + +export const LightMode = React.forwardRef(function LightMode(props, ref) { + return ( + + ); +}); + +export const DarkMode = React.forwardRef(function DarkMode(props, ref) { + return ( + + ); +}); diff --git a/packages/astriarch-ui/src/components/ui/provider.tsx b/packages/astriarch-ui/src/components/ui/provider.tsx new file mode 100644 index 00000000..c1f896c8 --- /dev/null +++ b/packages/astriarch-ui/src/components/ui/provider.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { ChakraProvider, defaultSystem } from "@chakra-ui/react"; +import { ColorModeProvider, type ColorModeProviderProps } from "./color-mode"; + +export function Provider(props: ColorModeProviderProps) { + return ( + + + + ); +} diff --git a/packages/astriarch-ui/src/components/ui/toaster.tsx b/packages/astriarch-ui/src/components/ui/toaster.tsx new file mode 100644 index 00000000..dbc16553 --- /dev/null +++ b/packages/astriarch-ui/src/components/ui/toaster.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast, createToaster } from "@chakra-ui/react"; + +export const toaster = createToaster({ + placement: "bottom-end", + pauseOnPageIdle: true, +}); + +export const Toaster = () => { + return ( + + + {(toast) => ( + + {toast.type === "loading" ? : } + + {toast.title && {toast.title}} + {toast.description && {toast.description}} + + {toast.action && {toast.action.label}} + {toast.closable && } + + )} + + + ); +}; diff --git a/packages/astriarch-ui/src/components/ui/tooltip.tsx b/packages/astriarch-ui/src/components/ui/tooltip.tsx new file mode 100644 index 00000000..c2de26be --- /dev/null +++ b/packages/astriarch-ui/src/components/ui/tooltip.tsx @@ -0,0 +1,35 @@ +import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"; +import * as React from "react"; + +export interface TooltipProps extends ChakraTooltip.RootProps { + showArrow?: boolean; + portalled?: boolean; + portalRef?: React.RefObject; + content: React.ReactNode; + contentProps?: ChakraTooltip.ContentProps; + disabled?: boolean; +} + +export const Tooltip = React.forwardRef(function Tooltip(props, ref) { + const { showArrow, children, disabled, portalled = true, content, contentProps, portalRef, ...rest } = props; + + if (disabled) return children; + + return ( + + {children} + + + + {showArrow && ( + + + + )} + {content} + + + + + ); +}); diff --git a/packages/astriarch-ui/src/index.ts b/packages/astriarch-ui/src/index.ts new file mode 100644 index 00000000..55e8b9db --- /dev/null +++ b/packages/astriarch-ui/src/index.ts @@ -0,0 +1,3 @@ +export * from "./components/Button"; +export * from "./components/Box"; +export * from "./components/Dialog"; diff --git a/packages/astriarch-ui/src/styles/global.css b/packages/astriarch-ui/src/styles/global.css new file mode 100644 index 00000000..895727eb --- /dev/null +++ b/packages/astriarch-ui/src/styles/global.css @@ -0,0 +1,7 @@ +svg { + pointer-events: none; +} + +/* path { + pointer-events: all; +} */ diff --git a/packages/astriarch-ui/src/theme/compatTheme.ts b/packages/astriarch-ui/src/theme/compatTheme.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/astriarch-ui/src/theme/components/Tabs.ts b/packages/astriarch-ui/src/theme/components/Tabs.ts new file mode 100644 index 00000000..dd13b93a --- /dev/null +++ b/packages/astriarch-ui/src/theme/components/Tabs.ts @@ -0,0 +1,24 @@ +import { tabsAnatomy } from "@chakra-ui/anatomy"; +import { createMultiStyleConfigHelpers } from "@chakra-ui/react"; + +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tabsAnatomy.keys); + +// define the base component styles +const baseStyle = definePartsStyle({ + // define the part you're going to style + tab: { + _selected: { + borderColor: "inherit", + borderBottom: "none", + mb: "0px", + }, + }, + tablist: { + mb: "-1px", + }, +}); + +type TabsStyle = { baseStyle?: typeof baseStyle }; + +// export the component theme +export const tabsTheme: TabsStyle = defineMultiStyleConfig({ baseStyle }); diff --git a/packages/astriarch-ui/src/theme/theme.ts b/packages/astriarch-ui/src/theme/theme.ts new file mode 100644 index 00000000..212b39c7 --- /dev/null +++ b/packages/astriarch-ui/src/theme/theme.ts @@ -0,0 +1,191 @@ +import { extendTheme } from "@chakra-ui/react"; +import { tabsTheme } from "./components/Tabs"; + +// example theme +const theme = extendTheme({ + colors: { + primaryFill: "#00FFFF", + primaryStroke: "#00FFFF", + gray: { + "50": "#F0F2F4", + "100": "#D6DAE1", + "200": "#BBC3CE", + "300": "#A1ABBA", + "400": "#8693A7", + "500": "#6C7C93", + "600": "#566376", + "700": "#414A58", + "800": "#2B313B", + "900": "#16191D", + }, + green: { + "50": "#EFFFE6", + "100": "#D3FFB8", + "200": "#B6FE8A", + "300": "#9AFE5D", + "400": "#7DFE2F", + "500": "#61FE01", + "600": "#4ECB01", + "700": "#3A9801", + "800": "#276600", + "900": "#133300", + }, + orange: { + "50": "#FFF3E5", + "100": "#FFDEB8", + "200": "#FFC98A", + "300": "#FFB45C", + "400": "#FF9E2E", + "500": "#FF8900", + "600": "#CC6E00", + "700": "#995200", + "800": "#663700", + "900": "#331B00", + }, + purple: { + "50": "#F7E5FF", + "100": "#E8B8FF", + "200": "#DA8AFF", + "300": "#CB5CFF", + "400": "#BC2EFF", + "500": "#AE00FF", + "600": "#8B00CC", + "700": "#680099", + "800": "#450066", + "900": "#230033", + }, + red: { + "50": "#FFE5E5", + "100": "#FFB8B8", + "200": "#FF8A8A", + "300": "#FF5C5C", + "400": "#FF2E2E", + "500": "#FF0000", + "600": "#CC0000", + "700": "#990000", + "800": "#660000", + "900": "#330000", + }, + yellow: { + "50": "#FFFEE5", + "100": "#FFFCB8", + "200": "#FFFA8A", + "300": "#FFF95C", + "400": "#FFF72E", + "500": "#FFF500", + "600": "#CCC400", + "700": "#999300", + "800": "#666200", + "900": "#333100", + }, + cyan: { + "50": "#E5FFFF", + "100": "#B8FFFF", + "200": "#8AFFFF", + "300": "#5CFFFF", + "400": "#2EFFFF", + "500": "#00FFFF", + "600": "#00CCCC", + "700": "#009999", + "800": "#006666", + "900": "#003333", + }, + blue: { + "50": "#E5F7FF", + "100": "#B8EAFF", + "200": "#8ADCFF", + "300": "#5CCEFF", + "400": "#2EC0FF", + "500": "#00B2FF", + "600": "#008FCC", + "700": "#006B99", + "800": "#004766", + "900": "#002433", + }, + teal: { + "50": "#E5FFF9", + "100": "#B8FFEE", + "200": "#8AFFE3", + "300": "#5CFFD8", + "400": "#2EFFCD", + "500": "#00FFC2", + "600": "#00CC9B", + "700": "#009974", + "800": "#00664E", + "900": "#003327", + }, + pink: { + "50": "#FEECE7", + "100": "#FCCABA", + "200": "#FAA88E", + "300": "#F98662", + "400": "#F76436", + "500": "#F5420A", + "600": "#C43508", + "700": "#932806", + "800": "#621A04", + "900": "#310D02", + }, + }, + fonts: { + body: "system-ui, sans-serif", + heading: "Georgia, serif", + mono: "Menlo, monospace", + }, + fontSizes: { + xs: "0.75rem", + sm: "0.875rem", + md: "1rem", + lg: "1.125rem", + xl: "1.25rem", + "2xl": "1.5rem", + "3xl": "1.875rem", + "4xl": "2.25rem", + "5xl": "3rem", + "6xl": "3.75rem", + "7xl": "4.5rem", + "8xl": "6rem", + "9xl": "8rem", + }, + fontWeights: { + hairline: 100, + thin: 200, + light: 300, + normal: 400, + medium: 500, + semibold: 600, + bold: 700, + extrabold: 800, + black: 900, + }, + lineHeights: { + normal: "normal", + none: 1, + shorter: 1.25, + short: 1.375, + base: 1.5, + tall: 1.625, + taller: "2", + "3": ".75rem", + "4": "1rem", + "5": "1.25rem", + "6": "1.5rem", + "7": "1.75rem", + "8": "2rem", + "9": "2.25rem", + "10": "2.5rem", + }, + letterSpacings: { + tighter: "-0.05em", + tight: "-0.025em", + normal: "0", + wide: "0.025em", + wider: "0.05em", + widest: "0.1em", + }, + components: { + Tabs: tabsTheme, + }, +}); + +export default theme; diff --git a/packages/astriarch-ui/tsconfig.json b/packages/astriarch-ui/tsconfig.json new file mode 100644 index 00000000..07aa7019 --- /dev/null +++ b/packages/astriarch-ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["."], + "exclude": ["dist", "node_modules", "**/*.spec.ts"], + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "skipLibCheck": true, + "strict": true, + "moduleResolution": "node", + "lib": ["dom", "ES2015"], + "module": "ESNext", + "target": "ES6", + "outDir": "./dist" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..7b0648b7 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,14567 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + husky: + specifier: ^9.1.7 + version: 9.1.7 + + apps/astriarch-backend: + dependencies: + astriarch-engine: + specifier: workspace:* + version: link:../../packages/astriarch-engine + async: + specifier: ^3.2.4 + version: 3.2.6 + config: + specifier: ^3.3.9 + version: 3.3.12 + connect-mongo: + specifier: ^5.1.0 + version: 5.1.0(express-session@1.18.1)(mongodb@6.17.0) + cookie-parser: + specifier: ^1.4.6 + version: 1.4.7 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.2 + version: 4.21.2 + express-session: + specifier: ^1.17.3 + version: 1.18.1 + helmet: + specifier: ^7.1.0 + version: 7.2.0 + mongoose: + specifier: ^8.0.3 + version: 8.16.3 + morgan: + specifier: ^1.10.0 + version: 1.10.0 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + ws: + specifier: ^8.14.2 + version: 8.18.3 + devDependencies: + '@types/async': + specifier: ^3.2.24 + version: 3.2.24 + '@types/config': + specifier: ^3.3.3 + version: 3.3.5 + '@types/cookie-parser': + specifier: ^1.4.6 + version: 1.4.9(@types/express@4.17.23) + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^4.17.21 + version: 4.17.23 + '@types/express-session': + specifier: ^1.17.10 + version: 1.18.2 + '@types/jest': + specifier: ^29.5.8 + version: 29.5.14 + '@types/morgan': + specifier: ^1.9.9 + version: 1.9.10 + '@types/node': + specifier: ^20.10.4 + version: 20.19.7 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@types/ws': + specifier: ^8.5.9 + version: 8.18.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.1 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^6.13.1 + version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) + eslint: + specifier: ^8.55.0 + version: 8.57.1 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.28.0))(jest-util@30.0.2)(jest@29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0))(typescript@5.8.3) + tsx: + specifier: ^4.6.2 + version: 4.20.3 + typescript: + specifier: ^5.3.3 + version: 5.8.3 + + apps/astriarch-frontend: + dependencies: + '@chakra-ui/react': + specifier: ^3.21.1 + version: 3.21.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.1.8)(react@19.1.0) + astriarch-engine: + specifier: workspace:* + version: link:../../packages/astriarch-engine + astriarch-ui: + specifier: workspace:* + version: link:../../packages/astriarch-ui + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.1.0) + devDependencies: + '@eslint/js': + specifier: ^9.29.0 + version: 9.30.1 + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react-swc': + specifier: ^3.10.2 + version: 3.10.2(@swc/helpers@0.5.17)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + eslint: + specifier: ^9.29.0 + version: 9.30.1(jiti@2.4.2) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.20(eslint@9.30.1(jiti@2.4.2)) + globals: + specifier: ^16.2.0 + version: 16.3.0 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.34.1 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + vite: + specifier: ^7.0.0 + version: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + + apps/astriarch-frontend-svelte: + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^5.1.0 + version: 5.1.0(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@4.1.11) + '@tsconfig/svelte': + specifier: ^5.0.4 + version: 5.0.4 + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + shadcn-svelte: + specifier: ^1.0.6 + version: 1.0.6 + svelte: + specifier: ^5.34.7 + version: 5.35.2 + svelte-check: + specifier: ^4.2.2 + version: 4.2.2(picomatch@4.0.2)(svelte@5.35.2)(typescript@5.8.3) + tailwindcss: + specifier: ^4.1.11 + version: 4.1.11 + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vite: + specifier: ^7.0.0 + version: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + apps/astriarch-frontend-sveltekit: + dependencies: + astriarch-engine: + specifier: workspace:* + version: link:../../packages/astriarch-engine + konva: + specifier: ^9.3.22 + version: 9.3.22 + devDependencies: + '@eslint/compat': + specifier: ^1.2.5 + version: 1.3.1(eslint@9.30.1(jiti@2.4.2)) + '@eslint/js': + specifier: ^9.18.0 + version: 9.30.1 + '@internationalized/date': + specifier: ^3.8.2 + version: 3.8.2 + '@lucide/svelte': + specifier: ^0.515.0 + version: 0.515.0(svelte@5.35.2) + '@storybook/addon-svelte-csf': + specifier: ^5.0.6 + version: 5.0.6(@storybook/svelte@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2))(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(babel-plugin-macros@3.1.0)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@storybook/sveltekit': + specifier: ^9.0.16 + version: 9.0.16(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@sveltejs/adapter-cloudflare': + specifier: ^7.0.0 + version: 7.0.5(@sveltejs/kit@2.22.5(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(wrangler@4.24.3(@cloudflare/workers-types@4.20250711.0)) + '@sveltejs/kit': + specifier: ^2.22.0 + version: 2.22.5(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@sveltejs/vite-plugin-svelte': + specifier: ^6.0.0 + version: 6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@tailwindcss/forms': + specifier: ^0.5.9 + version: 0.5.10(tailwindcss@4.1.11) + '@tailwindcss/typography': + specifier: ^0.5.15 + version: 0.5.16(tailwindcss@4.1.11) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.1.11(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@types/node': + specifier: ^22 + version: 22.16.3 + bits-ui: + specifier: ^2.8.10 + version: 2.8.10(@internationalized/date@3.8.2)(svelte@5.35.2) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + eslint: + specifier: ^9.18.0 + version: 9.30.1(jiti@2.4.2) + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.5(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-storybook: + specifier: ^9.0.16 + version: 9.0.16(eslint@9.30.1(jiti@2.4.2))(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3) + eslint-plugin-svelte: + specifier: ^3.0.0 + version: 3.10.1(eslint@9.30.1(jiti@2.4.2))(svelte@5.35.2) + globals: + specifier: ^16.0.0 + version: 16.3.0 + prettier: + specifier: ^3.4.2 + version: 3.6.2 + prettier-plugin-svelte: + specifier: ^3.3.3 + version: 3.4.0(prettier@3.6.2)(svelte@5.35.2) + prettier-plugin-tailwindcss: + specifier: ^0.6.11 + version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.35.2))(prettier@3.6.2) + shadcn-svelte: + specifier: ^1.0.6 + version: 1.0.6 + storybook: + specifier: ^9.0.16 + version: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + svelte: + specifier: ^5.0.0 + version: 5.35.2 + svelte-check: + specifier: ^4.0.0 + version: 4.2.2(picomatch@4.0.2)(svelte@5.35.2)(typescript@5.8.3) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwind-variants: + specifier: ^1.0.0 + version: 1.0.0(tailwindcss@4.1.11) + tailwindcss: + specifier: ^4.0.0 + version: 4.1.11 + tw-animate-css: + specifier: ^1.3.5 + version: 1.3.5 + typescript: + specifier: ^5.0.0 + version: 5.8.3 + typescript-eslint: + specifier: ^8.20.0 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + vite: + specifier: ^7.0.4 + version: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + vitest: + specifier: ^3.2.3 + version: 3.2.4(@types/node@22.16.3)(@vitest/browser@3.2.4)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + packages/astriarch-engine: + devDependencies: + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.0.10 + version: 24.0.10 + eslint: + specifier: ^9.30.1 + version: 9.30.1(jiti@2.4.2) + jest: + specifier: ^30.0.4 + version: 30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.4.0 + version: 29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.28.0))(esbuild@0.25.5)(jest-util@30.0.2)(jest@30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)))(typescript@5.8.3) + tslib: + specifier: ^2.8.1 + version: 2.8.1 + tsup: + specifier: ^8.5.0 + version: 8.5.0(@swc/core@1.12.9(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.35.1 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + + packages/astriarch-ui: + dependencies: + '@chakra-ui/react': + specifier: ^3.21.1 + version: 3.21.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.1.8)(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.1.0) + devDependencies: + '@babel/core': + specifier: ^7.28.0 + version: 7.28.0 + '@babel/preset-env': + specifier: ^7.28.0 + version: 7.28.0(@babel/core@7.28.0) + '@babel/preset-react': + specifier: ^7.27.1 + version: 7.27.1(@babel/core@7.28.0) + '@babel/preset-typescript': + specifier: ^7.27.1 + version: 7.27.1(@babel/core@7.28.0) + '@chakra-ui/storybook-addon': + specifier: ^5.2.5 + version: 5.2.5(@chakra-ui/react@3.21.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@storybook/components@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)))(@storybook/manager-api@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)))(@storybook/preview-api@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)))(@storybook/types@7.6.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/addon-docs': + specifier: ^9.0.15 + version: 9.0.16(@types/react@19.1.8)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/addon-links': + specifier: ^9.0.15 + version: 9.0.16(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/addons': + specifier: ^7.6.17 + version: 7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/api': + specifier: ^7.6.17 + version: 7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/react-webpack5': + specifier: ^9.0.15 + version: 9.0.16(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3) + '@storybook/testing-library': + specifier: ^0.2.2 + version: 0.2.2 + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + prop-types: + specifier: ^15.8.1 + version: 15.8.1 + storybook: + specifier: ^9.0.15 + version: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + tsup: + specifier: ^8.5.0 + version: 8.5.0(@swc/core@1.12.9(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + +packages: + + '@adobe/css-tools@4.4.3': + resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@ark-ui/react@5.15.4': + resolution: {integrity: sha512-+xBKqxmt0JHewOsYsHXtedcdPsPZirAwd9y80JpyYfp8bSpIhmombLTjh0Ue9ktKPr7LdoZhV7qcX1TNrX4grg==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.27.1': + resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + 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.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.27.1': + resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': + resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': + resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.27.1': + resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.0': + resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.27.1': + resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.0': + resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.0': + resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.0': + resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.27.1': + resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1': + resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.27.1': + resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.0': + resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.27.1': + resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.28.0': + resolution: {integrity: sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.27.1': + resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.28.0': + resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.27.1': + resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@chakra-ui/react@3.21.1': + resolution: {integrity: sha512-tc8SAeOZbOeOSY+BROE6o1FyzoS8sAuC6TAwlfUCZWhv9CMsxBisC88D4WI/puwnZVfUbzzhdVEQmWkCbJK6ag==} + peerDependencies: + '@emotion/react': '>=11' + react: '>=18' + react-dom: '>=18' + + '@chakra-ui/storybook-addon@5.2.5': + resolution: {integrity: sha512-Iv1LmrzSLHb0DYeDXwJnwBPgfwOl9wsapv5Rc3eWM8rZvFFRoDE/f73s3pEaz3A5dleH6y2shASoXeV7JoQNFg==} + peerDependencies: + '@chakra-ui/react': '>=2.0.0' + '@storybook/components': '>=7.0.12' + '@storybook/manager-api': '>=7.0.12' + '@storybook/preview-api': '>=7.0.12' + '@storybook/types': '>=7.0.24' + react: '>=16.8.x' + react-dom: '*' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@cloudflare/kv-asset-handler@0.4.0': + resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.3.3': + resolution: {integrity: sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==} + peerDependencies: + unenv: 2.0.0-rc.17 + workerd: ^1.20250508.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20250709.0': + resolution: {integrity: sha512-VqwcvnbI8FNCP87ZWNHA3/sAC5U9wMbNnjBG0sHEYzM7B9RPHKYHdVKdBEWhzZXnkQYMK81IHm4CZsK16XxAuQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20250709.0': + resolution: {integrity: sha512-A54ttSgXMM4huChPTThhkieOjpDxR+srVOO9zjTHVIyoQxA8zVsku4CcY/GQ95RczMV+yCKVVu/tAME7vwBFuA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20250709.0': + resolution: {integrity: sha512-no4O3OK+VXINIxv99OHJDpIgML2ZssrSvImwLtULzqm+cl4t1PIfXNRUqj89ujTkmad+L9y4G6dBQMPCLnmlGg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20250709.0': + resolution: {integrity: sha512-7cNICk2Qd+m4QGrcmWyAuZJXTHt1ud6isA+dic7Yk42WZmwXhlcUATyvFD9FSQNFcldjuRB4n8JlWEFqZBn+lw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20250709.0': + resolution: {integrity: sha512-j1AyO8V/62Q23EJplWgzBlRCqo/diXgox58AbDqSqgyzCBAlvUzXQRDBab/FPNG/erRqt7I1zQhahrBhrM0uLA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20250711.0': + resolution: {integrity: sha512-aocBpm0Mbbz6GIOLJDbPrl4gZZ2o2A1a7pjFWlt3j2T1xghP0h1V3LtWUhyk74bAY5qOEuGQyLgr+svY//yCrQ==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + 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.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@1.3.1': + resolution: {integrity: sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.1': + resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@internationalized/date@3.8.2': + resolution: {integrity: sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==} + + '@internationalized/number@3.6.3': + resolution: {integrity: sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/console@30.0.4': + resolution: {integrity: sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/core@30.0.4': + resolution: {integrity: sha512-MWScSO9GuU5/HoWjpXAOBs6F/iobvK1XlioelgOM9St7S0Z5WTI9kjCQLPeo4eQRRYusyLW25/J7J5lbFkrYXw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.0.1': + resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/environment@30.0.4': + resolution: {integrity: sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@30.0.4': + resolution: {integrity: sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@30.0.4': + resolution: {integrity: sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@30.0.4': + resolution: {integrity: sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.0.1': + resolution: {integrity: sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@30.0.4': + resolution: {integrity: sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/reporters@30.0.4': + resolution: {integrity: sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@30.0.1': + resolution: {integrity: sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.0.4': + resolution: {integrity: sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@30.0.4': + resolution: {integrity: sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@30.0.4': + resolution: {integrity: sha512-bj6ePmqi4uxAE8EHE0Slmk5uBYd9Vd/PcVt06CsBxzH4bbA8nGsI1YbXl/NH+eii4XRtyrRx+Cikub0x8H4vDg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@30.0.4': + resolution: {integrity: sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@30.0.1': + resolution: {integrity: sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lucide/svelte@0.515.0': + resolution: {integrity: sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA==} + peerDependencies: + svelte: ^5 + + '@mdx-js/react@3.1.0': + resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@mongodb-js/saslprep@1.3.0': + resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} + + '@napi-rs/wasm-runtime@0.2.11': + resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + + '@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'} + + '@pandacss/is-valid-prop@0.54.0': + resolution: {integrity: sha512-UhRgg1k9VKRCBAHl+XUK3lvN0k9bYifzYGZOqajDid4L1DyU813A1L0ZwN4iV9WX5TX3PfUugqtgG9LnIeFGBQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.7': + resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@poppinss/colors@4.1.5': + resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} + + '@poppinss/dumper@0.6.4': + resolution: {integrity: sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==} + + '@poppinss/exception@1.2.2': + resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + + '@rolldown/pluginutils@1.0.0-beta.11': + resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==} + + '@rollup/rollup-android-arm-eabi@4.44.2': + resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.44.2': + resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.44.2': + resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.44.2': + resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.44.2': + resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.44.2': + resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.44.2': + resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.44.2': + resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.44.2': + resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.44.2': + resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.44.2': + resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.44.2': + resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.44.2': + resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.44.2': + resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.44.2': + resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinclair/typebox@0.34.37': + resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==} + + '@sindresorhus/is@7.0.2': + resolution: {integrity: sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==} + engines: {node: '>=18'} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@sinonjs/fake-timers@13.0.5': + resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + + '@speed-highlight/core@1.2.7': + resolution: {integrity: sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==} + + '@storybook/addon-docs@9.0.16': + resolution: {integrity: sha512-/ZXaxMC/JqL0cnVuyPHXdJhNvgCrKvxcnM3ACdgBLsEIGcIqegPF+Ahkb2f9sjU36sR7ihT81cL/7cUvQwzd4Q==} + peerDependencies: + storybook: ^9.0.16 + + '@storybook/addon-links@9.0.16': + resolution: {integrity: sha512-8jiFKeW7gTGZ/WhBnB38dTVFDapyiyLMCvktIcrsVrJTBbj/bC1+ts7OoeuJCs7/EN+2zn9vf+V17ZqI0I2sIA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.16 + peerDependenciesMeta: + react: + optional: true + + '@storybook/addon-svelte-csf@5.0.6': + resolution: {integrity: sha512-Ryo28eV7YrptIt9PmpIpDHAGXAKwfNKMvN/d79Bv7kYhdQ+LvyJkNzNOn6Tpem4soUZTsf4+CdqbKs3eHHIXPg==} + peerDependencies: + '@storybook/svelte': ^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 + '@sveltejs/vite-plugin-svelte': ^4.0.0 || ^5.0.0 + storybook: ^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 + svelte: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 + + '@storybook/addons@7.6.17': + resolution: {integrity: sha512-Ok18Y698Ccyg++MoUNJNHY0cXUvo8ETFIRLJk1g9ElJ70j6kPgNnzW2pAtZkBNmswHtofZ7pT156cj96k/LgfA==} + + '@storybook/api@7.6.17': + resolution: {integrity: sha512-l92PI+5XL4zB/o4IBWFCKQWTXvPg9hR45DCJqlPHrLZStiR6Xj1mbrtOjUlgIOH+nYb/SZFZqO53hhrs7X4Nvg==} + + '@storybook/builder-vite@9.0.16': + resolution: {integrity: sha512-zXockUexeRy3ABG7DFLEvJqJe4mGL0JkI7FMrpwiKaHCQNaD87vR0xkRVeh0a3B8GKUNxoYtpYKvdzc9DobQHQ==} + peerDependencies: + storybook: ^9.0.16 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@storybook/builder-webpack5@9.0.16': + resolution: {integrity: sha512-Hb45cgUr6aYK24HBt42EGApbJVosunt7x6DTjawHD2BAqpfSxo6urLnKFIhx8H+af88XN37MTbp5bNozvw4WPA==} + peerDependencies: + storybook: ^9.0.16 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/channels@7.6.17': + resolution: {integrity: sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==} + + '@storybook/client-logger@7.6.17': + resolution: {integrity: sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==} + + '@storybook/components@8.6.14': + resolution: {integrity: sha512-HNR2mC5I4Z5ek8kTrVZlIY/B8gJGs5b3XdZPBPBopTIN6U/YHXiDyOjY3JlaS4fSG1fVhp/Qp1TpMn1w/9m1pw==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/core-events@7.6.17': + resolution: {integrity: sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==} + + '@storybook/core-webpack@9.0.16': + resolution: {integrity: sha512-U1SiqRywp6pu0DNy1uSH6b6B4aw7clYvICf+HNRzULYfS6V7ZHD2VaBmcf1xgx0aqVEPUGv6f46oZr2pzmcjGw==} + peerDependencies: + storybook: ^9.0.16 + + '@storybook/csf-plugin@9.0.16': + resolution: {integrity: sha512-MSmfPwI0j1mMAc+R3DVkVBQf2KLzaVn2SLdEwweesx63Nh9j3zu9CqKEa0zOuDX1lR2M0DZU0lV6K4sc2EYI4A==} + peerDependencies: + storybook: ^9.0.16 + + '@storybook/csf@0.1.13': + resolution: {integrity: sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q==} + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@1.4.0': + resolution: {integrity: sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + + '@storybook/manager-api@7.6.17': + resolution: {integrity: sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==} + + '@storybook/manager-api@8.6.14': + resolution: {integrity: sha512-ez0Zihuy17udLbfHZQXkGqwtep0mSGgHcNzGN7iZrMP1m+VmNo+7aGCJJdvXi7+iU3yq8weXSQFWg5DqWgLS7g==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/preset-react-webpack@9.0.16': + resolution: {integrity: sha512-esAGMKxeXfbW2O86u9foSOGegGQz78335u9RiCKaHXz6gl/xsoE60q3EEQhYxftO630YRfIXbZY7+N9rvbs78w==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.16 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/preview-api@7.6.17': + resolution: {integrity: sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==} + + '@storybook/preview-api@8.6.14': + resolution: {integrity: sha512-2GhcCd4dNMrnD7eooEfvbfL4I83qAqEyO0CO7JQAmIO6Rxb9BsOLLI/GD5HkvQB73ArTJ+PT50rfaO820IExOQ==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0': + resolution: {integrity: sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q==} + peerDependencies: + typescript: '>= 4.x' + webpack: '>= 4' + + '@storybook/react-dom-shim@9.0.16': + resolution: {integrity: sha512-5aIK+31R41mRUvDB4vmBv8hwh3IVHIk/Zbs6kkWF2a+swOsB2+a06aLX21lma4/0T/AuFVXHWat0+inQ4nrXRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.16 + + '@storybook/react-webpack5@9.0.16': + resolution: {integrity: sha512-0m2JKE+ZiEquC6CBwVnsgzOFlkRNNRKnBUJg/K38QSeK62l3zD8Dy7j19LQzUUyMCnYdKYql67kKn8tDOeekOw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.16 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/react@9.0.16': + resolution: {integrity: sha512-1jk9fBe8vEoZrba9cK19ZDdZgYMXUNl3Egjj5RsTMYMc1L2mtIu9o56VyK/1V4Q52N9IyawHvmIIuxc5pCZHkQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^9.0.16 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/router@7.6.17': + resolution: {integrity: sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==} + + '@storybook/svelte-vite@9.0.16': + resolution: {integrity: sha512-1r2lTjugwlfUhA/jqgC9Jp486Sbny8YfGCHcqJGr07+mqbItMdljAv+PqA1YUE5eeDAuo93ZysMSDz4+vFJQvw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + storybook: ^9.0.16 + svelte: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@storybook/svelte@9.0.16': + resolution: {integrity: sha512-K6uNaDSnfxyEAaQMndrBaTxz7dMeYULEKD6lK8aNmr6gcDkKUYXZQLQ69x1hAf/ysPCF6qAmI/Adny3Qrpvxtw==} + engines: {node: '>=20.0.0'} + peerDependencies: + storybook: ^9.0.16 + svelte: ^5.0.0 + + '@storybook/sveltekit@9.0.16': + resolution: {integrity: sha512-oHkdInlYlFh7q96gKes2i1b5Pc8Q+oBu0J2Fqq7roqSik8UpEnNO59BiNX91L6/nn4zuf/8SZ05BjTb3Ot4wJw==} + engines: {node: '>=20.0.0'} + peerDependencies: + storybook: ^9.0.16 + svelte: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@storybook/testing-library@0.2.2': + resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==} + deprecated: In Storybook 8, this package functionality has been integrated to a new package called @storybook/test, which uses Vitest APIs for an improved experience. When upgrading to Storybook 8 with 'npx storybook@latest upgrade', you will get prompted and will get an automigration for the new package. Please migrate when you can. + + '@storybook/theming@7.6.17': + resolution: {integrity: sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/types@7.6.17': + resolution: {integrity: sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==} + + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-cloudflare@7.0.5': + resolution: {integrity: sha512-wZw8MHUZs5E8n4ldEAjgaWY1vaGQgBblPePqIZQoSxlDdLtJHjoiy0esdk4MGnkzSgLSJ3QvX7IEFKpWZBkaeA==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + wrangler: ^4.0.0 + + '@sveltejs/kit@2.22.5': + resolution: {integrity: sha512-l5i+LcDaoymD2mg5ziptnHmzzF79+c9twJiDoLWAPKq7afMEe4mvGesJ+LVtm33A92mLzd2KUHgtGSqTrvfkvg==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte-inspector@5.0.0': + resolution: {integrity: sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@5.1.0': + resolution: {integrity: sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte@6.0.0': + resolution: {integrity: sha512-mma5GJ23pYiWpTNbN//g9XI3Hfob3aAlXPP42qRtvjgTAU6pfJyLyNPTdLjFuj+jfC9JslP4J3AkeiJNhjtLLA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@swc/core-darwin-arm64@1.12.9': + resolution: {integrity: sha512-GACFEp4nD6V+TZNR2JwbMZRHB+Yyvp14FrcmB6UCUYmhuNWjkxi+CLnEvdbuiKyQYv0zA+TRpCHZ+whEs6gwfA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.12.9': + resolution: {integrity: sha512-hv2kls7Ilkm2EpeJz+I9MCil7pGS3z55ZAgZfxklEuYsxpICycxeH+RNRv4EraggN44ms+FWCjtZFu0LGg2V3g==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.12.9': + resolution: {integrity: sha512-od9tDPiG+wMU9wKtd6y3nYJdNqgDOyLdgRRcrj1/hrbHoUPOM8wZQZdwQYGarw63iLXGgsw7t5HAF9Yc51ilFA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.12.9': + resolution: {integrity: sha512-6qx1ka9LHcLzxIgn2Mros+CZLkHK2TawlXzi/h7DJeNnzi8F1Hw0Yzjp8WimxNCg6s2n+o3jnmin1oXB7gg8rw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.12.9': + resolution: {integrity: sha512-yghFZWKPVVGbUdqiD7ft23G0JX6YFGDJPz9YbLLAwGuKZ9th3/jlWoQDAw1Naci31LQhVC+oIji6ozihSuwB2A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.12.9': + resolution: {integrity: sha512-SFUxyhWLZRNL8QmgGNqdi2Q43PNyFVkRZ2zIif30SOGFSxnxcf2JNeSeBgKIGVgaLSuk6xFVVCtJ3KIeaStgRg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.12.9': + resolution: {integrity: sha512-9FB0wM+6idCGTI20YsBNBg9xSWtkDBymnpaTCsZM3qDc0l4uOpJMqbfWhQvp17x7r/ulZfb2QY8RDvQmCL6AcQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.12.9': + resolution: {integrity: sha512-zHOusMVbOH9ik5RtRrMiGzLpKwxrPXgXkBm3SbUCa65HAdjV33NZ0/R9Rv1uPESALtEl2tzMYLUxYA5ECFDFhA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.12.9': + resolution: {integrity: sha512-aWZf0PqE0ot7tCuhAjRkDFf41AzzSQO0x2xRfTbnhpROp57BRJ/N5eee1VULO/UA2PIJRG7GKQky5bSGBYlFug==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.12.9': + resolution: {integrity: sha512-C25fYftXOras3P3anSUeXXIpxmEkdAcsIL9yrr0j1xepTZ/yKwpnQ6g3coj8UXdeJy4GTVlR6+Ow/QiBgZQNOg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.12.9': + resolution: {integrity: sha512-O+LfT2JlVMsIMWG9x+rdxg8GzpzeGtCZQfXV7cKc1PjIKUkLFf1QJ7okuseA4f/9vncu37dQ2ZcRrPKy0Ndd5g==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@swc/types@0.1.23': + resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + + '@tailwindcss/forms@0.5.10': + resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tsconfig/svelte@5.0.4': + resolution: {integrity: sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/async@3.2.24': + resolution: {integrity: sha512-8iHVLHsCCOBKjCF2KwFe0p9Z3rfM9mL+sSP8btyR5vTjJRAqpBYD28/ZLgXPf0pjG1VxOvtCV/BgXkQbpSe8Hw==} + + '@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.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/config@3.3.5': + resolution: {integrity: sha512-itq2HtXQBrNUKwMNZnb9mBRE3T99VYCdl1gjST9rq+9kFaB1iMMGuDeZnP88qid73DnpAMKH9ZolqDpS1Lz7+w==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.9': + resolution: {integrity: sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==} + peerDependencies: + '@types/express': '*' + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express-session@1.18.2': + resolution: {integrity: sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==} + + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/html-minifier-terser@6.1.0': + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/morgan@1.9.10': + resolution: {integrity: sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==} + + '@types/node@20.19.7': + resolution: {integrity: sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==} + + '@types/node@22.16.3': + resolution: {integrity: sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==} + + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.1.6': + resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.8': + resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@11.0.5': + resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/eslint-plugin@8.35.1': + resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.35.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.35.1': + resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.0': + resolution: {integrity: sha512-LRw5BW29sYj9NsQC6QoqeLVQhEa+BwVINYyMlcve+6stwdBsSt5UB7zw4UZB4+4PNqIVilHoMaPWCb/KhABHQw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.0': + resolution: {integrity: sha512-zYX8D2zcWCAHqghA8tPjbp7LwjVXbIZP++mpU/Mrf5jUVlk3BWIxkeB8yYzZi5GpFSlqMcRZQxQqbMI0c2lASQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.0': + resolution: {integrity: sha512-YsYOT049hevAY/lTYD77GhRs885EXPeAfExG5KenqMJ417nYLS2N/kpRpYbABhFZBVQn+2uRPasTe4ypmYoo3w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.0': + resolution: {integrity: sha512-PSjvk3OZf1aZImdGY5xj9ClFG3bC4gnSSYWrt+id0UAv+GwwVldhpMFjAga8SpMo2T1GjV9UKwM+QCsQCQmtdA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.0': + resolution: {integrity: sha512-KC/iFaEN/wsTVYnHClyHh5RSYA9PpuGfqkFua45r4sweXpC0KHZ+BYY7ikfcGPt5w1lMpR1gneFzuqWLQxsRKg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.0': + resolution: {integrity: sha512-CDh/0v8uot43cB4yKtDL9CVY8pbPnMV0dHyQCE4lFz6PW/+9tS0i9eqP5a91PAqEBVMqH1ycu+k8rP6wQU846w==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.0': + resolution: {integrity: sha512-+TE7epATDSnvwr3L/hNHX3wQ8KQYB+jSDTdywycg3qDqvavRP8/HX9qdq/rMcnaRDn4EOtallb3vL/5wCWGCkw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.0': + resolution: {integrity: sha512-VBAYGg3VahofpQ+L4k/ZO8TSICIbUKKTaMYOWHWfuYBFqPbSkArZZLezw3xd27fQkxX4BaLGb/RKnW0dH9Y/UA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.0': + resolution: {integrity: sha512-9IgGFUUb02J1hqdRAHXpZHIeUHRrbnGo6vrRbz0fREH7g+rzQy53/IBSyadZ/LG5iqMxukriNPu4hEMUn+uWEg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.0': + resolution: {integrity: sha512-LR4iQ/LPjMfivpL2bQ9kmm3UnTas3U+umcCnq/CV7HAkukVdHxrDD1wwx74MIWbbgzQTLPYY7Ur2MnnvkYJCBQ==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.0': + resolution: {integrity: sha512-HCupFQwMrRhrOg7YHrobbB5ADg0Q8RNiuefqMHVsdhEy9lLyXm/CxsCXeLJdrg27NAPsCaMDtdlm8Z2X8x91Tg==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.0': + resolution: {integrity: sha512-Ckxy76A5xgjWa4FNrzcKul5qFMWgP5JSQ5YKd0XakmWOddPLSkQT+uAvUpQNnFGNbgKzv90DyQlxPDYPQ4nd6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.0': + resolution: {integrity: sha512-HfO0PUCCRte2pMJmVyxPI+eqT7KuV3Fnvn2RPvMe5mOzb2BJKf4/Vth8sSt9cerQboMaTVpbxyYjjLBWIuI5BQ==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.0': + resolution: {integrity: sha512-9PZdjP7tLOEjpXHS6+B/RNqtfVUyDEmaViPOuSqcbomLdkJnalt5RKQ1tr2m16+qAufV0aDkfhXtoO7DQos/jg==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.0': + resolution: {integrity: sha512-qkE99ieiSKMnFJY/EfyGKVtNra52/k+lVF/PbO4EL5nU6AdvG4XhtJ+WHojAJP7ID9BNIra/yd75EHndewNRfA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.0': + resolution: {integrity: sha512-MjXek8UL9tIX34gymvQLecz2hMaQzOlaqYJJBomwm1gsvK2F7hF+YqJJ2tRyBDTv9EZJGMt4KlKkSD/gZWCOiw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.0': + resolution: {integrity: sha512-9LT6zIGO7CHybiQSh7DnQGwFMZvVr0kUjah6qQfkH2ghucxPV6e71sUXJdSM4Ba0MaGE6DC/NwWf7mJmc3DAng==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.0': + resolution: {integrity: sha512-HYchBYOZ7WN266VjoGm20xFv5EonG/ODURRgwl9EZT7Bq1nLEs6VKJddzfFdXEAho0wfFlt8L/xIiE29Pmy1RA==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.0': + resolution: {integrity: sha512-+oLKLHw3I1UQo4MeHfoLYF+e6YBa8p5vYUw3Rgt7IDzCs+57vIZqQlIo62NDpYM0VG6BjWOwnzBczMvbtH8hag==} + cpu: [x64] + os: [win32] + + '@vitejs/plugin-react-swc@3.10.2': + resolution: {integrity: sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7.0.0-beta.0 + + '@vitest/browser@3.2.4': + resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==} + peerDependencies: + playwright: '*' + safaridriver: '*' + vitest: 3.2.4 + webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + '@zag-js/accordion@1.17.4': + resolution: {integrity: sha512-WkzoksfxJjuSdq+hIHCINc6hQtoYo5Nf0SfuInBiehRnoJtVjmpqk8VLxhLWhwFD/KMqz0wtWcM0itUGNpOyiw==} + + '@zag-js/anatomy@1.17.4': + resolution: {integrity: sha512-EDc7dD5nnr5T3kujMc+EvWIAACZ45cyeKKiPDUCAsmrOAYxIpD+Efh5lvKum6XLIUyUNnkpEVTazVNOeaoZBtQ==} + + '@zag-js/angle-slider@1.17.4': + resolution: {integrity: sha512-atke7qq2dd2f4Om4T6k9GYi5bvUdBWDuwDIaBC39Kygyrj8IjShlcyv+QETbX0MaghIhbLBJQuvc+7G3eIMF1A==} + + '@zag-js/aria-hidden@1.17.4': + resolution: {integrity: sha512-P7aSTINxBwGbDUxhemto10JsajbE+kIzKrPMOWAbIipfFSwPtaN4XJRg2aQHZFBuHNm1n2x87n2TJBwjAlPiNQ==} + + '@zag-js/auto-resize@1.17.4': + resolution: {integrity: sha512-kCC0cvuxG/yf28P52waRlz7FaliPrOyPXH+XM+GrznLkC8/TpMeR092G9+oHiYauNESTb+yyQzGgKqW6xFd/Rw==} + + '@zag-js/avatar@1.17.4': + resolution: {integrity: sha512-+B4esXErOoiYNmHarg9aZWAhUhx6bzoIp31zCMkb6lNUKCDb8hBpFIWYpkgOrPmMaMka2zSYSvpfx6+4zA1Lcg==} + + '@zag-js/carousel@1.17.4': + resolution: {integrity: sha512-/n6nK5N9d+j3C+Q5GFnkeX4pMzZY/spKKhAbEMk2MPIHcbX50Ozdn+2MIGz0opAWtVwMXPhbl+WFeoNr8jbiSw==} + + '@zag-js/checkbox@1.17.4': + resolution: {integrity: sha512-nHrbGhHHUdtvkaJ4jAeCzAG5ioEm719a815oxji2rM1Ei+tCD0mrHCntIeuFejVCGnvR2wFnNJaWaZlES85Vqw==} + + '@zag-js/clipboard@1.17.4': + resolution: {integrity: sha512-WieXgxRCbBayngNSSMMj2zVcR0QO0cT5cZZuYLSn1eTbglo9J4sAX1QyEvHwbZWVt/rEokj3Gdp/Pme6rAQpwQ==} + + '@zag-js/collapsible@1.17.4': + resolution: {integrity: sha512-2bDQYGYoiHWECQPQNeC8ekCshXoXb1i3yY9U3siSyKxMZdBL4VdW5+0UOQoleperbN9NONeEcHW0H10cPofEIA==} + + '@zag-js/collection@1.17.4': + resolution: {integrity: sha512-N4FUhh6avw146IAUKxMj57clXOoN1XjY45ETWJMfahlmmmnttaCKuiiUj57/XIgmt3Vpg2bYIthcyTxeI+K4QQ==} + + '@zag-js/color-picker@1.17.4': + resolution: {integrity: sha512-Zue+eoBeTyKNiHW8lSN+GMWHWsPdl0yZozuRmtuxpKYnI30SSr6GIs88GCY9Inosxz9RqKx7t7TMxsyJlLiJVA==} + + '@zag-js/color-utils@1.17.4': + resolution: {integrity: sha512-gasEa7yNMRW3dyJPtSVgZkXB5yrDF21XEaT+x8QLzj7WDutXeCOVPpc1GzBD+DupCcb6mTMUbhYdaf52WQxmWA==} + + '@zag-js/combobox@1.17.4': + resolution: {integrity: sha512-E7mDsVEcIVbRUUIzsI8+OfXyTdPCih60/g7SRd5Mu8cLnzOxdC4tmeoIY+42otPr0e1bieVMjUXTEKR7wvQuAA==} + + '@zag-js/core@1.17.4': + resolution: {integrity: sha512-DIL2MXMLBYKR3pnjGGodiEUkY+ST/J81gtIJ32bLYxWWiMeX0SoPIvDZ9tqDHub9Kkd5CF07onXHkdAmB9Djrg==} + + '@zag-js/date-picker@1.17.4': + resolution: {integrity: sha512-yNYLFlNnmBI+9gzHmrGrDsGSeHa8cj6+pWhNutIVAT9pyEmg/6AciFndL5+P9bolKo59qtXLpX8libxZ4wLr2g==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/date-utils@1.17.4': + resolution: {integrity: sha512-kPw7GLnj560NdUpXJ1SeoJkNSIddZBa+Sd2fPlyDwqxB5lptqNeRK9FcascRL12PgI7EeM7/R9MVTkTPGdQNjg==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/dialog@1.17.4': + resolution: {integrity: sha512-UCTcGlAlbTSS2Po5XvOOl7FiLba5+kh0Vltz8NAZUNn4e87LeitQVTW68k/pxa2nnnaKfPN6CsAWYQ21aZOcwA==} + + '@zag-js/dismissable@1.17.4': + resolution: {integrity: sha512-LkFdUz2Ay3D/CsSjQSVjxQwzH6U5rU6cvEcUTOM90RUSozuV2pAK5NnI3JH3jAy1USlpTbjxHL+2bdep2jkAEg==} + + '@zag-js/dom-query@1.17.4': + resolution: {integrity: sha512-1fNDCWkHRZXB4dD2hoiyMy0cSkrB/u4fur3To5sOKteka5e9om1/YdbYxXNLmVfeTiC/SJtWNelXP7c/8uDwOw==} + + '@zag-js/editable@1.17.4': + resolution: {integrity: sha512-qTfvrhbHtfvFZv3l+qAlweOpWyzDwYRQ1xrI+Sc8pCHhml6QiZ1UFUpYbiQWPn7dqdzBEVUIhjzDX4lzjsWGSA==} + + '@zag-js/file-upload@1.17.4': + resolution: {integrity: sha512-onV7jN2l9oXcKAuO/KY0TNcqyaFroQ8JjY+QxOOrZEmhvo48h/Lbi0FwBfk3syNWCRK3ihpRQbKOa1lthupGjg==} + + '@zag-js/file-utils@1.17.4': + resolution: {integrity: sha512-eg+ywy2qJn+rXz7wBsJc0N0H6qmKEMvxaWtsynBZ+XDbyrEec/aHNRDaM+l5xdFjDKb5/R151nEDXgnBAT8miA==} + + '@zag-js/floating-panel@1.17.4': + resolution: {integrity: sha512-YgGP0PybQ0adlW6aOkFaho1tOzSk0rIVhCzsCQmln9mhSYgSCgwMoJIqfsFTLVpKB7TO155okOh5kwelH75Jfw==} + + '@zag-js/focus-trap@1.17.4': + resolution: {integrity: sha512-6exU3DOkyqE2LSRydhgQIho/XhNOvQ35AEbYN81I6yniJPARbkGmDcQaKHZXSL7+tAe0ynX09yfVo4Cskio8Ow==} + + '@zag-js/focus-visible@1.17.4': + resolution: {integrity: sha512-9P1GtsFqbuLcplwK/Y7MdnQz9NipYUjef8PS2/duQzRf3UM99/zu1ZbRqwNIW/Tf5ztvet3+dMBAN5HEyYW0Rw==} + + '@zag-js/highlight-word@1.17.4': + resolution: {integrity: sha512-uBK/5OsopYE5qBjkIoQuqvgd6CTnKpttt4+ODFjPV0NPImgcDuqBT1KlFZZZEPZ58fu1TtNU6hNVKHmZ4EzUnw==} + + '@zag-js/hover-card@1.17.4': + resolution: {integrity: sha512-yOVqj2KUxcMZx6B0LpkMRa1q736eVUXTzQD6Keh4cKxtnCFE+ydYVv70xHL4CLWFqz6+PFRYApgzd05IIbff7w==} + + '@zag-js/i18n-utils@1.17.4': + resolution: {integrity: sha512-HiRKMQGaZUpjqekq1h1UlMqquIBnQYSiGpW9vWCUbKs5hr7z3VIgJtKoxdCsBkno7vBEejl316DIIDh3N2qbeA==} + + '@zag-js/interact-outside@1.17.4': + resolution: {integrity: sha512-jd7/4V7ESS6FJILPWIm5CmXVR+maZ4fQmQUPV56WOURKdl2LZ2bPgfjvEaVI9BTm7qPTML6O55xgB87rS/sXlw==} + + '@zag-js/listbox@1.17.4': + resolution: {integrity: sha512-14OReAbUZNEYjy2eBPqI7FUxts0kTjQS268aukfzLvHcJHAHTcP9ru7XMftZlPbQBofPGr/lSLhIa4NZJF3vrw==} + + '@zag-js/live-region@1.17.4': + resolution: {integrity: sha512-fP2f6C6vEcWydvhYKMYWaVu8tqyiCnKJx8auJ2zL/yZGLz/W3xDdRRqHJCfneilN7m8C6tJhWBBZm5Th22bGmQ==} + + '@zag-js/menu@1.17.4': + resolution: {integrity: sha512-KzpvU/rPiPFDexcD+RmcLhPOII5SPgGSSdidpz3pTBy8yEwnwOSoN0PGHm8WnOD4US2wZOHvOqR+Rov8IbmKWw==} + + '@zag-js/number-input@1.17.4': + resolution: {integrity: sha512-lyrZwr3X1wicL8MThZvu4JH5pwldYO2gKQ+CVgMTx6H2epQNVJJ9i8v/+buUNB9/2ufjUV0MaxQ2fuGTXyjAKw==} + + '@zag-js/pagination@1.17.4': + resolution: {integrity: sha512-yTOcwRdJ0CozEzw0Q+lAUkpWUERFVCCSx9qqIAGqF5jEZSWefUWMQVcPRqupLQ51mhCXdt+wDDh2mTY6Mr+L3A==} + + '@zag-js/password-input@1.17.4': + resolution: {integrity: sha512-h77V18+KBvZHUcARnr+Qw+P5vGvvSC9UMzjnE2SpMIpyvOIr1Fp+4TCGKVEIIsWR0LzWnK79UNExVj1Th3t1TQ==} + + '@zag-js/pin-input@1.17.4': + resolution: {integrity: sha512-k2rhmS0oAPUE93DgdHtV7HkpBvTj3iGvUusVwmifE42ct1VnuuedXHKlicGbJ2ZXWelXmKd5675LHfwmF68h2A==} + + '@zag-js/popover@1.17.4': + resolution: {integrity: sha512-uDRfw5/F3FPeanOJbXnVmk5c+RFFkQozZ6dn3qdnynWn1sLh56Kf5Ys4X+MQInxqUKdtDCb7cO2tfkAZXE5ZOA==} + + '@zag-js/popper@1.17.4': + resolution: {integrity: sha512-ZdlDcaBzDx4XUzicTviaCP0Q6W1AXwRzdPVO2TzosqQyyn/tYqEfcJePYu9XVsr1Y6bkume4Pt0ucuRN+kUeYQ==} + + '@zag-js/presence@1.17.4': + resolution: {integrity: sha512-xFEITSvZjoNYh3Ea+48tFqwwsOtSeEa27c3GOa1ToCTs0J+7SrP19bj5w7Hnbk5cGY/4P5OD8OiMKvkPughjEw==} + + '@zag-js/progress@1.17.4': + resolution: {integrity: sha512-1FWUIizd8OMcK+0uUA/6ly3VJd5eHeOZkXC4lIWDGGwLhfEv2Lm+pgF5Ix5u1mtcmawBbhpkSlYjc1CbsjUTQQ==} + + '@zag-js/qr-code@1.17.4': + resolution: {integrity: sha512-z2FLUlGCLmKcNyXCdeWJkovLo4NvFdRAe43psn0M8rhd470rYCzol1/86s2G72DjqUT0ZwadkfgRjLfaLHkYdQ==} + + '@zag-js/radio-group@1.17.4': + resolution: {integrity: sha512-/u9ugWth+FPu3W1VUTuBIVq3TbJZMLYF8cFPhvTgIjBvbQw9Oe+TW+WywyH1z7Oaz03e4IYhW445sWGoC9TNvw==} + + '@zag-js/rating-group@1.17.4': + resolution: {integrity: sha512-5KQdf+CLX3RzCu7Bj8Xn7AKeDU+sxCjxCcbjs8VviLl6Rj/OaFUoUomZFf/wLsJLY1tqk6PD7dX4NczY7YC2YQ==} + + '@zag-js/react@1.17.4': + resolution: {integrity: sha512-43TEe1Afjh1RR3Byxib/jZ2Wn4UVdZY5Irx5v3tnp8NY8BFeswPhP28e6W2NT4c/UZoWeRxYlXDdrRS2p8L8Wg==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@zag-js/rect-utils@1.17.4': + resolution: {integrity: sha512-DiYNOwtVek9qwtbV906zjNpM8dmJL4sp131rPRgRStTg8MHpfW2PUOaxFklKh9/ykFwPDu6rx7kQ9Y2P4ez/xg==} + + '@zag-js/remove-scroll@1.17.4': + resolution: {integrity: sha512-EY+N1UodKfu2omYknbWfv+33pljfVW5ZX01iuSlTng3Vx5Zn6xlQCTxpVWvDidACEN6jjBn00QFbGWEhDDBpdw==} + + '@zag-js/scroll-snap@1.17.4': + resolution: {integrity: sha512-bdYtDdJjXcupjoTs5n3Z310wEDrsykgWIKVOy5r4daNp+aH99YHBvINt0BUzjfyCpoEH0KvM9KwKlwOhq7XUNA==} + + '@zag-js/select@1.17.4': + resolution: {integrity: sha512-Yy/83xydKl/Qz3BoeNCwu964lLRDqoF4fsOWPeOFEN6HHftLD7NNNO7eIqe2Qe84ZBwAeQeZ8cNNI2oYHFc/ag==} + + '@zag-js/signature-pad@1.17.4': + resolution: {integrity: sha512-nGv9uBNkq+jrLfdN+wuINA+ch0jZs/m1UUDcyUvpRfQa/AlkNdv9oC8p6KUJwNhunTQN6E2RCZqO43q49ioEtg==} + + '@zag-js/slider@1.17.4': + resolution: {integrity: sha512-Iq3pgLmJIvmQXaUm/+Xt1/s1IV1p73E7ySbThdZ8EADDn60m5ESVTwEymK9jnH10hpXuxDvI1GcbWPOTrIxwYQ==} + + '@zag-js/splitter@1.17.4': + resolution: {integrity: sha512-6uThEf+gD0z6Nf6CYvp28I2zjfGW0JOdFAJDpwyqyngvGbO4oPkWPozn8uUmbovQrzhiyUx1C6o5UPDsLgFWhw==} + + '@zag-js/steps@1.17.4': + resolution: {integrity: sha512-MSPtDEkPpQTQ/LTsTRhSeG/P4TCl9b0/nKf/cMT/KlmrK7pTonjkDvux/AQHLxkqZ+tMZYl7qYd/ocdARe1mtA==} + + '@zag-js/store@1.17.4': + resolution: {integrity: sha512-80i4/ggb2OrZ9+l1EJgYcp8uBy5oJwwae/kzy2/r93P+gotct5/qiyZYrybE8+YhU0u5zPiyjTxH0SILfP9Ofg==} + + '@zag-js/switch@1.17.4': + resolution: {integrity: sha512-d5kBKe+q7V87V6K3BcsfJ1jU2qiJvPLjBumUDFkrzU0E5jweVOOwYrqDzLX8X4cBXk9A2R6U8rYdgGwWDctmWQ==} + + '@zag-js/tabs@1.17.4': + resolution: {integrity: sha512-jvchw7erb8ryQTR92QQyP64nmJPJHCeOr6s09ghYqyNIVI5xgVy5hcfgrE4iMXODJ9CSAMsZHqY7QN5Xq10l3Q==} + + '@zag-js/tags-input@1.17.4': + resolution: {integrity: sha512-BYzvgIdqjv2LZSf5tfRECklCEt9u/uyc4gaGOiEseNIzcyQ9xrg9fq2Yk6Wt8mhWujdCbC/zJS2RB3LdcVePng==} + + '@zag-js/time-picker@1.17.4': + resolution: {integrity: sha512-HGMIWqmpo2/cybCLNaPuMfRZx/wjkNAJKm33oZJXqwpc6rxWvh8bpEtpEOp7WDwWifthc/6VBUI5Smc+aO6oVA==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/timer@1.17.4': + resolution: {integrity: sha512-jDUIz4jgZAFqAOra/9Ng3mraMMnh1fTHtUAzFgolzwY6V8l2eAMGX0DrXtoEVqxlh4IGE00xN6Kus9j3NfcUOA==} + + '@zag-js/toast@1.17.4': + resolution: {integrity: sha512-lhu0mhHLpT2DaI9d6BjlE2vJEL9/jFmyPGJ9QG9kkQAxDNtEJLiCJEe12mKs5S9LoxDHJGWGYkF2O/7XwLkDnA==} + + '@zag-js/toggle-group@1.17.4': + resolution: {integrity: sha512-cegFuo8X66MX7b06n6rIJlf4hFDPejmZeq1eSu7co4hVKAfqazBFh6SGsnKdIXhOUo162tFchNuKMkhZU3sWBQ==} + + '@zag-js/toggle@1.17.4': + resolution: {integrity: sha512-cKggg0TaGErAZmYXWGMHH81Gti+AXLMqT29V7EM2qI2tWQzzsmbDbUVoEQ7iZf8Ng6d/JfsZsLq6biZZHg6KsA==} + + '@zag-js/tooltip@1.17.4': + resolution: {integrity: sha512-lDRXZjd7anVb4h2ZvDCYYZ+puJZZwry5xi72jY6xhz3vVWX5qfkYjZ/MHuuDk/S+fEY+luWJXJ+cPh+v1zie0g==} + + '@zag-js/tour@1.17.4': + resolution: {integrity: sha512-RSnzJLTygsMPUXcMuYY0GWTskfwDsSeyM5Jbn5iMUUphnj/3nCtZttbsA22jnXCYE8bK+/+6PnfdcD0Elysf7Q==} + + '@zag-js/tree-view@1.17.4': + resolution: {integrity: sha512-XRc2DxB/gVrkmS7+ZTJBC8p0G1J+mqtFb5zzRxyNitp+VW7yMsRtAUJ7m5gT5bD71zOkk4fPhwuB+ZZtpPAaMQ==} + + '@zag-js/types@1.17.4': + resolution: {integrity: sha512-GHE1ykkMeHuIPHkkU1JNcIWMoFH322Yq65S4dhhsEgqMRX3BUHW8ids5e+7WOu9ZSH3PGJdpUXe8+jg3USpwaw==} + + '@zag-js/utils@1.17.4': + resolution: {integrity: sha512-FXici9HJG1ZBLCmbHO/ed4iurDriDjdx8XOfSD052bu22ViWl5jnO2K77OwagexbXGGAJNhswvDeQg5CSqYbvA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-jest@30.0.4: + resolution: {integrity: sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-istanbul@7.0.0: + resolution: {integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-jest-hoist@30.0.1: + resolution: {integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.5: + resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@30.0.1: + resolution: {integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bits-ui@2.8.10: + resolution: {integrity: sha512-MOobkqapDZNrpcNmeL2g664xFmH4tZBOKBTxFmsQYMZQuybSZHQnPXy+AjM5XZEXRmCFx5+XRmo6+fC3vHh1hQ==} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + bn.js@4.12.2: + resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + bson@6.10.4: + resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==} + engines: {node: '>=16.20.1'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + 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'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001726: + resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} + + case-sensitive-paths-webpack-plugin@2.4.0: + resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} + engines: {node: '>=4'} + + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + ci-info@4.3.0: + resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cjs-module-lexer@2.1.0: + resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + 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==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + config@3.3.12: + resolution: {integrity: sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==} + engines: {node: '>= 10.0.0'} + + connect-mongo@5.1.0: + resolution: {integrity: sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==} + engines: {node: '>=12.9.0'} + peerDependencies: + express-session: ^1.17.1 + mongodb: '>= 5.1.0 < 7' + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + core-js-compat@3.43.0: + resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + dedent@1.6.0: + resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + 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==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.179: + resolution: {integrity: sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + endent@2.1.0: + resolution: {integrity: sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==} + + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + 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-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + 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-toolkit@1.39.7: + resolution: {integrity: sha512-ek/wWryKouBrZIjkwW2BFf91CWOIMvoy2AE5YYgUrfWsJQM2Su1LoLtrw8uusEpN9RfqLlV/0FVNjT0WMv8Bxw==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.5: + resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-storybook@9.0.16: + resolution: {integrity: sha512-A9kJaYBGYswo11t9coo1rpY5i8qPJx9JX5/6YWK3L3zT9lCxJWkYFAed/1Jt92yk7EkOzLrwrIIjMj/+7erlgw==} + engines: {node: '>=20.0.0'} + peerDependencies: + eslint: '>=8' + storybook: ^9.0.16 + + eslint-plugin-svelte@3.10.1: + resolution: {integrity: sha512-csCh2x0ge/DugXC7dCANh46Igi7bjMZEy6rHZCdS13AoGVJSu7a90Kru3I8oMYLGEemPRE1hQXadxvRPVMAAXQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.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@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@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrap@1.2.2: + resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==} + + esrap@1.4.9: + resolution: {integrity: sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==} + + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + 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'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + expect@30.0.4: + resolution: {integrity: sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express-session@1.18.1: + resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==} + engines: {node: '>= 0.8.0'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + 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-parse@1.0.3: + resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-system-cache@2.3.0: + resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + 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'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + 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'} + + fork-ts-checker-webpack-plugin@8.0.0: + resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + + fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + 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==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + 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-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + 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@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + 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-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'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + helmet@7.2.0: + resolution: {integrity: sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==} + engines: {node: '>=16.0.0'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-webpack-plugin@5.6.3: + resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + 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'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + 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-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + 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-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + 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-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-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + 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-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + 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-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + 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-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-changed-files@30.0.2: + resolution: {integrity: sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@30.0.4: + resolution: {integrity: sha512-o6UNVfbXbmzjYgmVPtSQrr5xFZCtkDZGdTlptYvGFSN80RuOOlTe73djvMrs+QAuSERZWcHBNIOMH+OEqvjWuw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-cli@30.0.4: + resolution: {integrity: sha512-3dOrP3zqCWBkjoVG1zjYJpD9143N9GUCbwaF2pFF5brnIgRLHmKcCIw+83BvF1LxggfMWBA0gxkn6RuQVuRhIQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-config@30.0.4: + resolution: {integrity: sha512-3dzbO6sh34thAGEjJIW0fgT0GA0EVlkski6ZzMcbW6dzhenylXAE/Mj2MI4HonroWbkKc6wU6bLVQ8dvBSZ9lA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-diff@30.0.4: + resolution: {integrity: sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@30.0.1: + resolution: {integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@30.0.2: + resolution: {integrity: sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@30.0.4: + resolution: {integrity: sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@30.0.2: + resolution: {integrity: sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@30.0.2: + resolution: {integrity: sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@30.0.4: + resolution: {integrity: sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@30.0.2: + resolution: {integrity: sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@30.0.2: + resolution: {integrity: sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@30.0.4: + resolution: {integrity: sha512-EQBYow19B/hKr4gUTn+l8Z+YLlP2X0IoPyp0UydOtrcPbIOYzJ8LKdFd+yrbwztPQvmlBFUwGPPEzHH1bAvFAw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@30.0.2: + resolution: {integrity: sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@30.0.4: + resolution: {integrity: sha512-mxY0vTAEsowJwvFJo5pVivbCpuu6dgdXRmt3v3MXjBxFly7/lTk3Td0PaMyGOeNQUFmSuGEsGYqhbn7PA9OekQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@30.0.4: + resolution: {integrity: sha512-tUQrZ8+IzoZYIHoPDQEB4jZoPyzBjLjq7sk0KVyd5UPRjRDOsN7o6UlvaGF8ddpGsjznl9PW+KRgWqCNO+Hn7w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@30.0.4: + resolution: {integrity: sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@30.0.2: + resolution: {integrity: sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@30.0.2: + resolution: {integrity: sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@30.0.4: + resolution: {integrity: sha512-YESbdHDs7aQOCSSKffG8jXqOKFqw4q4YqR+wHYpR5GWEQioGvL0BfbcjvKIvPEM0XGfsfJrka7jJz3Cc3gI4VQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@30.0.2: + resolution: {integrity: sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest@30.0.4: + resolution: {integrity: sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + 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-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + konva@9.3.22: + resolution: {integrity: sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==} + + kruptein@3.0.8: + resolution: {integrity: sha512-0CyalFA0Cjp3jnziMp0u1uLZW2/ouhQ0mEMfYlroBXNe86na1RwAuwBcdRAegeWZNMfQy/G5fN47g/Axjtqrfw==} + engines: {node: '>8'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + 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'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-or-similar@1.5.0: + resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + memoizerific@1.11.3: + resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + 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'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + miniflare@4.20250709.0: + resolution: {integrity: sha512-dRGXi6Do9ArQZt7205QGWZ1tD6k6xQNY/mAZBAtiaQYvKxFuNyiHYlFnSN8Co4AFCVOozo/U52sVAaHvlcmnew==} + engines: {node: '>=18.0.0'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.17.0: + resolution: {integrity: sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@8.16.3: + resolution: {integrity: sha512-p2JOsRQG7j0vXhLpsWw5Slm2VnDeJK8sRyqSyegk5jQujuP9BTOZ1Di9VX/0lYfBhZ2DpAExi51QTd4pIqSgig==} + engines: {node: '>=16.20.1'} + + morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@5.0.0: + resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} + engines: {node: '>=14.0.0'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.0: + resolution: {integrity: sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-fetch-native@1.6.6: + resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + 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-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + 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'} + + objectorarray@1.0.5: + resolution: {integrity: sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + 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-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + 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-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + 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==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + 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-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + 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'} + + perfect-freehand@1.2.2: + resolution: {integrity: sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ==} + + 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.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.54.1: + resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} + engines: {node: '>=18'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.4.0: + resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier-plugin-tailwindcss@0.6.14: + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + pretty-format@30.0.2: + resolution: {integrity: sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + + proxy-memoize@3.0.1: + resolution: {integrity: sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + ramda@0.29.0: + resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@7.1.1: + resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} + engines: {node: '>=16.14.0'} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.44.2: + resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + runed@0.29.2: + resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} + peerDependencies: + svelte: ^5.7.0 + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + 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==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + 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'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn-svelte@1.0.6: + resolution: {integrity: sha512-9hQR84HkIK7pPkrbfs8ttYgoRip+NGrM9ojDXgMKft8PqhofqSi4VMmzyKZ91xzvwakFyRZKGXylKvfpITRtTA==} + hasBin: true + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + 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'} + + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + + store2@2.14.4: + resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} + + storybook@9.0.16: + resolution: {integrity: sha512-DzjzeggdzlXKKBK1L9iqNKqqNpyfeaL1hxxeAOmqgeMezwy5d5mCJmjNcZEmx+prsRmvj1OWm4ZZAg6iP/wABg==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + 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'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + + style-loader@3.3.4: + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@10.0.0: + resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-ast-print@0.4.2: + resolution: {integrity: sha512-hRHHufbJoArFmDYQKCpCvc0xUuIEfwYksvyLYEQyH+1xb5LD5sM/IthfooCdXZQtOIqXz6xm7NmaqdfwG4kh6w==} + engines: {node: '>=18'} + peerDependencies: + svelte: ^5.0.0 + + svelte-check@4.2.2: + resolution: {integrity: sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.2.0: + resolution: {integrity: sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-toolbelt@0.9.3: + resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte2tsx@0.7.40: + resolution: {integrity: sha512-Fgqe2lzC9DWT/kQTIXqN39O2ot9rUqzUu9dqpbuI6EsaEJ6+RSXVmXnxcNYMlKb2LRPDoIg9TVzXYWwi0zhCmQ==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@5.35.2: + resolution: {integrity: sha512-uW/rRXYrhZ7Dh4UQNZ0t+oVGL1dEM+95GavCO8afAk1IY2cPq9BcZv9C3um5aLIya2y8lIeLPxLII9ASGg9Dzw==} + engines: {node: '>=18'} + + synchronous-promise@2.0.17: + resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} + + synckit@0.11.8: + resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} + engines: {node: ^14.18.0 || >=16.0.0} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwind-merge@3.0.2: + resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwind-variants@1.0.0: + resolution: {integrity: sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwindcss: '*' + + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + telejson@7.2.0: + resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + 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.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + 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@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + 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.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-jest@29.4.0: + resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + tw-animate-css@1.3.5: + resolution: {integrity: sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typescript-eslint@8.35.1: + resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + unenv@2.0.0-rc.17: + resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + unrs-resolver@1.11.0: + resolution: {integrity: sha512-uw3hCGO/RdAEAb4zgJ3C/v6KIAFFOtBoxR86b2Ejc5TnH7HrhTWJR2o0A9ullC3eWMegKQCw/arQ/JivywQzkg==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uqr@0.1.2: + resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@7.0.2: + resolution: {integrity: sha512-hxdyZDY1CM6SNpKI4w4lcUc3Mtkd9ej4ECWVHSMrOdSinVc2zYOAppHeGc/hzmRo3pxM5blMzkuWHOJA/3NiFw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@7.0.4: + resolution: {integrity: sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + 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.2.4 + '@vitest/ui': 3.2.4 + 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 + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-dev-middleware@6.1.3: + resolution: {integrity: sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + + webpack-hot-middleware@2.26.1: + resolution: {integrity: sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==} + + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + webpack@5.99.9: + resolution: {integrity: sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + 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'} + + workerd@1.20250709.0: + resolution: {integrity: sha512-BqLPpmvRN+TYUSG61OkWamsGdEuMwgvabP8m0QOHIfofnrD2YVyWqE1kXJ0GH5EsVEuWamE5sR8XpTfsGBmIpg==} + engines: {node: '>=16'} + hasBin: true + + worktop@0.8.0-next.18: + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} + engines: {node: '>=12'} + + wrangler@4.24.3: + resolution: {integrity: sha512-stB1Wfs5NKlspsAzz8SBujBKsDqT5lpCyrL+vSUMy3uueEtI1A5qyORbKoJhIguEbwHfWS39mBsxzm6Vm1J2cg==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20250709.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + 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'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + + zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + +snapshots: + + '@adobe/css-tools@4.4.3': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@ark-ui/react@5.15.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@internationalized/date': 3.8.2 + '@zag-js/accordion': 1.17.4 + '@zag-js/anatomy': 1.17.4 + '@zag-js/angle-slider': 1.17.4 + '@zag-js/auto-resize': 1.17.4 + '@zag-js/avatar': 1.17.4 + '@zag-js/carousel': 1.17.4 + '@zag-js/checkbox': 1.17.4 + '@zag-js/clipboard': 1.17.4 + '@zag-js/collapsible': 1.17.4 + '@zag-js/collection': 1.17.4 + '@zag-js/color-picker': 1.17.4 + '@zag-js/color-utils': 1.17.4 + '@zag-js/combobox': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/date-picker': 1.17.4(@internationalized/date@3.8.2) + '@zag-js/date-utils': 1.17.4(@internationalized/date@3.8.2) + '@zag-js/dialog': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/editable': 1.17.4 + '@zag-js/file-upload': 1.17.4 + '@zag-js/file-utils': 1.17.4 + '@zag-js/floating-panel': 1.17.4 + '@zag-js/focus-trap': 1.17.4 + '@zag-js/highlight-word': 1.17.4 + '@zag-js/hover-card': 1.17.4 + '@zag-js/i18n-utils': 1.17.4 + '@zag-js/listbox': 1.17.4 + '@zag-js/menu': 1.17.4 + '@zag-js/number-input': 1.17.4 + '@zag-js/pagination': 1.17.4 + '@zag-js/password-input': 1.17.4 + '@zag-js/pin-input': 1.17.4 + '@zag-js/popover': 1.17.4 + '@zag-js/presence': 1.17.4 + '@zag-js/progress': 1.17.4 + '@zag-js/qr-code': 1.17.4 + '@zag-js/radio-group': 1.17.4 + '@zag-js/rating-group': 1.17.4 + '@zag-js/react': 1.17.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@zag-js/select': 1.17.4 + '@zag-js/signature-pad': 1.17.4 + '@zag-js/slider': 1.17.4 + '@zag-js/splitter': 1.17.4 + '@zag-js/steps': 1.17.4 + '@zag-js/switch': 1.17.4 + '@zag-js/tabs': 1.17.4 + '@zag-js/tags-input': 1.17.4 + '@zag-js/time-picker': 1.17.4(@internationalized/date@3.8.2) + '@zag-js/timer': 1.17.4 + '@zag-js/toast': 1.17.4 + '@zag-js/toggle': 1.17.4 + '@zag-js/toggle-group': 1.17.4 + '@zag-js/tooltip': 1.17.4 + '@zag-js/tour': 1.17.4 + '@zag-js/tree-view': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1 + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.0 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regenerator@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/preset-env@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-regenerator': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.0) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) + core-js-compat: 3.43.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.0 + esutils: 2.0.3 + + '@babel/preset-react@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@bcoe/v8-coverage@0.2.3': {} + + '@chakra-ui/react@3.21.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@ark-ui/react': 5.15.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@19.1.8)(react@19.1.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/utils': 1.4.2 + '@pandacss/is-valid-prop': 0.54.0 + csstype: 3.1.3 + fast-safe-stringify: 2.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@chakra-ui/storybook-addon@5.2.5(@chakra-ui/react@3.21.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@storybook/components@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)))(@storybook/manager-api@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)))(@storybook/preview-api@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)))(@storybook/types@7.6.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@chakra-ui/react': 3.21.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/components': 8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/manager-api': 8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/preview-api': 8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/types': 7.6.17 + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@cloudflare/kv-asset-handler@0.4.0': + dependencies: + mime: 3.0.0 + + '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250709.0)': + dependencies: + unenv: 2.0.0-rc.17 + optionalDependencies: + workerd: 1.20250709.0 + + '@cloudflare/workerd-darwin-64@1.20250709.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20250709.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20250709.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20250709.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20250709.0': + optional: true + + '@cloudflare/workers-types@4.20250711.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.27.6 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.3.1': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.8 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': + dependencies: + eslint: 9.30.1(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/compat@1.3.1(eslint@9.30.1(jiti@2.4.2))': + optionalDependencies: + eslint: 9.30.1(jiti@2.4.2) + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@eslint/js@9.30.1': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.3': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@fastify/busboy@2.1.1': {} + + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.1': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@internationalized/date@3.8.2': + dependencies: + '@swc/helpers': 0.5.17 + + '@internationalized/number@3.6.3': + dependencies: + '@swc/helpers': 0.5.17 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/console@30.0.4': + dependencies: + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + chalk: 4.1.2 + jest-message-util: 30.0.2 + jest-util: 30.0.2 + slash: 3.0.0 + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/core@30.0.4(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5))': + dependencies: + '@jest/console': 30.0.4 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.0.4 + '@jest/test-result': 30.0.4 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.0.2 + jest-config: 30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + jest-haste-map: 30.0.2 + jest-message-util: 30.0.2 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.2 + jest-resolve-dependencies: 30.0.4 + jest-runner: 30.0.4 + jest-runtime: 30.0.4 + jest-snapshot: 30.0.4 + jest-util: 30.0.2 + jest-validate: 30.0.2 + jest-watcher: 30.0.4 + micromatch: 4.0.8 + pretty-format: 30.0.2 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.0.1': {} + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + jest-mock: 29.7.0 + + '@jest/environment@30.0.4': + dependencies: + '@jest/fake-timers': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + jest-mock: 30.0.2 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect-utils@30.0.4': + dependencies: + '@jest/get-type': 30.0.1 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/expect@30.0.4': + dependencies: + expect: 30.0.4 + jest-snapshot: 30.0.4 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 24.0.10 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/fake-timers@30.0.4': + dependencies: + '@jest/types': 30.0.1 + '@sinonjs/fake-timers': 13.0.5 + '@types/node': 24.0.10 + jest-message-util: 30.0.2 + jest-mock: 30.0.2 + jest-util: 30.0.2 + + '@jest/get-type@30.0.1': {} + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/globals@30.0.4': + dependencies: + '@jest/environment': 30.0.4 + '@jest/expect': 30.0.4 + '@jest/types': 30.0.1 + jest-mock: 30.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 24.0.10 + jest-regex-util: 30.0.1 + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.29 + '@types/node': 24.0.10 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@30.0.4': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.0.4 + '@jest/test-result': 30.0.4 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + '@jridgewell/trace-mapping': 0.3.29 + '@types/node': 24.0.10 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit-x: 0.2.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + jest-message-util: 30.0.2 + jest-util: 30.0.2 + jest-worker: 30.0.2 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/schemas@30.0.1': + dependencies: + '@sinclair/typebox': 0.34.37 + + '@jest/snapshot-utils@30.0.4': + dependencies: + '@jest/types': 30.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-result@30.0.4': + dependencies: + '@jest/console': 30.0.4 + '@jest/types': 30.0.1 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/test-sequencer@30.0.4': + dependencies: + '@jest/test-result': 30.0.4 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.2 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.28.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.29 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/transform@30.0.4': + dependencies: + '@babel/core': 7.28.0 + '@jest/types': 30.0.1 + '@jridgewell/trace-mapping': 0.3.29 + babel-plugin-istanbul: 7.0.0 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.2 + jest-regex-util: 30.0.1 + jest-util: 30.0.2 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.0.10 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jest/types@30.0.1': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.1 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.0.10 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.10': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@lucide/svelte@0.515.0(svelte@5.35.2)': + dependencies: + svelte: 5.35.2 + + '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.1.8 + react: 19.1.0 + + '@mongodb-js/saslprep@1.3.0': + dependencies: + sparse-bitfield: 3.0.3 + + '@napi-rs/wasm-runtime@0.2.11': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@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.19.1 + + '@pandacss/is-valid-prop@0.54.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.7': {} + + '@polka/url@1.0.0-next.29': {} + + '@poppinss/colors@4.1.5': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.4': + dependencies: + '@poppinss/colors': 4.1.5 + '@sindresorhus/is': 7.0.2 + supports-color: 10.0.0 + + '@poppinss/exception@1.2.2': {} + + '@rolldown/pluginutils@1.0.0-beta.11': {} + + '@rollup/rollup-android-arm-eabi@4.44.2': + optional: true + + '@rollup/rollup-android-arm64@4.44.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.44.2': + optional: true + + '@rollup/rollup-darwin-x64@4.44.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.44.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.44.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.44.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.44.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.44.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.44.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.44.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.44.2': + optional: true + + '@sinclair/typebox@0.27.8': {} + + '@sinclair/typebox@0.34.37': {} + + '@sindresorhus/is@7.0.2': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@13.0.5': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@speed-highlight/core@1.2.7': {} + + '@storybook/addon-docs@9.0.16(@types/react@19.1.8)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) + '@storybook/csf-plugin': 9.0.16(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/icons': 1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/react-dom-shim': 9.0.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/addon-links@9.0.16(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + optionalDependencies: + react: 19.1.0 + + '@storybook/addon-svelte-csf@5.0.6(@storybook/svelte@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2))(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(babel-plugin-macros@3.1.0)(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@storybook/csf': 0.1.13 + '@storybook/svelte': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2) + '@sveltejs/vite-plugin-svelte': 6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + dedent: 1.6.0(babel-plugin-macros@3.1.0) + es-toolkit: 1.39.7 + esrap: 1.4.9 + magic-string: 0.30.17 + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + svelte: 5.35.2 + svelte-ast-print: 0.4.2(svelte@5.35.2) + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + zimmerframe: 1.1.2 + transitivePeerDependencies: + - babel-plugin-macros + + '@storybook/addons@7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@storybook/manager-api': 7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/preview-api': 7.6.17 + '@storybook/types': 7.6.17 + transitivePeerDependencies: + - react + - react-dom + + '@storybook/api@7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@storybook/client-logger': 7.6.17 + '@storybook/manager-api': 7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/builder-vite@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@storybook/csf-plugin': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2)) + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + ts-dedent: 2.2.0 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + '@storybook/builder-webpack5@9.0.16(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3)': + dependencies: + '@storybook/core-webpack': 9.0.16(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + case-sensitive-paths-webpack-plugin: 2.4.0 + cjs-module-lexer: 1.4.3 + css-loader: 6.11.0(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + es-module-lexer: 1.7.0 + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + html-webpack-plugin: 5.6.3(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + magic-string: 0.30.17 + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + style-loader: 3.3.4(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + terser-webpack-plugin: 5.3.14(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + ts-dedent: 2.2.0 + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + webpack-dev-middleware: 6.1.3(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + webpack-hot-middleware: 2.26.1 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@rspack/core' + - '@swc/core' + - esbuild + - uglify-js + - webpack-cli + + '@storybook/channels@7.6.17': + dependencies: + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/global': 5.0.0 + qs: 6.14.0 + telejson: 7.2.0 + tiny-invariant: 1.3.3 + + '@storybook/client-logger@7.6.17': + dependencies: + '@storybook/global': 5.0.0 + + '@storybook/components@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + + '@storybook/core-events@7.6.17': + dependencies: + ts-dedent: 2.2.0 + + '@storybook/core-webpack@9.0.16(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + ts-dedent: 2.2.0 + + '@storybook/csf-plugin@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))': + dependencies: + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + unplugin: 1.16.1 + + '@storybook/csf-plugin@9.0.16(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + unplugin: 1.16.1 + + '@storybook/csf@0.1.13': + dependencies: + type-fest: 2.19.0 + + '@storybook/global@5.0.0': {} + + '@storybook/icons@1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@storybook/manager-api@7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/csf': 0.1.13 + '@storybook/global': 5.0.0 + '@storybook/router': 7.6.17 + '@storybook/theming': 7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@storybook/types': 7.6.17 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + store2: 2.14.4 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + + '@storybook/manager-api@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + + '@storybook/preset-react-webpack@9.0.16(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3)': + dependencies: + '@storybook/core-webpack': 9.0.16(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + '@types/semver': 7.7.0 + find-up: 7.0.0 + magic-string: 0.30.17 + react: 19.1.0 + react-docgen: 7.1.1 + react-dom: 19.1.0(react@19.1.0) + resolve: 1.22.10 + semver: 7.7.2 + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + tsconfig-paths: 4.2.0 + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@storybook/preview-api@7.6.17': + dependencies: + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/csf': 0.1.13 + '@storybook/global': 5.0.0 + '@storybook/types': 7.6.17 + '@types/qs': 6.14.0 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + qs: 6.14.0 + synchronous-promise: 2.0.17 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + + '@storybook/preview-api@8.6.14(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5))': + dependencies: + debug: 4.4.1 + endent: 2.1.0 + find-cache-dir: 3.3.2 + flat-cache: 3.2.0 + micromatch: 4.0.8 + react-docgen-typescript: 2.4.0(typescript@5.8.3) + tslib: 2.8.1 + typescript: 5.8.3 + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + transitivePeerDependencies: + - supports-color + + '@storybook/react-dom-shim@9.0.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))': + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + + '@storybook/react-webpack5@9.0.16(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3)': + dependencies: + '@storybook/builder-webpack5': 9.0.16(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3) + '@storybook/preset-react-webpack': 9.0.16(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3) + '@storybook/react': 9.0.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@rspack/core' + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@storybook/react@9.0.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2))(typescript@5.8.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.0.16(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + storybook: 9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2) + optionalDependencies: + typescript: 5.8.3 + + '@storybook/router@7.6.17': + dependencies: + '@storybook/client-logger': 7.6.17 + memoizerific: 1.11.3 + qs: 6.14.0 + + '@storybook/svelte-vite@9.0.16(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@storybook/builder-vite': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@storybook/svelte': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2) + '@sveltejs/vite-plugin-svelte': 6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + magic-string: 0.30.17 + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + svelte: 5.35.2 + svelte2tsx: 0.7.40(svelte@5.35.2)(typescript@5.8.3) + typescript: 5.8.3 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + '@storybook/svelte@9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)': + dependencies: + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + svelte: 5.35.2 + ts-dedent: 2.2.0 + type-fest: 2.19.0 + + '@storybook/sveltekit@9.0.16(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@storybook/builder-vite': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@storybook/svelte': 9.0.16(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2) + '@storybook/svelte-vite': 9.0.16(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + svelte: 5.35.2 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + transitivePeerDependencies: + - '@sveltejs/vite-plugin-svelte' + + '@storybook/testing-library@0.2.2': + dependencies: + '@testing-library/dom': 9.3.4 + '@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4) + ts-dedent: 2.2.0 + + '@storybook/theming@7.6.17(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.0) + '@storybook/client-logger': 7.6.17 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@storybook/types@7.6.17': + dependencies: + '@storybook/channels': 7.6.17 + '@types/babel__core': 7.20.5 + '@types/express': 4.17.23 + file-system-cache: 2.3.0 + + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-cloudflare@7.0.5(@sveltejs/kit@2.22.5(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(wrangler@4.24.3(@cloudflare/workers-types@4.20250711.0))': + dependencies: + '@cloudflare/workers-types': 4.20250711.0 + '@sveltejs/kit': 2.22.5(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + worktop: 0.8.0-next.18 + wrangler: 4.24.3(@cloudflare/workers-types@4.20250711.0) + + '@sveltejs/kit@2.22.5(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.1.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.17 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.1 + sirv: 3.0.1 + svelte: 5.35.2 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.1.0(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + debug: 4.4.1 + svelte: 5.35.2 + vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + debug: 4.4.1 + svelte: 5.35.2 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + debug: 4.4.1 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.17 + svelte: 5.35.2 + vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + vitefu: 1.1.1(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.0.0(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)))(svelte@5.35.2)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + debug: 4.4.1 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.17 + svelte: 5.35.2 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + vitefu: 1.1.1(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + transitivePeerDependencies: + - supports-color + + '@swc/core-darwin-arm64@1.12.9': + optional: true + + '@swc/core-darwin-x64@1.12.9': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.12.9': + optional: true + + '@swc/core-linux-arm64-gnu@1.12.9': + optional: true + + '@swc/core-linux-arm64-musl@1.12.9': + optional: true + + '@swc/core-linux-x64-gnu@1.12.9': + optional: true + + '@swc/core-linux-x64-musl@1.12.9': + optional: true + + '@swc/core-win32-arm64-msvc@1.12.9': + optional: true + + '@swc/core-win32-ia32-msvc@1.12.9': + optional: true + + '@swc/core-win32-x64-msvc@1.12.9': + optional: true + + '@swc/core@1.12.9(@swc/helpers@0.5.17)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.23 + optionalDependencies: + '@swc/core-darwin-arm64': 1.12.9 + '@swc/core-darwin-x64': 1.12.9 + '@swc/core-linux-arm-gnueabihf': 1.12.9 + '@swc/core-linux-arm64-gnu': 1.12.9 + '@swc/core-linux-arm64-musl': 1.12.9 + '@swc/core-linux-x64-gnu': 1.12.9 + '@swc/core-linux-x64-musl': 1.12.9 + '@swc/core-win32-arm64-msvc': 1.12.9 + '@swc/core-win32-ia32-msvc': 1.12.9 + '@swc/core-win32-x64-msvc': 1.12.9 + '@swc/helpers': 0.5.17 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@swc/types@0.1.23': + dependencies: + '@swc/counter': 0.1.3 + + '@tailwindcss/forms@0.5.10(tailwindcss@4.1.11)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.1.11 + + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.2 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.11)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.11 + + '@tailwindcss/vite@4.1.11(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + tailwindcss: 4.1.11 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.27.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.27.6 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)': + dependencies: + '@testing-library/dom': 9.3.4 + + '@tsconfig/svelte@5.0.4': {} + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/async@3.2.24': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.0 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.7 + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/config@3.3.5': {} + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.7 + + '@types/cookie-parser@1.4.9(@types/express@4.17.23)': + dependencies: + '@types/express': 4.17.23 + + '@types/cookie@0.6.0': {} + + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.7 + + '@types/deep-eql@4.0.2': {} + + '@types/doctrine@0.0.9': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 20.19.7 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express-session@1.18.2': + dependencies: + '@types/express': 4.17.23 + + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.8 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 24.0.10 + + '@types/html-minifier-terser@6.1.0': {} + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/jest@30.0.0': + dependencies: + expect: 30.0.4 + pretty-format: 30.0.2 + + '@types/json-schema@7.0.15': {} + + '@types/mdx@2.0.13': {} + + '@types/mime@1.3.5': {} + + '@types/morgan@1.9.10': + dependencies: + '@types/node': 20.19.7 + + '@types/node@20.19.7': + dependencies: + undici-types: 6.21.0 + + '@types/node@22.16.3': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.0.10': + dependencies: + undici-types: 7.8.0 + + '@types/parse-json@4.0.2': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.1.6(@types/react@19.1.8)': + dependencies: + '@types/react': 19.1.8 + + '@types/react@19.1.8': + dependencies: + csstype: 3.1.3 + + '@types/resolve@1.20.6': {} + + '@types/semver@7.7.0': {} + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.7 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.7 + '@types/send': 0.17.5 + + '@types/stack-utils@2.0.3': {} + + '@types/uuid@10.0.0': {} + + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@11.0.5': + dependencies: + '@types/webidl-conversions': 7.0.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.7 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.1 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + eslint: 9.30.1(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.1 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/scope-manager@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + debug: 4.4.1 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/types@8.35.1': {} + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.1 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.0 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) + eslint: 8.57.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.0': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.0': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.0': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.0': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.0': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.0': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.0': + dependencies: + '@napi-rs/wasm-runtime': 0.2.11 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.0': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.0': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.0': + optional: true + + '@vitejs/plugin-react-swc@3.10.2(@swc/helpers@0.5.17)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.11 + '@swc/core': 1.12.9(@swc/helpers@0.5.17) + vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + transitivePeerDependencies: + - '@swc/helpers' + + '@vitest/browser@3.2.4(playwright@1.54.1)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))(vitest@3.2.4)': + dependencies: + '@testing-library/dom': 10.4.0 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/mocker': 3.2.4(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@vitest/utils': 3.2.4 + magic-string: 0.30.17 + sirv: 3.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.16.3)(@vitest/browser@3.2.4)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + ws: 8.18.3 + optionalDependencies: + playwright: 1.54.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@zag-js/accordion@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/anatomy@1.17.4': {} + + '@zag-js/angle-slider@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/rect-utils': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/aria-hidden@1.17.4': {} + + '@zag-js/auto-resize@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + + '@zag-js/avatar@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/carousel@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/scroll-snap': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/checkbox@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-visible': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/clipboard@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/collapsible@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/collection@1.17.4': + dependencies: + '@zag-js/utils': 1.17.4 + + '@zag-js/color-picker@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/color-utils': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/color-utils@1.17.4': + dependencies: + '@zag-js/utils': 1.17.4 + + '@zag-js/combobox@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/aria-hidden': 1.17.4 + '@zag-js/collection': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/core@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/date-picker@1.17.4(@internationalized/date@3.8.2)': + dependencies: + '@internationalized/date': 3.8.2 + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/date-utils': 1.17.4(@internationalized/date@3.8.2) + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/live-region': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/date-utils@1.17.4(@internationalized/date@3.8.2)': + dependencies: + '@internationalized/date': 3.8.2 + + '@zag-js/dialog@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/aria-hidden': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-trap': 1.17.4 + '@zag-js/remove-scroll': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/dismissable@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + '@zag-js/interact-outside': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/dom-query@1.17.4': + dependencies: + '@zag-js/types': 1.17.4 + + '@zag-js/editable@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/interact-outside': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/file-upload@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/file-utils': 1.17.4 + '@zag-js/i18n-utils': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/file-utils@1.17.4': + dependencies: + '@zag-js/i18n-utils': 1.17.4 + + '@zag-js/floating-panel@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/rect-utils': 1.17.4 + '@zag-js/store': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/focus-trap@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + + '@zag-js/focus-visible@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + + '@zag-js/highlight-word@1.17.4': {} + + '@zag-js/hover-card@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/i18n-utils@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + + '@zag-js/interact-outside@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/listbox@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/collection': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-visible': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/live-region@1.17.4': {} + + '@zag-js/menu@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/rect-utils': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/number-input@1.17.4': + dependencies: + '@internationalized/number': 3.6.3 + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/pagination@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/password-input@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/pin-input@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/popover@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/aria-hidden': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-trap': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/remove-scroll': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/popper@1.17.4': + dependencies: + '@floating-ui/dom': 1.7.1 + '@zag-js/dom-query': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/presence@1.17.4': + dependencies: + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + + '@zag-js/progress@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/qr-code@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + proxy-memoize: 3.0.1 + uqr: 0.1.2 + + '@zag-js/radio-group@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-visible': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/rating-group@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/react@1.17.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@zag-js/core': 1.17.4 + '@zag-js/store': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@zag-js/rect-utils@1.17.4': {} + + '@zag-js/remove-scroll@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + + '@zag-js/scroll-snap@1.17.4': + dependencies: + '@zag-js/dom-query': 1.17.4 + + '@zag-js/select@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/collection': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/signature-pad@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + perfect-freehand: 1.2.2 + + '@zag-js/slider@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/splitter@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/steps@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/store@1.17.4': + dependencies: + proxy-compare: 3.0.1 + + '@zag-js/switch@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-visible': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/tabs@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/tags-input@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/auto-resize': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/interact-outside': 1.17.4 + '@zag-js/live-region': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/time-picker@1.17.4(@internationalized/date@3.8.2)': + dependencies: + '@internationalized/date': 3.8.2 + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/timer@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/toast@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/toggle-group@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/toggle@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/tooltip@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-visible': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/store': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/tour@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dismissable': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/focus-trap': 1.17.4 + '@zag-js/interact-outside': 1.17.4 + '@zag-js/popper': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/tree-view@1.17.4': + dependencies: + '@zag-js/anatomy': 1.17.4 + '@zag-js/collection': 1.17.4 + '@zag-js/core': 1.17.4 + '@zag-js/dom-query': 1.17.4 + '@zag-js/types': 1.17.4 + '@zag-js/utils': 1.17.4 + + '@zag-js/types@1.17.4': + dependencies: + csstype: 3.1.3 + + '@zag-js/utils@1.17.4': {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.2: {} + + acorn@8.14.0: {} + + acorn@8.15.0: {} + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + 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 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-html-community@0.0.8: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.2 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async@3.2.6: {} + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001726 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axobject-query@4.1.0: {} + + babel-jest@29.7.0(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.28.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-jest@30.0.4(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@jest/transform': 30.0.4 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.0 + babel-preset-jest: 30.0.1(@babel/core@7.28.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.0: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + + babel-plugin-jest-hoist@30.0.1: + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + '@types/babel__core': 7.20.5 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.27.6 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0): + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.28.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + core-js-compat: 3.43.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.0) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.0) + + babel-preset-jest@29.6.3(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0) + + babel-preset-jest@30.0.1(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + babel-plugin-jest-hoist: 30.0.1 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0) + + balanced-match@1.0.2: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + + binary-extensions@2.3.0: {} + + bits-ui@2.8.10(@internationalized/date@3.8.2)(svelte@5.35.2): + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/dom': 1.7.1 + '@internationalized/date': 3.8.2 + esm-env: 1.2.2 + runed: 0.29.2(svelte@5.35.2) + svelte: 5.35.2 + svelte-toolbelt: 0.9.3(svelte@5.35.2) + tabbable: 6.2.0 + + blake3-wasm@2.1.5: {} + + bn.js@4.12.2: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.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 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001726 + electron-to-chromium: 1.5.179 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + bson@6.10.4: {} + + buffer-from@1.1.2: {} + + bundle-require@5.1.0(esbuild@0.25.5): + dependencies: + esbuild: 0.25.5 + load-tsconfig: 0.2.5 + + bytes@3.1.2: {} + + 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: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001726: {} + + case-sensitive-paths-webpack-plugin@2.4.0: {} + + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + check-error@2.1.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + chrome-trace-event@1.0.4: {} + + ci-info@3.9.0: {} + + ci-info@4.3.0: {} + + cjs-module-lexer@1.4.3: {} + + cjs-module-lexer@2.1.0: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colorette@2.0.20: {} + + commander@14.0.0: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@8.3.0: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + config@3.3.12: + dependencies: + json5: 2.2.3 + + connect-mongo@5.1.0(express-session@1.18.1)(mongodb@6.17.0): + dependencies: + debug: 4.4.1 + express-session: 1.18.1 + kruptein: 3.0.8 + mongodb: 6.17.0 + transitivePeerDependencies: + - supports-color + + consola@3.4.2: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie@0.6.0: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + cookie@1.0.2: {} + + core-js-compat@3.43.0: + dependencies: + browserslist: 4.25.1 + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + create-jest@29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-loader@6.11.0(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + semver: 7.7.2 + optionalDependencies: + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + css.escape@1.5.1: {} + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + dedent-js@1.0.1: {} + + dedent@0.7.0: {} + + dedent@1.6.0(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 + + deep-eql@5.0.2: {} + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + defu@6.1.4: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.0.4: {} + + detect-newline@3.1.0: {} + + devalue@5.1.1: {} + + diff-sequences@29.6.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + 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: {} + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.179: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + endent@2.1.0: + dependencies: + dedent: 0.7.0 + fast-json-parse: 1.0.3 + objectorarray: 1.0.5 + + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + entities@2.2.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-toolkit@1.39.7: {} + + esbuild-register@3.6.0(esbuild@0.25.5): + dependencies: + debug: 4.4.1 + esbuild: 0.25.5 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + + eslint-plugin-react-hooks@5.2.0(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + + eslint-plugin-react-refresh@0.4.20(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + + eslint-plugin-storybook@9.0.16(eslint@9.30.1(jiti@2.4.2))(storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + storybook: 9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-svelte@3.10.1(eslint@9.30.1(jiti@2.4.2))(svelte@5.35.2): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@jridgewell/sourcemap-codec': 1.5.4 + eslint: 9.30.1(jiti@2.4.2) + esutils: 2.0.3 + globals: 16.3.0 + known-css-properties: 0.37.0 + postcss: 8.5.6 + postcss-load-config: 3.1.4(postcss@8.5.6) + postcss-safe-parser: 7.0.1(postcss@8.5.6) + semver: 7.7.2 + svelte-eslint-parser: 1.2.0(svelte@5.35.2) + optionalDependencies: + svelte: 5.35.2 + transitivePeerDependencies: + - ts-node + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + eslint@9.30.1(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.30.1 + '@eslint/plugin-kit': 0.3.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.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.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrap@1.2.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@types/estree': 1.0.8 + + esrap@1.4.9: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + esrap@2.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-hook@2.2.1: {} + + exit-x@0.2.2: {} + + exit@0.1.2: {} + + expect-type@1.2.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + expect@30.0.4: + dependencies: + '@jest/expect-utils': 30.0.4 + '@jest/get-type': 30.0.1 + jest-matcher-utils: 30.0.4 + jest-message-util: 30.0.2 + jest-mock: 30.0.2 + jest-util: 30.0.2 + + express-session@1.18.1: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.7: {} + + 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-parse@1.0.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-system-cache@2.3.0: + dependencies: + fs-extra: 11.1.1 + ramda: 0.29.0 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-root@1.1.0: {} + + 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 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.17 + mlly: 1.7.4 + rollup: 4.44.2 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + 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 + + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 7.1.0 + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.2 + tapable: 2.2.2 + typescript: 5.8.3 + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fresh@0.5.2: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.1.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-monkey@1.0.6: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + 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-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-tsconfig@4.10.1: + 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-to-regexp@0.4.1: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@16.3.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 + + globrex@0.1.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + 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-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 + + he@1.2.0: {} + + helmet@7.2.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.43.1 + + html-webpack-plugin@5.6.3(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.2 + optionalDependencies: + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + human-signals@2.1.0: {} + + husky@9.1.7: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + inline-style-parser@0.2.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + ipaddr.js@1.9.1: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + 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-arrayish@0.3.2: {} + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@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-path-inside@3.0.3: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + 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-stream@2.0.1: {} + + 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-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.0 + '@babel/parser': 7.28.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.0 + '@babel/parser': 7.28.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + 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@4.0.1: + dependencies: + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-changed-files@30.0.2: + dependencies: + execa: 5.1.1 + jest-util: 30.0.2 + p-limit: 3.1.0 + + jest-circus@29.7.0(babel-plugin-macros@3.1.0): + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-circus@30.0.4(babel-plugin-macros@3.1.0): + dependencies: + '@jest/environment': 30.0.4 + '@jest/expect': 30.0.4 + '@jest/test-result': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.6.0(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 30.0.2 + jest-matcher-utils: 30.0.4 + jest-message-util: 30.0.2 + jest-runtime: 30.0.4 + jest-snapshot: 30.0.4 + jest-util: 30.0.2 + p-limit: 3.1.0 + pretty-format: 30.0.2 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-cli@30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)): + dependencies: + '@jest/core': 30.0.4(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + '@jest/test-result': 30.0.4 + '@jest/types': 30.0.1 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + jest-util: 30.0.2 + jest-validate: 30.0.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0): + dependencies: + '@babel/core': 7.28.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.7 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@29.7.0(@types/node@24.0.10)(babel-plugin-macros@3.1.0): + dependencies: + '@babel/core': 7.28.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.0.10 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)): + dependencies: + '@babel/core': 7.28.0 + '@jest/get-type': 30.0.1 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.0.4 + '@jest/types': 30.0.1 + babel-jest: 30.0.4(@babel/core@7.28.0) + chalk: 4.1.2 + ci-info: 4.3.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.0.4(babel-plugin-macros@3.1.0) + jest-docblock: 30.0.1 + jest-environment-node: 30.0.4 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.2 + jest-runner: 30.0.4 + jest-util: 30.0.2 + jest-validate: 30.0.2 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.0.2 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.0.10 + esbuild-register: 3.6.0(esbuild@0.25.5) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-diff@30.0.4: + dependencies: + '@jest/diff-sequences': 30.0.1 + '@jest/get-type': 30.0.1 + chalk: 4.1.2 + pretty-format: 30.0.2 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-docblock@30.0.1: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-each@30.0.2: + dependencies: + '@jest/get-type': 30.0.1 + '@jest/types': 30.0.1 + chalk: 4.1.2 + jest-util: 30.0.2 + pretty-format: 30.0.2 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-environment-node@30.0.4: + dependencies: + '@jest/environment': 30.0.4 + '@jest/fake-timers': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + jest-mock: 30.0.2 + jest-util: 30.0.2 + jest-validate: 30.0.2 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 24.0.10 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-haste-map@30.0.2: + dependencies: + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.0.2 + jest-worker: 30.0.2 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-leak-detector@30.0.2: + dependencies: + '@jest/get-type': 30.0.1 + pretty-format: 30.0.2 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@30.0.4: + dependencies: + '@jest/get-type': 30.0.1 + chalk: 4.1.2 + jest-diff: 30.0.4 + pretty-format: 30.0.2 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-message-util@30.0.2: + dependencies: + '@babel/code-frame': 7.27.1 + '@jest/types': 30.0.1 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.0.2 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + jest-util: 29.7.0 + + jest-mock@30.0.2: + dependencies: + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + jest-util: 30.0.2 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.0.2): + optionalDependencies: + jest-resolve: 30.0.2 + + jest-regex-util@29.6.3: {} + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve-dependencies@30.0.4: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.0.4 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.10 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-resolve@30.0.2: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.2 + jest-pnp-resolver: 1.2.3(jest-resolve@30.0.2) + jest-util: 30.0.2 + jest-validate: 30.0.2 + slash: 3.0.0 + unrs-resolver: 1.11.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runner@30.0.4: + dependencies: + '@jest/console': 30.0.4 + '@jest/environment': 30.0.4 + '@jest/test-result': 30.0.4 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.0.1 + jest-environment-node: 30.0.4 + jest-haste-map: 30.0.2 + jest-leak-detector: 30.0.2 + jest-message-util: 30.0.2 + jest-resolve: 30.0.2 + jest-runtime: 30.0.4 + jest-util: 30.0.2 + jest-watcher: 30.0.4 + jest-worker: 30.0.2 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.0.4: + dependencies: + '@jest/environment': 30.0.4 + '@jest/fake-timers': 30.0.4 + '@jest/globals': 30.0.4 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.0.4 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + chalk: 4.1.2 + cjs-module-lexer: 2.1.0 + collect-v8-coverage: 1.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-haste-map: 30.0.2 + jest-message-util: 30.0.2 + jest-mock: 30.0.2 + jest-regex-util: 30.0.1 + jest-resolve: 30.0.2 + jest-snapshot: 30.0.4 + jest-util: 30.0.2 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.28.0 + '@babel/generator': 7.28.0 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/types': 7.28.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.0.4: + dependencies: + '@babel/core': 7.28.0 + '@babel/generator': 7.28.0 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/types': 7.28.0 + '@jest/expect-utils': 30.0.4 + '@jest/get-type': 30.0.1 + '@jest/snapshot-utils': 30.0.4 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.0) + chalk: 4.1.2 + expect: 30.0.4 + graceful-fs: 4.2.11 + jest-diff: 30.0.4 + jest-matcher-utils: 30.0.4 + jest-message-util: 30.0.2 + jest-util: 30.0.2 + pretty-format: 30.0.2 + semver: 7.7.2 + synckit: 0.11.8 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-util@30.0.2: + dependencies: + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + chalk: 4.1.2 + ci-info: 4.3.0 + graceful-fs: 4.2.11 + picomatch: 4.0.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-validate@30.0.2: + dependencies: + '@jest/get-type': 30.0.1 + '@jest/types': 30.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.0.2 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 24.0.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-watcher@30.0.4: + dependencies: + '@jest/test-result': 30.0.4 + '@jest/types': 30.0.1 + '@types/node': 24.0.10 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.0.2 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 24.0.10 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 24.0.10 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@30.0.2: + dependencies: + '@types/node': 24.0.10 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.0.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest@30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)): + dependencies: + '@jest/core': 30.0.4(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + '@jest/types': 30.0.1 + import-local: 3.2.0 + jest-cli: 30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jiti@2.4.2: {} + + joycon@3.1.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kareem@2.6.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + known-css-properties@0.37.0: {} + + konva@9.3.22: {} + + kruptein@3.0.8: + dependencies: + asn1.js: 5.4.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lilconfig@2.1.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + loader-runner@4.3.0: {} + + locate-character@3.0.0: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.castarray@4.4.0: {} + + lodash.debounce@4.0.8: {} + + lodash.isplainobject@4.0.6: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.sortby@4.7.0: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.4: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-or-similar@1.5.0: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.0.6 + + memoizerific@1.11.3: + dependencies: + map-or-similar: 1.5.0 + + memory-pager@1.5.0: {} + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + 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 + + mime@1.6.0: {} + + mime@3.0.0: {} + + mimic-fn@2.1.0: {} + + min-indent@1.0.1: {} + + mini-svg-data-uri@1.4.4: {} + + miniflare@4.20250709.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + sharp: 0.33.5 + stoppable: 1.1.0 + undici: 5.29.0 + workerd: 1.20250709.0 + ws: 8.18.0 + youch: 4.1.0-beta.10 + zod: 3.22.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimalistic-assert@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.17.0: + dependencies: + '@mongodb-js/saslprep': 1.3.0 + bson: 6.10.4 + mongodb-connection-string-url: 3.0.2 + + mongoose@8.16.3: + dependencies: + bson: 6.10.4 + kareem: 2.6.3 + mongodb: 6.17.0 + mpath: 0.9.0 + mquery: 5.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + - supports-color + + morgan@1.10.0: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + + mpath@0.9.0: {} + + mquery@5.0.0: + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.0: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-abort-controller@3.1.1: {} + + node-fetch-native@1.6.6: {} + + node-int64@0.4.0: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + 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 + + objectorarray@1.0.5: {} + + ohash@2.0.11: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + 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 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-freehand@1.2.2: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + playwright-core@1.54.1: + optional: true + + playwright@1.54.1: + dependencies: + playwright-core: 1.54.1 + optionalDependencies: + fsevents: 2.3.2 + optional: true + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@3.1.4(postcss@8.5.6): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.5.6 + + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.4.2 + postcss: 8.5.6 + tsx: 4.20.3 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.35.2): + dependencies: + prettier: 3.6.2 + svelte: 5.35.2 + + prettier-plugin-tailwindcss@0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.35.2))(prettier@3.6.2): + dependencies: + prettier: 3.6.2 + optionalDependencies: + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.35.2) + + prettier@3.6.2: {} + + pretty-error@4.0.0: + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + pretty-format@30.0.2: + dependencies: + '@jest/schemas': 30.0.1 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-compare@3.0.1: {} + + proxy-memoize@3.0.1: + dependencies: + proxy-compare: 3.0.1 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + pure-rand@7.0.1: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + ramda@0.29.0: {} + + random-bytes@1.0.0: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-docgen-typescript@2.4.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + react-docgen@7.1.1: + dependencies: + '@babel/core': 7.28.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.7 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.10 + strip-indent: 4.0.0 + transitivePeerDependencies: + - supports-color + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-icons@5.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react@19.1.0: {} + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + 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 + + regexparam@3.0.0: {} + + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + + relateurl@0.2.7: {} + + renderkid@3.0.0: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.44.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.44.2 + '@rollup/rollup-android-arm64': 4.44.2 + '@rollup/rollup-darwin-arm64': 4.44.2 + '@rollup/rollup-darwin-x64': 4.44.2 + '@rollup/rollup-freebsd-arm64': 4.44.2 + '@rollup/rollup-freebsd-x64': 4.44.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.2 + '@rollup/rollup-linux-arm-musleabihf': 4.44.2 + '@rollup/rollup-linux-arm64-gnu': 4.44.2 + '@rollup/rollup-linux-arm64-musl': 4.44.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-musl': 4.44.2 + '@rollup/rollup-linux-s390x-gnu': 4.44.2 + '@rollup/rollup-linux-x64-gnu': 4.44.2 + '@rollup/rollup-linux-x64-musl': 4.44.2 + '@rollup/rollup-win32-arm64-msvc': 4.44.2 + '@rollup/rollup-win32-ia32-msvc': 4.44.2 + '@rollup/rollup-win32-x64-msvc': 4.44.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + runed@0.29.2(svelte@5.35.2): + dependencies: + esm-env: 1.2.2 + svelte: 5.35.2 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + 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: {} + + scheduler@0.26.0: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + semver@6.3.1: {} + + semver@7.7.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.1: {} + + 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 + + setprototypeof@1.2.0: {} + + shadcn-svelte@1.0.6: + dependencies: + commander: 14.0.0 + node-fetch-native: 1.6.6 + postcss: 8.5.6 + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + 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 + + sift@17.1.3: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackback@0.0.2: {} + + statuses@2.0.1: {} + + std-env@3.9.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stoppable@1.1.0: {} + + store2@2.14.4: {} + + storybook@9.0.16(@testing-library/dom@10.4.0)(prettier@3.6.2): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.6.3 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.5 + esbuild-register: 3.6.0(esbuild@0.25.5) + recast: 0.23.11 + semver: 7.7.2 + ws: 8.18.3 + optionalDependencies: + prettier: 3.6.2 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - supports-color + - utf-8-validate + + storybook@9.0.16(@testing-library/dom@9.3.4)(prettier@3.6.2): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.6.3 + '@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.5 + esbuild-register: 3.6.0(esbuild@0.25.5) + recast: 0.23.11 + semver: 7.7.2 + ws: 8.18.3 + optionalDependencies: + prettier: 3.6.2 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - supports-color + - utf-8-validate + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + 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.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + + style-loader@3.3.4(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)): + dependencies: + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + + style-to-object@1.0.9: + dependencies: + inline-style-parser: 0.2.4 + + stylis@4.2.0: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-color@10.0.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-ast-print@0.4.2(svelte@5.35.2): + dependencies: + esrap: 1.2.2 + svelte: 5.35.2 + zimmerframe: 1.1.2 + + svelte-check@4.2.2(picomatch@4.0.2)(svelte@5.35.2)(typescript@5.8.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + chokidar: 4.0.3 + fdir: 6.4.6(picomatch@4.0.2) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.35.2 + typescript: 5.8.3 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.2.0(svelte@5.35.2): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) + postcss-selector-parser: 7.1.0 + optionalDependencies: + svelte: 5.35.2 + + svelte-toolbelt@0.9.3(svelte@5.35.2): + dependencies: + clsx: 2.1.1 + runed: 0.29.2(svelte@5.35.2) + style-to-object: 1.0.9 + svelte: 5.35.2 + + svelte2tsx@0.7.40(svelte@5.35.2)(typescript@5.8.3): + dependencies: + dedent-js: 1.0.1 + pascal-case: 3.1.2 + svelte: 5.35.2 + typescript: 5.8.3 + + svelte@5.35.2: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.4 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 2.1.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.17 + zimmerframe: 1.1.2 + + synchronous-promise@2.0.17: {} + + synckit@0.11.8: + dependencies: + '@pkgr/core': 0.2.7 + + tabbable@6.2.0: {} + + tailwind-merge@3.0.2: {} + + tailwind-merge@3.3.1: {} + + tailwind-variants@1.0.0(tailwindcss@4.1.11): + dependencies: + tailwind-merge: 3.0.2 + tailwindcss: 4.1.11 + + tailwindcss@4.1.11: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + telejson@7.2.0: + dependencies: + memoizerific: 1.11.3 + + terser-webpack-plugin@5.3.14(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + optionalDependencies: + '@swc/core': 1.12.9(@swc/helpers@0.5.17) + esbuild: 0.25.5 + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-api-utils@1.4.3(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-dedent@2.2.0: {} + + ts-interface-checker@0.1.13: {} + + ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.28.0))(esbuild@0.25.5)(jest-util@30.0.2)(jest@30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)))(typescript@5.8.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 30.0.4(@types/node@24.0.10)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.5)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.0 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + babel-jest: 30.0.4(@babel/core@7.28.0) + esbuild: 0.25.5 + jest-util: 30.0.2 + + ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@30.0.4)(@jest/types@30.0.1)(babel-jest@30.0.4(@babel/core@7.28.0))(jest-util@30.0.2)(jest@29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0))(typescript@5.8.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.19.7)(babel-plugin-macros@3.1.0) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.8.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.0 + '@jest/transform': 30.0.4 + '@jest/types': 30.0.1 + babel-jest: 30.0.4(@babel/core@7.28.0) + jest-util: 30.0.2 + + tsconfck@3.1.6(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsup@8.5.0(@swc/core@1.12.9(@swc/helpers@0.5.17))(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.5) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1 + esbuild: 0.25.5 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.20.3) + resolve-from: 5.0.0 + rollup: 4.44.2 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.12.9(@swc/helpers@0.5.17) + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + tw-animate-css@1.3.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@2.19.0: {} + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typescript-eslint@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + undici-types@6.21.0: {} + + undici-types@7.8.0: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + unenv@2.0.0-rc.17: + dependencies: + defu: 6.1.4 + exsolve: 1.0.7 + ohash: 2.0.11 + pathe: 2.0.3 + ufo: 1.6.1 + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unicorn-magic@0.1.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin@1.16.1: + dependencies: + acorn: 8.15.0 + webpack-virtual-modules: 0.6.2 + + unrs-resolver@1.11.0: + dependencies: + napi-postinstall: 0.3.0 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.0 + '@unrs/resolver-binding-android-arm64': 1.11.0 + '@unrs/resolver-binding-darwin-arm64': 1.11.0 + '@unrs/resolver-binding-darwin-x64': 1.11.0 + '@unrs/resolver-binding-freebsd-x64': 1.11.0 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.0 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.0 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.0 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.0 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.0 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.0 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.0 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.0 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.0 + '@unrs/resolver-binding-linux-x64-musl': 1.11.0 + '@unrs/resolver-binding-wasm32-wasi': 1.11.0 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.0 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.0 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.0 + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uqr@0.1.2: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utila@0.4.0: {} + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + vary@1.1.2: {} + + vite-node@3.2.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)): + dependencies: + debug: 4.4.1 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.8.3) + optionalDependencies: + vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + transitivePeerDependencies: + - supports-color + - typescript + + vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.0.10 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.43.1 + tsx: 4.20.3 + + vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.16.3 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.43.1 + tsx: 4.20.3 + + vitefu@1.1.1(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)): + optionalDependencies: + vite: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + vitefu@1.1.1(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)): + optionalDependencies: + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + + vitest@3.2.4(@types/node@22.16.3)(@vitest/browser@3.2.4)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + vite-node: 3.2.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.16.3 + '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@7.0.4(@types/node@22.16.3)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3))(vitest@3.2.4) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + webidl-conversions@4.0.2: {} + + webidl-conversions@7.0.0: {} + + webpack-dev-middleware@6.1.3(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.2 + optionalDependencies: + webpack: 5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5) + + webpack-hot-middleware@2.26.1: + dependencies: + ansi-html-community: 0.0.8 + html-entities: 2.6.0 + strip-ansi: 6.0.1 + + webpack-sources@3.3.3: {} + + webpack-virtual-modules@0.6.2: {} + + webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + browserslist: 4.25.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.2 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.99.9(@swc/core@1.12.9(@swc/helpers@0.5.17))(esbuild@0.25.5)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + 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-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.19: + 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: {} + + workerd@1.20250709.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20250709.0 + '@cloudflare/workerd-darwin-arm64': 1.20250709.0 + '@cloudflare/workerd-linux-64': 1.20250709.0 + '@cloudflare/workerd-linux-arm64': 1.20250709.0 + '@cloudflare/workerd-windows-64': 1.20250709.0 + + worktop@0.8.0-next.18: + dependencies: + mrmime: 2.0.1 + regexparam: 3.0.0 + + wrangler@4.24.3(@cloudflare/workers-types@4.20250711.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.0 + '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250709.0) + blake3-wasm: 2.1.5 + esbuild: 0.25.4 + miniflare: 4.20250709.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.17 + workerd: 1.20250709.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20250711.0 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + 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.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@8.18.0: {} + + ws@8.18.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.2 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.5 + '@poppinss/dumper': 0.6.4 + '@speed-highlight/core': 1.2.7 + cookie: 1.0.2 + youch-core: 0.3.3 + + zimmerframe@1.1.2: {} + + zod@3.22.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..19471a48 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +# pnpm-workspace.yaml +packages: + # executable/launchable applications + - "apps/*" + # all packages in subdirs of packages/ and components/ + - "packages/*"