diff --git a/.devcontainer/.zshrc b/.devcontainer/.zshrc new file mode 100644 index 0000000..72a0a2c --- /dev/null +++ b/.devcontainer/.zshrc @@ -0,0 +1,10 @@ +# Path to your oh-my-zsh installation. +export ZSH="/root/.oh-my-zsh" + +# https://typewritten.dev/#/git_status_indicators +ZSH_THEME="typewritten" + +# Which plugins would you like to load? +# plugins=(zsh-autosuggestions nvm git) + +source $ZSH/oh-my-zsh.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..83a24ff --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +ARG VARIANT="14-buster" +FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} + +RUN apt update && export DEBIAN_FRONTEND=noninteractive \ + && apt -y install --no-install-recommends git-lfs \ + && rm -rf /var/lib/apt/lists/* + +RUN git lfs install + +ENV ZSH_CUSTOM /root/.oh-my-zsh/ +RUN git clone https://github.com/reobin/typewritten.git $ZSH_CUSTOM/themes/typewritten +RUN ln -s "$ZSH_CUSTOM/themes/typewritten/typewritten.zsh-theme" "$ZSH_CUSTOM/themes/typewritten.zsh-theme" +RUN ln -s "$ZSH_CUSTOM/themes/typewritten/async.zsh" "$ZSH_CUSTOM/themes/async" +COPY .zshrc /root/.zshrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..1856878 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,53 @@ +{ + "name": "Node.js & TypeScript", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "14" + } + }, + "settings": { + "terminal.integrated.shell.linux": "/bin/zsh", + "files.exclude": { + "**/node_modules": true, + "**/package-lock.json": true, + "**/dist": true, + "**/lib": true, + "**/.firebase": true + }, + "editor.tabSize": 2, + "editor.detectIndentation": false, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.enable": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "eslint.validate": ["json"], + "markdown.extension.toc.levels": "2..4", + // + }, + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "yzhang.markdown-all-in-one", + "hediet.vscode-drawio", + "pkief.material-icon-theme", + "equinusocio.vsc-community-material-theme" + ], + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached", + "workspaceFolder": "/workspace", + "mounts": [ + "source=decla-red-root-node_modules-volume,target=/workspace/node_modules,type=volume", + "source=decla-red-frontend-node_modules-volume,target=/workspace/frontend/node_modules,type=volume", + "source=decla-red-backend-node_modules-volume,target=/workspace/backend/node_modules,type=volume", + "source=decla-red-shared-node_modules-volume,target=/workspace/shared/node_modules,type=volume" + ], + "forwardPorts": [3000, 8080], + "postCreateCommand": "chown node:node ./**/node_modules && npm install && npm run initialize && npm run build" +} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2bd8264..0000000 --- a/.dockerignore +++ /dev/null @@ -1,14 +0,0 @@ -# The server image only needs `shared/` and `backend/`. -**/node_modules -**/dist -**/lib -.git -.github -.forgejo -.devcontainer -.vscode -frontend -media -*.log -Dockerfile -.dockerignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..ef20d97 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +**/node_modules/**/*.js +node_modules/**/*.js +**/package-lock.json +package-lock.json \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e646fad --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "plugins": ["unused-imports", "@typescript-eslint", "json-format", "prettier"], + "settings": { + "json/sort-package-json": true + }, + "rules": { + "prettier/prettier": "error", + "no-unused-vars": "off", + "unused-imports/no-unused-imports-ts": "error", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-var-requires": "off" + } +} diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml deleted file mode 100644 index 1136a06..0000000 --- a/.forgejo/workflows/deploy.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Build & deploy - -on: - push: - branches: [main] - tags: ['v*'] - pull_request: - branches: [main] - workflow_dispatch: - -concurrency: - group: ${{ gitea.workflow }}-${{ gitea.ref }} - cancel-in-progress: true - -jobs: - # ---- Static website -> /pages mount on the runner host -------------------- - website: - name: Build & deploy website - runs-on: docker - steps: - - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - - - name: Install dependencies - # No lockfiles are committed (see .gitignore), so use `npm install`. - run: npm install && npm run init - - - name: Build (shared -> frontend -> backend) - run: npm run build - - - name: Test - run: npm test - - - name: Lint - run: npm run lint:check - - - name: Deploy to host pages mount - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: http://forgejo:3000/andras/ci-actions/deploy-pages@main - with: - source: frontend/dist - target: declared - - # ---- Server Docker image -> Forgejo container registry -------------------- - server-image: - name: Build & publish server image - runs-on: docker - # No registry push on PRs; build validation still happens in the website job. - if: github.event_name != 'pull_request' - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build and publish server image - uses: http://forgejo:3000/andras/ci-actions/docker-publish@main - with: - context: . - image-suffix: -server - token: ${{ secrets.FORGEJO_PACKAGE_TOKEN }} diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..c4b95c6 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,54 @@ +name: Build and deploy project +on: + push: + branches: + - main +env: + CONTAINER_REGISTRY: schmelczera + DOMAIN: '174.138.103.56' + +jobs: + build-project: + runs-on: ubuntu-latest + steps: + - name: Checkout current branch with lfs + uses: actions/checkout@master + with: + lfs: true + - name: Build project + run: | + npm install + npm run initialize + npm run build + - name: Deploy frontend + uses: w9jds/firebase-action@master + with: + args: deploy --only hosting --project decla-red + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + PROJECT_PATH: frontend + - name: Authenticate with dockerhub + run: | + docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }} + - name: Install buildx + id: buildx + uses: crazy-max/ghaction-docker-buildx@v1 + with: + version: latest + - name: Build and push server image + run: | + docker buildx build \ + --tag $CONTAINER_REGISTRY/decla-red-server:latest \ + --platform linux/amd64,linux/arm/v7,linux/arm64 . --push + working-directory: backend + - name: Setup auth tokens + run: | + mkdir ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 400 ~/.ssh/id_ed25519 + ssh -o StrictHostKeyChecking=no root@$DOMAIN uptime + - name: Stack deploy + run: | + DOCKER_HOST=ssh://root@$DOMAIN docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }} + DOCKER_HOST=ssh://root@$DOMAIN docker stack deploy decla-red-server -c docker-compose.yml --with-registry-auth + working-directory: backend diff --git a/.gitignore b/.gitignore index 6e9e967..713f044 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ dist lib node_modules -.firebase -yarm-error.log -yarn.lock package-lock.json -.DS_Store +.firebase diff --git a/.node-version b/.node-version deleted file mode 100644 index 2bd5a0a..0000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 2bd5a0a..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -22 diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 3b0b653..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": ["/**"], - "program": "${workspaceFolder}/backend/dist/main.js", - "outFiles": ["${workspaceFolder}/**/*.js"] - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 57b1058..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "cSpell.words": [ - "Deserializable", - "Deserialization", - "Respawn", - "doppler", - "overridable", - "serializable" - ] -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 327d1f2..ae21f1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,50 +1,17 @@ -# syntax=docker/dockerfile:1 +FROM node:14.13.0-alpine3.10 as base -# doppler game server (the `doppler-server` package). -# The frontend is a static site and is NOT built here — see .forgejo/workflows. +COPY . . -# ---- Stage 1: build the shared lib, then bundle the server ------------------- -# Node 22 (current LTS) matches the project toolchain (webpack 5 / TypeScript 6, -# see .devcontainer). The full (non-slim) image carries the build tools that -# socket.io's optional native deps (bufferutil/utf-8-validate) compile against. -FROM node:22-bookworm AS build -WORKDIR /app +RUN npm install && npm run initialize && npm run build -# `shared` is consumed by the backend as `file:../shared` and is bundled into -# the server bundle by webpack, so it must be installed and built first. -COPY shared/package.json shared/ -RUN cd shared && npm install -COPY shared/ shared/ -RUN cd shared && npm run build +FROM node:14.13.0-alpine3.10 -# Install the backend deps. `file:../shared` now resolves to the built /app/shared. -COPY backend/package.json backend/ -RUN cd backend && npm install -COPY backend/ backend/ -RUN cd backend && npm run build +COPY backend/package.json . -# Drop devDependencies; the runtime only needs the production deps that webpack -# left external (express, socket.io, cors, gl-matrix, minimist, msgpack parser). -RUN cd backend && npm prune --production +RUN npm install --production -# ---- Stage 2: minimal runtime ---------------------------------------------- -FROM node:22-bookworm-slim -WORKDIR /app -ENV NODE_ENV=production +COPY --from=base backend/dist/main.js main.js -# Run as an unprivileged user. -RUN groupadd -r app && useradd -r -g app -d /app app - -COPY --from=build /app/backend/dist ./dist -COPY --from=build /app/backend/node_modules ./node_modules - -USER app EXPOSE 3000 -# Hits the same /state endpoint the website polls (see serverInformationEndpoint). -HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" - -ENTRYPOINT ["node", "dist/main.js"] -# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--scoreLimit`. -CMD ["--port", "3000"] +CMD [ "node", "main.js" ] diff --git a/README.md b/README.md index ce57285..dd035bf 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,3 @@ -# doppler +# decla.red -A 2-dimensional multiplayer game utilising ray tracing. - -> **Available at [doppler.schmelczer.dev](https://doppler.schmelczer.dev).** - -![screenshot of in-game fight](media/game-pc.png) -![three screenshots taken at iPhone SE screen size](media/collage-iphone.png) - -For optimised 2D ray tracing, [SDF-2D](https://github.com/schmelczerandras/sdf-2d) is used. - -## Deployment - -CI/CD runs on Forgejo Actions (`.forgejo/workflows/deploy.yml`). On a push to -`main` it: - -- builds the static frontend and rsyncs `frontend/dist/` to the `/pages/declared` - mount on the runner host (the mount keeps its pre-rebrand name), and -- builds the server image from the root `Dockerfile` and pushes it to the Forgejo - container registry as `//-server`. - -The registry job needs a `FORGEJO_PACKAGE_TOKEN` secret (with package write -scope) and, optionally, a `CONTAINER_REGISTRY_HOST` variable to override the -registry host. - -The website's server list is hardcoded in -[`frontend/src/scripts/configuration.ts`](frontend/src/scripts/configuration.ts) — -edit it to add or remove game-server origins. - -Run the server image locally: - -```sh -docker build -t doppler-server . -docker run -p 3000:3000 doppler-server --name "My server" --playerLimit 16 -``` +![Deploy everything](https://github.com/schmelczerandras/decla.red/workflows/Deploy%20everything/badge.svg) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..a9c8231 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +.dockerignore +Dockerfile +node_modules diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3ea763a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:14.13.0-alpine3.10 + +COPY package.json . + +RUN npm install --production + +COPY dist/main.js main.js + +EXPOSE 3000 + +CMD [ "node", "main.js" ] diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..c44fd30 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + decla-red-server: + init: true + image: schmelczera/decla-red-server + networks: + - network + deploy: + resources: + limits: + cpus: '1.0' + memory: 256M + reservations: + cpus: '0.25' + memory: 256M + restart_policy: + condition: on-failure + window: 30s + +networks: + network: diff --git a/backend/package.json b/backend/package.json index d655b4c..43627a7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,43 +1,35 @@ { - "name": "doppler-server", - "version": "0.1.0", - "description": "Game server for doppler", + "name": "decla.red-server", + "private": true, + "description": "Game server for decla.red", "keywords": [], "author": "András Schmelczer (https://schmelczer.dev/)", - "main": "dist/main.js", - "bin": { - "doppler-server": "dist/main.js" - }, - "engines": { - "node": ">=20" - }, + "main": "index.js", "scripts": { "build": "npx webpack --mode production", - "dev": "concurrently --kill-others-on-fail \"webpack --mode development -w\" \"nodemon --legacy-watch dist/main.js\"", - "try-build": "npm run build && cd dist && node main.js && cd -" + "initialize": "npm install", + "start": "concurrently --kill-others \"npx webpack --mode development -w\" \"nodemon --legacy-watch dist/main.js\"", + "try-build": "npm run build && node dist/main.js" }, "dependencies": { - "cors": "^2.8.6", - "express": "^5.2.1", - "gl-matrix": "3.3.0", - "minimist": "^1.2.8", - "socket.io": "^4.8.3", - "socket.io-msgpack-parser": "^3.0.2" + "cors": "^2.8.5", + "express": "^4.17.1", + "http": "0.0.1-security", + "socket.io": "^2.3.0", + "uws": "^10.148.1", + "webpack-node-externals": "^2.5.2" }, "devDependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "@types/minimist": "^1.2.5", - "@types/node": "^22.0.0", - "clean-webpack-plugin": "^4.0.0", - "concurrently": "^10.0.3", - "nodemon": "^3.1.14", - "shared": "file:../shared", - "terser-webpack-plugin": "^5.6.1", - "ts-loader": "^9.6.0", - "typescript": "^6.0.3", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.3", - "webpack-node-externals": "^3.0.0" + "clean-webpack-plugin": "^3.0.0", + "concurrently": "^5.3.0", + "esbuild-loader": "^2.4.0", + "nodemon": "^2.0.4", + "raw-loader": "^4.0.1", + "resolve-url-loader": "^3.1.1", + "terser-webpack-plugin": "^2.3.5", + "typescript": "^4.0.3", + "webpack": "^4.43.0", + "webpack-cli": "^3.3.11", + "webpack-dev-server": "^3.10.3" } } diff --git a/backend/src/commands/announce.ts b/backend/src/commands/announce.ts deleted file mode 100644 index 2e8aadf..0000000 --- a/backend/src/commands/announce.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Command } from 'shared'; - -// Internal request from a game object (e.g. a keystone planet flipping) asking -// the GameServer to broadcast a one-off announcement to every connected client. -export class AnnounceCommand extends Command { - public constructor(public readonly text: string) { - super(); - } -} diff --git a/backend/src/commands/generate-points.ts b/backend/src/commands/generate-points.ts deleted file mode 100644 index 3f1ac98..0000000 --- a/backend/src/commands/generate-points.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Command } from 'shared'; - -export class GeneratePointsCommand extends Command { - public constructor( - public readonly blue: number, - public readonly red: number, - ) { - super(); - } -} diff --git a/backend/src/commands/react-to-collision.ts b/backend/src/commands/react-to-collision.ts deleted file mode 100644 index fdcd492..0000000 --- a/backend/src/commands/react-to-collision.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Command, GameObject } from 'shared'; - -export class ReactToCollisionCommand extends Command { - public constructor(public readonly other: GameObject) { - super(); - } -} diff --git a/backend/src/commands/step.ts b/backend/src/commands/step.ts deleted file mode 100644 index ee0f6f7..0000000 --- a/backend/src/commands/step.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Command, CommandReceiver } from 'shared'; - -export class StepCommand extends Command { - public constructor( - public readonly deltaTimeInSeconds: number, - public readonly game: CommandReceiver, - ) { - super(); - } -} diff --git a/backend/src/create-world.ts b/backend/src/create-world.ts deleted file mode 100644 index a1039fc..0000000 --- a/backend/src/create-world.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Random, PlanetBase, hsl, settings, evaluateSdf } from 'shared'; -import { LampPhysical } from './objects/lamp-physical'; -import { PlanetPhysical } from './objects/planet-physical'; -import { PhysicalContainer } from './physics/containers/physical-container'; -import { Physical } from './physics/physicals/physical'; - -export const createWorld = (objectContainer: PhysicalContainer) => { - const objects: Array = []; - const lights: Array = []; - - for (let r = 0; r < settings.worldRadius; r += settings.radiusSteps) { - const circumference = 2 * Math.PI * r; - const stepCount = circumference * settings.objectsOnCircleLength; - for (let rad = 0; rad < 2 * Math.PI; rad += (2 * Math.PI) / stepCount) { - const position = vec2.rotate( - vec2.create(), - vec2.fromValues(r, 0), - vec2.create(), - rad, - ); - - if (objects.length !== 0 && Random.getRandom() > 0.5) { - if ( - evaluateSdf(position, objects) > 200 && - !lights.find((l) => l.distance(position) < 2500) - ) { - lights.push( - new LampPhysical( - position, - hsl( - (rad / (2 * Math.PI)) * 360, - Random.getRandomInRange(50, 100), - Random.getRandomInRange(40, 50), - ), - Random.getRandomInRange(0.35, 1), - ), - ); - } - } else { - if ( - evaluateSdf(position, objects) > 1400 && - !lights.find((l) => l.distance(position) < 1700) - ) { - const planet = - objects.length === 0 - ? // The first, central giant is the keystone "Heart": a named, - // always-contested focal objective the whole match orbits. - new PlanetPhysical( - PlanetBase.createPlanetVertices( - position, - Random.getRandomInRange(1600, 2400), - Random.getRandomInRange(1600, 2400), - Random.getRandomInRange(80, 300), - ), - true, - ) - : new PlanetPhysical( - PlanetBase.createPlanetVertices( - position, - Random.getRandomInRange(300, 1600), - Random.getRandomInRange(300, 1600), - Random.getRandomInRange(20, 100), - ), - ); - - objects.push(planet); - } - } - } - } - console.info(`Generated ${objects.length} planets`); - console.info(`Generated ${lights.length} light`); - - // Associate each lamp with its NEAREST planet, so a planet can repaint "its" - // lamps to the owning team's colour when it flips. Lamps are already placed by - // proximity during world-gen, so the nearest planet is the one whose capture - // they should advertise. Distances use the planet SDF (negative inside), which - // is exactly the "closest planet" metric we want. - const planets = objects.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical); - lights - .filter((l): l is LampPhysical => l instanceof LampPhysical) - .forEach((lamp) => { - let nearest: PlanetPhysical | undefined; - let nearestDistance = Infinity; - planets.forEach((planet) => { - const distance = planet.distance(lamp.center); - if (distance < nearestDistance) { - nearestDistance = distance; - nearest = planet; - } - }); - nearest?.addLamp(lamp); - }); - - [...objects, ...lights].forEach((o) => objectContainer.addObject(o)); -}; diff --git a/backend/src/default-options.ts b/backend/src/default-options.ts deleted file mode 100644 index 1580103..0000000 --- a/backend/src/default-options.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Options } from './options'; - -export const defaultOptions: Options = { - port: 3000, - name: 'Test server', - playerLimit: 16, - npcCount: 8, - seed: Math.random(), - scoreLimit: 2500, -}; diff --git a/backend/src/game-server.ts b/backend/src/game-server.ts deleted file mode 100644 index 5e92f3b..0000000 --- a/backend/src/game-server.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { PhysicalContainer } from './physics/containers/physical-container'; -import { Server, Socket } from 'socket.io'; -import { - TransportEvents, - deserialize, - settings, - ServerInformation, - PlayerInformation, - UpdateGameState, - CharacterTeam, - GameEndCommand, - GameStartCommand, - Command, - CommandReceiver, - CommandExecutors, - ServerAnnouncement, -} from 'shared'; -import { createWorld } from './create-world'; -import { DeltaTimeCalculator } from './helper/delta-time-calculator'; -import { Options } from './options'; -import { PlayerContainer } from './players/player-container'; -import { StepCommand } from './commands/step'; -import { GeneratePointsCommand } from './commands/generate-points'; -import { AnnounceCommand } from './commands/announce'; - -const gameStateSubscribedRoom = 'gameStateSubscribedRoom'; - -export class GameServer extends CommandReceiver { - private objects!: PhysicalContainer; - private players!: PlayerContainer; - private deltaTimes!: Array; - private deltaTimeCalculator!: DeltaTimeCalculator; - - private bluePoints = 0; - private redPoints = 0; - private matchPointAnnounced: Partial> = {}; - - private isInEndGame = false; - private timeScaling = 1; - - private serverName: string; - private playerLimit: number; - - private initialize() { - const previousPlayers = this.players; - this.objects = new PhysicalContainer(); - createWorld(this.objects); - this.objects.initialize(); - this.players = new PlayerContainer( - this.objects, - this.options.playerLimit, - this.options.npcCount, - ); - this.deltaTimeCalculator = new DeltaTimeCalculator(); - this.deltaTimes = []; - this.bluePoints = 0; - this.redPoints = 0; - this.matchPointAnnounced = {}; - this.isInEndGame = false; - this.timeScaling = 1; - previousPlayers?.queueCommandForEachClient(new GameStartCommand()); - previousPlayers?.sendQueuedCommands(); - } - - protected commandExecutors: CommandExecutors = { - [GeneratePointsCommand.type]: this.addPoints.bind(this), - [AnnounceCommand.type]: ({ text }: AnnounceCommand) => - this.players.queueCommandForEachClient(new ServerAnnouncement(text)), - }; - - constructor( - private readonly io: Server, - private options: Options, - ) { - super(); - - this.serverName = options.name; - this.playerLimit = options.playerLimit; - - this.initialize(); - - io.on('connection', (socket: Socket) => { - socket.on(TransportEvents.PlayerJoining, (playerInfo: PlayerInformation) => { - try { - const player = this.players.createPlayer(playerInfo, socket); - socket.on(TransportEvents.PlayerToServer, (json: string) => { - try { - const commands: Array = deserialize(json); - commands.forEach((c) => player.handleCommand(c)); - } catch (e) { - console.error('Error while processing command', e); - } - }); - - this.sendServerStateUpdate(); - - socket.on('disconnect', () => { - player.destroy(); - this.players.deletePlayer(player); - this.sendServerStateUpdate(); - }); - } catch (e) { - console.error('Failed to register joining player; disconnecting socket', e); - socket.disconnect(); - } - }); - - socket.on(TransportEvents.SubscribeForServerInfoUpdates, () => { - socket.join(gameStateSubscribedRoom); - }); - }); - } - - private timeSinceLastServerStateUpdate = 0; - public sendServerStateUpdate() { - this.io - .to(gameStateSubscribedRoom) - .emit(TransportEvents.ServerInfoUpdate, [this.players.count, this.gameProgress]); - } - - public start() { - this.handlePhysics(); - } - - private addPoints({ blue, red }: GeneratePointsCommand) { - if (this.isInEndGame) { - return; - } - - this.bluePoints += blue; - this.redPoints += red; - if (this.bluePoints >= this.options.scoreLimit) { - this.endGame(CharacterTeam.blue); - } else if (this.redPoints >= this.options.scoreLimit) { - this.endGame(CharacterTeam.red); - } else { - this.announceMatchPointOnce(CharacterTeam.blue, this.bluePoints); - this.announceMatchPointOnce(CharacterTeam.red, this.redPoints); - } - } - - private announceMatchPointOnce(team: CharacterTeam, points: number) { - if ( - !this.matchPointAnnounced[team] && - points >= this.options.scoreLimit * settings.matchPointScoreRatio - ) { - this.matchPointAnnounced[team] = true; - this.players.queueCommandForEachClient( - new ServerAnnouncement( - `Match point — team ${team}!`, - ), - ); - } - } - - private endGame(winningTeam: CharacterTeam) { - this.isInEndGame = true; - const endTitleLength = 6; - this.players.endGame(winningTeam); - this.players.queueCommandForEachClient( - new GameEndCommand(winningTeam, endTitleLength, true), - ); - setTimeout(() => this.destroy(), endTitleLength * 1000 * 1.1); - } - - private destroy() { - this.initialize(); - } - - private timeSinceLastPointUpdate = 0; - private physicsAccumulator = 0; - // Frames since the last stats report where physics ran over budget (more - // substeps than the cap). Surfaced by handleStats as a saturation signal. - private saturatedFrames = 0; - - private handlePhysics() { - const delta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds({ setAsBase: true }); - this.deltaTimes.push(delta); - - this.handleStats(); - - if ((this.timeSinceLastServerStateUpdate += delta) > 4) { - this.timeSinceLastServerStateUpdate = 0; - this.sendServerStateUpdate(); - } - - if ((this.timeSinceLastPointUpdate += delta) > 0.5) { - this.timeSinceLastPointUpdate = 0; - this.players.queueCommandForEachClient( - new UpdateGameState(this.bluePoints, this.redPoints, this.options.scoreLimit), - ); - } - - const fixedDelta = settings.targetPhysicsDeltaTimeInSeconds; - const maxSubstepsPerFrame = 5; - // Cap on retained physics backlog when saturated, so a long stall can't - // accumulate an unrecoverable catch-up. - const maxBacklogSeconds = 0.25; - - this.physicsAccumulator += delta; - let substeps = Math.floor(this.physicsAccumulator / fixedDelta); - if (substeps > maxSubstepsPerFrame) { - // Saturated: run the cap's worth of substeps but KEEP the remaining - // backlog (clamped) instead of zeroing it. Dropping it silently slowed - // simulated time for everyone — and diverged client prediction, whose - // wall-clock keeps running. Clamping bounds the catch-up so a transient - // spike recovers without a death spiral. - this.saturatedFrames++; - this.physicsAccumulator = Math.min( - this.physicsAccumulator - maxSubstepsPerFrame * fixedDelta, - maxBacklogSeconds, - ); - substeps = maxSubstepsPerFrame; - } else { - this.physicsAccumulator -= substeps * fixedDelta; - } - - for (let i = 0; i < substeps; i++) { - let scaledDelta = fixedDelta; - if (this.isInEndGame) { - this.timeScaling *= Math.pow(settings.endGameDeltaScaling, fixedDelta); - scaledDelta /= this.timeScaling; - } - this.objects.handleCommand(new StepCommand(scaledDelta, this)); - this.players.step(scaledDelta); - } - - this.players.stepCommunication(delta); - this.objects.resetRemoteCalls(); - - const physicsDelta = this.deltaTimeCalculator.getNextDeltaTimeInSeconds(); - - setTimeout( - this.handlePhysics.bind(this), - Math.max(0, fixedDelta - physicsDelta) * 1000, - ); - } - - private handleStats() { - const framesBetweenDeltaTimeCalculation = 10000; - - if (this.deltaTimes.length > framesBetweenDeltaTimeCalculation) { - this.deltaTimes.sort((a, b) => a - b); - console.info( - `Median physics time: ${( - this.deltaTimes[Math.floor(framesBetweenDeltaTimeCalculation / 2)] * 1000 - ).toFixed(2)} ms`, - ); - console.info( - 'Tail times: ', - this.deltaTimes.slice(-20).map((v) => `${(v * 1000).toFixed(2)} ms`), - ); - console.info( - `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, - ); - - const rtts = this.players.connectedPlayerRttsMs.filter((r) => r > 0); - if (rtts.length > 0) { - rtts.sort((a, b) => a - b); - console.info( - `Player RTT median ${rtts[Math.floor(rtts.length / 2)].toFixed(0)} ms ` + - `(min ${rtts[0].toFixed(0)}, max ${rtts[rtts.length - 1].toFixed(0)}, n=${rtts.length})`, - ); - } - - if (this.saturatedFrames > 0) { - console.warn( - `Physics saturated on ${this.saturatedFrames} frame(s) since last report — shedding backlog`, - ); - this.saturatedFrames = 0; - } - - this.deltaTimes = []; - } - } - - private get gameProgress(): number { - return (Math.max(this.bluePoints, this.redPoints) / this.options.scoreLimit) * 100; - } - - public get serverInfo(): ServerInformation { - return { - serverName: this.serverName, - playerCount: this.players.count, - playerLimit: this.playerLimit, - gameStatePercent: this.gameProgress, - }; - } -} diff --git a/backend/src/helper/delta-time-calculator.ts b/backend/src/helper/delta-time-calculator.ts index 4d3723e..d52526c 100644 --- a/backend/src/helper/delta-time-calculator.ts +++ b/backend/src/helper/delta-time-calculator.ts @@ -1,13 +1,20 @@ export class DeltaTimeCalculator { private previousTime: [number, number] = process.hrtime(); - public getNextDeltaTimeInSeconds( - { setAsBase = false }: { setAsBase: boolean } = { setAsBase: false }, - ): number { - const [seconds, nanoSeconds] = process.hrtime(this.previousTime); - if (setAsBase) { - this.previousTime = process.hrtime(); - } - return seconds + nanoSeconds / 1e9; + public getNextDeltaTimeInMilliseconds(): number { + const deltaTime = process.hrtime(this.previousTime); + this.previousTime = process.hrtime(); + + const [seconds, nanoSeconds] = deltaTime; + + return seconds * 1000 + nanoSeconds / 1000 / 1000; + } + + public getDeltaTimeInMilliseconds(): number { + const deltaTime = process.hrtime(this.previousTime); + + const [seconds, nanoSeconds] = deltaTime; + + return seconds * 1000 + nanoSeconds / 1000 / 1000; } } diff --git a/backend/src/helper/get-time-in-milliseconds.ts b/backend/src/helper/get-time-in-milliseconds.ts new file mode 100644 index 0000000..3ff0437 --- /dev/null +++ b/backend/src/helper/get-time-in-milliseconds.ts @@ -0,0 +1,5 @@ +export const getTimeInMilliseconds = (): number => { + const [seconds, nanoSeconds] = process.hrtime(); + + return seconds * 1000 + nanoSeconds / 1000 / 1000; +}; diff --git a/backend/src/index.html b/backend/src/index.html new file mode 100644 index 0000000..17aef93 --- /dev/null +++ b/backend/src/index.html @@ -0,0 +1,31 @@ + + + + + + + + Server info + + + + + + + \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index c492c7a..a104f8a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,52 +1,112 @@ -import { Server as IoServer } from 'socket.io'; +import ioserver, { Socket } from 'socket.io'; import express from 'express'; import { Server } from 'http'; import cors from 'cors'; -import { applyArrayPlugins, Random, serverInformationEndpoint } from 'shared'; -import minimist from 'minimist'; +import { + applyArrayPlugins, + Random, + TransportEvents, + deserialize, + StepCommand, + settings, +} from 'shared'; +import './index.html'; +import { Player } from './players/player'; +import { PhysicalContainer } from './physics/containers/physical-container'; +import { createDungeon } from './map/create-dungeon'; import { glMatrix } from 'gl-matrix'; -import { GameServer } from './game-server'; -import { defaultOptions } from './default-options'; -import parser from 'socket.io-msgpack-parser'; +import { DeltaTimeCalculator } from './helper/delta-time-calculator'; glMatrix.setMatrixArrayType(Array); + applyArrayPlugins(); -const optionOverrides = minimist(process.argv.slice(2)); -const options = { - ...defaultOptions, - ...optionOverrides, -}; +Random.seed = 42; -Random.seed = options.seed; +const objects = new PhysicalContainer(); +createDungeon(objects); + +objects.initialize(); + +let players: Array = []; const app = express(); -const server = new Server(app); -const io = new IoServer(server, { - parser, - cors: { - origin: true, - credentials: true, - }, -} as any); - -const gameServer = new GameServer(io, options); +const deltaTimeCalculator = new DeltaTimeCalculator(); app.use( cors({ - origin: (_, callback) => { + origin: (origin, callback) => { callback(null, true); }, credentials: true, }), ); -app.get(serverInformationEndpoint, (_, res) => { - res.json(gameServer.serverInfo); +const port = 3000; +const server = new Server(app); +const io = ioserver(server); + +/* +const log = (text: string) => { + io.to('insights').emit('insights', text + '\n'); +}; +*/ + +app.get('/', function (req, res) { + res.sendFile('dist/index.html', { root: '.' }); }); -server.listen(options.port, () => { - console.info(`Server started on port ${options.port}`); +io.on('connection', (socket: SocketIO.Socket) => { + socket.on(TransportEvents.PlayerJoining, () => { + const player = new Player(objects, socket); + players.push(player); + socket.on(TransportEvents.PlayerToServer, (json: string) => { + const command = deserialize(json); + player.sendCommand(command); + }); + + socket.on('disconnect', () => { + player.destroy(); + players = players.filter((p) => p !== player); + }); + }); + + socket.on('join', (room_name: string) => { + socket.join(room_name); + }); }); -gameServer.start(); +server.listen(port, () => { + console.log(`server started at http://localhost:${port}`); +}); + +let deltas: Array = []; + +const handlePhysics = () => { + const delta = deltaTimeCalculator.getNextDeltaTimeInMilliseconds(); + deltas.push(delta); + const step = new StepCommand(delta); + if (deltas.length > 100) { + deltas.sort((a, b) => a - b); + console.log(`Median physics time: ${deltas[50].toFixed(2)} ms`); + console.log( + `Memory used: ${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB`, + ); + deltas = []; + console.log(players.map((p) => p.latency)); + } + + objects.sendCommand(step); + players.forEach((p) => p.sendCommand(step)); + + const physicsDelta = deltaTimeCalculator.getDeltaTimeInMilliseconds(); + deltas.push(physicsDelta); + const sleepTime = settings.targetPhysicsDeltaTimeInMilliseconds - physicsDelta; + if (sleepTime >= settings.minPhysicsSleepTime) { + setTimeout(handlePhysics, sleepTime); + } else { + setImmediate(handlePhysics); + } +}; + +handlePhysics(); diff --git a/backend/src/map/create-dungeon.ts b/backend/src/map/create-dungeon.ts new file mode 100644 index 0000000..0edef3d --- /dev/null +++ b/backend/src/map/create-dungeon.ts @@ -0,0 +1,61 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { Random } from 'shared'; +import { LampPhysical } from '../objects/lamp-physical'; + +import { TunnelPhysical } from '../objects/tunnel-physical'; +import { PhysicalContainer } from '../physics/containers/physical-container'; + +export const createDungeon = (objects: PhysicalContainer) => { + const lightPositions: Array = []; + + for (let j = 0; j < 6; j++) { + let previousRadius = 500; + let previousEnd = vec2.fromValues( + j === 0 ? 0 : Random.getRandomInRange(-1000, 1000), + j === 0 ? 0 : Random.getRandomInRange(-1000, 1000), + ); + for (let i = 0; i < 500; i++) { + const delta = vec2.fromValues(j % 2 ? 1 : -1, Random.getRandomInRange(-1, 1)); + + vec2.normalize(delta, delta); + vec2.scale(delta, delta, 500); + + const currentEnd = vec2.add(delta, delta, previousEnd); + + const currentToRadius = Random.getRandom() * 250 + 150; + + const tunnel = new TunnelPhysical( + previousEnd, + currentEnd, + previousRadius, + currentToRadius, + ); + + objects.addObject(tunnel); + + if (Random.getRandom() > 0.7) { + const position = currentEnd; + if (!lightPositions.find((p) => vec2.dist(p, position) < 2000)) { + lightPositions.push(position); + objects.addObject( + new LampPhysical( + currentEnd, + vec3.normalize( + vec3.create(), + vec3.fromValues( + Random.getRandomInRange(0.5, 1), + 0, + Random.getRandomInRange(0.5, 1), + ), + ), + Random.getRandomInRange(0.5, 1), + ), + ); + } + } + + previousEnd = currentEnd; + previousRadius = currentToRadius; + } + } +}; diff --git a/backend/src/objects/character-physical.ts b/backend/src/objects/character-physical.ts index bdc8873..c0ed88a 100644 --- a/backend/src/objects/character-physical.ts +++ b/backend/src/objects/character-physical.ts @@ -1,357 +1,90 @@ import { vec2 } from 'gl-matrix'; import { id, + CharacterBase, + StepCommand, settings, + CommandExecutors, MoveActionCommand, serializesTo, + clamp, last, Circle, - CharacterBase, - CharacterTeam, - PropertyUpdatesForObject, - UpdatePropertyCommand, - CommandExecutors, - CommandReceiver, - mix, - clamp01, - stepCharacterMovement, - applyLeapImpulse, - decayMomentum, - CharacterMovementState, - CharacterWorld, - GroundSurface, - headRadius, - feetRadius, - headOffset, - leftFootOffset, - rightFootOffset, - boundRadius, } from 'shared'; -import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; +import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; +import { Physical } from '../physics/physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; -import { ProjectilePhysical } from './projectile-physical'; -import { forceAtPosition } from '../physics/functions/force-at-position'; -import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; -import { PlanetPhysical } from './planet-physical'; -import { StepCommand } from '../commands/step'; -import { ReactToCollisionCommand } from '../commands/react-to-collision'; -import { GeneratePointsCommand } from '../commands/generate-points'; +import { Spring } from './spring'; @serializesTo(CharacterBase) -export class CharacterPhysical extends CharacterBase implements DynamicPhysical { +export class CharacterPhysical extends CharacterBase implements Physical { public readonly canCollide = true; + public readonly isInverted = false; public readonly canMove = true; - private projectileStrength = settings.playerMaxStrength; - - // Body geometry (head/foot radii, posture offsets, bound radius) is defined - // once in the shared movement module and imported here, so the authoritative - // body and the client's predicted body are bit-identical by construction - // instead of by a hand-synced "copied verbatim" duplicate. Re-exposed as a - // static only because external callers reference CharacterPhysical.boundRadius. - public static readonly boundRadius = boundRadius; - - private timeSinceDying = 0; - private isDestroyed = false; - private timeSinceBorn = 0; - private hasJustBorn = true; - private timeAlive = 0; - - private timeSinceLastShot = settings.projectileCreationInterval; - private timeSinceLastDamage = settings.playerOutOfCombatDelaySeconds; - private lastSyncedHealth = settings.playerMaxHealth; - - private killStreak = 0; - - private direction = 0; - private currentPlanet?: PlanetPhysical; - private secondsSinceOnSurface = settings.planetDetachmentSeconds; - - private bodyVelocity = vec2.create(); - private timeSinceLastLeap = settings.leapCooldownSeconds; + private jumpEnergyLeft = settings.defaultJumpEnergy; public head: CirclePhysical; public leftFoot: CirclePhysical; public rightFoot: CirclePhysical; - private movementState!: CharacterMovementState; - private movementWorld!: CharacterWorld; - private movementActions: Array = []; private lastMovementAction: MoveActionCommand = new MoveActionCommand(vec2.create()); - private headVelocity = new Circle(vec2.create(), 0); - private leftFootVelocity = new Circle(vec2.create(), 0); - private rightFootVelocity = new Circle(vec2.create(), 0); - protected commandExecutors: CommandExecutors = { [StepCommand.type]: this.step.bind(this), - [ReactToCollisionCommand.type]: this.onCollision.bind(this), + [MoveActionCommand.type]: (c: MoveActionCommand) => this.movementActions.push(c), }; - constructor( - name: string, - killCount: number, - deathCount: number, - team: CharacterTeam, - private readonly container: PhysicalContainer, - startPosition: vec2, - ) { - super(id(), name, killCount, deathCount, team, settings.playerMaxHealth); + private static readonly headOffset = vec2.fromValues(0, 40); + private static readonly leftFootOffset = vec2.fromValues(-20, -35); + private static readonly rightFootOffset = vec2.fromValues(20, -35); + + constructor(private readonly container: PhysicalContainer) { + super(id()); this.head = new CirclePhysical( - vec2.add(vec2.create(), startPosition, headOffset), - headRadius, + //vec2.clone(CharacterPhysical.headOffset), + [-2952.911, 215.241], + 50, this, container, ); this.leftFoot = new CirclePhysical( - vec2.add(vec2.create(), startPosition, leftFootOffset), - feetRadius, + // vec2.clone(CharacterPhysical.leftFootOffset), + [-2930.603, 162.542], + 20, this, container, ); this.rightFoot = new CirclePhysical( - vec2.add(vec2.create(), startPosition, rightFootOffset), - feetRadius, + //vec2.clone(CharacterPhysical.rightFootOffset), + [-2973.152, 167.921], + 20, this, container, ); container.addObject(this.head); container.addObject(this.leftFoot); container.addObject(this.rightFoot); - - this.initMovementBridge(); } - private initMovementBridge() { - // The movementState object-literal getters/setters below can't use `this` - // (it would bind to the literal), so alias the character instance. - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - this.movementState = { - head: this.head, - leftFoot: this.leftFoot, - rightFoot: this.rightFoot, - get direction() { - return self.direction; - }, - set direction(value: number) { - self.direction = value; - }, - get currentPlanet() { - return self.currentPlanet; - }, - set currentPlanet(value: GroundSurface | undefined) { - // On the server every ground is a PlanetPhysical (the world only ever - // hands back planets), so this narrowing is safe. - self.currentPlanet = value as PlanetPhysical | undefined; - }, - get secondsSinceOnSurface() { - return self.secondsSinceOnSurface; - }, - set secondsSinceOnSurface(value: number) { - self.secondsSinceOnSurface = value; - }, - bodyVelocity: this.bodyVelocity, - }; + private _boundingBox?: ImmutableBoundingBox; - this.movementWorld = { - // Same set and order forceAtPosition used: planets in the force field, - // in container-traversal order (so the f64 gravity sum is unchanged). - groundsNear: (center, radius) => - self.container - .findIntersecting(getBoundingBoxOfCircle(new Circle(center, radius))) - .filter((o): o is PlanetPhysical => o instanceof PlanetPhysical), - stepBody: (body, deltaTimeInSeconds) => { - const { hitObject } = (body as CirclePhysical).stepManually(deltaTimeInSeconds); - return hitObject instanceof PlanetPhysical ? hitObject : undefined; - }, - }; - } - - private get isSpawnProtected(): boolean { - return ( - this.timeAlive < - settings.spawnDespawnTime + settings.spawnInvulnerabilityExtraSeconds - ); - } - - private hasGeneratedPoints = false; - private getPoints(game: CommandReceiver) { - if (!this.isAlive && !this.hasGeneratedPoints) { - this.hasGeneratedPoints = true; - const blue = this.team === CharacterTeam.blue ? 0 : settings.playerKillPoint; - const red = this.team === CharacterTeam.red ? 0 : settings.playerKillPoint; - - game.handleCommand(new GeneratePointsCommand(blue, red)); - } - } - - public get isAlive(): boolean { - return !this.isDestroyed; - } - - public handleMovementAction(c: MoveActionCommand) { - this.movementActions.push(c); - } - - public get groundPlanet(): PlanetPhysical | undefined { - return this.currentPlanet; - } - - // Persistent launch momentum, streamed to the owning client so its predictor - // can reproduce a leap/slingshot/recoil flight instead of only snapping to it. - public get launchMomentum(): vec2 { - return this.bodyVelocity; - } - - public addKill(victimName: string, charge = 0) { - this.killCount++; - this.killStreak++; - this.remoteCall('setKillCount', this.killCount); - - this.health = Math.min( - settings.playerMaxHealth, - this.health + settings.playerKillHealthReward, - ); - this.syncHealth(); - this.remoteCall('onKillConfirmed', victimName, this.killStreak, charge); - } - - public registerHit(charge = 0) { - this.remoteCall('onHitConfirmed', charge); - } - - private syncHealth() { - const rounded = Math.round(this.health); - if (rounded !== this.lastSyncedHealth) { - this.lastSyncedHealth = rounded; - this.remoteCall('setHealth', this.health); - } - } - - public onCollision({ other }: ReactToCollisionCommand) { - if ( - // A corpse keeps its collidable circles for the despawn animation; the - // isAlive guard stops a flying corpse from eating shots aimed past it. - this.isAlive && - other instanceof ProjectilePhysical && - other.team !== this.team && - other.isAlive - ) { - other.destroy(); - - if (this.isSpawnProtected) { - return; - } - - this.timeSinceLastDamage = 0; - this.health -= other.strength; - this.lastSyncedHealth = Math.round(this.health); - this.remoteCall('setHealth', this.health); - - if (this.health <= 0 && this.isAlive) { - // Throw the corpse along the killing shot, harder for charged hits. - vec2.scaleAndAdd( - this.bodyVelocity, - this.bodyVelocity, - other.direction, - mix(settings.deathImpulseMin, settings.deathImpulseMax, other.charge), - ); - this.onDie(); - other.originator.addKill(this.name, other.charge); - } else { - other.originator.registerHit(other.charge); - } - } - } - - public shootTowards(position: vec2, charge = 0) { - if ( - !this.isAlive || - this.timeSinceLastShot < settings.projectileCreationInterval || - this.projectileStrength < settings.chargeShotStrengthMin - ) { - return; + public get boundingBox(): ImmutableBoundingBox { + if (!this._boundingBox) { + this._boundingBox = (this.head as CirclePhysical).boundingBox; } - this.timeSinceLastShot = 0; - - const c = clamp01(charge); - const desiredStrength = mix( - settings.chargeShotStrengthMin, - settings.chargeShotStrengthMax, - c, - ); - const strength = Math.min(desiredStrength, this.projectileStrength); - this.projectileStrength -= strength; - - const radius = mix(settings.chargeShotRadiusMin, settings.chargeShotRadiusMax, c); - const speed = mix(settings.chargeShotSpeedMin, settings.chargeShotSpeedMax, c); - - const direction = vec2.subtract(vec2.create(), position, this.center); - vec2.normalize(direction, direction); - // Keep the unit direction before vec2.scale repurposes it as the velocity. - const shotDirection = vec2.clone(direction); - const velocity = vec2.scale(direction, direction, speed); - const projectile = new ProjectilePhysical( - vec2.clone(this.center), - radius, - strength, - this.team, - velocity, - this, - this.container, - c, - ); - this.container.addObject(projectile); - - if (c > 0) { - vec2.scaleAndAdd( - this.bodyVelocity, - this.bodyVelocity, - shotDirection, - -settings.chargeShotRecoilMax * c, - ); - } - - this.remoteCall('onShoot', strength); + return this._boundingBox; } - public leap() { - if ( - !this.isAlive || - this.hasJustBorn || - !this.currentPlanet || - this.timeSinceLastLeap < settings.leapCooldownSeconds || - this.projectileStrength < settings.leapStrengthCost - ) { - return; - } - - this.timeSinceLastLeap = 0; - this.projectileStrength -= settings.leapStrengthCost; - - // Same impulse the client predicts with (shared), so a leap launches - // identically on both sides. - applyLeapImpulse(this.movementState, this.lastMovementAction.direction); - this.remoteCall('onLeap'); - } - - public get boundingBox(): BoundingBoxBase { - return getBoundingBoxOfCircle(new Circle(this.center, CharacterPhysical.boundRadius)); - } - - public get gameObject(): this { + public get gameObject(): CharacterPhysical { return this; } public get center(): vec2 { - const bodyCenter = vec2.add(vec2.create(), this.head.center, this.leftFoot.center); - vec2.add(bodyCenter, bodyCenter, this.rightFoot.center); - return vec2.scale(bodyCenter, bodyCenter, 1 / 3); + return this.head.center; } public distance(target: vec2): number { @@ -360,11 +93,11 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical this.head.distance(target), this.leftFoot.distance(target), this.rightFoot.distance(target), - ) - 5 + ) - 20 ); } - private averageAndResetMovementActions(): vec2 { + private sumAndResetMovementActions(): vec2 { let direction: vec2; if (this.movementActions.length === 0) { direction = vec2.clone(this.lastMovementAction.direction); @@ -380,183 +113,74 @@ export class CharacterPhysical extends CharacterBase implements DynamicPhysical this.movementActions = []; } - return vec2.length(direction) > 0 - ? vec2.normalize(direction, direction) - : vec2.create(); + return direction; } - private animateScaling(q: number) { - this.head.radius = headRadius * q; - this.leftFoot.radius = this.rightFoot.radius = feetRadius * q; - } + public step(c: StepCommand) { + const deltaTime = c.deltaTimeInMiliseconds / 1000; - public getPropertyUpdates(): PropertyUpdatesForObject { - return new PropertyUpdatesForObject(this.id, [ - new UpdatePropertyCommand('head', this.head, this.headVelocity), - new UpdatePropertyCommand('leftFoot', this.leftFoot, this.leftFootVelocity), - new UpdatePropertyCommand('rightFoot', this.rightFoot, this.rightFootVelocity), - new UpdatePropertyCommand( - 'strength', - this.projectileStrength, - settings.playerStrengthRegenerationPerSeconds, - ), - ]); - } + const direction = this.sumAndResetMovementActions(); + const isAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne; + this.jumpEnergyLeft += isAirborne ? -deltaTime : deltaTime; + this.jumpEnergyLeft = clamp(this.jumpEnergyLeft, 0, settings.defaultJumpEnergy); - private setPropertyUpdates( - oldHead: Circle, - oldLeftFoot: Circle, - oldRightFoot: Circle, - deltaTime: number, - ) { - this.headVelocity = new Circle( - vec2.scale( - oldHead.center, - vec2.subtract(oldHead.center, this.head.center, oldHead.center), - 1 / deltaTime, - ), - (this.head.radius - oldHead.radius) / deltaTime, - ); - - this.leftFootVelocity = new Circle( - vec2.scale( - oldLeftFoot.center, - vec2.subtract(oldLeftFoot.center, this.leftFoot.center, oldLeftFoot.center), - 1 / deltaTime, - ), - (this.leftFoot.radius - oldLeftFoot.radius) / deltaTime, - ); - - this.rightFootVelocity = new Circle( - vec2.scale( - oldRightFoot.center, - vec2.subtract(oldRightFoot.center, this.rightFoot.center, oldRightFoot.center), - 1 / deltaTime, - ), - (this.rightFoot.radius - oldRightFoot.radius) / deltaTime, - ); - - this.animateScaling(1); - } - - private step({ deltaTimeInSeconds, game }: StepCommand) { - this.getPoints(game); - this.timeAlive += deltaTimeInSeconds; - this.timeSinceLastLeap += deltaTimeInSeconds; - const oldHead = new Circle(vec2.clone(this.head.center), this.head.radius); - const oldLeftFoot = new Circle( - vec2.clone(this.leftFoot.center), - this.leftFoot.radius, - ); - const oldRightFoot = new Circle( - vec2.clone(this.rightFoot.center), - this.rightFoot.radius, - ); - - if (this.isDestroyed) { - if ((this.timeSinceDying += deltaTimeInSeconds) > settings.spawnDespawnTime) { - this.destroy(); - } else { - this.freeFallCorpse(deltaTimeInSeconds); - this.animateScaling(1 - this.timeSinceDying / settings.spawnDespawnTime); - } - this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTimeInSeconds); - return; - } - - if (this.hasJustBorn) { - if ((this.timeSinceBorn += deltaTimeInSeconds) > settings.spawnDespawnTime) { - this.hasJustBorn = false; - } else { - this.animateScaling(this.timeSinceBorn / settings.spawnDespawnTime); - } - this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTimeInSeconds); - return; - } - - if ( - (this.secondsSinceOnSurface += deltaTimeInSeconds) > - settings.planetDetachmentSeconds - ) { - this.currentPlanet = undefined; - } - - this.timeSinceLastShot += deltaTimeInSeconds; - - this.projectileStrength = Math.min( - settings.playerMaxStrength, - this.projectileStrength + - settings.playerStrengthRegenerationPerSeconds * deltaTimeInSeconds, - ); - - this.regenerateHealth(deltaTimeInSeconds); - - // The planet tallies who is standing on it and resolves capture itself, so - // a contested rock can freeze instead of two squads silently cancelling. - this.currentPlanet?.registerPresence(this); - - // The whole walking model — gravity gather, movement force, on/off-planet - // branch, posture springs, body-momentum, and stepping the three parts — - // is the shared simulation the client predicts with, so the two can never - // drift. Server-only concerns (scoring, health, shooting, spawn/death, - // ownership) stay here around it. - const direction = this.averageAndResetMovementActions(); - stepCharacterMovement( - this.movementState, - this.movementWorld, + const xMax = deltaTime * settings.maxAccelerationX; + const yMax = this.jumpEnergyLeft > 0 ? settings.maxAccelerationY : 0; + const movementForce = vec2.multiply( direction, - deltaTimeInSeconds, + direction, + vec2.fromValues(xMax, yMax), ); - this.setPropertyUpdates(oldHead, oldLeftFoot, oldRightFoot, deltaTimeInSeconds); - } + const sumBody = vec2.add(vec2.create(), this.head.center, this.leftFoot.center); + vec2.add(sumBody, sumBody, this.rightFoot.center); + vec2.scale(sumBody, sumBody, 1 / 3); - private freeFallCorpse(deltaTime: number) { - const intersecting = this.container.findIntersecting( - getBoundingBoxOfCircle( - new Circle( - this.center, - CharacterPhysical.boundRadius + settings.maxGravityDistance, - ), - ), + const headPosition = vec2.add(vec2.create(), sumBody, CharacterPhysical.headOffset); + + Spring.step(new Circle(headPosition, 0), this.head, 0, 30, deltaTime); + + const footDistance = vec2.distance( + CharacterPhysical.headOffset, + CharacterPhysical.leftFootOffset, ); - let grounded = false; - for (const part of [this.leftFoot, this.rightFoot, this.head]) { - part.applyForce(forceAtPosition(part.center, intersecting), deltaTime); - vec2.add(part.velocity, part.velocity, this.bodyVelocity); - const { hitObject } = part.stepManually(deltaTime); - if (hitObject instanceof PlanetPhysical) { - grounded = true; - } - } - // Brake with the exact shared model the living body uses: stiff on contact - // so the corpse skids to rest, gentle in the air, plus the constant stop and - // the speed cap — so a flung corpse comes to a definite stop instead of - // sliding forever. - decayMomentum(this.bodyVelocity, grounded, deltaTime); + + Spring.step( + new Circle(this.head.center, this.head.radius), + this.leftFoot, + footDistance, + 25, + deltaTime, + ); + Spring.step( + new Circle(this.head.center, this.head.radius), + this.rightFoot, + footDistance, + 25, + deltaTime, + ); + Spring.step( + this.leftFoot, + this.rightFoot, + vec2.distance(CharacterPhysical.leftFootOffset, CharacterPhysical.rightFootOffset), + 100, + deltaTime, + ); + + this.head.applyForce(movementForce, deltaTime); + this.leftFoot.applyForce(movementForce, deltaTime); + this.rightFoot.applyForce(movementForce, deltaTime); + + this.head.applyForce(settings.gravitationalForce, deltaTime); + this.leftFoot.applyForce(settings.gravitationalForce, deltaTime); + this.rightFoot.applyForce(settings.gravitationalForce, deltaTime); + + this.head.step(deltaTime); + this.leftFoot.step(deltaTime); + this.rightFoot.step(deltaTime); } - private regenerateHealth(deltaTimeInSeconds: number) { - this.timeSinceLastDamage += deltaTimeInSeconds; - if ( - this.timeSinceLastDamage > settings.playerOutOfCombatDelaySeconds && - this.health < settings.playerMaxHealth - ) { - this.health = Math.min( - settings.playerMaxHealth, - this.health + settings.playerHealthRegenerationPerSeconds * deltaTimeInSeconds, - ); - this.syncHealth(); - } - } - - public onDie() { - this.isDestroyed = true; - this.remoteCall('onDie'); - } - - private destroy() { - this.container.removeObject(this); + public destroy() { this.container.removeObject(this.head); this.container.removeObject(this.leftFoot); this.container.removeObject(this.rightFoot); diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index e1a6abf..b72d9c6 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -1,43 +1,33 @@ import { vec2 } from 'gl-matrix'; -import { - Circle, - CommandExecutors, - CommandReceiver, - GameObject, - resolveCircleMovement, - serializesTo, -} from 'shared'; +import { Circle, clamp, GameObject, serializesTo, settings } from 'shared'; +import { Physical } from '../physics/physical'; + import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { BoundingBoxBase } from '../physics/bounding-boxes/bounding-box-base'; +import { moveCircle } from '../physics/move-circle'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; -import { Physical } from '../physics/physicals/physical'; -import { ReactToCollisionCommand } from '../commands/react-to-collision'; @serializesTo(Circle) -export class CirclePhysical extends CommandReceiver implements Circle, DynamicPhysical { +export class CirclePhysical implements Circle, Physical { + readonly isInverted = false; readonly canCollide = true; readonly canMove = true; - public velocity = vec2.create(); - public lastNormal = vec2.fromValues(0, 1); + private _isAirborne = true; + private velocity = vec2.create(); + + public get isAirborne(): boolean { + return this._isAirborne; + } private _boundingBox: BoundingBox; - protected commandExecutors: CommandExecutors = { - [ReactToCollisionCommand.type]: this.onCollision.bind(this), - }; - constructor( private _center: vec2, private _radius: number, public owner: GameObject, private readonly container: PhysicalContainer, - // Public + readonly so a CirclePhysical structurally satisfies the shared - // PhysicsBody interface the movement simulation operates on. - public readonly restitution = 0, ) { - super(); this._boundingBox = new BoundingBox(); this.recalculateBoundingBox(); } @@ -55,10 +45,6 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh this.recalculateBoundingBox(); } - public onCollision(c: ReactToCollisionCommand) { - this.owner.handleCommand(c); - } - public get gameObject(): GameObject { return this.owner; } @@ -76,6 +62,31 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh return vec2.distance(target, this.center) - this.radius; } + public distanceBetween(target: Circle): number { + return vec2.distance(target.center, this.center) - this.radius - target.radius; + } + + public areIntersecting(other: Physical): boolean { + return other.distance(this.center) < this.radius; + } + + public isInside(other: Physical): boolean { + return other.distance(this.center) < -this.radius; + } + + public getPerimeterPoints(count: number): Array { + const result: Array = []; + for (let i = 0; i < count; i++) { + result.push( + vec2.fromValues( + Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x, + Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y, + ), + ); + } + return result; + } + private recalculateBoundingBox() { this._boundingBox.xMin = this.center.x - this._radius; this._boundingBox.xMax = this.center.x + this._radius; @@ -89,51 +100,48 @@ export class CirclePhysical extends CommandReceiver implements Circle, DynamicPh this.velocity, vec2.scale(vec2.create(), force, timeInSeconds), ); + + vec2.set( + this.velocity, + clamp(this.velocity.x, -settings.maxVelocityX, settings.maxVelocityX), + clamp(this.velocity.y, -settings.maxVelocityY, settings.maxVelocityY), + ); } - // Position-resolution for one tick. Delegates to the shared - // resolveCircleMovement so the server integrates a body with the exact same - // geometry the client predictor runs (shared/physics) — no parallel copy to - // keep in sync. The onHit callback dispatches the collision reactions at the - // same points the old inline move-circle did (both the initial march and the - // post-bounce slide). `possibleIntersectors`, when supplied, lets a caller - // that already broadphased (e.g. a projectile's gravity query) avoid a second - // container query; otherwise it is self-gathered from the swept bounding box. - public stepManually( - deltaTimeInSeconds: number, - possibleIntersectors?: Array, - ): { - hitObject: GameObject | undefined; - velocity: vec2; - } { - const intersecting = ( - possibleIntersectors ?? this.sweptBroadphase(deltaTimeInSeconds) - ).filter((b) => b.gameObject !== this.gameObject && b.canCollide); - - const { hitObject, velocity } = resolveCircleMovement( - this, - deltaTimeInSeconds, - intersecting, - (intersected) => { - const physical = intersected as Physical; - physical.handleCommand(new ReactToCollisionCommand(this.gameObject)); - this.handleCommand(new ReactToCollisionCommand(physical.gameObject)); - }, - ); - - return { hitObject: (hitObject as Physical | undefined)?.gameObject, velocity }; + public resetVelocity() { + this.velocity = vec2.create(); } - // Query the container with the bounding box grown by this tick's travel, so a - // fast-moving body still sees what it is about to sweep into. - private sweptBroadphase(deltaTimeInSeconds: number): Array { - const sweep = vec2.length( - vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds), + public step(deltaTimeInSeconds: number): boolean { + vec2.scale( + this.velocity, + this.velocity, + Math.pow(settings.velocityAttenuation, deltaTimeInSeconds), ); - this.radius += sweep; - const intersecting = this.container.findIntersecting(this.boundingBox); - this.radius -= sweep; - return intersecting; + + const distance = vec2.scale(vec2.create(), this.velocity, deltaTimeInSeconds); + + const distanceLength = vec2.length(distance); + const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep); + vec2.scale(distance, distance, 1 / stepCount); + + let wasHit = false; + + for (let i = 0; i < stepCount; i++) { + const { tangent, hitSurface } = moveCircle( + this, + vec2.clone(distance), + this.container.findIntersecting(this.boundingBox), + ); + + if (hitSurface) { + vec2.scale(this.velocity, tangent!, vec2.dot(tangent!, this.velocity)); + wasHit = true; + } + } + + this._isAirborne = !wasHit; + return wasHit; } public toArray(): Array { diff --git a/backend/src/objects/lamp-physical.ts b/backend/src/objects/lamp-physical.ts index 591b17c..f93e9d9 100644 --- a/backend/src/objects/lamp-physical.ts +++ b/backend/src/objects/lamp-physical.ts @@ -2,11 +2,13 @@ import { vec2, vec3 } from 'gl-matrix'; import { LampBase, settings, id, serializesTo } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; -import { StaticPhysical } from '../physics/physicals/static-physical'; + +import { Physical } from '../physics/physical'; @serializesTo(LampBase) -export class LampPhysical extends LampBase implements StaticPhysical { +export class LampPhysical extends LampBase implements Physical { public readonly canCollide = false; + public readonly isInverted = false; public readonly canMove = false; constructor(center: vec2, color: vec3, lightness: number) { @@ -28,15 +30,12 @@ export class LampPhysical extends LampBase implements StaticPhysical { return this._boundingBox; } - public get gameObject(): this { + public get gameObject(): LampPhysical { return this; } - public queueSetLight(color: vec3, lightness: number) { - this.remoteCall('setLight', color, lightness); - } - - public distance(target: vec2): number { - return vec2.distance(this.center, target); + // todo + public distance(_: vec2): number { + return 0; } } diff --git a/backend/src/objects/planet-physical.ts b/backend/src/objects/planet-physical.ts deleted file mode 100644 index 4305b18..0000000 --- a/backend/src/objects/planet-physical.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -import { - clamp, - clamp01, - id, - mix, - Random, - serializesTo, - settings, - PlanetBase, - CharacterTeam, - PropertyUpdatesForObject, - UpdatePropertyCommand, - CommandExecutors, - CommandReceiver, -} from 'shared'; -import { GeneratePointsCommand } from '../commands/generate-points'; -import { AnnounceCommand } from '../commands/announce'; -import { StepCommand } from '../commands/step'; - -import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; -import { StaticPhysical } from '../physics/physicals/static-physical'; -import { LampPhysical } from './lamp-physical'; -import type { CharacterPhysical } from './character-physical'; - -@serializesTo(PlanetBase) -export class PlanetPhysical extends PlanetBase implements StaticPhysical { - public readonly canCollide = true; - public readonly canMove = false; - // Marks this as standable ground for the shared movement simulation (a body - // landing on it latches it as currentPlanet). See shared GroundSurface. - public readonly isGround = true; - - public readonly sizePointMultiplier: number; - - // Planets slowly spin. The angle is authoritative here and streamed to the - // client (see getPropertyUpdates), so the rendered outline and this collision - // polygon turn as one rigid body. cos/sin are memoised per angle because - // distance() is called many times per tick by the raymarcher and SDF sampling. - private rotation = 0; - private readonly rotationSpeed: number; - private cachedRotation = Number.NaN; - private cosRotation = 1; - private sinRotation = 0; - - private _boundingBox?: ImmutableBoundingBox; - - private readonly lamps: Array = []; - - private lastTeam: CharacterTeam = CharacterTeam.neutral; - - // Characters standing on the planet this tick. Filled by registerPresence as - // each grounded character steps, drained when the planet resolves capture in - // its own step(). Drives the head-count tug-of-war. - private presentCharacters: Array = []; - private isContested = false; - - protected commandExecutors: CommandExecutors = { - [StepCommand.type]: this.step.bind(this), - }; - - public addLamp(lamp: LampPhysical) { - this.lamps.push(lamp); - } - - constructor(vertices: Array, isKeystone = false) { - super(id(), vertices, 0.5, isKeystone); - - const sizeClass = clamp01( - (this.radius - settings.planetMinReferenceRadius) / - (settings.planetMaxReferenceRadius - settings.planetMinReferenceRadius), - ); - - this.sizePointMultiplier = mix(1, settings.planetSizePointMultiplierMax, sizeClass); - - this.rotationSpeed = - (0.05 + Random.getRandom() * 0.07) * (Random.getRandom() < 0.5 ? -1 : 1); - } - - // A grounded character announces itself each tick so the planet can resolve - // contested capture from the net head-count. - public registerPresence(character: CharacterPhysical) { - this.presentCharacters.push(character); - } - - public distance(target: vec2): number { - // Evaluate the SDF in the planet's own rotating frame so this collision - // outline turns in lockstep with the rendered planet (see planet-shape.ts). - const local = this.toLocalFrame(target); - - const startEnd = this.vertices[0]; - let vb = startEnd; - - let d = vec2.dist(local, vb); - let sign = 1; - - for (let i = 1; i <= this.vertices.length; i++) { - const va = vb; - vb = i === this.vertices.length ? startEnd : this.vertices[i]; - const targetFromDelta = vec2.subtract(vec2.create(), local, va); - const toFromDelta = vec2.subtract(vec2.create(), vb, va); - const h = clamp01( - vec2.dot(targetFromDelta, toFromDelta) / vec2.squaredLength(toFromDelta), - ); - - const ds = vec2.fromValues( - vec2.dist(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)), - toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x, - ); - - if ( - (local.y >= va.y && local.y < vb.y && ds.y > 0) || - (local.y < va.y && local.y >= vb.y && ds.y <= 0) - ) { - sign *= -1; - } - - d = Math.min(d, ds.x); - } - - return sign * d; - } - - // Rotate a world point by -rotation about the centre, matching the shader's - // `localTarget = center + R(rotation) * (target - center)` transform exactly. - private toLocalFrame(target: vec2): vec2 { - if (this.rotation !== this.cachedRotation) { - this.cachedRotation = this.rotation; - this.cosRotation = Math.cos(this.rotation); - this.sinRotation = Math.sin(this.rotation); - } - - const dx = target.x - this.center.x; - const dy = target.y - this.center.y; - - return vec2.fromValues( - this.center.x + this.cosRotation * dx - this.sinRotation * dy, - this.center.y + this.sinRotation * dx + this.cosRotation * dy, - ); - } - - // Signed angular velocity in rad/s, exposed so a character standing on the - // planet can ride its spin (see carryWithRotatingPlanet in shared). - public get angularVelocity(): number { - return this.rotationSpeed; - } - - public get team(): CharacterTeam { - return Math.abs(this.ownership - 0.5) < settings.planetControlThreshold - ? CharacterTeam.neutral - : this.ownership < 0.5 - ? CharacterTeam.blue - : CharacterTeam.red; - } - - private timeSinceLastPointGeneration = 0; - private getPoints(game: CommandReceiver) { - if (this.timeSinceLastPointGeneration > settings.planetPointGenerationInterval) { - this.timeSinceLastPointGeneration = 0; - - const value = Math.round( - settings.planetPointGenerationValue * this.sizePointMultiplier, - ); - game.handleCommand( - new GeneratePointsCommand( - this.team === CharacterTeam.blue ? value : 0, - this.team === CharacterTeam.red ? value : 0, - ), - ); - } - } - - private step({ deltaTimeInSeconds, game }: StepCommand) { - this.rotation += deltaTimeInSeconds * this.rotationSpeed; - this.timeSinceLastPointGeneration += deltaTimeInSeconds; - - // In reverse order, so that teams can achieve a 100% control. - this.getPoints(game); - this.resolveCapture(deltaTimeInSeconds); - this.detectFlip(game); - - this.presentCharacters = []; - } - - // One capture step per tick driven by the net team head-count, so grouping up - // pays off and an equal standoff freezes the planet (contested) instead of - // both sides silently cancelling with no feedback. - private resolveCapture(deltaTime: number) { - let blue = 0; - let red = 0; - for (const c of this.presentCharacters) { - if (c.team === CharacterTeam.blue) { - blue++; - } else if (c.team === CharacterTeam.red) { - red++; - } - } - const net = red - blue; - const occupied = blue + red > 0; - - if (net !== 0) { - const lead = Math.min(Math.abs(net), settings.maxContestLeadMultiplier); - this.takeControl( - net > 0 ? CharacterTeam.red : CharacterTeam.blue, - deltaTime * lead, - ); - } else if (!occupied) { - // Empty planets drift back to neutral; the keystone drifts much slower so - // it lingers as a live flashpoint. - this.takeControl( - CharacterTeam.neutral, - this.isKeystone ? deltaTime / settings.keystoneLoseControlScale : deltaTime, - ); - } - // occupied tie -> frozen tug-of-war: no ownership change, ring pulses. - - const contested = occupied && net === 0; - if (contested !== this.isContested) { - this.isContested = contested; - this.remoteCall('setContested', contested); - } - } - - private detectFlip(game: CommandReceiver) { - const currentTeam = this.team; - if (currentTeam === this.lastTeam) { - return; - } - this.lastTeam = currentTeam; - - if (currentTeam !== CharacterTeam.neutral) { - const reward = Math.round( - settings.captureFlipPointReward * this.sizePointMultiplier, - ); - this.remoteCall('generatedPoints', reward); - game.handleCommand( - new GeneratePointsCommand( - currentTeam === CharacterTeam.blue ? reward : 0, - currentTeam === CharacterTeam.red ? reward : 0, - ), - ); - - if (this.isKeystone) { - game.handleCommand( - new AnnounceCommand( - `Team ${currentTeam} captured the Heart`, - ), - ); - } - } - - const control = Math.abs(this.ownership - 0.5) / 0.5; - const lightness = mix(settings.lampMinLightness, settings.lampMaxLightness, control); - const color = settings.palette[settings.colorIndices[currentTeam]]; - - this.lamps.forEach((lamp) => lamp.queueSetLight(color, lightness)); - - this.remoteCall('onFlipped', currentTeam); - } - - public getPropertyUpdates(): PropertyUpdatesForObject { - return new PropertyUpdatesForObject(this.id, [ - new UpdatePropertyCommand('ownership', this.ownership, 0), - // Stream the spin rate as the rate-of-change so the client can keep the - // angle moving when snapshots run late (see planet-view.ts). - new UpdatePropertyCommand('rotation', this.rotation, this.rotationSpeed), - ]); - } - - public takeControl(team: CharacterTeam, deltaTime: number) { - if (team === CharacterTeam.blue) { - this.ownership -= (0.5 / settings.takeControlTimeInSeconds) * deltaTime; - } else if (team === CharacterTeam.red) { - this.ownership += (0.5 / settings.takeControlTimeInSeconds) * deltaTime; - } else { - const previous = this.ownership; - this.ownership += - -Math.sign(this.ownership - 0.5) * - (0.5 / settings.loseControlTimeInSeconds) * - deltaTime; - if ( - (previous < 0.5 && this.ownership > 0.5) || - (previous > 0.5 && this.ownership < 0.5) - ) { - this.ownership = 0.5; - } - } - - this.ownership = clamp01(this.ownership); - } - - public get boundingBox(): ImmutableBoundingBox { - if (!this._boundingBox) { - // The polygon spins about its centre (see distance), so this static box - // has to cover every orientation, not just the spawn-time one: take the - // circumscribed circle around the rotation centre. - const maxVertexDistance = this.vertices.reduce( - (max, vertex) => Math.max(max, vec2.distance(this.center, vertex)), - 0, - ); - - this._boundingBox = new ImmutableBoundingBox( - this.center.x - maxVertexDistance, - this.center.x + maxVertexDistance, - this.center.y - maxVertexDistance, - this.center.y + maxVertexDistance, - ); - } - - return this._boundingBox; - } - - public getForce(position: vec2): vec2 { - const diff = vec2.subtract(vec2.create(), this.center, position); - const dist = Math.max(settings.minGravityDistance, vec2.length(diff) - this.radius); - vec2.normalize(diff, diff); - const scale = clamp( - settings.maxGravityQ * ((settings.maxGravityDistance / dist) ** 1.5 - 1), - 0, - settings.maxGravityStrength, - ); - return vec2.scale(diff, diff, scale); - } - - // GroundSurface alias the shared movement simulation calls for gravity. - public gravityAt(position: vec2): vec2 { - return this.getForce(position); - } - - public get gameObject(): this { - return this; - } -} diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index 8a967f1..bca9efb 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -1,88 +1,41 @@ import { vec2 } from 'gl-matrix'; import { id, + StepCommand, settings, + CommandExecutors, serializesTo, ProjectileBase, - CharacterTeam, - PropertyUpdatesForObject, - UpdatePropertyCommand, - CommandExecutors, - Circle, - marchCircle, } from 'shared'; import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; import { CirclePhysical } from './circle-physical'; -import { DynamicPhysical } from '../physics/physicals/dynamic-physical'; +import { Physical } from '../physics/physical'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { CharacterPhysical } from './character-physical'; -import { forceAtPosition } from '../physics/functions/force-at-position'; -import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; -import { StepCommand } from '../commands/step'; -import { ReactToCollisionCommand } from '../commands/react-to-collision'; @serializesTo(ProjectileBase) -export class ProjectilePhysical extends ProjectileBase implements DynamicPhysical { +export class ProjectilePhysical extends ProjectileBase implements Physical { public readonly canCollide = true; + public readonly isInverted = false; public readonly canMove = true; - private isDestroyed = false; - private bounceCount = 0; - private _boundingBox?: ImmutableBoundingBox; - public object: CirclePhysical; protected commandExecutors: CommandExecutors = { - [StepCommand.type]: this.handleStep.bind(this), - [ReactToCollisionCommand.type]: this.onCollision.bind(this), + [StepCommand.type]: this.step.bind(this), }; constructor( center: vec2, radius: number, - public strength: number, - team: CharacterTeam, - private velocity: vec2, - public readonly originator: CharacterPhysical, + startingForce: vec2, readonly container: PhysicalContainer, - // Normalised charge (0..1) of the shot that fired this, used by the victim - // to scale hit/kill feedback and the death fling. - public readonly charge: number = 0, ) { - super(id(), center, radius, team, strength); - this.object = new CirclePhysical(center, radius, this, container, 0.9); - - this.moveOutsideOfObject(); + super(id(), center, radius); + this.object = new CirclePhysical(center, radius, this, container); + this.object.applyForce(startingForce, 1000); } - public get isAlive(): boolean { - return !this.isDestroyed; - } - - public get direction(): vec2 { - const direction = vec2.clone(this.velocity); - return vec2.length(direction) > 0 - ? vec2.normalize(direction, direction) - : vec2.fromValues(0, -1); - } - - private moveOutsideOfObject() { - let wasCollision = true; - const delta = vec2.scale( - vec2.create(), - vec2.normalize(vec2.create(), this.velocity), - 10, - ); - while (wasCollision) { - const intersecting = this.container - .findIntersecting(this.boundingBox) - .filter((g) => g instanceof CharacterPhysical && g.team === this.team); - const { hitSurface } = marchCircle(this.object, delta, intersecting, true); - wasCollision = hitSurface; - } - vec2.add(this.center, this.center, delta); - vec2.add(this.center, this.center, delta); - } + private _boundingBox?: ImmutableBoundingBox; public get boundingBox(): ImmutableBoundingBox { if (!this._boundingBox) { @@ -100,61 +53,9 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica return this.object.distance(target); } - public destroy() { - if (!this.isDestroyed) { - this.isDestroyed = true; - this.container.removeObject(this); - } - } - - public onCollision({ other }: ReactToCollisionCommand) { - if ( - !(other instanceof CharacterPhysical && other.team === this.team) && - this.bounceCount++ === settings.projectileMaxBounceCount - ) { - this.destroy(); - } - } - - public getPropertyUpdates(): PropertyUpdatesForObject { - return new PropertyUpdatesForObject(this.id, [ - new UpdatePropertyCommand('center', this.center, this.velocity), - ]); - } - - private handleStep({ deltaTimeInSeconds }: StepCommand) { - super.step(deltaTimeInSeconds); - - if (this.strength <= 0) { - this.destroy(); - return; - } - - // Curveball: the same planetary gravity that pulls on a free-falling - // character bends the shot, so slower (charged) shots arc and can be lobbed - // over a planet's horizon. Scale is tiny — near-surface gravity is huge. - // This single broadphase is reused for the step below: the gravity radius - // (maxGravityDistance) dwarfs one tick's travel, so the set is a superset of - // the swept-collision box and the step needn't query the container again. - const intersecting = settings.projectileGravityEnabled - ? this.container.findIntersecting( - getBoundingBoxOfCircle( - new Circle(this.center, this.object.radius + settings.maxGravityDistance), - ), - ) - : undefined; - - if (intersecting) { - vec2.scaleAndAdd( - this.velocity, - this.velocity, - forceAtPosition(this.center, intersecting), - settings.projectileGravityScale * deltaTimeInSeconds, - ); - } - - vec2.copy(this.object.velocity, this.velocity); - const { velocity } = this.object.stepManually(deltaTimeInSeconds, intersecting); - vec2.copy(this.velocity, velocity); + public step(c: StepCommand) { + const deltaTime = c.deltaTimeInMiliseconds / 1000; + this.object.applyForce(settings.gravitationalForce, deltaTime); + this.object.step(deltaTime); } } diff --git a/backend/src/objects/spring.ts b/backend/src/objects/spring.ts new file mode 100644 index 0000000..91f121b --- /dev/null +++ b/backend/src/objects/spring.ts @@ -0,0 +1,37 @@ +import { vec2 } from 'gl-matrix'; +import { Circle } from 'shared'; +import { CirclePhysical } from './circle-physical'; + +export class Spring { + constructor( + private a: CirclePhysical | Circle, + private b: CirclePhysical | Circle, + private distance: number, + private strength: number, + ) {} + + public step(deltaTimeInSeconds: number) { + Spring.step(this.a, this.b, this.distance, this.strength, deltaTimeInSeconds); + } + + public static step( + a: CirclePhysical | Circle, + b: CirclePhysical | Circle, + distance: number, + strength: number, + deltaTimeInSeconds: number, + ) { + const length = vec2.dist(a.center, b.center) - distance; + + const abDirection = vec2.subtract(vec2.create(), b.center, a.center); + vec2.normalize(abDirection, abDirection); + const force = vec2.scale(abDirection, abDirection, strength * length); + if (a instanceof CirclePhysical) { + a.applyForce(force, deltaTimeInSeconds); + } + if (b instanceof CirclePhysical) { + vec2.scale(force, force, -1); + b.applyForce(force, deltaTimeInSeconds); + } + } +} diff --git a/backend/src/objects/tunnel-physical.ts b/backend/src/objects/tunnel-physical.ts new file mode 100644 index 0000000..54042f7 --- /dev/null +++ b/backend/src/objects/tunnel-physical.ts @@ -0,0 +1,48 @@ +import { vec2 } from 'gl-matrix'; +import { clamp01, mix, TunnelBase, id, serializesTo } from 'shared'; + +import { ImmutableBoundingBox } from '../physics/bounding-boxes/immutable-bounding-box'; +import { StaticPhysical } from '../physics/containers/static-physical-object'; + +@serializesTo(TunnelBase) +export class TunnelPhysical extends TunnelBase implements StaticPhysical { + public readonly canCollide = true; + public readonly isInverted = true; + public readonly canMove = false; + + private _boundingBox?: ImmutableBoundingBox; + + constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) { + super(id(), from, to, fromRadius, toRadius); + } + + public distance(target: vec2): number { + const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); + const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); + + const h = clamp01( + vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta), + ); + + return ( + vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - + mix(this.fromRadius, this.toRadius, h) + ); + } + + public get boundingBox(): ImmutableBoundingBox { + if (!this._boundingBox) { + const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius); + const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius); + const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius); + const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius); + this._boundingBox = new ImmutableBoundingBox(xMin, xMax, yMin, yMax); + } + + return this._boundingBox; + } + + public get gameObject(): TunnelPhysical { + return this; + } +} diff --git a/backend/src/options.ts b/backend/src/options.ts deleted file mode 100644 index ba28c16..0000000 --- a/backend/src/options.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Options { - port: number; - name: string; - playerLimit: number; - scoreLimit: number; - npcCount: number; - seed: number; -} diff --git a/backend/src/physics/bounding-boxes/bounding-box-base.ts b/backend/src/physics/bounding-boxes/bounding-box-base.ts index 91946c9..8bb1c1b 100644 --- a/backend/src/physics/bounding-boxes/bounding-box-base.ts +++ b/backend/src/physics/bounding-boxes/bounding-box-base.ts @@ -1,7 +1,6 @@ import { vec2 } from 'gl-matrix'; -// axis-aligned -export abstract class BoundingBoxBase { +export class BoundingBoxBase { constructor( protected _xMin: number = 0, protected _xMax: number = 0, @@ -9,8 +8,6 @@ export abstract class BoundingBoxBase { protected _yMax: number = 0, ) {} - [key: number]: number | undefined; - public get 0(): number { return this._xMin; } diff --git a/backend/src/physics/containers/bounding-box-list.ts b/backend/src/physics/containers/bounding-box-list.ts index 4311f62..df48c07 100644 --- a/backend/src/physics/containers/bounding-box-list.ts +++ b/backend/src/physics/containers/bounding-box-list.ts @@ -1,25 +1,21 @@ -import { DynamicPhysical } from '../physicals/dynamic-physical'; +import { Physical } from '../physical'; import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; export class BoundingBoxList { - constructor(private objects: Array = []) {} + constructor(private objects: Array = []) {} - public insert(object: DynamicPhysical) { + public insert(object: Physical) { this.objects.push(object); } - public remove(object: DynamicPhysical) { + public remove(object: Physical) { this.objects.splice( this.objects.findIndex((i) => i === object), 1, ); } - public forEach(func: (object: DynamicPhysical) => unknown) { - this.objects.forEach(func); - } - - public findIntersecting(boundingBox: BoundingBoxBase): Array { + public findIntersecting(boundingBox: BoundingBoxBase): Array { return this.objects.filter((b) => b.boundingBox.intersects(boundingBox)); } } diff --git a/backend/src/physics/containers/bounding-box-tree.ts b/backend/src/physics/containers/bounding-box-tree.ts index 39981ff..3821033 100644 --- a/backend/src/physics/containers/bounding-box-tree.ts +++ b/backend/src/physics/containers/bounding-box-tree.ts @@ -1,18 +1,15 @@ import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; -import { StaticPhysical } from '../physicals/static-physical'; - +import { StaticPhysical } from './static-physical-object'; // source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js + class Node { - public left: Node | null = null; - public right: Node | null = null; - constructor( - public object: StaticPhysical, - public parent: Node | null, - ) {} + public left?: Node = null; + public right?: Node = null; + constructor(public object: StaticPhysical, public parent: Node) {} } export class BoundingBoxTree { - root?: Node | null; + root?: Node; constructor(objects: Array = []) { this.build(objects); @@ -25,8 +22,8 @@ export class BoundingBoxTree { private buildRecursive( objects: Array, depth: number, - parent: Node | null, - ): Node | null { + parent: Node, + ): Node { if (objects.length === 0) { return null; } @@ -37,7 +34,7 @@ export class BoundingBoxTree { const dimension = depth % 4; - objects.sort((a, b) => a.boundingBox[dimension]! - b.boundingBox[dimension]!); + objects.sort((a, b) => a.boundingBox[dimension] - b.boundingBox[dimension]); const median = Math.floor(objects.length / 2); @@ -57,9 +54,7 @@ export class BoundingBoxTree { const node = new Node(object, insertPosition); const dimension = depth % 4; - if ( - object.boundingBox[dimension]! < insertPosition.object.boundingBox[dimension]! - ) { + if (object.boundingBox[dimension] < insertPosition.object.boundingBox[dimension]) { insertPosition.left = node; } else { insertPosition.right = node; @@ -68,14 +63,14 @@ export class BoundingBoxTree { } public findIntersecting(boundingBox: BoundingBoxBase): Array { - const maybeResults = this.findMaybeIntersecting(boundingBox, this.root!, 0); + const maybeResults = this.findMaybeIntersecting(boundingBox, this.root, 0); const results = maybeResults.filter((b) => b.boundingBox.intersects(boundingBox)); return results; } private findMaybeIntersecting( boundingBox: BoundingBoxBase, - node: Node | null, + node: Node, depth: number, ): Array { if (node === null) { @@ -107,17 +102,17 @@ export class BoundingBoxTree { private findParent( object: StaticPhysical, - node: Node | null | undefined, + node: Node, depth: number, - parent: Node | null, - ): [Node | null, number] { - if (!node) { + parent: Node, + ): [Node, number] { + if (node === null) { return [parent, depth - 1]; } const dimension = depth % 4; - if (object.boundingBox[dimension]! < node.object.boundingBox[dimension]!) { + if (object.boundingBox[dimension] < node.object.boundingBox[dimension]) { return this.findParent(object, node.left, depth + 1, node); } diff --git a/backend/src/physics/containers/physical-container.ts b/backend/src/physics/containers/physical-container.ts index 604672e..ab34fda 100644 --- a/backend/src/physics/containers/physical-container.ts +++ b/backend/src/physics/containers/physical-container.ts @@ -1,30 +1,39 @@ +import { GameObject, Id } from 'shared'; import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; + import { BoundingBoxList } from './bounding-box-list'; import { BoundingBoxTree } from './bounding-box-tree'; -import { Physical } from '../physicals/physical'; -import { StaticPhysical } from '../physicals/static-physical'; -import { DynamicPhysical } from '../physicals/dynamic-physical'; -import { Command, CommandReceiver } from 'shared'; +import { Command } from 'shared'; +import { Physical } from '../physical'; +import { StaticPhysical } from './static-physical-object'; -export class PhysicalContainer extends CommandReceiver { +export class PhysicalContainer { private isTreeInitialized = false; private staticBoundingBoxesWaitList: Array = []; private staticBoundingBoxes = new BoundingBoxTree(); private dynamicBoundingBoxes = new BoundingBoxList(); - private objects: Array = []; - protected defaultCommandExecutor(c: Command) { - this.objects.forEach((o) => o.handleCommand(c)); - } + protected objects: Map = new Map(); + private objectsGroupedByAbilities: Map> = new Map(); public initialize() { this.staticBoundingBoxes.build(this.staticBoundingBoxesWaitList); this.isTreeInitialized = true; } - public addObject(physical: Physical) { - this.objects.push(physical); + public addObject(object: Physical) { + this.objects.set(object.gameObject.id, object.gameObject); + for (const command of this.objectsGroupedByAbilities.keys()) { + if (object.gameObject.reactsToCommand(command)) { + this.objectsGroupedByAbilities.get(command).push(object.gameObject); + } + } + + this.addPhysical(object); + } + + public addPhysical(physical: Physical) { if (physical.canMove) { this.dynamicBoundingBoxes.insert(physical); } else { @@ -36,13 +45,39 @@ export class PhysicalContainer extends CommandReceiver { } } - public removeObject(object: DynamicPhysical) { - this.objects = this.objects.filter((p) => p !== object); + public removeObject(object: Physical) { + this.objects.delete(object.gameObject.id); + + for (const command of this.objectsGroupedByAbilities.keys()) { + if (object.gameObject.reactsToCommand(command)) { + const array = this.objectsGroupedByAbilities.get(command); + array.splice( + array.findIndex((i) => i.id == object.gameObject.id), + 1, + ); + } + } + this.dynamicBoundingBoxes.remove(object); } - public resetRemoteCalls() { - this.objects.forEach((o) => o.gameObject.resetRemoteCalls()); + public sendCommand(e: Command) { + if (!this.objectsGroupedByAbilities.has(e.type)) { + this.createGroupForCommand(e.type); + } + + this.objectsGroupedByAbilities.get(e.type).forEach((o, _) => o.sendCommand(e)); + } + + private createGroupForCommand(commandType: string) { + const objectsReactingToCommand = []; + this.objects.forEach((o, _) => { + if (o.reactsToCommand(commandType)) { + objectsReactingToCommand.push(o); + } + }); + + this.objectsGroupedByAbilities.set(commandType, objectsReactingToCommand); } public findIntersecting(box: BoundingBoxBase): Array { diff --git a/backend/src/physics/physicals/static-physical.ts b/backend/src/physics/containers/static-physical-object.ts similarity index 50% rename from backend/src/physics/physicals/static-physical.ts rename to backend/src/physics/containers/static-physical-object.ts index eef168b..2ad7303 100644 --- a/backend/src/physics/physicals/static-physical.ts +++ b/backend/src/physics/containers/static-physical-object.ts @@ -1,7 +1,6 @@ -import { PhysicalBase } from './physical-base'; +import { Physical } from '../physical'; import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box'; -export interface StaticPhysical extends PhysicalBase { - readonly canMove: false; +export interface StaticPhysical extends Physical { readonly boundingBox: ImmutableBoundingBox; } diff --git a/backend/src/physics/functions/force-at-position.ts b/backend/src/physics/functions/force-at-position.ts deleted file mode 100644 index 85bf2e3..0000000 --- a/backend/src/physics/functions/force-at-position.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { PlanetPhysical } from '../../objects/planet-physical'; -import { Physical } from '../physicals/physical'; - -export const forceAtPosition = (position: vec2, objects: Array) => - objects - .filter((o) => o instanceof PlanetPhysical) - .reduce( - (sum: vec2, o: Physical) => - vec2.add(sum, sum, (o as PlanetPhysical).getForce(position)), - vec2.create(), - ); diff --git a/backend/src/physics/functions/get-bounding-box-of-circle.ts b/backend/src/physics/functions/get-bounding-box-of-circle.ts deleted file mode 100644 index 9c5b2eb..0000000 --- a/backend/src/physics/functions/get-bounding-box-of-circle.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Circle } from 'shared'; -import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; -import { ImmutableBoundingBox } from '../bounding-boxes/immutable-bounding-box'; - -export const getBoundingBoxOfCircle = (circle: Circle): BoundingBoxBase => - new ImmutableBoundingBox( - circle.center.x - circle.radius, - circle.center.x + circle.radius, - circle.center.y - circle.radius, - circle.center.y + circle.radius, - ); diff --git a/backend/src/physics/functions/is-circle-intersecting.ts b/backend/src/physics/functions/is-circle-intersecting.ts deleted file mode 100644 index 203eb1d..0000000 --- a/backend/src/physics/functions/is-circle-intersecting.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Circle, evaluateSdf } from 'shared'; -import { PhysicalBase } from '../physicals/physical-base'; - -export const isCircleIntersecting = ( - circle: Circle, - intersectors: Array, -): boolean => evaluateSdf(circle.center, intersectors) < circle.radius; diff --git a/backend/src/physics/move-circle.ts b/backend/src/physics/move-circle.ts new file mode 100644 index 0000000..234a814 --- /dev/null +++ b/backend/src/physics/move-circle.ts @@ -0,0 +1,59 @@ +import { vec2 } from 'gl-matrix'; +import { Circle, rotate90Deg } from 'shared'; +import { CirclePhysical } from '../objects/circle-physical'; +import { Physical } from './physical'; + +export const moveCircle = ( + circle: CirclePhysical, + delta: vec2, + possibleIntersectors: Array, +): { + realDelta: vec2; + hitSurface: boolean; + normal?: vec2; + tangent?: vec2; +} => { + const nextCircle = new Circle(vec2.clone(circle.center), circle.radius); + vec2.add(nextCircle.center, nextCircle.center, delta); + + possibleIntersectors = possibleIntersectors.filter( + (b) => b.gameObject !== circle.gameObject && b.canCollide, + ); + + const getSdfAtPoint = (point: vec2): number => { + const sdf = possibleIntersectors + .filter((i) => i.isInverted) + .reduce((min, i) => (min = Math.max(min, -i.distance(point))), -1000); + + return possibleIntersectors + .filter((i) => !i.isInverted) + .reduce((min, i) => (min = Math.min(min, i.distance(point))), sdf); + }; + + const sdfAtCenter = getSdfAtPoint(nextCircle.center); + + if (sdfAtCenter > nextCircle.radius) { + circle.center = vec2.add(circle.center, circle.center, delta); + return { + realDelta: delta, + hitSurface: false, + }; + } + + const dx = + getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(1, 0))) - + sdfAtCenter; + const dy = + getSdfAtPoint(vec2.add(vec2.create(), nextCircle.center, vec2.fromValues(0, 1))) - + sdfAtCenter; + const normal = vec2.fromValues(dx, dy); + + vec2.normalize(normal, normal); + + return { + realDelta: delta, + hitSurface: true, + normal, + tangent: rotate90Deg(normal), + }; +}; diff --git a/backend/src/physics/physicals/physical-base.ts b/backend/src/physics/physical.ts similarity index 53% rename from backend/src/physics/physicals/physical-base.ts rename to backend/src/physics/physical.ts index ab41eac..0948fd3 100644 --- a/backend/src/physics/physicals/physical-base.ts +++ b/backend/src/physics/physical.ts @@ -1,8 +1,9 @@ import { vec2 } from 'gl-matrix'; -import { CommandReceiver, GameObject } from 'shared'; -import { BoundingBoxBase } from '../bounding-boxes/bounding-box-base'; +import { GameObject } from 'shared'; +import { BoundingBoxBase } from './bounding-boxes/bounding-box-base'; -export interface PhysicalBase extends CommandReceiver { +export interface Physical { + readonly isInverted: boolean; readonly canCollide: boolean; readonly canMove: boolean; readonly boundingBox: BoundingBoxBase; diff --git a/backend/src/physics/physicals/dynamic-physical.ts b/backend/src/physics/physicals/dynamic-physical.ts deleted file mode 100644 index d72cd9d..0000000 --- a/backend/src/physics/physicals/dynamic-physical.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PhysicalBase } from './physical-base'; - -export interface DynamicPhysical extends PhysicalBase { - readonly canMove: true; -} diff --git a/backend/src/physics/physicals/physical.ts b/backend/src/physics/physicals/physical.ts deleted file mode 100644 index 2e3189b..0000000 --- a/backend/src/physics/physicals/physical.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { DynamicPhysical } from './dynamic-physical'; -import { StaticPhysical } from './static-physical'; - -export type Physical = StaticPhysical | DynamicPhysical; diff --git a/backend/src/players/npc.ts b/backend/src/players/npc.ts deleted file mode 100644 index 655ade7..0000000 --- a/backend/src/players/npc.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - PlayerInformation, - settings, - Circle, - Random, - MoveActionCommand, - CharacterTeam, - Id, -} from 'shared'; -import { PhysicalContainer } from '../physics/containers/physical-container'; -import { PlayerContainer } from './player-container'; -import { PlayerBase } from './player-base'; -import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; -import { CharacterPhysical } from '../objects/character-physical'; -import { PlanetPhysical } from '../objects/planet-physical'; -import { ProjectilePhysical } from '../objects/projectile-physical'; -import { Physical } from '../physics/physicals/physical'; - -const npcTuning = { - planIntervalSeconds: 1, - shootIntervalSeconds: 1.5, - reactionIntervalSeconds: 1 / 10, - reactionObserveRadius: 1400, - planScanRadius: 3500, - - aggressionMin: 0.25, - aggressionMax: 1, - - wanderReconsiderSeconds: 5, - wanderProbability: 0.4, - wanderTurn: 0.3, - - fleeBaseRange: 240, - fleeAggressionFalloff: 1.4, - - chaseBaseRange: 700, - chaseAggressionRange: 1600, - captureHoldEnemyDistance: 500, - - dodgeThreatRange: 450, - dodgeApproachDot: 0.6, - - dodgeBaseChance: 0.25, - dodgeAggressionChance: 0.35, - dodgeCommitSeconds: 0.35, - dodgeCooldownSeconds: 0.5, - - lineOfSightClearance: 20, - lineOfSightStartOffset: 100, - - fireBaseChance: 0.45, - fireAggressionChance: 0.45, - - chargeRangeThreshold: 500, - chargeBaseChance: 0.3, - chargeAggressionChance: 0.4, - chargeMin: 0.6, - - spreadBase: 60, - spreadPerDistance: 0.08, - spreadAggressionFalloff: 1.3, - - // Per-second chance to hop while grounded and on the move, so bots use the - // leap verb too instead of being grounded targets. - leapChancePerSecond: 0.35, -}; - -export class NPC extends PlayerBase { - private direction = vec2.fromValues(Random.getRandom() - 0.5, Random.getRandom() - 0.5); - private timeSinceLastPlan = 10000; - private timeSinceLastShoot = 10000; - private isWandering = false; - private timeSinceLastWanderingConsideration = 0; - private isComingBack = false; - - private readonly aggression = Random.getRandomInRange( - npcTuning.aggressionMin, - npcTuning.aggressionMax, - ); - - private aimTargetId: Id = null; - private readonly aimTargetLastPosition = vec2.create(); - - private readonly dodgeDirection = vec2.create(); - private dodgeCommitRemaining = 0; - private dodgeCooldownRemaining = 0; - - private timeSinceObserve = npcTuning.reactionIntervalSeconds; - private nearObjects: Array = []; - - constructor( - playerInfo: PlayerInformation, - playerContainer: PlayerContainer, - objectContainer: PhysicalContainer, - team: CharacterTeam, - ) { - super(playerInfo, playerContainer, objectContainer, team); - this.createCharacter(); - this.step(0); - } - - private timeUntilRespawn = 0; - public step(deltaTimeInSeconds: number) { - if (this.character) { - this.center = this.character.center; - - if (!this.character.isAlive) { - this.sumDeaths++; - this.sumKills = this.character.killCount; - this.character = null; - this.timeUntilRespawn = settings.playerDiedTimeout; - return; - } - } else { - if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { - this.createCharacter(); - this.center = this.character!.center; - } - return; - } - - if ( - (this.timeSinceLastWanderingConsideration += deltaTimeInSeconds) > - npcTuning.wanderReconsiderSeconds - ) { - this.timeSinceLastWanderingConsideration = 0; - this.isWandering = Random.getRandom() < npcTuning.wanderProbability; - } - - if ((this.timeSinceLastPlan += deltaTimeInSeconds) > npcTuning.planIntervalSeconds) { - this.timeSinceLastPlan = 0; - this.plan(); - } - - if ( - (this.timeSinceObserve += deltaTimeInSeconds) > npcTuning.reactionIntervalSeconds - ) { - this.timeSinceObserve = 0; - this.nearObjects = this.observe(npcTuning.reactionObserveRadius); - } - - this.dodgeCommitRemaining -= deltaTimeInSeconds; - this.dodgeCooldownRemaining -= deltaTimeInSeconds; - const movement = this.decideMovement(this.nearObjects); - this.character.handleMovementAction(new MoveActionCommand(movement)); - - if ( - !this.isComingBack && - this.character.groundPlanet && - vec2.length(movement) > 0 && - Random.getRandom() < - npcTuning.leapChancePerSecond * this.aggression * deltaTimeInSeconds - ) { - this.character.leap(); - } - - if ( - (this.timeSinceLastShoot += deltaTimeInSeconds) > npcTuning.shootIntervalSeconds - ) { - this.timeSinceLastShoot = 0; - this.tryShoot(this.nearObjects); - } - } - - private plan() { - const distanceFromCentre = vec2.length(this.center); - if ( - (!this.isComingBack && distanceFromCentre > settings.worldRadius) || - (this.isComingBack && distanceFromCentre > settings.worldRadius / 2) - ) { - this.isComingBack = true; - vec2.negate(this.direction, this.center); - return; - } - this.isComingBack = false; - - const nearObjects = this.observe(npcTuning.planScanRadius); - const enemies = this.enemiesByDistance(nearObjects); - - if (enemies.length > 0) { - const nearest = enemies[0]; - const fleeRange = - npcTuning.fleeBaseRange * (npcTuning.fleeAggressionFalloff - this.aggression); - if (nearest.distance < fleeRange) { - vec2.subtract(this.direction, this.center, nearest.character.center); - return; - } - } - - if (enemies.length > 0) { - const nearest = enemies[0]; - const chaseRange = - npcTuning.chaseBaseRange + npcTuning.chaseAggressionRange * this.aggression; - if (nearest.distance < chaseRange) { - vec2.subtract(this.direction, nearest.character.center, this.center); - return; - } - } - - if (!this.isWandering) { - const planet = this.capturablePlanetsByDistance(nearObjects)[0]; - if (planet) { - vec2.subtract(this.direction, planet.planet.center, this.center); - return; - } - } - - vec2.rotate( - this.direction, - this.direction, - vec2.create(), - Random.getRandomInRange(-npcTuning.wanderTurn, npcTuning.wanderTurn), - ); - } - - private decideMovement(nearObjects: Array): vec2 { - if (this.dodgeCommitRemaining > 0) { - return vec2.clone(this.dodgeDirection); - } - if (this.dodgeCooldownRemaining <= 0) { - const dodge = this.dodgeVector(nearObjects); - if (dodge) { - if ( - Random.getRandom() < - npcTuning.dodgeBaseChance + npcTuning.dodgeAggressionChance * this.aggression - ) { - vec2.copy(this.dodgeDirection, dodge); - this.dodgeCommitRemaining = npcTuning.dodgeCommitSeconds; - return vec2.clone(this.dodgeDirection); - } - this.dodgeCooldownRemaining = npcTuning.dodgeCooldownSeconds; - } - } - - const planet = this.character!.groundPlanet; - if (planet && planet.team !== this.team) { - const enemies = this.enemiesByDistance(nearObjects); - if ( - enemies.length === 0 || - enemies[0].distance > npcTuning.captureHoldEnemyDistance - ) { - return vec2.create(); - } - } - - return vec2.normalize(vec2.create(), this.direction); - } - - private dodgeVector(nearObjects: Array): vec2 | undefined { - let threat: ProjectilePhysical | undefined; - let threatDistance = Infinity; - - for (const o of nearObjects) { - const p = o.gameObject; - if (!(p instanceof ProjectilePhysical) || p.team === this.team || !p.isAlive) { - continue; - } - const toMe = vec2.subtract(vec2.create(), this.center, p.center); - const distance = vec2.length(toMe); - if (distance > npcTuning.dodgeThreatRange || distance === 0) { - continue; - } - vec2.normalize(toMe, toMe); - if ( - vec2.dot(p.direction, toMe) > npcTuning.dodgeApproachDot && - distance < threatDistance - ) { - threatDistance = distance; - threat = p; - } - } - - if (!threat) { - return undefined; - } - - const perpendicular = vec2.fromValues(-threat.direction.y, threat.direction.x); - const toMe = vec2.subtract(vec2.create(), this.center, threat.center); - if (vec2.dot(perpendicular, toMe) < 0) { - vec2.negate(perpendicular, perpendicular); - } - vec2.normalize(perpendicular, perpendicular); - - return perpendicular; - } - - private tryShoot(nearObjects: Array) { - const enemies = this.enemiesByDistance(nearObjects); - const visible = enemies.find((e) => - this.hasLineOfSightTo(e.character.center, nearObjects), - ); - if (!visible) { - this.aimTargetId = null; - return; - } - - const target = visible.character; - const distance = visible.distance; - - const velocity = vec2.create(); - if (this.aimTargetId === target.id) { - vec2.subtract(velocity, target.center, this.aimTargetLastPosition); - vec2.scale(velocity, velocity, 1 / npcTuning.shootIntervalSeconds); - } - this.aimTargetId = target.id; - vec2.copy(this.aimTargetLastPosition, target.center); - - if ( - Random.getRandom() > - npcTuning.fireBaseChance + npcTuning.fireAggressionChance * this.aggression - ) { - return; - } - - const charge = - distance > npcTuning.chargeRangeThreshold && - Random.getRandom() < - npcTuning.chargeBaseChance + npcTuning.chargeAggressionChance * this.aggression - ? Random.getRandomInRange(npcTuning.chargeMin, 1) - : 0; - - const projectileSpeed = - charge > 0 ? settings.chargeShotSpeedMax : settings.chargeShotSpeedMin; - const leadTime = distance / projectileSpeed; - const aim = vec2.scaleAndAdd(vec2.create(), target.center, velocity, leadTime); - - const spread = - (npcTuning.spreadBase + distance * npcTuning.spreadPerDistance) * - (npcTuning.spreadAggressionFalloff - this.aggression); - aim.x += Random.getRandomInRange(-spread, spread); - aim.y += Random.getRandomInRange(-spread, spread); - - this.character!.shootTowards(aim, charge); - } - - private hasLineOfSightTo(target: vec2, nearObjects: Array): boolean { - const planets: Array = []; - for (const o of nearObjects) { - if (o.gameObject instanceof PlanetPhysical) { - planets.push(o.gameObject); - } - } - if (planets.length === 0) { - return true; - } - - const direction = vec2.subtract(vec2.create(), target, this.center); - const totalDistance = vec2.length(direction); - if (totalDistance <= npcTuning.lineOfSightStartOffset) { - return true; - } - vec2.normalize(direction, direction); - - let traveled = npcTuning.lineOfSightStartOffset; - const position = vec2.scaleAndAdd(vec2.create(), this.center, direction, traveled); - while (traveled < totalDistance) { - let sdf = Infinity; - for (const planet of planets) { - sdf = Math.min(sdf, planet.distance(position)); - } - if (sdf < npcTuning.lineOfSightClearance) { - return false; - } - traveled += sdf; - vec2.scaleAndAdd(position, position, direction, sdf); - } - return true; - } - - private observe(radius: number): Array { - return this.objectContainer.findIntersecting( - getBoundingBoxOfCircle(new Circle(this.center, radius)), - ); - } - - private enemiesByDistance( - nearObjects: Array, - ): Array<{ character: CharacterPhysical; distance: number }> { - const seen = new Set(); - const enemies: Array<{ character: CharacterPhysical; distance: number }> = []; - for (const o of nearObjects) { - const c = o.gameObject; - if ( - c instanceof CharacterPhysical && - c !== this.character && - c.isAlive && - c.team !== this.team && - !seen.has(c) - ) { - seen.add(c); - enemies.push({ character: c, distance: vec2.distance(this.center, c.center) }); - } - } - enemies.sort((a, b) => a.distance - b.distance); - return enemies; - } - - private capturablePlanetsByDistance( - nearObjects: Array, - ): Array<{ planet: PlanetPhysical; distance: number }> { - const planets = nearObjects - .filter( - (o): o is Physical => - o.gameObject instanceof PlanetPhysical && o.gameObject.team !== this.team, - ) - .map((o) => ({ - planet: o.gameObject as PlanetPhysical, - distance: vec2.distance(this.center, (o.gameObject as PlanetPhysical).center), - })); - planets.sort((a, b) => a.distance - b.distance); - return planets; - } -} diff --git a/backend/src/players/player-base.ts b/backend/src/players/player-base.ts deleted file mode 100644 index 6de34cd..0000000 --- a/backend/src/players/player-base.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - CommandReceiver, - Circle, - PlayerInformation, - CharacterTeam, - Random, - settings, -} from 'shared'; -import { PhysicalContainer } from '../physics/containers/physical-container'; -import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle'; -import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting'; -import { CharacterPhysical } from '../objects/character-physical'; -import { PlanetPhysical } from '../objects/planet-physical'; -import { PlayerContainer } from './player-container'; - -export abstract class PlayerBase extends CommandReceiver { - public character?: CharacterPhysical | null; - public center: vec2 = vec2.create(); - - protected sumKills = 0; - protected sumDeaths = 0; - - constructor( - protected readonly playerInfo: PlayerInformation, - protected readonly playerContainer: PlayerContainer, - protected readonly objectContainer: PhysicalContainer, - public readonly team: CharacterTeam, - ) { - super(); - } - - protected createCharacter() { - this.character = new CharacterPhysical( - this.playerInfo.name.slice(0, 20), - this.sumKills, - this.sumDeaths, - this.team, - this.objectContainer, - this.findEmptyPositionForPlayer(this.findSpawnCenter()), - ); - - this.objectContainer.addObject(this.character); - } - - public abstract step(deltaTimeInSeconds: number): void; - - private findSpawnCenter(): vec2 { - const planets = this.objectContainer - .findIntersecting( - getBoundingBoxOfCircle(new Circle(vec2.create(), settings.worldRadius * 2)), - ) - .filter((o): o is PlanetPhysical => o instanceof PlanetPhysical); - - const friendly = planets.filter((p) => p.team === this.team); - const neutral = planets.filter((p) => p.team === CharacterTeam.neutral); - const candidates = friendly.length ? friendly : neutral.length ? neutral : planets; - if (candidates.length === 0) { - return vec2.create(); - } - - const isContested = (planet: PlanetPhysical) => - this.playerContainer.players.some( - (p) => - p.team !== this.team && - p.character?.isAlive && - vec2.distance(p.center, planet.center) < settings.spawnSafetyDistance, - ); - const safe = candidates.filter((p) => !isContested(p)); - - // candidates is non-empty here, so choose() always returns a planet. - return vec2.clone(Random.choose(safe.length ? safe : candidates)!.center); - } - - protected findEmptyPositionForPlayer(preferredCenter: vec2): vec2 { - let rotation = 0; - let radius = 0; - for (;;) { - const playerPosition = vec2.fromValues( - radius * Math.cos(rotation) + preferredCenter.x, - radius * Math.sin(rotation) + preferredCenter.y, - ); - - const playerBoundingCircle = new Circle( - playerPosition, - CharacterPhysical.boundRadius, - ); - - const playerBoundingBox = getBoundingBoxOfCircle(playerBoundingCircle); - const possibleIntersectors = - this.objectContainer.findIntersecting(playerBoundingBox); - if (!isCircleIntersecting(playerBoundingCircle, possibleIntersectors)) { - return playerPosition; - } - - rotation += Math.PI / 8; - radius += 30; - } - } - - public destroy() { - this.character?.onDie(); - } -} diff --git a/backend/src/players/player-container.ts b/backend/src/players/player-container.ts deleted file mode 100644 index 01a1c96..0000000 --- a/backend/src/players/player-container.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { CharacterTeam, PlayerInformation, Random, settings, Command } from 'shared'; -import { Socket } from 'socket.io'; -import { PhysicalContainer } from '../physics/containers/physical-container'; -import { NPC } from './npc'; -import { Player } from './player'; -import { PlayerBase } from './player-base'; - -export class PlayerContainer { - private _players: Array = []; - private _npcs: Array = []; - - constructor( - private readonly objects: PhysicalContainer, - private readonly playerMaxCount: number, - private readonly npcMaxCount: number, - ) { - this.createNPCs(); - } - - public createNPCs() { - const newNpcCount = Math.min( - this.playerMaxCount - this._players.length - this._npcs.length, - this.npcMaxCount - this._npcs.length, - ); - for (let i = 0; i < newNpcCount; i++) { - const name = `🤖 ${Random.choose(settings.npcNames)}`; - this._npcs.push( - new NPC({ name }, this, this.objects, this.getTeamOfNextPlayer(true)), - ); - } - } - - public createPlayer(playerInfo: PlayerInformation, socket: Socket): Player { - if (this._players.length === this.playerMaxCount) { - throw new Error('Too many players'); - } - - const team = this.getTeamOfNextPlayer(); - let npcToReplace = this._npcs.find((n) => n.team === team); - if (!npcToReplace) { - npcToReplace = this._npcs.find((n) => n.team !== team); - } - npcToReplace?.destroy(); - this._npcs = this._npcs.filter((n) => n !== npcToReplace); - - const player = new Player(playerInfo, this, this.objects, team, socket); - this._players.push(player); - - return player; - } - - public get players(): Array { - return [...this._players, ...this._npcs]; - } - - public get count(): number { - return this._players.length; - } - - // Measured round-trip times (ms) of the real connected players, for - // server-side latency stats. NPCs have no socket and are excluded. - public get connectedPlayerRttsMs(): Array { - return this._players.map((p) => p.rttMs); - } - - public step(deltaTimeInSeconds: number) { - this.players.forEach((p) => p.step(deltaTimeInSeconds)); - } - - public stepCommunication(deltaTimeInSeconds: number) { - this._players.forEach((p) => p.stepCommunications(deltaTimeInSeconds)); - } - - public endGame(winner: CharacterTeam) { - this._players.forEach((p) => p.onGameEnded(winner)); - } - - public queueCommandForEachClient(command: Command) { - this._players.forEach((p) => p.queueCommandSend(command)); - } - - public sendQueuedCommands() { - this._players.forEach((p) => p.sendQueuedCommandsToClient()); - } - - private getTeamOfNextPlayer(isNpc = false): CharacterTeam { - const players = isNpc ? this.players : this._players; - const blueCount = players.filter((p) => p.team === CharacterTeam.blue).length; - const redCount = players.filter((p) => p.team === CharacterTeam.red).length; - - if ((blueCount === redCount && Random.getRandom() >= 0.5) || blueCount < redCount) { - return CharacterTeam.blue; - } else { - return CharacterTeam.red; - } - } - - public deletePlayer(player: Player) { - this._players = this._players.filter((p) => p !== player); - this.createNPCs(); - } -} diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index 923be65..9fc51c8 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -1,243 +1,144 @@ -import { performance } from 'perf_hooks'; import { vec2 } from 'gl-matrix'; import { CommandExecutors, + CommandReceiver, CreateObjectsCommand, CreatePlayerCommand, DeleteObjectsCommand, MoveActionCommand, serialize, TransportEvents, + UpdateObjectsCommand, + StepCommand, SetAspectRatioActionCommand, calculateViewArea, - settings, - PlayerInformation, - CharacterTeam, - UpdateMinimap, - GameObject, - Command, - MinimapPlayer, - RemoteCallsForObject, - RemoteCallsForObjects, - ServerAnnouncement, - PropertyUpdatesForObjects, - PropertyUpdatesForObject, - PrimaryActionCommand, - LeapActionCommand, - InputAcknowledgement, + SecondaryActionCommand, } from 'shared'; -import { Socket } from 'socket.io'; +import { getTimeInMilliseconds } from '../helper/get-time-in-milliseconds'; +import { CharacterPhysical } from '../objects/character-physical'; +import { ProjectilePhysical } from '../objects/projectile-physical'; + import { BoundingBox } from '../physics/bounding-boxes/bounding-box'; import { PhysicalContainer } from '../physics/containers/physical-container'; -import { PlayerContainer } from './player-container'; -import { PlayerBase } from './player-base'; +import { Physical } from '../physics/physical'; -// How often the server pings each client to measure round-trip time. -const pingIntervalSeconds = 1; - -export class Player extends PlayerBase { - // default, until the clients sends its real value +export class Player extends CommandReceiver { + private character: CharacterPhysical; private aspectRatio: number = 16 / 9; - private timeUntilRespawn = 0; - private timeSinceLastMessage = 0; - private objectsPreviouslyInViewArea: Array = []; - private lastInputClientTimeMs = 0; - private lastLeapClientTimeMs = 0; + private isActive = true; - // Measured round-trip time to this client (ms) — the latency primitive that - // lag compensation, a latency HUD, and adaptive interpolation build on. - public rttMs = 0; - private timeSinceLastPing = 0; - private lastPingSentMs = 0; + private objectsPreviouslyInViewArea: Array = []; + private objectsInViewArea: Array = []; + + private pingTime?: number; + private _latency?: number; + public measureLatency() { + this.pingTime = getTimeInMilliseconds(); + this.socket.emit(TransportEvents.Ping); + if (this.isActive) { + setTimeout(this.measureLatency.bind(this), 10000); + } + } + + public get latency(): number | undefined { + return this._latency; + } protected commandExecutors: CommandExecutors = { + [StepCommand.type]: this.sendObjects.bind(this), [SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) => (this.aspectRatio = v.aspectRatio), - [MoveActionCommand.type]: (c: MoveActionCommand) => { - // Remember how far into this client's input timeline we've consumed, to - // echo back for client-side prediction reconciliation. - this.lastInputClientTimeMs = c.clientTimeMs; - this.character?.handleMovementAction(c); - }, - [PrimaryActionCommand.type]: (c: PrimaryActionCommand) => - this.character?.shootTowards(c.position, c.charge), - [LeapActionCommand.type]: (c: LeapActionCommand) => { - // Record receipt (whether or not leap() accepts it): either way its effect - // on bodyVelocity is now reflected in the streamed launch momentum, so the - // predictor must stop replaying this leap. - this.lastLeapClientTimeMs = c.clientTimeMs; - this.character?.leap(); + [MoveActionCommand.type]: (c: MoveActionCommand) => this.character.sendCommand(c), + [SecondaryActionCommand.type]: (c: SecondaryActionCommand) => { + const start = vec2.clone(this.character.center); + const direction = vec2.subtract(vec2.create(), c.position, start); + vec2.normalize(direction, direction); + vec2.add(start, start, vec2.scale(vec2.create(), direction, 100)); + const force = vec2.scale(direction, direction, 1000); + const projectile = new ProjectilePhysical(start, 20, force, this.objects); + this.objects.addObject(projectile); }, }; constructor( - playerInfo: PlayerInformation, - playerContainer: PlayerContainer, - objectContainer: PhysicalContainer, - team: CharacterTeam, - private readonly socket: Socket, + private readonly objects: PhysicalContainer, + private readonly socket: SocketIO.Socket, ) { - super(playerInfo, playerContainer, objectContainer, team); - this.createCharacter(); - this.step(0); + super(); + this.character = new CharacterPhysical(objects); + this.objectsPreviouslyInViewArea.push(this.character); + this.objectsInViewArea.push(this.character); - // The client already echoes a Pong for every Ping (see game.ts). Only one - // ping is ever in flight, so RTT is simply now − send-time; no payload - // needed and no client change required. - this.socket.on(TransportEvents.Pong, () => { - if (this.lastPingSentMs > 0) { - this.rttMs = performance.now() - this.lastPingSentMs; - } - }); + this.objects.addObject(this.character); + + socket.emit( + TransportEvents.ServerToPlayer, + serialize(new CreatePlayerCommand(this.character)), + ); + + socket.on( + TransportEvents.Pong, + () => (this._latency = getTimeInMilliseconds() - this.pingTime!), + ); + + this.measureLatency(); + this.sendObjects(); } - protected createCharacter() { - super.createCharacter(); - - this.objectsPreviouslyInViewArea.push(this.character!); - this.queueCommandSend(new CreatePlayerCommand(this.character!)); - } - - private winnerTeam?: CharacterTeam; - public onGameEnded(winnerTeam: CharacterTeam) { - this.winnerTeam = winnerTeam; - } - - public step(deltaTimeInSeconds: number) { - if (this.character) { - this.center = this.character?.center; - - if (!this.character.isAlive) { - this.sumDeaths++; - this.sumKills = this.character.killCount; - - this.character = null; - this.timeUntilRespawn = settings.playerDiedTimeout; - } - } else { - if ((this.timeUntilRespawn -= deltaTimeInSeconds) < 0) { - this.createCharacter(); - this.center = this.character!.center; - } - } - } - - private handleViewAreaUpdate() { - const viewArea = calculateViewArea(this.center, this.aspectRatio, 1.2); + public sendObjects() { + const viewArea = calculateViewArea(this.character.center, this.aspectRatio, 1.5); const bb = new BoundingBox(); bb.topLeft = viewArea.topLeft; bb.size = viewArea.size; - const objectsInViewArea = Array.from( - new Set(this.objectContainer.findIntersecting(bb).map((o) => o.gameObject)), - ); + this.objectsInViewArea = this.objects.findIntersecting(bb); - // The owning character must always be in its own snapshot, regardless of the - // view-area query, so the client predictor never loses its authoritative - // anchor (the body can ride a fast spinner to the very edge of the box). - if (this.character && !objectsInViewArea.includes(this.character)) { - objectsInViewArea.push(this.character); - } - - const newlyIntersecting = objectsInViewArea.filter( + const newlyIntersecting = this.objectsInViewArea.filter( (o) => !this.objectsPreviouslyInViewArea.includes(o), ); const noLongerIntersecting = this.objectsPreviouslyInViewArea.filter( - (o) => !objectsInViewArea.includes(o), + (o) => !this.objectsInViewArea.includes(o), ); - - this.objectsPreviouslyInViewArea = objectsInViewArea; + this.objectsPreviouslyInViewArea = this.objectsInViewArea; if (noLongerIntersecting.length > 0) { - this.queueCommandSend( - new DeleteObjectsCommand(noLongerIntersecting.map((g) => g.id)), + this.socket.emit( + TransportEvents.ServerToPlayer, + serialize( + new DeleteObjectsCommand([ + ...new Set(noLongerIntersecting.map((p) => p.gameObject.id)), + ]), + ), ); } if (newlyIntersecting.length > 0) { - this.queueCommandSend(new CreateObjectsCommand(newlyIntersecting)); - } - - this.queueCommandSend(new UpdateMinimap(this.getMinimapPlayers())); - - this.queueCommandSend( - new PropertyUpdatesForObjects( - this.objectsPreviouslyInViewArea - .map((o) => o.getPropertyUpdates()) - .filter((u) => u) as Array, - performance.now() / 1000, - ), - ); - - // Tell the client how much of its own input is reflected in the snapshot it - // just received, so its predictor can replay the rest. Only while alive — - // a dead player isn't predicting. - if (this.character) { - this.queueCommandSend( - new InputAcknowledgement( - this.lastInputClientTimeMs, - this.character.launchMomentum, - this.lastLeapClientTimeMs, + this.socket.emit( + TransportEvents.ServerToPlayer, + serialize( + new CreateObjectsCommand([ + ...new Set(newlyIntersecting.map((p) => p.gameObject)), + ]), ), ); } + + this.socket.volatile.emit( + TransportEvents.ServerToPlayer, + serialize( + new UpdateObjectsCommand([ + ...new Set( + this.objectsInViewArea.filter((p) => p.canMove).map((p) => p.gameObject), + ), + ]), + ), + ); } - // Every living player except this one, reported by absolute world position so - // the client can plot the whole circular arena on its minimap. - private getMinimapPlayers(): Array { - return this.playerContainer.players - .filter((p) => p !== this && p.character?.isAlive) - .map( - (p) => - new MinimapPlayer(p.character!.id, vec2.clone(p.character!.center), p.team), - ); - } - - private commandsToBeSent: Array = []; - public queueCommandSend(command: Command) { - this.commandsToBeSent.push(command); - } - - public stepCommunications(deltaTime: number) { - const remoteCalls = this.objectsPreviouslyInViewArea - .map((g) => new RemoteCallsForObject(g.id, g.getRemoteCalls())) - .filter((c) => c.calls.length > 0); - - if (remoteCalls.length > 0) { - this.queueCommandSend(new RemoteCallsForObjects(remoteCalls)); - } - - if ((this.timeSinceLastPing += deltaTime) > pingIntervalSeconds) { - this.timeSinceLastPing = 0; - this.lastPingSentMs = performance.now(); - this.socket.emit(TransportEvents.Ping); - } - - if ((this.timeSinceLastMessage += deltaTime) > settings.updateMessageInterval) { - this.handleAnnouncements(); - this.handleViewAreaUpdate(); - this.sendQueuedCommandsToClient(); - this.timeSinceLastMessage = 0; - } - } - - public sendQueuedCommandsToClient() { - this.socket.emit(TransportEvents.ServerToPlayer, serialize(this.commandsToBeSent)); - this.commandsToBeSent = []; - } - - private handleAnnouncements() { - let announcement = ''; - if (this.winnerTeam) { - announcement = `Team ${this.winnerTeam} won 🎉`; - } else if (!this.character) { - announcement = `Reviving in ${Math.round(this.timeUntilRespawn)}…`; - } - - if (announcement) { - this.queueCommandSend(new ServerAnnouncement(announcement)); - } + public destroy() { + this.isActive = false; + this.character.destroy(); + this.objects.removeObject(this.character); } } diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index e9e0884..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "skipLibCheck": true, - "target": "es6", - "esModuleInterop": true, - "strict": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "ignoreDeprecations": "6.0", - "module": "commonjs", - "lib": ["dom", "es2017"], - "typeRoots": ["./types", "./node_modules/@types"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "typings"] -} diff --git a/backend/types/socket.io-msgpack-parser/index.d.ts b/backend/types/socket.io-msgpack-parser/index.d.ts deleted file mode 100644 index 4065a1e..0000000 --- a/backend/types/socket.io-msgpack-parser/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'socket.io-msgpack-parser'; diff --git a/backend/webpack.config.js b/backend/webpack.config.js index 20e8a57..b0586e3 100644 --- a/backend/webpack.config.js +++ b/backend/webpack.config.js @@ -1,8 +1,8 @@ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const nodeExternals = require('webpack-node-externals'); +const { ESBuildPlugin } = require('esbuild-loader'); const TerserJSPlugin = require('terser-webpack-plugin'); -const webpack = require('webpack'); const PATHS = { entryPoint: path.resolve(__dirname, 'src/main.ts'), @@ -13,7 +13,6 @@ module.exports = (env, argv) => ({ entry: { main: [PATHS.entryPoint], }, - externals: [ nodeExternals({ allowlist: [/(^shared)/], @@ -32,9 +31,9 @@ module.exports = (env, argv) => ({ minimize: argv.mode !== 'development', minimizer: [ new TerserJSPlugin({ + sourceMap: false, + test: /\.js$/, exclude: /node_modules/, - // The custom serialization protocol keys on class names, so they must - // survive minification (see shared/src/serialization). terserOptions: { keep_classnames: true, }, @@ -42,7 +41,7 @@ module.exports = (env, argv) => ({ ], }, plugins: [ - new webpack.BannerPlugin({ banner: '#!/usr/bin/env node', raw: true }), + new ESBuildPlugin(), new CleanWebpackPlugin({ protectWebpackAssets: false, cleanAfterEveryBuildPatterns: [], @@ -51,9 +50,21 @@ module.exports = (env, argv) => ({ module: { rules: [ { - test: /\.ts$/, + test: /\.html$/, use: { - loader: 'ts-loader', + loader: 'file-loader', + query: { + outputPath: '/', + name: '[name].[ext]', + }, + }, + }, + { + test: /\.ts$/, + loader: 'esbuild-loader', + options: { + loader: 'ts', + target: 'es2015', }, exclude: /node_modules/, }, diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 84a6892..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,32 +0,0 @@ -// Flat config (ESLint 9+/10). Ported from the former .eslintrc.json. -const tseslint = require('typescript-eslint'); -const prettierRecommended = require('eslint-plugin-prettier/recommended'); -const unusedImports = require('eslint-plugin-unused-imports'); - -module.exports = tseslint.config( - { - ignores: ['**/node_modules/**', '**/dist/**', '**/lib/**'], - }, - ...tseslint.configs.recommended, - prettierRecommended, - { - files: ['**/*.ts'], - plugins: { - 'unused-imports': unusedImports, - }, - languageOptions: { - sourceType: 'module', - }, - rules: { - 'no-unused-vars': 'off', - 'unused-imports/no-unused-imports': 'error', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-function': 'off', - // Renamed from the former no-var-requires; keep require() allowed. - '@typescript-eslint/no-require-imports': 'off', - }, - }, -); diff --git a/frontend/.firebaserc b/frontend/.firebaserc new file mode 100644 index 0000000..2f95982 --- /dev/null +++ b/frontend/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "decla-red + } +} \ No newline at end of file diff --git a/frontend/firebase.json b/frontend/firebase.json new file mode 100644 index 0000000..2c33c29 --- /dev/null +++ b/frontend/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index bfa9f49..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,6758 +0,0 @@ -{ - "name": "doppler-frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "doppler-frontend", - "version": "0.0.0", - "devDependencies": { - "@plausible-analytics/tracker": "^0.4.5", - "autoprefixer": "^10.5.0", - "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^14.0.0", - "css-loader": "^7.1.4", - "file-loader": "^6.2.0", - "gl-matrix": "3.3.0", - "html-inline-css-webpack-plugin": "^1.11.2", - "html-webpack-plugin": "^5.6.7", - "mini-css-extract-plugin": "^2.10.2", - "postcss": "^8.5.15", - "postcss-loader": "^8.2.1", - "resize-observer-polyfill": "^1.5.1", - "sass": "^1.100.0", - "sass-loader": "^17.0.0", - "sdf-2d": "file:../../sdf-2d", - "shared": "file:../shared", - "socket.io-client": "^4.8.3", - "socket.io-msgpack-parser": "^3.0.2", - "source-map-loader": "^5.0.0", - "terser-webpack-plugin": "^5.6.1", - "ts-loader": "^9.6.0", - "typescript": "^6.0.3", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.3", - "webpack-dev-server": "^5.2.4" - } - }, - "../../sdf-2d": { - "version": "0.7.6", - "dev": true, - "license": "ISC", - "dependencies": { - "gl-matrix": "^3.4.4", - "resize-observer-polyfill": "^1.5.1" - }, - "devDependencies": { - "@eslint/js": "^10.0.1", - "@typescript-eslint/eslint-plugin": "^8.60.1", - "@typescript-eslint/parser": "^8.60.1", - "eslint": "^10.4.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.6", - "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.6.0", - "prettier": "^3.8.3", - "raw-loader": "^4.0.2", - "terser-webpack-plugin": "^5.6.1", - "ts-loader": "^9.6.0", - "typedoc": "^0.28.19", - "typedoc-plugin-extras": "^4.0.1", - "typescript": "^6.0.3", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.3" - } - }, - "../../sdf-2d/dist": { - "extraneous": true - }, - "../../sdf-2d/lib": { - "extraneous": true - }, - "../shared": { - "version": "0.0.0", - "dev": true, - "devDependencies": { - "clean-webpack-plugin": "^4.0.0", - "file-loader": "^6.2.0", - "gl-matrix": "^3.3.0", - "prettier": "^3.8.3", - "terser-webpack-plugin": "^5.6.1", - "ts-loader": "^9.6.0", - "typescript": "^6.0.3", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", - "integrity": "sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.17.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.6.tgz", - "integrity": "sha512-uI++Wx6VkBJqVmkb4ZeExwAVpZiA2Do5NrEtXoDk0Pdvce3ytFXJoviT1sLOj16+qDIMnD5nWPfOhVpnDmRJKg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.6", - "@jsonjoy.com/fs-node-utils": "4.57.6", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.6.tgz", - "integrity": "sha512-pKkw/yC5CzSZKhIIUIsH1przOa+K5jGmZIg1sWaSF24JojyrUFbjcQv7QrcGAudriei6HQ6R0BFj+V8NbQinJw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.6", - "@jsonjoy.com/fs-node-builtins": "4.57.6", - "@jsonjoy.com/fs-node-utils": "4.57.6", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.6.tgz", - "integrity": "sha512-Kbn1jdkvDN4F2+BhoB6mMu7NCbhP0bgA5NcI1aJj/Q5UcU+I1JLLW+dEQean33iV4tXv35AzBVKPICnDltBpxw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.6", - "@jsonjoy.com/fs-node-builtins": "4.57.6", - "@jsonjoy.com/fs-node-utils": "4.57.6", - "@jsonjoy.com/fs-print": "4.57.6", - "@jsonjoy.com/fs-snapshot": "4.57.6", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.6.tgz", - "integrity": "sha512-V4DgEFT3Cg5S9fCMOZSCVdTxdJWWLBO0WnAazV7hnCM96u5zXHyW/ubDAfcSVwqjkMJ50W1Y44IXtxRoIwaCVg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.6.tgz", - "integrity": "sha512-+JptNw3iifihxH2rEXrninDzX4FFVW8JD/wPR8GbJPAeL9CQUSblrlumOPB5gZuS7tYRX+PJPLtT7XzKoRhv/Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.6", - "@jsonjoy.com/fs-node-builtins": "4.57.6", - "@jsonjoy.com/fs-node-utils": "4.57.6" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.6.tgz", - "integrity": "sha512-foyUrfS7WmYEUzqYXSNxmJBcSj04TABrkpFabwO9SCDCpVCfJ+qG+2sk5FjfiflG2n0SDFZDCJ6vYlJAEpxJFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.6" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.6.tgz", - "integrity": "sha512-96eAn4Dudtt67LTeuU47yUD+pg9/G/oKpI10zei9ljk3X3WK4lYKc+n3cpaPCAbKPzoyfxl0mXm8f8Y7BOSFXw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.6", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.6.tgz", - "integrity": "sha512-V57CMzbOgTzUWGOWQ8GzHQdpJP6JnrYVNCtTBNxVYEnlVRvo4uEJqHhtAT8vhDFrIuJOXLrTL1Fki4h5oI7xxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.6", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", - "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", - "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", - "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "17.67.0", - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0", - "@jsonjoy.com/json-pointer": "17.67.0", - "@jsonjoy.com/util": "17.67.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", - "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/util": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", - "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz", - "integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "@peculiar/asn1-x509-attr": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz", - "integrity": "sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz", - "integrity": "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz", - "integrity": "sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.7.0", - "@peculiar/asn1-pkcs8": "^2.7.0", - "@peculiar/asn1-rsa": "^2.7.0", - "@peculiar/asn1-schema": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz", - "integrity": "sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz", - "integrity": "sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.7.0", - "@peculiar/asn1-pfx": "^2.7.0", - "@peculiar/asn1-pkcs8": "^2.7.0", - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "@peculiar/asn1-x509-attr": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz", - "integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", - "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz", - "integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz", - "integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/asn1-x509": "^2.7.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", - "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/x509": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", - "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "reflect-metadata": "^0.2.2", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@plausible-analytics/tracker": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@plausible-analytics/tracker/-/tracker-0.4.5.tgz", - "integrity": "sha512-6BfAGejXY+YA3Cw6LYT2Zpn4hTxDtPQAawFsYUsQCOg78wIS5C4deAGXTfJffa5VleMWITv5lpJ/EYuQBl1tPA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" - } - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "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" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "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" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "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" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "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" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", - "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "dev": true, - "license": "MIT", - "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.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/bonjour-service": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.0.tgz", - "integrity": "sha512-fGQtj1qdR9vIKjFiWPQd52qIqwjaYqhcI40JEiDuvlZ86E7ZBPBwY9fPgHy9r2rYGIjiRfctNPYz6OQU73ww2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "del": "^4.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": ">=4.0.0 <6.0.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/copy-webpack-plugin": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", - "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^7.0.3", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": ">= 20.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-loader": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", - "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.40", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.6.3" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.366", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz", - "integrity": "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.5.tgz", - "integrity": "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.20.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.22.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.2.tgz", - "integrity": "sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "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.15.1", - "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" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "license": "MIT", - "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.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gl-matrix": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz", - "integrity": "sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-inline-css-webpack-plugin": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/html-inline-css-webpack-plugin/-/html-inline-css-webpack-plugin-1.11.2.tgz", - "integrity": "sha512-jzJptIAOrYQh9ITxiDeNWQvPwk1nHZIo0P9/ZlLsfSYKqggMENJdO+Hcw67xSHoxtL2t+X9QO00eHu6W/c8V0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "peerDependencies": { - "html-webpack-plugin": "^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.7", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", - "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/immutable": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", - "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "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.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-network-error": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", - "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/launch-editor": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", - "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.4" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", - "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "4.57.6", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.6.tgz", - "integrity": "sha512-WQK+DGjKCnPdpSyJUXphz+COF2uEhhsxQ3VIWBSbzpbbXuch3h4FePMqXrXGdLjsTgo4JFzBFsP6AWd9pVazGw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.6", - "@jsonjoy.com/fs-fsa": "4.57.6", - "@jsonjoy.com/fs-node": "4.57.6", - "@jsonjoy.com/fs-node-builtins": "4.57.6", - "@jsonjoy.com/fs-node-to-fsa": "4.57.6", - "@jsonjoy.com/fs-node-utils": "4.57.6", - "@jsonjoy.com/fs-print": "4.57.6", - "@jsonjoy.com/fs-snapshot": "4.57.6", - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", - "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/notepack.io": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.2.0.tgz", - "integrity": "sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkijs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", - "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@noble/hashes": "1.4.0", - "asn1js": "^3.0.6", - "bytestreamjs": "^2.0.1", - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-loader": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.1.tgz", - "integrity": "sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==", - "dev": true, - "license": "MIT", - "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^2.5.1", - "semver": "^7.6.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", - "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sass": { - "version": "1.100.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", - "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^5.0.0", - "immutable": "^5.1.5", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=20.19.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/sass-loader": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-17.0.0.tgz", - "integrity": "sha512-0Ybm8ohBQ9LcrycVrFQp/KQBNX5a3Wda9/smS0mE/xLffzEnwvV8nykOzrbiSWNzTE3IB/jiXx8O4QmDPG2+Gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 22.11.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/sdf-2d": { - "resolved": "../../sdf-2d", - "link": true - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", - "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@peculiar/x509": "^1.14.2", - "pkijs": "^3.3.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serialize-javascript": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", - "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", - "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.8.0", - "mime-types": "~2.1.35", - "parseurl": "~1.3.3" - }, - "engines": { - "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shared": { - "resolved": "../shared", - "link": true - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/socket.io-client": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", - "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-msgpack-parser": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/socket.io-msgpack-parser/-/socket.io-msgpack-parser-3.0.2.tgz", - "integrity": "sha512-1e76bJ1PCKi9H+JiYk+S29PBJvknHjQWM7Mtj0hjF2KxDA6b6rQxv3rTsnwBoz/haZOhlCDIMQvPATbqYeuMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "~1.3.0", - "notepack.io": "~2.2.0" - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", - "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "html-minifier-terser": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/thingies": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", - "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/ts-loader": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.0.tgz", - "integrity": "sha512-dsJO0S+T7grTDWTc4a0nTygXGjKncVUpx8Y+af8EvI/D5WgTJby5UEk5eoMCB9EcLQmnvitqh99MqtjtHgAwFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "loader-utils": "*", - "typescript": "*", - "webpack": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "loader-utils": { - "optional": true - } - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tsyringe": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", - "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.9.3" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/tsyringe/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/webpack": { - "version": "5.107.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz", - "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@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.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.22.0", - "es-module-lexer": "^2.1.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.2", - "mime-db": "^1.54.0", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.5.0", - "watchpack": "^2.5.1", - "webpack-sources": "^3.5.0" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.0.3.tgz", - "integrity": "sha512-2E2C6A1e2El7791zQgTH7LPIuwLjRliow9OHS/qlJc9pwhZlCoL/uiwqd/1WSlXT83wJfmfDbkcqHXuXoPJZ3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^1.1.0", - "commander": "^14.0.3", - "cross-spawn": "^7.0.6", - "envinfo": "^7.14.0", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^6.0.1" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.101.0", - "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", - "webpack-dev-server": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", - "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.43.1", - "mime-types": "^3.0.1", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/webpack-dev-server": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", - "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.25", - "@types/express-serve-static-core": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.8.1", - "connect-history-api-fallback": "^2.0.0", - "express": "^4.22.1", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^5.5.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", - "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", - "dev": true, - "license": "MIT", - "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 - } - } - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json index 229f1e5..4e10c8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,55 +1,46 @@ { - "name": "doppler-frontend", - "version": "0.0.0", - "description": "", + "name": "decla.red-frontend", + "private": true, + "description": "![logo](media/declared.png)", "keywords": [], "author": "András Schmelczer (https://schmelczer.dev/)", "sideEffects": [ "*.scss" ], "main": "index.js", - "engines": { - "node": ">=20" - }, "scripts": { "build": "npx webpack --mode production", - "dev": "npx webpack-dev-server --mode development", + "initialize": "npm install", + "start": "npx webpack-dev-server --mode development", "try-build": "npm run build && cd dist && python3 -m http.server 8080" }, "browserslist": [ "defaults" ], + "devDependencies": { + "clean-webpack-plugin": "^3.0.0", + "esbuild-loader": "^2.4.0", + "html-webpack-inline-source-plugin": "0.0.10", + "html-webpack-plugin": "^3.2.0", + "image-webpack-loader": "^6.0.0", + "mini-css-extract-plugin": "^0.9.0", + "optimize-css-assets-webpack-plugin": "^5.0.3", + "postcss-loader": "^3.0.0", + "raw-loader": "^4.0.1", + "resolve-url-loader": "^3.1.1", + "sass": "^1.26.3", + "sass-loader": "^9.0.2", + "svg-url-loader": "^6.0.0", + "terser-webpack-plugin": "^4.2.2", + "typescript": "^4.0.3", + "webpack": "^4.43.0", + "webpack-bundle-analyzer": "^3.9.0", + "webpack-cli": "^3.3.11", + "webpack-dev-server": "^3.10.3" + }, "postcss": { "plugins": { "autoprefixer": {} } - }, - "devDependencies": { - "@plausible-analytics/tracker": "^0.4.5", - "autoprefixer": "^10.5.0", - "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^14.0.0", - "css-loader": "^7.1.4", - "file-loader": "^6.2.0", - "gl-matrix": "3.3.0", - "html-inline-css-webpack-plugin": "^1.11.2", - "html-webpack-plugin": "^5.6.7", - "mini-css-extract-plugin": "^2.10.2", - "postcss": "^8.5.15", - "postcss-loader": "^8.2.1", - "resize-observer-polyfill": "^1.5.1", - "sass": "^1.100.0", - "sass-loader": "^17.0.0", - "sdf-2d": "^0.8.0", - "shared": "file:../shared", - "socket.io-client": "^4.8.3", - "socket.io-msgpack-parser": "^3.0.2", - "source-map-loader": "^5.0.0", - "terser-webpack-plugin": "^5.6.1", - "ts-loader": "^9.6.0", - "typescript": "^6.0.3", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.3", - "webpack-dev-server": "^5.2.4" } } diff --git a/frontend/src/custom.d.ts b/frontend/src/custom.d.ts deleted file mode 100644 index f053a20..0000000 --- a/frontend/src/custom.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module '*.mp3' { - const content: string; - export default content; -} - -// Asset imports handled by webpack loaders. TypeScript 5.x+ requires ambient -// declarations for side-effect imports of these, so declare them here. -declare module '*.png'; -declare module '*.ico'; -declare module '*.svg'; -declare module '*.scss'; - -// webpack's `mode` inlines `process.env.NODE_ENV` via DefinePlugin. Declare just -// that one global so browser code can read it without pulling in all of @types/node. -declare const process: { env: { NODE_ENV?: string } }; diff --git a/frontend/src/index.html b/frontend/src/index.html index 0d4d201..c63c46c 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1,125 +1,21 @@ - - - - - - - - - - - + + + + - - - + + - doppler - + decla.red + - - + + +
+ + - -
- -
-

doppler

-
-
- Join a game - -
- - -
- -
- -
-
-
- -
-
- - - - - - - logout -
-
- -
- toggle-settings -
- - minimize-application - maximize-application - -
- waiting -
- - + \ No newline at end of file diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 00f04c0..d7b4619 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,156 +1,23 @@ import { glMatrix } from 'gl-matrix'; -import { - LampBase, - overrideDeserialization, - PlanetBase, - CharacterBase, - ProjectileBase, -} from 'shared'; -import './main.scss'; -import './scripts/analytics'; -import '../static/og-image.png'; -import '../static/favicons/apple-touch-icon.png'; -import '../static/favicons/favicon-16x16.png'; -import '../static/favicons/favicon-32x32.png'; -import '../static/favicons/favicon.ico'; -import { LandingPageBackground } from './scripts/landing-page-background'; -import { JoinFormHandler } from './scripts/join-form-handler'; -import { handleFullScreen } from './scripts/helper/handle-full-screen'; +import { CharacterBase, LampBase, overrideDeserialization, TunnelBase } from 'shared'; +import { ProjectileBase } from 'shared/src/objects/types/projectile-base'; import { Game } from './scripts/game'; -import ResizeObserver from 'resize-observer-polyfill'; -import { OptionsHandler } from './scripts/options-handler'; -import { hide } from './scripts/helper/hide'; -import { show } from './scripts/helper/show'; -import { SoundHandler, Sounds } from './scripts/sound-handler'; -import { VibrationHandler } from './scripts/vibration-handler'; -import { CharacterView } from './scripts/objects/types/character-view'; -import { LampView } from './scripts/objects/types/lamp-view'; -import { PlanetView } from './scripts/objects/types/planet-view'; -import { ProjectileView } from './scripts/objects/types/projectile-view'; +import { CharacterView } from './scripts/objects/character-view'; +import { LampView } from './scripts/objects/lamp-view'; +import { ProjectileView } from './scripts/objects/projectile-view'; +import { TunnelView } from './scripts/objects/tunnel-view'; +import './styles/main.scss'; glMatrix.setMatrixArrayType(Array); overrideDeserialization(CharacterBase, CharacterView); -overrideDeserialization(PlanetBase, PlanetView); +overrideDeserialization(TunnelBase, TunnelView); overrideDeserialization(LampBase, LampView); overrideDeserialization(ProjectileBase, ProjectileView); -const landingUI = document.querySelector('#landing-ui') as HTMLElement; -const nameInput = document.querySelector('#name') as HTMLInputElement; -const joinGameForm = document.querySelector('#join-game-form') as HTMLFormElement; -const serverContainer = document.querySelector('#server-container') as HTMLElement; -const canvas = document.querySelector('canvas') as HTMLCanvasElement; -const overlay = document.querySelector('#overlay') as HTMLElement; -const settings = document.querySelector('#settings') as HTMLElement; -const toggleSettingsButton = document.querySelector( - '#toggle-settings-container', -) as HTMLElement; -const minimize = document.querySelector('#minimize') as HTMLElement; -const maximize = document.querySelector('#maximize') as HTMLElement; -const logoutButton = document.querySelector('#logout') as HTMLElement; -const enableSounds = document.querySelector('#enable-sounds') as HTMLInputElement; -const enableMusic = document.querySelector('#enable-music') as HTMLInputElement; -const enableVibration = document.querySelector('#enable-vibration') as HTMLInputElement; -const spinner = document.querySelector('#spinner-container') as HTMLElement; - -const toggleSettings = () => { - settings.className = settings.className === 'open' ? '' : 'open'; - SoundHandler.play(Sounds.click); -}; - -const applyServerContainerShadows = () => { - const topShadow = 'inset 0 -8px 8px -8px rgba(0, 0, 0, 0.4)'; - const bottomShadow = 'inset 0 8px 8px -8px rgba(0, 0, 0, 0.4)'; - - const { scrollHeight, clientHeight, scrollTop } = serverContainer; - if (scrollHeight > clientHeight) { - if (scrollTop <= 0) { - serverContainer.style.boxShadow = topShadow; - } else if (scrollTop + clientHeight >= scrollHeight) { - serverContainer.style.boxShadow = bottomShadow; - } else { - serverContainer.style.boxShadow = topShadow + ',' + bottomShadow; - } - } else { - serverContainer.style.boxShadow = ''; - } -}; - const main = async () => { try { - let game: Game; - - const storedUserName = localStorage?.getItem('userName'); - if (storedUserName) { - nameInput.value = JSON.parse(storedUserName); - } - - const firstClickListener = () => { - SoundHandler.initialize( - () => { - enableMusic.checked = true; - enableMusic.dispatchEvent(new Event('change')); - }, - () => { - enableMusic.checked = false; - enableMusic.dispatchEvent(new Event('change')); - }, - ); - document.removeEventListener('click', firstClickListener); - }; - document.addEventListener('click', firstClickListener); - - if (!VibrationHandler.isVibrationEnabledHeuristics) { - hide(document.querySelector("label[for='enable-vibration']") as HTMLElement, true); - } - - handleFullScreen(minimize, maximize); - toggleSettingsButton.addEventListener('click', toggleSettings); - - new ResizeObserver(applyServerContainerShadows).observe(serverContainer); - serverContainer.addEventListener('scroll', applyServerContainerShadows); - - OptionsHandler.initialize({ - soundsEnabled: enableSounds, - vibrationEnabled: enableVibration, - musicEnabled: enableMusic, - }); - - logoutButton.addEventListener('click', () => { - game.destroy(); - toggleSettings(); - }); - window.onpopstate = () => game.destroy(); - - for (;;) { - show(spinner); - hide(logoutButton, true); - show(landingUI, true, 'flex'); - - const background = new LandingPageBackground(canvas); - const joinHandler = new JoinFormHandler(joinGameForm, serverContainer); - - await background.renderer; - hide(spinner); - - const playerDecision = await joinHandler.getPlayerDecision(); - - localStorage?.setItem('userName', JSON.stringify(playerDecision.name)); - - if (!history.state) { - history.pushState(true, ''); - } - - hide(landingUI, true); - show(spinner); - background.destroy(); - game = new Game(playerDecision, canvas, overlay); - const gameOver = game.start(); - await game.started; - hide(spinner); - show(logoutButton, true, 'block'); - await gameOver; - } + await new Game().start(); } catch (e) { console.error(e); alert(e); diff --git a/frontend/src/main.scss b/frontend/src/main.scss deleted file mode 100644 index 3ab46d2..0000000 --- a/frontend/src/main.scss +++ /dev/null @@ -1,940 +0,0 @@ -@use 'styles/vars' as *; -@use 'styles/button'; -@use 'styles/form'; -@use 'styles/mixins' as *; -@use 'styles/settings'; - -@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300&family=Open+Sans&display=swap'); - -* { - margin: 0; - box-sizing: border-box; - color: white; - font-family: 'Open Sans', 'Segoe UI Emoji', sans-serif; - - &::selection { - color: white; - background-color: $accent; - } -} - -html { - font-size: 0.85rem; - - @media (max-width: $breakpoint) { - font-size: 0.6rem; - } -} - -img { - user-select: none; -} - -h1 { - - &, - * { - font-family: 'Comfortaa', sans-serif; - font-weight: 400; - } - - font-size: 6rem; - text-align: center; - padding-bottom: $medium-padding; - width: fit-content; - margin-inline: auto; - background: linear-gradient(90deg, $bright-blue, $bright-red); - background-clip: text; - -webkit-background-clip: text; - color: transparent; - - &::selection { - color: white; - -webkit-text-fill-color: white; - background-color: $accent; - } - - @media (max-width: $height-breakpoint) { - font-size: 4.5rem; - } - - @media (max-height: $height-breakpoint) { - display: none; - } -} - -html, -body, -canvas { - height: 100%; - width: 100%; - background: black; -} - -body { - overflow: hidden; - - #spinner-container { - width: 100%; - height: 100%; - position: absolute; - display: flex; - justify-content: center; - align-items: center; - top: 0; - - @include background; - - #spinner { - @include square(20vmax); - - @media (max-width: $breakpoint) { - @include square(20vmax); - } - } - } - - #landing-ui { - width: 100%; - height: 100%; - position: absolute; - top: 0; - - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - #overlay { - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - - pointer-events: none; - user-select: none; - - overflow: hidden; - - &>* { - display: inline-block; - position: absolute; - top: 0; - left: 0; - } - - .player-tag { - border-radius: 1000px; - transition: transform 200ms; - - &.blue { - color: $bright-blue; - - .health { - background-color: $bright-blue; - - &:before { - background-color: $bright-blue; - opacity: 0.3; - } - } - } - - &.red { - color: $bright-red; - - .health { - background-color: $bright-red; - - &:before { - background-color: $bright-red; - opacity: 0.4; - } - } - } - - .health { - position: relative; - height: 5px; - border-radius: 1000px; - - &:before { - content: ''; - position: absolute; - height: 5px; - width: 50px; - box-sizing: border-box; - border-radius: 1000px; - } - } - - .charge { - height: 3px; - margin-top: 2px; - border-radius: 1000px; - background-color: rgba(255, 255, 255, 0.65); - } - - .stats { - display: flex; - align-items: center; - gap: 8px; - font-size: 0.8em; - font-weight: 700; - text-shadow: 0 0 4px rgba(0, 0, 0, 0.9); - - .stat { - display: inline-flex; - align-items: center; - gap: 3px; - } - - .icon { - width: 1.1em; - height: 1.1em; - fill: currentColor; - filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); - } - } - } - - .ownership { - font-size: 0; - box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.2); - - @include square(50px); - border-radius: 1000px; - mask-image: url('../static/mask.svg'); - - &.contested { - animation: contested-pulse 0.7s ease-in-out infinite; - } - - &.keystone { - @include square(72px); - } - } - - .falling-point { - font-size: 1.2em; - font-weight: 700; - animation: falling-point 2s ease-out forwards; - - &.blue { - color: $bright-blue; - } - - &.red { - color: $bright-red; - } - } - - // Top-down radar of the whole arena (see Minimap). The circular border is - // the world boundary; dots are painted onto the canvas. - .minimap { - top: $small-padding; - left: $small-padding; - @include square(132px); - - // On narrow screens the centred scoreboard bar reaches close to the left - // edge, so drop the minimap below it to avoid a corner overlap. - @media (max-width: $breakpoint) { - @include square(96px); - top: calc(#{$small-padding} + 32px); - } - - border-radius: 1000px; - background-color: rgba(0, 0, 0, 0.35); - box-shadow: - inset 0 0 3px 0 rgba(0, 0, 0, 0.4), - 0 0 10px rgba(0, 0, 0, 0.3); - border: $border-width solid rgba(255, 255, 255, 0.25); - z-index: 100; - } - - // Off-screen pointer to the keystone "Heart", tinted by its current owner. - .keystone-arrow { - transition: transform 150ms; - opacity: 0.9; - - @include square($large-icon); - - @media (max-width: $breakpoint) { - @include square($small-icon); - } - - mask-image: url('../static/chevron.svg'); - mask-size: contain; - background-color: $bright-neutral; - - &.blue { - background-color: $bright-blue; - } - - &.red { - background-color: $bright-red; - } - - &.neutral { - background-color: $bright-neutral; - } - } - - .joystick { - @include square($large-icon * 1.3); - background-color: white; - box-shadow: inset 0 0 8px 3px rgba(0, 0, 0, 0.33); - opacity: 0.35; - border-radius: 1000px; - - div { - position: absolute; - top: 50%; - left: 50%; - background-color: #444; - box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.33); - - @include square($small-icon); - border-radius: 1000px; - } - } - - .touch-button { - display: none; - pointer-events: auto; - touch-action: none; - cursor: pointer; - - $size: $large-icon * 1.6; - @include square($size); - top: auto; - left: auto; - bottom: $medium-padding; - border-radius: 1000px; - background-color: rgba(255, 255, 255, 0.12); - box-shadow: inset 0 0 8px 3px rgba(0, 0, 0, 0.33); - border: $border-width solid rgba(255, 255, 255, 0.4); - - font-size: 1.6rem; - line-height: $size - 2 * $border-width; - text-align: center; - text-shadow: 0 0 6px rgba(0, 0, 0, 0.8); - - @media (hover: none) and (pointer: coarse) { - display: inline-block; - } - - &.fire { - right: $medium-padding; - - &::after { - content: '\25C9'; - } - - // Twin-stick aim indicator: drawn from the button centre toward the - // current drag direction while aiming a shot (see TouchListener). - .aim-line { - position: absolute; - left: 50%; - top: 50%; - width: 130px; - height: 3px; - border-radius: 2px; - transform-origin: left center; - transform: translateY(-50%) rotate(0rad); - background: linear-gradient(90deg, - rgba(255, 255, 255, 0.85), - rgba(255, 255, 255, 0)); - box-shadow: 0 0 6px rgba(0, 0, 0, 0.6); - opacity: 0; - pointer-events: none; - transition: opacity 80ms; - } - } - - &.leap { - right: $medium-padding; - bottom: $medium-padding + $large-icon * 1.6 + $small-padding; - - &::after { - content: '\25B2'; - } - } - - .strength-ring { - position: absolute; - top: -3px; - left: -3px; - width: calc(100% + 6px); - height: calc(100% + 6px); - border-radius: 1000px; - pointer-events: none; - -webkit-mask: radial-gradient(farthest-side, - transparent calc(100% - 5px), - #000 calc(100% - 5px)); - mask: radial-gradient(farthest-side, - transparent calc(100% - 5px), - #000 calc(100% - 5px)); - } - } - - .announcement { - top: 25%; - transform: translateX(calc(-50% + 50vw)) translateY(-50%); - - font-size: 3rem; - @include background; - z-index: 1000; - padding: $medium-padding; - border-radius: 16px; - - &:empty { - display: none; - } - - .blue { - color: $bright-blue; - } - - .red { - color: $bright-red; - } - } - - .tutorial-hint { - top: auto; - bottom: calc(#{$medium-padding} + #{$large-icon} * 1.6 + #{$medium-padding}); - left: 50%; - transform: translateX(-50%); - white-space: nowrap; - - font-size: 1.6rem; - @include background; - z-index: 1000; - padding: $small-padding $medium-padding; - border-radius: 1000px; - opacity: 0.9; - - &:empty { - display: none; - } - } - - .planet-progress { - top: $small-padding; - left: 50%; - transform: translateX(-50%); - width: 50%; - $height: 20px; - height: $height; - - z-index: 100; - pointer-events: none; - - background-color: rgba(0, 0, 0, 0.35); - box-shadow: inset 0 0 3px 0px rgba(0, 0, 0, 0.4); - border-radius: 1000px; - - .fill { - position: absolute; - top: 0; - height: $height; - transition: width $animation-time; - } - - .fill.blue { - right: 50%; - background: $bright-blue; - color: $bright-blue; - border-radius: 1000px 0 0 1000px; - } - - .fill.red { - left: 50%; - background: $bright-red; - color: $bright-red; - border-radius: 0 1000px 1000px 0; - } - - .fill.match-point { - z-index: 1; - animation: match-point-pulse 1.2s ease-in-out infinite; - } - - &::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - transform: translateX(-50%) translateY(-50%); - background-color: #fff; - height: $height + 8px; - width: 3px; - border-radius: 1000px; - z-index: 2; - } - - .score { - position: absolute; - top: 50%; - transform: translateY(-50%); - z-index: 3; - font-size: 0.95rem; - font-weight: 700; - line-height: 1; - color: #fff; - text-shadow: - 0 0 3px rgba(0, 0, 0, 0.95), - 0 0 6px rgba(0, 0, 0, 0.8); - opacity: 0.8; - transition: - opacity $animation-time, - text-shadow $animation-time, - font-size $animation-time; - } - - .score.blue { - right: calc(50% + #{$small-padding}); - } - - .score.red { - left: calc(50% + #{$small-padding}); - } - - .score.leading { - opacity: 1; - font-size: 1.2rem; - } - - .score.blue.leading { - text-shadow: - 0 0 3px rgba(0, 0, 0, 0.95), - 0 0 8px $bright-blue; - } - - .score.red.leading { - text-shadow: - 0 0 3px rgba(0, 0, 0, 0.95), - 0 0 8px $bright-red; - } - - .you-marker { - position: absolute; - top: 100%; - margin-top: 4px; - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.08em; - white-space: nowrap; - text-shadow: 0 0 4px rgba(0, 0, 0, 0.9); - - &::after { - content: ''; - position: absolute; - left: 50%; - bottom: 100%; - transform: translateX(-50%); - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-bottom: 5px solid currentColor; - } - - &.blue { - right: calc(50% + #{$small-padding}); - color: $bright-blue; - } - - &.red { - left: calc(50% + #{$small-padding}); - color: $bright-red; - } - } - } - } - - #server-container { - max-height: 30vh; - overflow-y: auto; - - &::-webkit-scrollbar-track, - &::-webkit-scrollbar { - background-color: transparent; - width: 3px; - } - - &::-webkit-scrollbar-thumb { - background-color: $accent; - border-radius: $border-radius; - } - - transition: box-shadow $animation-time; - } - - .full-screen-controllers { - position: absolute; - bottom: 0; - left: 0; - - @media (hover: none) and (pointer: coarse) { - display: none; - } - - box-sizing: content-box; - - user-select: none; - cursor: pointer; - - padding: $medium-padding; - @include square($large-icon); - - @media (max-width: $breakpoint) { - @include square($small-icon); - padding: $small-padding; - } - - &:not(:first-child) { - visibility: hidden; - } - } -} - -.feedback-hud { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - user-select: none; - z-index: 2000; - overflow: hidden; - - .hitmarker { - position: absolute; - transform: translate(-50%, -50%); - @include square(34px); - opacity: 0.95; - - &::before, - &::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 16px; - height: 3px; - border-radius: 2px; - background-color: white; - box-shadow: - 0 0 6px rgba(255, 255, 255, 0.9), - 0 0 3px rgba(0, 0, 0, 0.9); - transform-origin: center; - } - - &::before { - transform: translate(-50%, -50%) rotate(45deg); - } - - &::after { - transform: translate(-50%, -50%) rotate(-45deg); - } - - animation: hitmarker-pop 220ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards; - - &.charged { - @include square(44px); - - &::before, - &::after { - width: 24px; - height: 4px; - background-color: $accent; - box-shadow: - 0 0 10px rgba($accent, 0.9), - 0 0 4px rgba(0, 0, 0, 0.9); - } - } - } - - .kill-flash { - position: absolute; - inset: 0; - pointer-events: none; - background: radial-gradient(ellipse at center, - transparent 42%, - rgba($accent, 0) 56%, - rgba($accent, 0.55) 100%); - animation: kill-flash 420ms ease-out forwards; - - &.charged { - background: radial-gradient(ellipse at center, - transparent 36%, - rgba($accent, 0) 50%, - rgba($accent, 0.8) 100%); - } - } - - .kill-popup { - position: absolute; - transform: translate(-50%, -50%); - font-weight: 800; - font-size: 1.7rem; - white-space: nowrap; - color: $bright-neutral; - text-shadow: - 0 0 10px rgba(0, 0, 0, 0.9), - 0 0 18px rgba(255, 255, 255, 0.35); - animation: kill-popup-rise 1200ms cubic-bezier(0.18, 0.9, 0.3, 1) forwards; - - &.charged { - color: $accent; - text-shadow: - 0 0 10px rgba(0, 0, 0, 0.9), - 0 0 18px rgba($accent, 0.6); - } - - .heal { - color: #6fcf6f; - } - } - - .streak-callout { - position: absolute; - top: 33%; - left: 50%; - transform: translateX(-50%); - font-weight: 800; - font-size: 3rem; - color: $accent; - text-shadow: - 0 0 12px rgba(0, 0, 0, 0.9), - 0 0 24px rgba($accent, 0.7); - white-space: nowrap; - animation: streak-pop 1400ms cubic-bezier(0.18, 0.9, 0.3, 1) forwards; - } - - .killfeed { - position: absolute; - top: calc(#{$small-padding} + 36px); - right: $medium-padding; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 6px; - - .kill-entry { - font-size: 1rem; - padding: 4px 10px; - border-radius: 1000px; - @include background; - text-shadow: 0 0 4px rgba(0, 0, 0, 0.9); - animation: kill-entry-fade 4500ms ease-out forwards; - - b { - color: $accent; - } - } - } - - // Centred "Eliminated" overlay shown while the local player is dead; the - // respawn countdown is the server-driven "Reviving in N…" announcement. - .elimination { - position: absolute; - top: 42%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - animation: elimination-in 220ms ease-out; - - .elimination-title { - font-weight: 800; - font-size: 3.4rem; - letter-spacing: 0.04em; - color: $accent; - text-shadow: - 0 0 12px rgba(0, 0, 0, 0.9), - 0 0 28px rgba($accent, 0.6); - } - - .elimination-sub { - margin-top: 6px; - font-size: 1.3rem; - opacity: 0.85; - text-shadow: 0 0 8px rgba(0, 0, 0, 0.9); - } - } -} - -@keyframes contested-pulse { - - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0.35; - } -} - -.charge-ring { - position: fixed; - @include square(56px); - margin: -28px 0 0 -28px; - border-radius: 1000px; - pointer-events: none; - user-select: none; - z-index: 2000; - opacity: 0; - transition: opacity 100ms; - -webkit-mask: radial-gradient(farthest-side, - transparent calc(100% - 6px), - #000 calc(100% - 6px)); - mask: radial-gradient(farthest-side, - transparent calc(100% - 6px), - #000 calc(100% - 6px)); - - &.full { - filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.8)); - } -} - -@keyframes match-point-pulse { - - 0%, - 100% { - box-shadow: 0 0 4px 0 currentColor; - } - - 50% { - box-shadow: 0 0 14px 3px currentColor; - } -} - -@keyframes falling-point { - 0% { - opacity: 1; - transform: translate(-50%, -50%); - } - - 100% { - opacity: 0; - transform: translate(-50%, calc(-50% - 90px)); - } -} - -@keyframes hitmarker-pop { - 0% { - opacity: 0; - transform: translate(-50%, -50%) scale(2); - } - - 18% { - opacity: 1; - transform: translate(-50%, -50%) scale(0.82); - } - - 36% { - transform: translate(-50%, -50%) scale(1.06); - } - - 100% { - opacity: 0; - transform: translate(-50%, -50%) scale(0.95); - } -} - -@keyframes kill-flash { - 0% { - opacity: 0; - } - - 12% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -@keyframes kill-popup-rise { - 0% { - opacity: 0; - transform: translate(-50%, -20%) scale(0.6); - } - - 12% { - opacity: 1; - transform: translate(-50%, -56%) scale(1.25); - } - - 28% { - transform: translate(-50%, -62%) scale(1); - } - - 100% { - opacity: 0; - transform: translate(-50%, -170%) scale(1); - } -} - -@keyframes streak-pop { - 0% { - opacity: 0; - transform: translateX(-50%) scale(0.5); - } - - 16% { - opacity: 1; - transform: translateX(-50%) scale(1.25); - } - - 32% { - transform: translateX(-50%) scale(0.95); - } - - 70% { - opacity: 1; - transform: translateX(-50%) scale(1); - } - - 100% { - opacity: 0; - transform: translateX(-50%) scale(1); - } -} - -@keyframes kill-entry-fade { - 0% { - opacity: 0; - transform: translateX(20px); - } - - 10% { - opacity: 1; - transform: translateX(0); - } - - 80% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -@keyframes elimination-in { - 0% { - opacity: 0; - transform: translate(-50%, -42%) scale(0.9); - } - - 100% { - opacity: 1; - transform: translate(-50%, -50%) scale(1); - } -} \ No newline at end of file diff --git a/frontend/src/scripts/analytics.ts b/frontend/src/scripts/analytics.ts deleted file mode 100644 index e732cc4..0000000 --- a/frontend/src/scripts/analytics.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { init as plausibleInit } from '@plausible-analytics/tracker'; - -const ANALYTICS_AUTO_CAPTURE_PAGEVIEWS = true; -const ANALYTICS_DOMAIN = 'doppler.schmelczer.dev'; -const ANALYTICS_ENDPOINT = 'https://stats.schmelczer.dev/status'; -const ANALYTICS_LOGGING = process.env.NODE_ENV !== 'production'; - -try { - plausibleInit({ - domain: ANALYTICS_DOMAIN, - endpoint: ANALYTICS_ENDPOINT, - autoCapturePageviews: ANALYTICS_AUTO_CAPTURE_PAGEVIEWS, - logging: ANALYTICS_LOGGING, - }); -} catch (error) { - console.warn('Could not initialize analytics.', error); -} diff --git a/frontend/src/scripts/charge-indicator.ts b/frontend/src/scripts/charge-indicator.ts deleted file mode 100644 index 239e33b..0000000 --- a/frontend/src/scripts/charge-indicator.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { holdDurationToCharge } from 'shared'; -import { Pointer } from './helper/pointer'; - -export abstract class ChargeIndicator { - private static element?: HTMLElement; - private static heldSince = 0; - private static raf = 0; - private static followPointer = false; - - public static begin(x: number, y: number, followPointer = false) { - this.end(); - - const element = document.createElement('div'); - element.className = 'charge-ring'; - element.style.left = `${x}px`; - element.style.top = `${y}px`; - document.body.appendChild(element); - - this.element = element; - this.heldSince = performance.now(); - this.followPointer = followPointer; - this.raf = requestAnimationFrame(this.update); - } - - public static end() { - if (this.element) { - cancelAnimationFrame(this.raf); - this.element.parentElement?.removeChild(this.element); - this.element = undefined; - } - } - - private static update = () => { - const element = ChargeIndicator.element; - if (!element) { - return; - } - - const charge = holdDurationToCharge( - (performance.now() - ChargeIndicator.heldSince) / 1000, - ); - element.style.opacity = charge < 0.12 ? '0' : '1'; - element.style.background = `conic-gradient(rgba(255, 255, 255, 0.85) ${ - charge * 360 - }deg, rgba(255, 255, 255, 0.15) 0deg)`; - element.classList.toggle('full', charge >= 1); - - if (ChargeIndicator.followPointer) { - const cursor = Pointer.getDisplayPosition(); - if (cursor) { - element.style.left = `${cursor.x}px`; - element.style.top = `${cursor.y}px`; - } - } - - ChargeIndicator.raf = requestAnimationFrame(ChargeIndicator.update); - }; -} diff --git a/frontend/src/scripts/commands/command-socket.ts b/frontend/src/scripts/commands/command-socket.ts deleted file mode 100644 index 830b19b..0000000 --- a/frontend/src/scripts/commands/command-socket.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Command, CommandReceiver, serialize, TransportEvents } from 'shared'; -import { Socket } from 'socket.io-client'; - -export class CommandSocket extends CommandReceiver { - constructor(private readonly socket: Socket) { - super(); - } - - private commandQueue: Array = []; - protected defaultCommandExecutor(command: Command) { - this.commandQueue.push(command); - } - - public sendQueuedCommands() { - if (this.commandQueue.length > 0) { - this.socket.emit(TransportEvents.PlayerToServer, serialize(this.commandQueue)); - this.commandQueue = []; - } - } -} diff --git a/frontend/src/scripts/commands/generators/keyboard-listener.ts b/frontend/src/scripts/commands/generators/keyboard-listener.ts new file mode 100644 index 0000000..acde088 --- /dev/null +++ b/frontend/src/scripts/commands/generators/keyboard-listener.ts @@ -0,0 +1,43 @@ +import { vec2 } from 'gl-matrix'; +import { CommandGenerator, MoveActionCommand } from 'shared'; + +export class KeyboardListener extends CommandGenerator { + private keysDown: Set = new Set(); + + constructor(target: HTMLElement) { + super(); + + target.addEventListener('keydown', (event: KeyboardEvent) => { + const key = this.normalize(event.key); + this.keysDown.add(key); + this.generateCommands(); + }); + + target.addEventListener('keyup', (event: KeyboardEvent) => { + const key = this.normalize(event.key); + this.keysDown.delete(key); + this.generateCommands(); + }); + } + + private generateCommands() { + const up = ~~( + this.keysDown.has('w') || + this.keysDown.has('arrowup') || + this.keysDown.has(' ') + ); + const down = ~~(this.keysDown.has('s') || this.keysDown.has('arrowdown')); + const left = ~~(this.keysDown.has('a') || this.keysDown.has('arrowleft')); + const right = ~~(this.keysDown.has('d') || this.keysDown.has('arrowright')); + + const movement = vec2.fromValues(right - left, up - down); + if (vec2.squaredLength(movement) > 0) { + vec2.normalize(movement, movement); + } + this.sendCommandToSubcribers(new MoveActionCommand(movement)); + } + + private normalize(key: string): string { + return key.toLowerCase(); + } +} diff --git a/frontend/src/scripts/commands/generators/mouse-listener.ts b/frontend/src/scripts/commands/generators/mouse-listener.ts new file mode 100644 index 0000000..b8cb30a --- /dev/null +++ b/frontend/src/scripts/commands/generators/mouse-listener.ts @@ -0,0 +1,39 @@ +import { vec2 } from 'gl-matrix'; +import { + CommandGenerator, + PrimaryActionCommand, + SecondaryActionCommand, + TernaryActionCommand, +} from 'shared'; +import { Game } from '../../game'; + +export class MouseListener extends CommandGenerator { + constructor(target: HTMLElement, private readonly game: Game) { + super(); + + target.addEventListener('mousemove', (event: MouseEvent) => { + const position = this.positionFromEvent(event); + this.sendCommandToSubcribers(new PrimaryActionCommand(position)); + }); + + target.addEventListener('mousedown', (event: MouseEvent) => { + const position = this.positionFromEvent(event); + + if (event.button == 0) { + this.sendCommandToSubcribers(new SecondaryActionCommand(position)); + } + }); + + target.addEventListener('contextmenu', (event: MouseEvent) => { + event.preventDefault(); + const position = this.positionFromEvent(event); + this.sendCommandToSubcribers(new TernaryActionCommand(position)); + }); + } + + private positionFromEvent(event: MouseEvent): vec2 { + return this.game.displayToWorldCoordinates( + vec2.fromValues(event.clientX, event.clientY), + ); + } +} diff --git a/frontend/src/scripts/commands/generators/touch-listener.ts b/frontend/src/scripts/commands/generators/touch-listener.ts new file mode 100644 index 0000000..681518c --- /dev/null +++ b/frontend/src/scripts/commands/generators/touch-listener.ts @@ -0,0 +1,65 @@ +import { vec2 } from 'gl-matrix'; +import { + CommandGenerator, + PrimaryActionCommand, + SecondaryActionCommand, + TernaryActionCommand, + MoveActionCommand, +} from 'shared'; +import { Game } from '../../game'; + +export class TouchListener extends CommandGenerator { + private previousPosition = vec2.create(); + + constructor(target: HTMLElement, private readonly game: Game) { + super(); + + target.addEventListener('touchstart', (event: TouchEvent) => { + event.preventDefault(); + + const touchCount = event.touches.length; + const position = this.positionFromEvent(event); + this.previousPosition = position; + + if (touchCount == 1) { + this.sendCommandToSubcribers(new PrimaryActionCommand(position)); + } else if (touchCount == 2) { + this.sendCommandToSubcribers(new SecondaryActionCommand(position)); + } else { + this.sendCommandToSubcribers(new TernaryActionCommand(position)); + } + }); + + target.addEventListener('touchmove', (event: TouchEvent) => { + event.preventDefault(); + + const position = this.positionFromEvent(event); + const movement = vec2.subtract(vec2.create(), position, this.previousPosition); + + if (vec2.squaredLength(movement) > 0) { + vec2.normalize(movement, movement); + this.sendCommandToSubcribers(new MoveActionCommand(movement)); + } + + this.previousPosition = position; + }); + + target.addEventListener('touchend', (event: TouchEvent) => { + event.preventDefault(); + this.sendCommandToSubcribers(new MoveActionCommand(vec2.create())); + }); + } + + private positionFromEvent(event: TouchEvent): vec2 { + const touches = Array.prototype.slice.call(event.touches); + const center = touches.reduce( + (center: vec2, touch: Touch) => + vec2.add(center, center, vec2.fromValues(-touch.clientX, touch.clientY)), + vec2.create(), + ); + + return this.game.displayToWorldCoordinates( + vec2.scale(center, center, 1 / event.touches.length), + ); + } +} diff --git a/frontend/src/scripts/commands/keyboard-listener.ts b/frontend/src/scripts/commands/keyboard-listener.ts deleted file mode 100644 index 342d851..0000000 --- a/frontend/src/scripts/commands/keyboard-listener.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { CommandGenerator, LeapActionCommand, MoveActionCommand } from 'shared'; -import { localCharacterPredictor } from '../helper/prediction/local-character-predictor'; - -export class KeyboardListener extends CommandGenerator { - private keysDown: Set = new Set(); - - constructor() { - super(); - - addEventListener('keydown', this.keyDownListener); - addEventListener('keyup', this.keyUpListener); - addEventListener('blur', this.blurListener); - } - - private keyDownListener = (event: KeyboardEvent) => { - const key = event.key.toLowerCase(); - // Space leaps (W / ArrowUp already cover walking up). Edge-triggered so a - // held key's auto-repeat doesn't spam leaps. - if ((key === ' ' || key === 'shift') && !this.keysDown.has(key)) { - const clientTimeMs = localCharacterPredictor.recordLeap(); - this.sendCommandToSubscribers(new LeapActionCommand(clientTimeMs)); - } - this.keysDown.add(key); - this.generateCommands(); - }; - - private keyUpListener = (event: KeyboardEvent) => { - this.keysDown.delete(event.key.toLowerCase()); - this.generateCommands(); - }; - - private blurListener = () => { - this.keysDown.clear(); - this.generateCommands(); - }; - - private generateCommands() { - const up = ~~(this.keysDown.has('w') || this.keysDown.has('arrowup')); - const down = ~~(this.keysDown.has('s') || this.keysDown.has('arrowdown')); - const left = ~~(this.keysDown.has('a') || this.keysDown.has('arrowleft')); - const right = ~~(this.keysDown.has('d') || this.keysDown.has('arrowright')); - - const movement = vec2.fromValues(right - left, up - down); - if (vec2.squaredLength(movement) > 0) { - vec2.normalize(movement, movement); - } - - const clientTimeMs = localCharacterPredictor.recordInput(movement); - this.sendCommandToSubscribers(new MoveActionCommand(movement, clientTimeMs)); - } - - public destroy() { - removeEventListener('keydown', this.keyDownListener); - removeEventListener('keyup', this.keyUpListener); - removeEventListener('blur', this.blurListener); - } -} diff --git a/frontend/src/scripts/commands/mouse-listener.ts b/frontend/src/scripts/commands/mouse-listener.ts deleted file mode 100644 index c4bd30d..0000000 --- a/frontend/src/scripts/commands/mouse-listener.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { CommandGenerator, PrimaryActionCommand, holdDurationToCharge } from 'shared'; -import { Game } from '../game'; -import { ChargeIndicator } from '../charge-indicator'; -import { Pointer } from '../helper/pointer'; - -export class MouseListener extends CommandGenerator { - // Timestamp (ms) of the primary press, or null when not held. On release the - // held duration is mapped to the charge scalar; a quick tap reads as ~0. - private primaryDownAt: number | null = null; - - constructor( - private target: HTMLElement, - private readonly game: Game, - ) { - super(); - - target.addEventListener('mousedown', this.mouseDownListener); - target.addEventListener('mouseup', this.mouseUpListener); - target.addEventListener('mousemove', this.mouseMoveListener); - target.addEventListener('contextmenu', this.contextMenuListener); - } - - // Only the screen position is stored; it is reprojected to world space each - // frame so the gaze stays correct even while the camera pans under a still - // cursor. - private mouseMoveListener = (event: MouseEvent) => { - Pointer.setDisplayPosition(event.clientX, event.clientY); - }; - - private mouseDownListener = (event: MouseEvent) => { - if (event.button === 0) { - this.primaryDownAt = performance.now(); - // The ring follows the cursor and only fades in once this press has - // clearly become a hold. - ChargeIndicator.begin(event.clientX, event.clientY, true); - } - }; - - private mouseUpListener = (event: MouseEvent) => { - if (event.button !== 0 || this.primaryDownAt === null) { - return; - } - - ChargeIndicator.end(); - const charge = holdDurationToCharge((performance.now() - this.primaryDownAt) / 1000); - this.primaryDownAt = null; - this.sendCommandToSubscribers( - new PrimaryActionCommand(this.positionFromEvent(event), charge), - ); - }; - - // Suppress the browser context menu on the canvas; right-click has no action. - private contextMenuListener = (event: MouseEvent) => { - event.preventDefault(); - }; - - private positionFromEvent(event: MouseEvent): vec2 { - return this.game.displayToWorldCoordinates( - vec2.fromValues(event.clientX, event.clientY), - ); - } - - public destroy() { - ChargeIndicator.end(); - this.target.removeEventListener('mousedown', this.mouseDownListener); - this.target.removeEventListener('mouseup', this.mouseUpListener); - this.target.removeEventListener('mousemove', this.mouseMoveListener); - this.target.removeEventListener('contextmenu', this.contextMenuListener); - } -} diff --git a/frontend/src/scripts/commands/receivers/command-receiver-socket.ts b/frontend/src/scripts/commands/receivers/command-receiver-socket.ts new file mode 100644 index 0000000..16a2687 --- /dev/null +++ b/frontend/src/scripts/commands/receivers/command-receiver-socket.ts @@ -0,0 +1,11 @@ +import { Command, CommandReceiver, serialize, TransportEvents } from 'shared'; + +export class CommandReceiverSocket extends CommandReceiver { + constructor(private readonly socket: SocketIOClient.Socket) { + super(); + } + + protected defaultCommandExecutor(command: Command) { + this.socket.emit(TransportEvents.PlayerToServer, serialize(command)); + } +} diff --git a/frontend/src/scripts/commands/touch-listener.ts b/frontend/src/scripts/commands/touch-listener.ts deleted file mode 100644 index ab91ce8..0000000 --- a/frontend/src/scripts/commands/touch-listener.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - CommandGenerator, - MoveActionCommand, - last, - PrimaryActionCommand, - LeapActionCommand, - holdDurationToCharge, - settings, -} from 'shared'; -import { Game } from '../game'; -import { ChargeIndicator } from '../charge-indicator'; -import { localCharacterPredictor } from '../helper/prediction/local-character-predictor'; - -export class TouchListener extends CommandGenerator { - private static readonly deadZone = 8; - private static readonly deltaScaling = 0.4; - // Min screen drag (px) from the fire button before a shot is aimed by the - // drag direction instead of firing straight ahead. - private static readonly aimDeadZone = 18; - - private joystick: HTMLElement; - private joystickButton: HTMLElement; - private isJoystickActive = false; - private touchStartPosition!: vec2; - private primaryDownAt: number | null = null; - - private fireButton: HTMLElement; - private fireStrengthRing: HTMLElement; - private fireAimLine!: HTMLElement; - private leapButton: HTMLElement; - - private fireDownAt: number | null = null; - private fireButtonCenter: vec2 | null = null; - private fireAimScreen: vec2 | null = null; - - constructor( - private target: HTMLElement, - private overlay: HTMLElement, - private readonly game: Game, - ) { - super(); - - this.joystick = document.createElement('div'); - this.joystick.className = 'joystick'; - this.joystickButton = document.createElement('div'); - this.joystick.appendChild(this.joystickButton); - - this.fireButton = document.createElement('div'); - this.fireButton.className = 'touch-button fire'; - this.fireStrengthRing = document.createElement('div'); - this.fireStrengthRing.className = 'strength-ring'; - this.fireButton.appendChild(this.fireStrengthRing); - this.fireAimLine = document.createElement('div'); - this.fireAimLine.className = 'aim-line'; - this.fireButton.appendChild(this.fireAimLine); - - this.fireButton.addEventListener('touchstart', this.fireButtonDownListener); - this.fireButton.addEventListener('touchmove', this.fireButtonMoveListener); - this.fireButton.addEventListener('touchend', this.fireButtonUpListener); - - this.leapButton = document.createElement('div'); - this.leapButton.className = 'touch-button leap'; - this.leapButton.addEventListener('touchstart', this.leapButtonListener); - - this.overlay.appendChild(this.fireButton); - this.overlay.appendChild(this.leapButton); - - target.addEventListener('touchstart', this.touchStartListener); - target.addEventListener('touchmove', this.touchMoveListener); - target.addEventListener('touchend', this.touchEndListener); - } - - private touchStartListener = (event: TouchEvent) => { - event.preventDefault(); - if (this.isJoystickActive) { - const center = vec2.fromValues( - last(event.touches)!.clientX, - last(event.touches)!.clientY, - ); - this.sendCommandToSubscribers( - new PrimaryActionCommand(this.game.displayToWorldCoordinates(center)), - ); - } else { - this.touchStartPosition = vec2.fromValues( - event.touches[0].clientX, - event.touches[0].clientY, - ); - this.primaryDownAt = performance.now(); - ChargeIndicator.begin(this.touchStartPosition.x, this.touchStartPosition.y); - } - }; - - private touchMoveListener = (event: TouchEvent) => { - event.preventDefault(); - - const touchPosition = vec2.fromValues( - event.touches[0].clientX, - event.touches[0].clientY, - ); - - const delta = vec2.subtract(vec2.create(), touchPosition, this.touchStartPosition); - vec2.scale(delta, delta, TouchListener.deltaScaling); - const deltaLength = vec2.length(delta); - - if (!this.isJoystickActive && deltaLength > TouchListener.deadZone) { - this.isJoystickActive = true; - this.primaryDownAt = null; - ChargeIndicator.end(); - this.overlay.appendChild(this.joystick); - this.joystickButton.style.transform = `translateX(-50%) translateY(-50%)`; - this.joystick.style.transform = `translateX(${this.touchStartPosition.x}px) translateY(${this.touchStartPosition.y}px) translateX(-50%) translateY(-50%)`; - } - - const maxLength = 20; - vec2.scale(delta, delta, Math.min(1, maxLength / deltaLength)); - this.joystickButton.style.transform = `translateX(${delta.x}px) translateY(${delta.y}px) translateX(-50%) translateY(-50%)`; - - vec2.set(delta, delta.x, -delta.y); - if (deltaLength > TouchListener.deadZone) { - const direction = vec2.normalize(delta, delta); - this.sendMove(direction); - } else { - this.sendMove(vec2.create()); - } - }; - - private sendMove(direction: vec2) { - const clientTimeMs = localCharacterPredictor.recordInput(direction); - this.sendCommandToSubscribers(new MoveActionCommand(direction, clientTimeMs)); - } - - private touchEndListener = (event: TouchEvent) => { - event.preventDefault(); - - if (!this.isJoystickActive) { - ChargeIndicator.end(); - const charge = - this.primaryDownAt === null - ? 0 - : holdDurationToCharge((performance.now() - this.primaryDownAt) / 1000); - this.primaryDownAt = null; - const center = vec2.fromValues( - event.changedTouches[0].clientX, - event.changedTouches[0].clientY, - ); - this.sendCommandToSubscribers( - new PrimaryActionCommand(this.game.displayToWorldCoordinates(center), charge), - ); - } else if (event.touches.length === 0) { - this.isJoystickActive = false; - this.joystick.parentElement?.removeChild(this.joystick); - this.sendMove(vec2.create()); - } - }; - - private swallowTouch = (event: TouchEvent) => { - event.preventDefault(); - event.stopPropagation(); - }; - - private leapButtonListener = (event: TouchEvent) => { - this.swallowTouch(event); - const clientTimeMs = localCharacterPredictor.recordLeap(); - this.sendCommandToSubscribers(new LeapActionCommand(clientTimeMs)); - }; - - private fireButtonDownListener = (event: TouchEvent) => { - this.swallowTouch(event); - this.fireDownAt = performance.now(); - const rect = this.fireButton.getBoundingClientRect(); - this.fireButtonCenter = vec2.fromValues( - rect.left + rect.width / 2, - rect.top + rect.height / 2, - ); - this.fireAimScreen = null; - ChargeIndicator.begin(this.fireButtonCenter[0], this.fireButtonCenter[1]); - }; - - // Dragging from the fire button aims the shot: the drag vector sets the - // direction, decoupling aim from movement so a touch player can fire one way - // while walking another. A tap with no meaningful drag fires straight ahead. - private fireButtonMoveListener = (event: TouchEvent) => { - this.swallowTouch(event); - if (this.fireDownAt === null || !this.fireButtonCenter) { - return; - } - const touch = event.targetTouches[0] ?? event.changedTouches[0]; - if (!touch) { - return; - } - this.fireAimScreen = vec2.fromValues(touch.clientX, touch.clientY); - const dx = this.fireAimScreen[0] - this.fireButtonCenter[0]; - const dy = this.fireAimScreen[1] - this.fireButtonCenter[1]; - if (dx * dx + dy * dy > TouchListener.aimDeadZone * TouchListener.aimDeadZone) { - this.fireAimLine.style.opacity = '1'; - this.fireAimLine.style.transform = `translateY(-50%) rotate(${Math.atan2(dy, dx)}rad)`; - } else { - this.fireAimLine.style.opacity = '0'; - } - }; - - private fireButtonUpListener = (event: TouchEvent) => { - this.swallowTouch(event); - ChargeIndicator.end(); - this.fireAimLine.style.opacity = '0'; - if (this.fireDownAt === null) { - return; - } - - const charge = holdDurationToCharge((performance.now() - this.fireDownAt) / 1000); - this.fireDownAt = null; - - const character = this.game.gameObjects.player; - if (!character) { - this.fireButtonCenter = null; - this.fireAimScreen = null; - return; - } - - // Screen drag → world aim direction (flip Y: screen +y is down). Below the - // dead-zone it's a tap, so fall back to firing along the facing direction. - let direction = character.facingDirection; - if (this.fireButtonCenter && this.fireAimScreen) { - const dx = this.fireAimScreen[0] - this.fireButtonCenter[0]; - const dy = this.fireAimScreen[1] - this.fireButtonCenter[1]; - if (dx * dx + dy * dy > TouchListener.aimDeadZone * TouchListener.aimDeadZone) { - direction = vec2.normalize(vec2.create(), vec2.fromValues(dx, -dy)); - } - } - this.fireButtonCenter = null; - this.fireAimScreen = null; - - const aim = vec2.scaleAndAdd( - vec2.create(), - character.bodyCenter, - direction, - settings.touchAimRange, - ); - this.sendCommandToSubscribers(new PrimaryActionCommand(aim, charge)); - }; - - public update(_deltaTimeInSeconds: number) { - if (!this.fireButton.parentElement) { - this.overlay.appendChild(this.fireButton); - } - if (!this.leapButton.parentElement) { - this.overlay.appendChild(this.leapButton); - } - - const character = this.game.gameObjects.player; - if (character) { - this.fireStrengthRing.style.background = `conic-gradient(rgba(255, 255, 255, 0.75) ${ - character.strengthFraction * 360 - }deg, transparent 0deg)`; - } - } - - public destroy() { - ChargeIndicator.end(); - this.target.removeEventListener('touchstart', this.touchStartListener); - this.target.removeEventListener('touchmove', this.touchMoveListener); - this.target.removeEventListener('touchend', this.touchEndListener); - - this.fireButton.removeEventListener('touchstart', this.fireButtonDownListener); - this.fireButton.removeEventListener('touchmove', this.fireButtonMoveListener); - this.fireButton.removeEventListener('touchend', this.fireButtonUpListener); - this.leapButton.removeEventListener('touchstart', this.leapButtonListener); - - this.fireButton.parentElement?.removeChild(this.fireButton); - this.leapButton.parentElement?.removeChild(this.leapButton); - } -} diff --git a/frontend/src/scripts/commands/types/before-destroy.ts b/frontend/src/scripts/commands/types/before-destroy.ts deleted file mode 100644 index 3020623..0000000 --- a/frontend/src/scripts/commands/types/before-destroy.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Command } from 'shared'; - -export class BeforeDestroyCommand extends Command { - public constructor() { - super(); - } -} diff --git a/frontend/src/scripts/commands/types/move-to.ts b/frontend/src/scripts/commands/types/move-to.ts new file mode 100644 index 0000000..ed15ddb --- /dev/null +++ b/frontend/src/scripts/commands/types/move-to.ts @@ -0,0 +1,8 @@ +import { vec2 } from 'gl-matrix'; +import { Command } from 'shared'; + +export class MoveToCommand extends Command { + public constructor(public readonly position: vec2) { + super(); + } +} diff --git a/frontend/src/scripts/commands/types/render.ts b/frontend/src/scripts/commands/types/render.ts index c83f5a1..54a7563 100644 --- a/frontend/src/scripts/commands/types/render.ts +++ b/frontend/src/scripts/commands/types/render.ts @@ -2,11 +2,7 @@ import { Renderer } from 'sdf-2d'; import { Command } from 'shared'; export class RenderCommand extends Command { - public constructor( - public readonly renderer: Renderer, - public readonly overlay: HTMLElement, - public readonly shouldChangeLayout: boolean, - ) { + public constructor(public readonly renderer: Renderer) { super(); } } diff --git a/frontend/src/scripts/commands/types/step.ts b/frontend/src/scripts/commands/types/step.ts deleted file mode 100644 index ff0d3cc..0000000 --- a/frontend/src/scripts/commands/types/step.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Command } from 'shared'; - -export class StepCommand extends Command { - public constructor(public readonly deltaTimeInSeconds: number) { - super(); - } -} diff --git a/frontend/src/scripts/config/configuration.ts b/frontend/src/scripts/config/configuration.ts new file mode 100644 index 0000000..fedb2e5 --- /dev/null +++ b/frontend/src/scripts/config/configuration.ts @@ -0,0 +1,38 @@ +import firebase from 'firebase/app'; +import 'firebase/firebase-remote-config'; + +export abstract class Configuration { + private static remoteConfig: firebase.remoteConfig.RemoteConfig; + private static initialized = false; + + public static async initialize(): Promise { + const firebaseConfig = { + apiKey: 'AIzaSyBG85dp-AhaCW-qi_6mu77wDPSipzipIF4', + authDomain: 'decla-red.firebaseapp.com', + projectId: 'decla-red', + appId: '1:635208271441:web:c910843ae7e0549dadda70', + }; + + firebase.initializeApp(firebaseConfig); + + this.remoteConfig = firebase.remoteConfig(); + + this.remoteConfig.settings = { + minimumFetchIntervalMillis: 0, // todo: 3600 * 1000, + fetchTimeoutMillis: 15 * 1000, + } as any; + + await this.remoteConfig.ensureInitialized(); + await this.remoteConfig.fetchAndActivate(); + + this.initialized = true; + } + + public static get servers(): Array { + if (!this.initialized) { + throw new Error('Configuration should be initialized'); + } + + return JSON.parse(this.remoteConfig.getValue('online_servers').asString()); + } +} diff --git a/frontend/src/scripts/configuration.ts b/frontend/src/scripts/configuration.ts deleted file mode 100644 index 9ab3fb6..0000000 --- a/frontend/src/scripts/configuration.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Hardcoded list of game servers the landing page offers to players. - * - * Each entry is the public origin of a dockerized `doppler-server` instance. - * The join screen polls `/state` (see `serverInformationEndpoint`) and - * only shows a server once it responds, so listing an offline origin here is - * harmless. Add or remove origins as you deploy more server containers. - */ -// This origin predates the rebrand; update it once the game server is -// redeployed under a doppler subdomain. -const productionServers: Array = ['https://declared.schmelczer.dev']; - -/** - * When the page is served from localhost (i.e. the webpack-dev-server), also - * offer the local backend so a `npm start` server can be joined during - * development. The backend's default port is 3000 (see backend/src/options.ts). - * In production the page is served from its own hostname, so this never leaks. - */ -const isDevelopment = - typeof location !== 'undefined' && - (location.hostname === 'localhost' || location.hostname === '127.0.0.1'); - -const servers: Array = isDevelopment - ? [`http://${location.hostname}:3000`, ...productionServers] - : productionServers; - -export abstract class Configuration { - public static async initialize(): Promise { - // Kept async for call-site compatibility; the server list is static now. - } - - public static get servers(): Array { - return servers; - } -} diff --git a/frontend/src/scripts/feedback-hud.ts b/frontend/src/scripts/feedback-hud.ts deleted file mode 100644 index adbf068..0000000 --- a/frontend/src/scripts/feedback-hud.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { settings } from 'shared'; -import { Pointer } from './helper/pointer'; - -export abstract class FeedbackHud { - private static root?: HTMLElement; - private static killfeed?: HTMLElement; - private static elimination?: HTMLElement; - - private static ensureRoot(): { root: HTMLElement; killfeed: HTMLElement } { - if (!this.root || !this.killfeed) { - this.root = document.createElement('div'); - this.root.className = 'feedback-hud'; - - this.killfeed = document.createElement('div'); - this.killfeed.className = 'killfeed'; - this.root.appendChild(this.killfeed); - - document.body.appendChild(this.root); - } - return { root: this.root, killfeed: this.killfeed }; - } - - private static focusPoint(): { x: number; y: number } { - const cursor = Pointer.getDisplayPosition(); - if (cursor) { - return { x: cursor.x, y: cursor.y }; - } - return { x: window.innerWidth / 2, y: window.innerHeight / 2 }; - } - - private static addTransient(element: HTMLElement, lifetimeMs: number) { - const { root } = this.ensureRoot(); - root.appendChild(element); - setTimeout(() => element.parentElement?.removeChild(element), lifetimeMs); - } - - public static hitMarker(charge = 0) { - const { x, y } = this.focusPoint(); - const marker = document.createElement('div'); - marker.className = - 'hitmarker' + (charge >= settings.chargedHitThreshold ? ' charged' : ''); - marker.style.left = `${x}px`; - marker.style.top = `${y}px`; - this.addTransient(marker, 250); - } - - public static killConfirmed(victimName?: string, streak = 1, charge = 0) { - const { killfeed } = this.ensureRoot(); - const charged = charge >= settings.chargedHitThreshold; - - // A quick crimson vignette pulse around the whole frame to punctuate the kill. - const flash = document.createElement('div'); - flash.className = 'kill-flash' + (charged ? ' charged' : ''); - this.addTransient(flash, 420); - - const entry = document.createElement('div'); - entry.className = 'kill-entry'; - entry.innerHTML = `Eliminated ${this.escape(victimName ?? 'enemy')}`; - killfeed.insertBefore(entry, killfeed.firstChild); - setTimeout(() => entry.parentElement?.removeChild(entry), 4500); - - const { x, y } = this.focusPoint(); - const popup = document.createElement('div'); - popup.className = 'kill-popup' + (charged ? ' charged' : ''); - popup.innerHTML = `+${settings.playerKillPoint} +${settings.playerKillHealthReward}❤`; - popup.style.left = `${x}px`; - popup.style.top = `${y}px`; - this.addTransient(popup, 1200); - - const callout = this.streakName(streak) ?? (charged ? 'Charged Kill!' : undefined); - if (callout) { - const el = document.createElement('div'); - el.className = 'streak-callout'; - el.innerText = callout; - this.addTransient(el, 1400); - } - } - - // Persistent centred overlay shown while the local player is dead and waiting - // to respawn. The countdown itself is the server-driven "Reviving in N…" - // announcement; this makes the death state unmistakable and stays up until - // hideElimination() is called on respawn. - public static showElimination(): void { - if (this.elimination) { - return; - } - const { root } = this.ensureRoot(); - const el = document.createElement('div'); - el.className = 'elimination'; - el.innerHTML = - '
Eliminated
' + - '
Respawning…
'; - root.appendChild(el); - this.elimination = el; - } - - public static hideElimination(): void { - this.elimination?.parentElement?.removeChild(this.elimination); - this.elimination = undefined; - } - - private static streakName(streak: number): string | undefined { - switch (streak) { - case 2: - return 'Double Kill!'; - case 3: - return 'Triple Kill!'; - case 4: - return 'Quad Kill!'; - default: - return streak >= 5 ? 'Rampage!' : undefined; - } - } - - private static escape(text: string): string { - const div = document.createElement('div'); - div.innerText = text; - return div.innerHTML; - } -} diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 4b8678f..46ef3df 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -1,361 +1,183 @@ import { vec2 } from 'gl-matrix'; import { + Circle, CircleLight, + compile, FilteringOptions, + Flashlight, + InvertedTunnel, + PolygonFactory, Renderer, renderNoise, - runAnimation, WrapOptions, } from 'sdf-2d'; import { + broadcastCommands, deserialize, + prettyPrint, + serialize, + settings, + StepCommand, TransportEvents, SetAspectRatioActionCommand, - UpdateMinimap, - UpdateGameState, - GameEndCommand, - ServerAnnouncement, - GameStartCommand, - CommandReceiver, - CommandExecutors, - Command, - settings, - InputAcknowledgement, } from 'shared'; -import { io, Socket } from 'socket.io-client'; -import { KeyboardListener } from './commands/keyboard-listener'; -import { MouseListener } from './commands/mouse-listener'; -import { TouchListener } from './commands/touch-listener'; -import { CommandSocket } from './commands/command-socket'; -import { PlayerDecision } from './join-form-handler'; -import { GameObjectContainer } from './objects/game-object-container'; -import parser from 'socket.io-msgpack-parser'; -import { CharacterShape } from './shapes/character-shape'; -import { PlanetShape } from './shapes/planet-shape'; +import io from 'socket.io-client'; +import { KeyboardListener } from './commands/generators/keyboard-listener'; +import { MouseListener } from './commands/generators/mouse-listener'; +import { TouchListener } from './commands/generators/touch-listener'; +import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; import { RenderCommand } from './commands/types/render'; -import { StepCommand } from './commands/types/step'; -import { serverTimeline } from './helper/server-timeline'; -import { localCharacterPredictor } from './helper/prediction/local-character-predictor'; -import { Tutorial } from './tutorial'; -import { Scoreboard } from './scoreboard'; -import { Minimap } from './minimap'; -import { ScreenShake } from './screen-shake'; -export class Game extends CommandReceiver { - public gameObjects = new GameObjectContainer(this); - public renderer?: Renderer; - private socket!: Socket; - private isBetweenGames = false; +import { DeltaTimeCalculator } from './helper/delta-time-calculator'; +import { rgb } from './helper/rgb'; - public started: Promise; - private resolveStarted!: () => unknown; +import { GameObjectContainer } from './objects/game-object-container'; +import { BlobShape } from './shapes/blob-shape'; - private keyboardListener: KeyboardListener; - private mouseListener: MouseListener; - private touchListener: TouchListener; +const Polygon = PolygonFactory(20); +export class Game { + public readonly gameObjects = new GameObjectContainer(this); + private readonly canvas: HTMLCanvasElement = document.querySelector( + 'canvas#main', + ) as HTMLCanvasElement; + private renderer!: Renderer; + private socket!: SocketIOClient.Socket; + private deltaTimeCalculator = new DeltaTimeCalculator(); + private overlay: HTMLElement = document.querySelector('#overlay') as HTMLDivElement; - private scoreboard = new Scoreboard(); - private minimap = new Minimap(); - private announcementText = document.createElement('h2'); - private keystoneArrow?: HTMLElement; - private socketReceiver!: CommandSocket; - private tutorial!: Tutorial; + private async setupCommunication(): Promise { + // await Configuration.initialize(); - constructor( - private readonly playerDecision: PlayerDecision, - private readonly canvas: HTMLCanvasElement, - private readonly overlay: HTMLElement, - ) { - super(); - this.started = new Promise((r) => (this.resolveStarted = r)); - this.announcementText.className = 'announcement'; + this.socket = io( + 'http://localhost:3000', + /*Configuration.servers[1],*/ { + reconnectionDelayMax: 10000, + transports: ['websocket'], + }, + ); - this.keyboardListener = new KeyboardListener(); - this.mouseListener = new MouseListener(this.canvas, this); - this.touchListener = new TouchListener(this.canvas, this.overlay, this); - } - - private initialize() { - this.isBetweenGames = true; - - this.socket?.close(); - serverTimeline.reset(); - localCharacterPredictor.reset(); - // Clear any leftover shake/zoom so a kill at the end of one match can't bleed - // its camera impact into the next. - ScreenShake.reset(); - this.gameObjects = new GameObjectContainer(this); - this.overlay.innerHTML = ''; - this.keystoneArrow = undefined; - this.lastMinimap = undefined; - this.isEnding = false; - this.lastAnnouncementText = ''; - this.overlay.appendChild(this.scoreboard.element); - this.overlay.appendChild(this.minimap.element); - this.announcementText.innerText = ''; - this.timeScaling = 1; - this.overlay.appendChild(this.announcementText); - this.tutorial = new Tutorial(this.overlay); - - this.socket = io(this.playerDecision.server, { - reconnectionDelayMax: 10000, - transports: ['websocket'], - forceNew: true, - parser, - } as any); - - // In socket.io-client v4 reconnection events are emitted by the Manager - // (`socket.io`), not the Socket itself. - this.socket.io.on('reconnect_attempt', () => { + this.socket.on('reconnect_attempt', () => { this.socket.io.opts.transports = ['polling', 'websocket']; }); - this.socket.on('disconnect', () => { - if (!this.isBetweenGames) { - this.destroy(); - } + this.socket.on(TransportEvents.ServerToPlayer, (serialized: string) => { + const command = deserialize(serialized); + this.gameObjects.sendCommand(command); }); this.socket.on(TransportEvents.Ping, () => { this.socket.emit(TransportEvents.Pong); }); - this.socket.on(TransportEvents.ServerToPlayer, (serializedCommands: string) => { - const commands: Array = deserialize(serializedCommands); - commands.forEach((c) => this.handleCommand(c)); - }); + this.socket.emit(TransportEvents.PlayerJoining, null); - this.socketReceiver = new CommandSocket(this.socket); - // The tutorial listens to the same input streams as the socket, so its - // stages clear off the player's own commands without any server involvement. - this.keyboardListener.clearSubscribers(); - this.keyboardListener.subscribe(this.socketReceiver); - this.keyboardListener.subscribe(this.tutorial); - this.mouseListener.clearSubscribers(); - this.mouseListener.subscribe(this.socketReceiver); - this.mouseListener.subscribe(this.tutorial); - this.touchListener.clearSubscribers(); - this.touchListener.subscribe(this.socketReceiver); - this.touchListener.subscribe(this.tutorial); - - this.isBetweenGames = false; - - this.socket.emit(TransportEvents.PlayerJoining, this.playerDecision); + broadcastCommands( + [ + new KeyboardListener(document.body), + new MouseListener(this.canvas, this), + new TouchListener(this.canvas, this), + ], + [this.gameObjects, new CommandReceiverSocket(this.socket)], + ); } - protected defaultCommandExecutor(c: Command) { - this.gameObjects.handleCommand(c); - } + private async setupRenderer(): Promise { + const noiseTexture = await renderNoise([64, 1], 40, 1 / 10); - private lastGameState?: UpdateGameState; - private isEnding = false; - private timeScaling = 1; - - private lastAnnouncementText = ''; - protected commandExecutors: CommandExecutors = { - [ServerAnnouncement.type]: (c: ServerAnnouncement) => { - this.lastAnnouncementText = c.text; - this.timeSinceLastAnnouncement = 0; - }, - [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c), - [InputAcknowledgement.type]: (c: InputAcknowledgement) => - localCharacterPredictor.acknowledge( - c.clientTimeMs, - c.bodyVelocity, - c.lastLeapClientTimeMs, - ), - [GameEndCommand.type]: () => (this.isEnding = true), - [UpdateMinimap.type]: (c: UpdateMinimap) => (this.lastMinimap = c), - [GameStartCommand.type]: this.initialize.bind(this), - }; - - private lastMinimap?: UpdateMinimap; - - public async start(): Promise { - const noiseTexture = await renderNoise([256, 256], 2, 1); - - this.initialize(); - - await runAnimation( + this.renderer = await compile( this.canvas, [ - PlanetShape.descriptor, - CharacterShape.descriptor, + { + ...InvertedTunnel.descriptor, + shaderCombinationSteps: [0, 2, 6, 16, 32], + }, + { + ...Polygon.descriptor, + shaderCombinationSteps: [0, 1], + }, + { + ...BlobShape.descriptor, + shaderCombinationSteps: [0, 1, 2, 8], + }, + { + ...Circle.descriptor, + shaderCombinationSteps: [0, 2, 16, 32], + }, { ...CircleLight.descriptor, - shaderCombinationSteps: [0, 1, 2, 4, 8, 16], + shaderCombinationSteps: [0, 1, 2, 4, 8], + }, + { + ...Flashlight.descriptor, + shaderCombinationSteps: [0, 1], }, ], - this.gameLoop.bind(this), { shadowTraceCount: 16, - paletteSize: settings.paletteDim.length, - colorPalette: settings.paletteDim, - enableHighDpiRendering: true, - lightCutoffDistance: settings.lightCutoffDistance, - lightOverlapReduction: settings.lightOverlapReduction, - textures: { - noiseTexture: { - source: noiseTexture, - overrides: { - maxFilter: FilteringOptions.LINEAR, - wrapS: WrapOptions.MIRRORED_REPEAT, - wrapT: WrapOptions.MIRRORED_REPEAT, - }, + paletteSize: 10, + enableStopwatch: true, + ignoreWebGL2: true, + }, + ); + + this.renderer.setRuntimeSettings({ + //isWorldInverted: true, + ambientLight: rgb(0.35, 0.1, 0.45), + colorPalette: [ + rgb(0.4, 1, 0.6), + rgb(1, 1, 0), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + ], + enableHighDpiRendering: false, + lightCutoffDistance: settings.lightCutoffDistance, + textures: { + noiseTexture: { + source: noiseTexture, + overrides: { + maxFilter: FilteringOptions.LINEAR, + wrapS: WrapOptions.MIRRORED_REPEAT, }, }, }, + }); + this.renderer.addDrawable( + new Polygon([ + [10.0, 10.0], + [200, 500], + [500, 400], + ]), ); - this.socket.close(); - this.overlay.innerHTML = ''; - this.keyboardListener.destroy(); - this.mouseListener.destroy(); - this.touchListener.destroy(); + this.renderer.renderDrawables(); + } + + public async start(): Promise { + await Promise.all([/*this.setupCommunication(),*/ this.setupRenderer()]); + // requestAnimationFrame(this.gameLoop.bind(this)); } public displayToWorldCoordinates(p: vec2): vec2 { - return this.renderer?.displayToWorldCoordinates(p) ?? vec2.create(); + return this.renderer.displayToWorldCoordinates(p); } public aspectRatioChanged(aspectRatio: number) { - this.socketReceiver.handleCommand(new SetAspectRatioActionCommand(aspectRatio)); - } - - private isActive = true; - public destroy() { - this.isActive = false; - } - - private timeSinceLastAnnouncement = 0; - private framesSinceLastLayoutUpdate = 0; - private gameLoop( - renderer: Renderer, - _: DOMHighResTimeStamp, - deltaTime: DOMHighResTimeStamp, - ): boolean { - this.resolveStarted(); - deltaTime /= 1000; - - // Decay the camera impact effects on raw wall-clock time, before any of the - // end-game slow-motion scaling below. These only adjust the rendered view, - // never the simulation, so they stay decoupled from prediction and netcode. - ScreenShake.step(deltaTime); - - // Stepped before the end-game time scaling on purpose: the slow motion is - // already baked into the snapshots the server sends, so the playback - // cursor itself must keep running on wall-clock time. - serverTimeline.step(deltaTime); - - let shouldChangeLayout = false; - if (++this.framesSinceLastLayoutUpdate > 1) { - shouldChangeLayout = true; - this.framesSinceLastLayoutUpdate = 0; - this.draw(); - } - - if ( - (this.timeSinceLastAnnouncement += deltaTime) > settings.announcementVisibleSeconds - ) { - this.lastAnnouncementText = ''; - } - - if (this.isEnding) { - this.timeScaling *= Math.pow(settings.endGameDeltaScaling, deltaTime); - deltaTime /= this.timeScaling; - } - - this.renderer = renderer; - - this.gameObjects.handleCommand(new StepCommand(deltaTime)); - this.gameObjects.handleCommand( - new RenderCommand(this.renderer, this.overlay, shouldChangeLayout), + this.socket.emit( + TransportEvents.PlayerToServer, + serialize(new SetAspectRatioActionCommand(aspectRatio)), ); - - this.touchListener.update(deltaTime); - - this.tutorial.step(this.gameObjects); - - this.socketReceiver.sendQueuedCommands(); - - return this.isActive; } - private draw() { - if (this.lastGameState) { - // The local player's team is read off the main character once it exists. - this.scoreboard.update(this.lastGameState, this.gameObjects.player?.team); - } + private gameLoop(time: DOMHighResTimeStamp) { + const deltaTime = this.deltaTimeCalculator.getNextDeltaTimeInMilliseconds(time); - this.minimap.update( - this.gameObjects.localPlayerPosition, - this.lastMinimap?.players ?? [], - ); + this.gameObjects.sendCommand(new StepCommand(deltaTime)); + this.gameObjects.sendCommand(new RenderCommand(this.renderer)); + this.renderer.renderDrawables(); - this.handleKeystoneArrow(); - - this.announcementText.innerHTML = this.lastAnnouncementText; - } - - // Points an off-screen chevron toward the keystone "Heart" planet, tinted by - // who currently holds it, so the match's focal objective is always findable. - private handleKeystoneArrow() { - if (!this.renderer) { - return; - } - const keystone = this.gameObjects.planets.find((p) => p.isKeystone); - if (!keystone) { - if (this.keystoneArrow) { - this.keystoneArrow.style.display = 'none'; - } - return; - } - - if (!this.keystoneArrow) { - this.keystoneArrow = document.createElement('div'); - this.overlay.appendChild(this.keystoneArrow); - } - - const width = this.renderer.canvasSize.x; - const height = this.renderer.canvasSize.y; - const display = this.renderer.worldToDisplayCoordinates(keystone.center); - const margin = 48; - const onScreen = - display.x >= margin && - display.x <= width - margin && - display.y >= margin && - display.y <= height - margin; - - const control = Math.abs(keystone.ownership - 0.5); - const team = - control < settings.planetControlThreshold - ? 'neutral' - : keystone.ownership < 0.5 - ? 'blue' - : 'red'; - this.keystoneArrow.className = 'keystone-arrow ' + team; - - if (onScreen) { - this.keystoneArrow.style.display = 'none'; - return; - } - this.keystoneArrow.style.display = 'block'; - - const center = vec2.fromValues(width / 2, height / 2); - const dir = vec2.fromValues(display.x - center.x, display.y - center.y); - const angle = Math.atan2(dir.y, dir.x); - - const aspectRatio = width / height; - const directionRatio = dir.x / dir.y; - let deltaX: number, deltaY: number; - if (aspectRatio < Math.abs(directionRatio)) { - deltaX = (width / 2 - margin) * Math.sign(dir.x); - deltaY = deltaX / directionRatio; - } else { - deltaY = (height / 2 - margin) * Math.sign(dir.y); - deltaX = deltaY * directionRatio; - } - - const p = vec2.add(center, center, vec2.fromValues(deltaX, deltaY)); - this.keystoneArrow.style.transform = `translateX(${p.x}px) translateY(${p.y}px) translateX(-50%) translateY(-50%) rotate(${angle + Math.PI / 2}rad)`; + this.overlay.innerText = prettyPrint(this.renderer.insights); + requestAnimationFrame(this.gameLoop.bind(this)); } } diff --git a/frontend/src/scripts/helper/delta-time-calculator.ts b/frontend/src/scripts/helper/delta-time-calculator.ts new file mode 100644 index 0000000..7f93741 --- /dev/null +++ b/frontend/src/scripts/helper/delta-time-calculator.ts @@ -0,0 +1,25 @@ +export class DeltaTimeCalculator { + private previousTime: DOMHighResTimeStamp | null = null; + + constructor() { + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + } + + public getNextDeltaTimeInMilliseconds( + currentTime: DOMHighResTimeStamp, + ): DOMHighResTimeStamp { + if (this.previousTime === null) { + this.previousTime = currentTime; + } + + const delta = currentTime - this.previousTime; + this.previousTime = currentTime; + return delta; + } + + private handleVisibilityChange() { + if (!document.hidden) { + this.previousTime = null; + } + } +} diff --git a/frontend/src/scripts/helper/handle-full-screen.ts b/frontend/src/scripts/helper/handle-full-screen.ts deleted file mode 100644 index c5b414c..0000000 --- a/frontend/src/scripts/helper/handle-full-screen.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SoundHandler, Sounds } from '../sound-handler'; - -export const handleFullScreen = ( - minimizeButton: HTMLElement, - maximizeButton: HTMLElement, -) => { - if (!document.fullscreenEnabled) { - minimizeButton.style.visibility = 'hidden'; - maximizeButton.style.visibility = 'hidden'; - return; - } - - const isInFullScreen = (): boolean => document.fullscreenElement !== null; - - const showButtons = () => { - minimizeButton.style.visibility = isInFullScreen() ? 'visible' : 'hidden'; - maximizeButton.style.visibility = isInFullScreen() ? 'hidden' : 'visible'; - }; - - showButtons(); - - let currentWindowHeight = innerHeight; - - const followToggle = () => { - SoundHandler.play(Sounds.click); - showButtons(); - currentWindowHeight = innerHeight; - }; - - const triggerToggle = async () => { - await (isInFullScreen() - ? document.exitFullscreen() - : document.body.requestFullscreen()); - followToggle(); - }; - - addEventListener('keydown', (e) => { - if (e.key === 'F11') { - triggerToggle(); - e.preventDefault(); - } - }); - - addEventListener('resize', () => { - if (isInFullScreen() && currentWindowHeight > innerHeight) { - followToggle(); - } - }); - - maximizeButton.addEventListener('click', triggerToggle); - minimizeButton.addEventListener('click', triggerToggle); -}; diff --git a/frontend/src/scripts/helper/hide.ts b/frontend/src/scripts/helper/hide.ts deleted file mode 100644 index 8283913..0000000 --- a/frontend/src/scripts/helper/hide.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const hide = (e: HTMLElement, useDisplay = false) => { - if (useDisplay) { - e.style.display = 'none'; - } else { - e.style.visibility = 'hidden'; - } -}; diff --git a/frontend/src/scripts/helper/interpolators/circle-interpolator.ts b/frontend/src/scripts/helper/interpolators/circle-interpolator.ts deleted file mode 100644 index 62816e5..0000000 --- a/frontend/src/scripts/helper/interpolators/circle-interpolator.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Circle } from 'shared'; -import { LinearInterpolator } from './linear-interpolator'; -import { Vec2Interpolator } from './vec2-interpolator'; - -export class CircleInterpolator { - private center: Vec2Interpolator; - private radius: LinearInterpolator; - - constructor(currentValue: Circle) { - this.center = new Vec2Interpolator(currentValue.center); - this.radius = new LinearInterpolator(currentValue.radius); - } - - public addFrame(value: Circle, rateOfChange: Circle) { - this.center.addFrame(value.center, rateOfChange.center); - this.radius.addFrame(value.radius, rateOfChange.radius); - } - - public getValue(deltaTime: number): Circle { - return new Circle(this.center.getValue(deltaTime), this.radius.getValue(deltaTime)); - } -} diff --git a/frontend/src/scripts/helper/interpolators/linear-interpolator.ts b/frontend/src/scripts/helper/interpolators/linear-interpolator.ts deleted file mode 100644 index bcc23b5..0000000 --- a/frontend/src/scripts/helper/interpolators/linear-interpolator.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { last, mix } from 'shared'; -import { serverTimeline } from '../server-timeline'; - -interface Frame { - time: number; - value: number; - velocity: number; -} - -// How far past the newest snapshot the value may coast on its last known -// velocity (network hiccup) before freezing in place. -const maxCoastSeconds = 0.06; - -// When fresh snapshots arrive after a coast, they usually disagree with where -// the coast ended up; the difference decays away with this time constant -// instead of being shown as a jump. -const blendSeconds = 0.12; - -// At 25 snapshots per second this spans over a second of state, far more than -// rendering ever needs; it only bounds memory while the tab is hidden and -// snapshots keep arriving without being consumed. -const maxFrames = 32; - -/** - * Replays a server-streamed scalar by interpolating between timestamped - * snapshots at the shared timeline's render time, which trails the newest - * snapshot. The streamed rate of change is only used to coast briefly when - * the buffer runs dry. - */ -export class LinearInterpolator { - private frames: Array = []; - private blendOffset = 0; - private lastValue: number; - private isCoasting = false; - - constructor(private readonly initialValue: number) { - this.lastValue = initialValue; - } - - public addFrame(value: number, rateOfChange: number) { - const time = serverTimeline.snapshotTime; - const newest = last(this.frames); - const wasCoasting = this.isCoasting; - - if (newest && time <= newest.time) { - this.frames[this.frames.length - 1] = { - time: newest.time, - value, - velocity: rateOfChange, - }; - } else { - this.frames.push({ time, value, velocity: rateOfChange }); - if (this.frames.length > maxFrames) { - this.frames.shift(); - } - } - - if (wasCoasting) { - // Continue from where the coast left off and let the offset decay, - // instead of jumping onto the freshly revealed trajectory. - this.blendOffset = this.lastValue - this.sample(serverTimeline.renderTime); - } - } - - public getValue(deltaTimeInSeconds: number): number { - this.blendOffset *= Math.exp(-deltaTimeInSeconds / blendSeconds); - return (this.lastValue = this.sample(serverTimeline.renderTime) + this.blendOffset); - } - - private sample(time: number): number { - if (this.frames.length === 0) { - return this.initialValue; - } - - while (this.frames.length >= 2 && this.frames[1].time <= time) { - this.frames.shift(); - } - - const [current, next] = this.frames; - this.isCoasting = false; - - if (time <= current.time) { - return current.value; - } - - if (next) { - return mix( - current.value, - next.value, - (time - current.time) / (next.time - current.time), - ); - } - - this.isCoasting = true; - return ( - current.value + current.velocity * Math.min(time - current.time, maxCoastSeconds) - ); - } -} diff --git a/frontend/src/scripts/helper/interpolators/vec2-interpolator.ts b/frontend/src/scripts/helper/interpolators/vec2-interpolator.ts deleted file mode 100644 index ae6fb52..0000000 --- a/frontend/src/scripts/helper/interpolators/vec2-interpolator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { LinearInterpolator } from './linear-interpolator'; - -export class Vec2Interpolator { - private x: LinearInterpolator; - private y: LinearInterpolator; - - constructor(currentValue: vec2) { - this.x = new LinearInterpolator(currentValue.x); - this.y = new LinearInterpolator(currentValue.y); - } - - public addFrame(value: vec2, rateOfChange: vec2) { - this.x.addFrame(value.x, rateOfChange.x); - this.y.addFrame(value.y, rateOfChange.y); - } - - public getValue(deltaTime: number): vec2 { - return vec2.fromValues(this.x.getValue(deltaTime), this.y.getValue(deltaTime)); - } -} diff --git a/frontend/src/scripts/helper/pointer.ts b/frontend/src/scripts/helper/pointer.ts deleted file mode 100644 index ab8886d..0000000 --- a/frontend/src/scripts/helper/pointer.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -export class Pointer { - private static displayPosition: vec2 | null = null; - - public static setDisplayPosition(x: number, y: number): void { - Pointer.displayPosition = vec2.fromValues(x, y); - } - - public static getDisplayPosition(): vec2 | null { - return Pointer.displayPosition; - } -} diff --git a/frontend/src/scripts/helper/prediction/client-character-world.ts b/frontend/src/scripts/helper/prediction/client-character-world.ts deleted file mode 100644 index a08dbc5..0000000 --- a/frontend/src/scripts/helper/prediction/client-character-world.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - CharacterWorld, - GroundSurface, - Id, - PhysicsBody, - planetDistance, - planetGravity, - resolveCircleMovement, -} from 'shared'; - -// What the predictor needs to know about a planet to collide and be pulled by -// it. The client reads this off its PlanetViews. -export interface PredictablePlanet { - id: Id; - vertices: Array; - center: vec2; - radius: number; - rotation: number; - rotationSpeed: number; -} - -// A planet collision/gravity surface whose rotation can be advanced during -// replay, so its outline turns in lockstep with the body the carry term moves — -// exactly as the server steps the planet before the character each tick. -class PlanetSurface implements GroundSurface { - public readonly canCollide = true; - public readonly isGround = true; - public readonly id: Id; - public center: vec2; - public angularVelocity: number; - private vertices: Array; - private radius: number; - private rotation = 0; - private cos = 1; - private sin = 0; - - constructor(planet: PredictablePlanet) { - this.id = planet.id; - this.center = planet.center; - this.vertices = planet.vertices; - this.radius = planet.radius; - this.angularVelocity = planet.rotationSpeed; - this.setRotation(planet.rotation); - } - - public sync(planet: PredictablePlanet) { - this.center = planet.center; - this.vertices = planet.vertices; - this.radius = planet.radius; - this.angularVelocity = planet.rotationSpeed; - this.setRotation(planet.rotation); - } - - private setRotation(rotation: number) { - this.rotation = rotation; - this.cos = Math.cos(rotation); - this.sin = Math.sin(rotation); - } - - public advance(deltaTimeInSeconds: number) { - this.setRotation(this.rotation + this.angularVelocity * deltaTimeInSeconds); - } - - public distance(target: vec2): number { - return planetDistance(target, this.vertices, this.center, this.cos, this.sin); - } - - public gravityAt(target: vec2): vec2 { - return planetGravity(this.center, this.radius, target); - } -} - -// The planets-only collision world the local predictor runs against. It holds -// persistent surfaces keyed by planet id (so a `currentPlanet` reference stays -// valid across frames) and never dispatches collision reactions — damage, -// scoring and the like are server-authoritative. -export class ClientCharacterWorld implements CharacterWorld { - private surfaces = new Map(); - private ordered: Array = []; - - // Refresh from the current PlanetViews. Far planets contribute zero gravity - // (the falloff clamps to 0 past maxGravityDistance) and never collide, so the - // whole set can be handed to every query without a range filter. Ordered by - // id so the (rare) two-surface contact picks a stable surface. - public sync(planets: Array) { - const seen = new Set(); - for (const planet of planets) { - seen.add(planet.id); - const existing = this.surfaces.get(planet.id); - if (existing) { - existing.sync(planet); - } else { - this.surfaces.set(planet.id, new PlanetSurface(planet)); - } - } - for (const id of [...this.surfaces.keys()]) { - if (!seen.has(id)) { - this.surfaces.delete(id); - } - } - this.ordered = [...this.surfaces.entries()] - .sort((a, b) => Number(a[0]) - Number(b[0])) - .map((e) => e[1]); - } - - // Advance every planet's collision frame by one replay substep, so surfaces - // and the carried body rotate together. The surfaces are re-synced to the - // newest snapshot rotation each frame (see sync), so the replay only ever - // steps forward from there. - public advance(deltaTimeInSeconds: number) { - for (const surface of this.ordered) { - surface.advance(deltaTimeInSeconds); - } - } - - public surfaceById(id: Id | undefined): GroundSurface | undefined { - return id == null ? undefined : this.surfaces.get(id); - } - - public idOf(surface: GroundSurface | undefined): Id | undefined { - return surface instanceof PlanetSurface ? surface.id : undefined; - } - - public groundsNear(): Array { - return this.ordered; - } - - public stepBody( - body: PhysicsBody, - deltaTimeInSeconds: number, - ): GroundSurface | undefined { - const { hitObject } = resolveCircleMovement(body, deltaTimeInSeconds, this.ordered); - return hitObject && (hitObject as GroundSurface).isGround - ? (hitObject as GroundSurface) - : undefined; - } -} diff --git a/frontend/src/scripts/helper/prediction/input-history.ts b/frontend/src/scripts/helper/prediction/input-history.ts deleted file mode 100644 index 3ec2604..0000000 --- a/frontend/src/scripts/helper/prediction/input-history.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -interface InputSample { - timeMs: number; - direction: vec2; -} - -// Keep a little over a second of input — far more than any sane reconciliation -// window — so a brief stall (or a backgrounded tab catching up) can still be -// replayed, while old samples are pruned to bound memory. -const retainMs = 1500; - -// A timeline of the local player's movement directions in client-clock time. -// Each generated MoveActionCommand is stamped with the time this hands out, and -// the same sample is recorded here so the predictor replays exactly the input -// the server will eventually receive. Direction is piecewise-constant: the -// active direction at any instant is the most recent sample at or before it. -export class InputHistory { - private samples: Array = []; - - public record(direction: vec2, timeMs: number): void { - this.samples.push({ timeMs, direction: vec2.clone(direction) }); - - const cutoff = timeMs - retainMs; - while (this.samples.length > 1 && this.samples[1].timeMs <= cutoff) { - this.samples.shift(); - } - } - - // The held direction at `timeMs`: the latest sample at or before it, or zero - // before any input exists. - public directionAt(timeMs: number): vec2 { - let direction = vec2.create(); - for (const sample of this.samples) { - if (sample.timeMs <= timeMs) { - direction = sample.direction; - } else { - break; - } - } - return vec2.clone(direction); - } - - public reset(): void { - this.samples = []; - } -} diff --git a/frontend/src/scripts/helper/prediction/local-character-predictor.ts b/frontend/src/scripts/helper/prediction/local-character-predictor.ts deleted file mode 100644 index 5d23c1c..0000000 --- a/frontend/src/scripts/helper/prediction/local-character-predictor.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - Circle, - CharacterMovementState, - Id, - PhysicsBody, - settings, - stepCharacterMovement, - applyLeapImpulse, - tickPlanetDetachment, - feetRadius, - headRadius, -} from 'shared'; -import { ClientCharacterWorld, PredictablePlanet } from './client-character-world'; -import { InputHistory } from './input-history'; - -const stepMs = 1000 / 200; // match the server's 200 Hz fixed tick -const stepSeconds = 1 / 200; - -// Clock source for the predictor. Injectable so deterministic reconciliation -// tests can drive prediction without real time passing; defaults to the -// browser wall clock in production. -let nowMs: () => number = () => performance.now(); -export const setPredictorClockForTesting = (clock: () => number): void => { - nowMs = clock; -}; - -// Don't replay more than this far back: if the last acknowledged input is older -// (a stall, or a backgrounded tab catching up) snap to the authoritative pose -// instead of grinding through hundreds of steps. -const maxReplayMs = 300; - -// Render-side easing of the correction the reconciliation produces each frame, -// so a snapshot that disagrees with the prediction is smoothed out instead of -// popping. Short, so the local player still feels immediate. -const smoothSeconds = 0.06; - -// A correction bigger than this isn't prediction error — it's a respawn, -// teleport, or a server-side impulse (leap / slingshot / recoil / death throw) -// the predictor doesn't model. Snap to it rather than gliding across the gap. -const snapDistance = 250; - -const makeBody = (center: vec2, radius: number): PhysicsBody => ({ - center: vec2.clone(center), - radius, - velocity: vec2.create(), - lastNormal: vec2.fromValues(0, 1), - restitution: 0, -}); - -// Predicts the local player's character so it responds to input immediately, -// instead of lagging ~100 ms + RTT behind like the interpolated remote objects. -// Each frame it resets to the latest authoritative snapshot and replays the -// player's own un-acknowledged input through the SAME movement simulation the -// server runs (shared/stepCharacterMovement), then eases the rendered pose -// toward the result. Discrete server-side impulses it can't model show up as a -// large correction and snap rather than rubber-band. -export class LocalCharacterPredictor { - private readonly inputHistory = new InputHistory(); - private readonly world = new ClientCharacterWorld(); - - private authoritative?: { head: Circle; leftFoot: Circle; rightFoot: Circle }; - private lastAckClientTimeMs?: number; - // Wall-clock (client) time the latest authoritative snapshot was received. The - // replay predicts forward from HERE by the snapshot's age, so the local pose - // advances smoothly with real time between the 25 Hz snapshots. Anchoring to - // the last *acked input* time instead breaks when input is sent only on change - // (a held key sends nothing): that time freezes, the window pins to the - // maxReplayMs clamp, the replay displacement goes constant, and the pose - // stair-steps at the snapshot rate. - private authReceiptMs = 0; - // Authoritative launch momentum at the last snapshot — seeds each replay so a - // leap/slingshot/recoil flight is reproduced and continuously corrected. - private authoritativeBodyVelocity = vec2.create(); - // Wall-clock times the player issued a leap, replayed (with the impulse - // applied locally) so the launch is felt immediately, not after a round trip. - private leapHistory: Array = []; - // clientTimeMs of the last leap the server has folded into the streamed - // momentum. Leaps at or before this are already in authoritativeBodyVelocity; - // only newer ones are replayed, so a leap is never applied twice. - private lastLeapAckMs = -Infinity; - // Latest streamed shooting-strength, to gate predicted leaps as the server does. - private currentStrength = settings.playerMaxStrength; - - // Whether the local body is alive on the server. While dead (awaiting respawn) - // prediction is suppressed so the corpse/ghost can't keep walking in response - // to input — the server ignores a dead player's movement, so a predicted body - // that still moved would be a pure client-side desync. - private alive = true; - - // Continuous state carried between replays (the snapshot carries only poses). - // The facing direction is NOT carried — it is re-derived from the pose each - // frame (see directionFromPose / simulate); only the latched planet and the - // time-since-surface persist. - private carriedPlanetId?: Id; - private carriedSecondsSinceSurface = 1; - - // The eased, rendered pose handed to the view. - private renderHead = new Circle(vec2.create(), headRadius); - private renderLeftFoot = new Circle(vec2.create(), feetRadius); - private renderRightFoot = new Circle(vec2.create(), feetRadius); - private hasRender = false; - - public get head(): Circle { - return this.renderHead; - } - public get leftFoot(): Circle { - return this.renderLeftFoot; - } - public get rightFoot(): Circle { - return this.renderRightFoot; - } - - // Stamp a movement command and record it for replay. Returns the wall-clock - // time the command should carry so the server can echo it back. - public recordInput(direction: vec2): number { - const timeMs = Math.round(nowMs()); - this.inputHistory.record(direction, timeMs); - return timeMs; - } - - public acknowledge( - clientTimeMs: number, - bodyVelocity: vec2, - lastLeapClientTimeMs: number, - ): void { - // Inputs only advance the acknowledgement forward; the launch momentum and - // leap boundary always adopt the latest authoritative values. - if ( - this.lastAckClientTimeMs === undefined || - clientTimeMs > this.lastAckClientTimeMs - ) { - this.lastAckClientTimeMs = clientTimeMs; - } - vec2.set(this.authoritativeBodyVelocity, bodyVelocity[0], bodyVelocity[1]); - this.lastLeapAckMs = lastLeapClientTimeMs; - } - - public setAuthoritative(head: Circle, leftFoot: Circle, rightFoot: Circle): void { - this.authoritative = { head, leftFoot, rightFoot }; - this.authReceiptMs = Math.round(nowMs()); - } - - // The player pressed leap; remember when, so the replay applies the same - // impulse the server will. Recorded regardless of whether the server accepts - // it — a rejected leap (no strength/cooldown) self-corrects via the streamed - // authoritative momentum. - public recordLeap(): number { - const timeMs = Math.round(nowMs()); - this.leapHistory.push(timeMs); - const cutoff = timeMs - 1500; - while (this.leapHistory.length > 0 && this.leapHistory[0] <= cutoff) { - this.leapHistory.shift(); - } - return timeMs; - } - - public setStrength(strength: number): void { - this.currentStrength = strength; - } - - public setAlive(alive: boolean): void { - this.alive = alive; - } - - public reset(): void { - this.inputHistory.reset(); - this.leapHistory = []; - this.lastLeapAckMs = -Infinity; - this.authoritative = undefined; - this.lastAckClientTimeMs = undefined; - this.authReceiptMs = 0; - vec2.zero(this.authoritativeBodyVelocity); - this.currentStrength = settings.playerMaxStrength; - this.carriedPlanetId = undefined; - this.carriedSecondsSinceSurface = 1; - this.hasRender = false; - this.alive = true; - } - - public get canPredict(): boolean { - return this.authoritative !== undefined && this.lastAckClientTimeMs !== undefined; - } - - // During spawn-in and death the server freezes walking and only scales the - // body (CharacterPhysical.step returns early), so its head radius is below - // nominal. Predicting then would walk the body off a position the server is - // holding still — let interpolation show the animation instead. - private get isAnimatingInOrOut(): boolean { - return ( - this.authoritative !== undefined && - this.authoritative.head.radius < headRadius - 0.5 - ); - } - - // Run one frame of prediction. Returns true if it produced a rendered pose the - // caller should use (otherwise fall back to interpolation). `planets` is the - // current collision world; `frameSeconds` is the render delta. - public update(planets: Array, frameSeconds: number): boolean { - if (!this.alive || !this.canPredict || this.isAnimatingInOrOut) { - // Resume from the authoritative pose with a snap rather than gliding from - // a stale rendered one. - this.hasRender = false; - return false; - } - - this.world.sync(planets); - const predicted = this.simulate(); - - if (!this.hasRender) { - this.snapRenderTo(predicted); - this.hasRender = true; - } else { - this.easeRenderTo(predicted, frameSeconds); - } - return true; - } - - // The body's facing angle is encoded in the pose: each part is sprung toward - // center + R(direction)*offset and the head's offset points +y, so - // direction = atan2(head - center) - PI/2. Re-deriving it from the snapshot - // each frame (rather than carrying the previous frame's evolved value onto - // this past pose, re-evolved by a variable substep count) keeps the posture - // seed a pure function of the snapshot — carrying it fed a frame-rate-dependent - // loop that wobbled the rendered limbs. - private directionFromPose(head: Circle, leftFoot: Circle, rightFoot: Circle): number { - const cx = (head.center[0] + leftFoot.center[0] + rightFoot.center[0]) / 3; - const cy = (head.center[1] + leftFoot.center[1] + rightFoot.center[1]) / 3; - return Math.atan2(head.center[1] - cy, head.center[0] - cx) - Math.PI / 2; - } - - private simulate(): CharacterMovementState { - const auth = this.authoritative!; - const now = Math.round(nowMs()); - // Predict forward from the latest snapshot by its age, clamped so a stall - // (or a backgrounded tab catching up) snaps instead of grinding hundreds of - // steps. This advances with wall-clock time even while a held key sends no - // fresh input, so the pose no longer pins to the 25 Hz snapshot cadence. - const startMs = Math.max(this.authReceiptMs, now - maxReplayMs); - const windowMs = Math.max(0, now - startMs); - const steps = Math.floor(windowMs / stepMs); - const remainderSeconds = (windowMs - steps * stepMs) / 1000; - - const state: CharacterMovementState = { - head: makeBody(auth.head.center, auth.head.radius), - leftFoot: makeBody(auth.leftFoot.center, auth.leftFoot.radius), - rightFoot: makeBody(auth.rightFoot.center, auth.rightFoot.radius), - direction: this.directionFromPose(auth.head, auth.leftFoot, auth.rightFoot), - currentPlanet: this.world.surfaceById(this.carriedPlanetId), - secondsSinceOnSurface: this.carriedSecondsSinceSurface, - // Leaps before the replay window are already baked into this; leaps inside - // the window are re-applied below, so neither is double-counted. - bodyVelocity: vec2.clone(this.authoritativeBodyVelocity), - }; - - // The planet collision frames were synced (in update(), just before this) - // to the NEWEST snapshot's rotation — the same instant the authoritative - // body pose is from — so the body and the surface start the replay at the - // same phase. The loop below advances the surfaces FORWARD in lockstep with - // the body's carry from that shared phase, so no rewind is needed (and the - // persistent surfaces are re-synced next frame, so this never accumulates). - - const cooldownMs = settings.leapCooldownSeconds * 1000; - // Mirror the server: each accepted leap spends leapStrengthCost, so a burst - // of leaps in one replay window is gated by the running strength rather than - // a single up-front affordability check. - let availableStrength = this.currentStrength; - let lastLeapMs = -Infinity; - - let t = startMs; - for (let i = 0; i < steps; i++) { - const input = this.inputHistory.directionAt(t); - tickPlanetDetachment(state, stepSeconds); - stepCharacterMovement(state, this.world, input, stepSeconds); - this.world.advance(stepSeconds); - - // A leap issued during this step launches now (the next step injects the - // momentum), gated like the server: on a surface, off cooldown, with - // strength. applyLeapImpulse is a no-op off-surface. - for (const leapMs of this.leapHistory) { - if ( - leapMs >= t && - leapMs < t + stepMs && - // Only leaps the server hasn't yet folded into the seed, so a leap - // is never both seeded and replayed. - leapMs > this.lastLeapAckMs && - state.currentPlanet && - leapMs - lastLeapMs >= cooldownMs && - availableStrength >= settings.leapStrengthCost - ) { - applyLeapImpulse(state, this.inputHistory.directionAt(leapMs)); - availableStrength -= settings.leapStrengthCost; - lastLeapMs = leapMs; - } - } - - t += stepMs; - } - - // Final sub-tick of the leftover (< stepMs) so the predicted pose is a - // continuous function of the window length rather than advancing in 5 ms - // quanta — the floor() above would otherwise surface that as a per-frame - // wobble on top of the ~16.7 ms render cadence. - if (remainderSeconds > 0) { - tickPlanetDetachment(state, remainderSeconds); - stepCharacterMovement( - state, - this.world, - this.inputHistory.directionAt(now), - remainderSeconds, - ); - this.world.advance(remainderSeconds); - } - - this.carriedPlanetId = this.world.idOf(state.currentPlanet); - this.carriedSecondsSinceSurface = state.secondsSinceOnSurface; - - return state; - } - - private snapRenderTo(state: CharacterMovementState): void { - this.renderHead = new Circle(vec2.clone(state.head.center), state.head.radius); - this.renderLeftFoot = new Circle( - vec2.clone(state.leftFoot.center), - state.leftFoot.radius, - ); - this.renderRightFoot = new Circle( - vec2.clone(state.rightFoot.center), - state.rightFoot.radius, - ); - } - - private easeRenderTo(state: CharacterMovementState, frameSeconds: number): void { - const q = 1 - Math.exp(-frameSeconds / smoothSeconds); - this.easePart(this.renderHead, state.head, q); - this.easePart(this.renderLeftFoot, state.leftFoot, q); - this.easePart(this.renderRightFoot, state.rightFoot, q); - } - - private easePart(render: Circle, target: PhysicsBody, q: number): void { - if (vec2.distance(render.center, target.center) > snapDistance) { - vec2.copy(render.center, target.center); - } else { - vec2.lerp(render.center, render.center, target.center, q); - } - render.radius = target.radius; - } -} - -export const localCharacterPredictor = new LocalCharacterPredictor(); diff --git a/shared/src/helper/rgb.ts b/frontend/src/scripts/helper/rgb.ts similarity index 100% rename from shared/src/helper/rgb.ts rename to frontend/src/scripts/helper/rgb.ts diff --git a/shared/src/helper/rgb255.ts b/frontend/src/scripts/helper/rgb255.ts similarity index 100% rename from shared/src/helper/rgb255.ts rename to frontend/src/scripts/helper/rgb255.ts diff --git a/frontend/src/scripts/helper/server-timeline.ts b/frontend/src/scripts/helper/server-timeline.ts deleted file mode 100644 index 28aab75..0000000 --- a/frontend/src/scripts/helper/server-timeline.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { clamp, settings } from 'shared'; - -// Convergence rate of the playback cursor towards its target; a deviation -// decays with a time constant of 1 / rateGain seconds. -const rateGain = 2; - -// Playback speed stays within [0.75, 1.25]× so corrections are invisible. -const maxRateAdjustment = 0.25; - -// Beyond this divergence (tab was in the background, server changed) chasing -// the target is pointless: jump straight to it. -const resyncSeconds = 0.3; - -/** - * The playback clock for state streamed from the server. - * - * Snapshot timestamps estimate the server's clock: the newest received - * timestamp plus the time elapsed since it arrived. Rendering happens - * settings.interpolationDelaySeconds behind that estimate, so there is - * normally a newer snapshot to interpolate towards and network jitter is - * absorbed by the buffer instead of being shown. - * - * The cursor never jumps under normal operation: it runs at a gently adjusted - * rate to stay on target and only snaps after a long divergence. - */ -class ServerTimeline { - private cursor?: number; - private newestSnapshotTime?: number; - private sinceNewestSnapshot = 0; - private _snapshotTime = 0; - - /** Timestamp of the update batch currently being applied. */ - public get snapshotTime(): number { - return this._snapshotTime; - } - - /** The point on the server's clock that should be rendered this frame. */ - public get renderTime(): number { - return this.cursor ?? 0; - } - - public onSnapshot(timestamp: number) { - this._snapshotTime = timestamp; - if (this.newestSnapshotTime === undefined || timestamp > this.newestSnapshotTime) { - this.newestSnapshotTime = timestamp; - this.sinceNewestSnapshot = 0; - } - } - - // Must be called with unscaled wall-clock time, even during the end-game - // slow motion: the slowdown is already baked into the snapshots. - public step(deltaTimeInSeconds: number) { - if (this.newestSnapshotTime === undefined) { - return; - } - - this.sinceNewestSnapshot += deltaTimeInSeconds; - const target = - this.newestSnapshotTime + - this.sinceNewestSnapshot - - settings.interpolationDelaySeconds; - - if (this.cursor === undefined || Math.abs(target - this.cursor) > resyncSeconds) { - this.cursor = target; - return; - } - - const rate = clamp( - 1 + (target - this.cursor) * rateGain, - 1 - maxRateAdjustment, - 1 + maxRateAdjustment, - ); - this.cursor += deltaTimeInSeconds * rate; - } - - public reset() { - this.cursor = undefined; - this.newestSnapshotTime = undefined; - this.sinceNewestSnapshot = 0; - this._snapshotTime = 0; - } -} - -export const serverTimeline = new ServerTimeline(); diff --git a/frontend/src/scripts/helper/show.ts b/frontend/src/scripts/helper/show.ts deleted file mode 100644 index cb43718..0000000 --- a/frontend/src/scripts/helper/show.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const show = (e: HTMLElement, useDisplay = false, normalDisplayValue?: string) => { - if (useDisplay) { - e.style.display = normalDisplayValue!; - } else { - e.style.visibility = 'inherit'; - } -}; diff --git a/frontend/src/scripts/join-form-handler.ts b/frontend/src/scripts/join-form-handler.ts deleted file mode 100644 index c2bd37f..0000000 --- a/frontend/src/scripts/join-form-handler.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { ServerInformation, serverInformationEndpoint, TransportEvents } from 'shared'; -import { io, Socket } from 'socket.io-client'; -import { Configuration } from './configuration'; -import parser from 'socket.io-msgpack-parser'; -import { SoundHandler, Sounds } from './sound-handler'; - -export type PlayerDecision = { - name: string; - server: string; -}; - -const pollInterval = 8000; -export class JoinFormHandler { - private joinButton: HTMLButtonElement; - private waitingForDecision: Promise; - private resolvePlayerDecision!: (d: PlayerDecision) => void; - private pollServersTimer: any; - private keyUpListener = (e: KeyboardEvent) => { - // KeyboardEvent.key for Return is 'Enter' (capital E); the old lowercase - // comparison never matched, so pressing Enter silently did nothing. - // requestSubmit() (unlike submit()) fires the form's onsubmit handler and - // runs HTML5 validation, so Enter behaves exactly like clicking Join. - if (e.key === 'Enter' && !this.joinButton.disabled) { - this.form.requestSubmit(); - } - }; - - constructor( - private form: HTMLFormElement, - private readonly container: HTMLElement, - ) { - this.joinButton = form.querySelector('button[type="submit"]') as HTMLButtonElement; - this.joinButton.disabled = true; - this.waitingForDecision = new Promise((r) => (this.resolvePlayerDecision = r)); - - new FormData(form); - - addEventListener('keyup', this.keyUpListener); - - form.onsubmit = (e) => { - SoundHandler.play(Sounds.click); - const result: PlayerDecision = ( - Array.from((new FormData(form) as any).entries()) as Array<[string, any]> - ).reduce((result, [name, value]) => { - (result as any)[name] = value; - return result; - }, {}) as any; - - this.resolvePlayerDecision(result); - - e.preventDefault(); - }; - - this.pollServersTimer = setInterval(this.loadServers.bind(this), pollInterval); - this.loadServers(); - this.waitForFinish(); - } - - private async waitForFinish() { - await this.waitingForDecision; - this.destroy(); - } - - private destroy() { - removeEventListener('keyup', this.keyUpListener); - clearInterval(this.pollServersTimer); - this.servers.forEach((s) => s.destroy()); - } - - private servers: Array = []; - private async loadServers() { - await Configuration.initialize(); - - const serverList = Configuration.servers.filter( - (u) => !this.servers.find((s) => s.url === u), - ); - - serverList.map(async (url) => { - const controller = new AbortController(); - const signal = controller.signal; - setTimeout(() => controller.abort(), pollInterval * 0.8); - - let response: Response | undefined; - try { - response = await fetch(url + serverInformationEndpoint, { signal }); - } catch { - // it's okay - } - - if (response?.ok) { - const content: ServerInformation = await response.json(); - if (!this.servers.find((s) => s.url === url)) { - const server = new ServerChooserOption( - content, - url, - this.removeServer.bind(this), - this.servers.length === 0, - ); - this.servers.push(server); - this.joinButton.disabled = false; - this.container.appendChild(server.element); - } - } - }); - } - - private removeServer(server: ServerChooserOption) { - this.servers = this.servers.filter((s) => s !== server); - if (!this.servers.length) { - this.joinButton.disabled = true; - } - } - - public async getPlayerDecision(): Promise { - return this.waitingForDecision; - } -} - -class ServerChooserOption { - private divElement = document.createElement('div'); - private inputElement = document.createElement('input'); - private labelElement = document.createElement('label'); - private serverNameElement = document.createElement('span'); - private completionElement = document.createElement('span'); - - private socket: Socket; - - constructor( - private content: ServerInformation, - public readonly url: string, - private onDestroy: (v: ServerChooserOption) => unknown, - isFirst: boolean, - ) { - this.inputElement.required = true; - this.inputElement.type = 'radio'; - this.inputElement.id = this.inputElement.value = url; - this.inputElement.name = 'server'; - this.inputElement.checked = isFirst; - this.labelElement.htmlFor = url; - this.labelElement.onclick = () => SoundHandler.play(Sounds.click); - this.divElement.appendChild(this.inputElement); - this.divElement.appendChild(this.labelElement); - this.labelElement.appendChild(this.serverNameElement); - this.labelElement.appendChild(document.createElement('br')); - this.labelElement.appendChild(this.completionElement); - this.completionElement.className = 'completion'; - this.setServerInfoLabelText(); - - this.socket = io(url, { - reconnection: true, - reconnectionAttempts: 5, - timeout: 4000, - parser, - } as any); - - this.socket.io.on('reconnect_failed', this.destroy.bind(this)); - - this.socket.on('connect', () => - this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates), - ); - this.socket.on( - TransportEvents.ServerInfoUpdate, - ([playerCount, gameState]: [number, number]) => { - this.content.playerCount = playerCount; - this.content.gameStatePercent = gameState; - this.setServerInfoLabelText(); - }, - ); - } - - public destroy() { - this.socket.close(); - this.divElement.parentElement?.removeChild(this.divElement); - this.onDestroy(this); - } - - private getRoundCompletionText(percent: number): string { - const texts = [ - 'Just started', - 'Just started', - 'Ongoing', - 'Halfway through', - 'Nearly over', - 'About to finish', - 'Game is over', - ]; - - return texts[Math.floor((percent / 100) * (texts.length - 1))]; - } - - private setServerInfoLabelText() { - this.serverNameElement.innerText = `${this.content.serverName} - ${this.content.playerCount}/${this.content.playerLimit} players`; - this.completionElement.innerText = this.getRoundCompletionText( - this.content.gameStatePercent, - ); - } - - public get element(): HTMLElement { - return this.divElement; - } -} diff --git a/frontend/src/scripts/landing-page-background.ts b/frontend/src/scripts/landing-page-background.ts deleted file mode 100644 index bafe559..0000000 --- a/frontend/src/scripts/landing-page-background.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - CircleLight, - FilteringOptions, - hsl, - Renderer, - renderNoise, - runAnimation, - WrapOptions, -} from 'sdf-2d'; -import { settings, rgb, PlanetBase, Random } from 'shared'; -import { PlanetShape } from './shapes/planet-shape'; - -// PlanetShape colours by mixing blue (0) -> red (1). The two backdrop planets -// read as the two in-game teams; the red planet is pulled a little off the -// pure-red end so it shows as a more muted, less saturated red. -const bluePlanet = 0; -const redPlanet = 0.85; - -export class LandingPageBackground { - private isActive = true; - public renderer: Promise; - private resolveRenderer!: (r: Renderer) => unknown; - - constructor(canvas: HTMLCanvasElement) { - this.start(canvas); - this.renderer = new Promise((r) => (this.resolveRenderer = r)); - } - - private async start(canvas: HTMLCanvasElement): Promise { - const noiseTexture = await renderNoise([256, 256], 1.2, 2); - - runAnimation( - canvas, - [ - { - ...PlanetShape.descriptor, - shaderCombinationSteps: [0, 1, 2], - }, - { - ...CircleLight.descriptor, - shaderCombinationSteps: [0, 2], - }, - ], - this.gameLoop.bind(this), - { - shadowTraceCount: 16, - paletteSize: 1, - ambientLight: rgb(0, 0, 0), - lightCutoffDistance: settings.lightCutoffDistance, - textures: { - noiseTexture: { - source: noiseTexture, - overrides: { - maxFilter: FilteringOptions.LINEAR, - wrapS: WrapOptions.MIRRORED_REPEAT, - wrapT: WrapOptions.MIRRORED_REPEAT, - }, - }, - }, - }, - ); - } - - private gameLoop(renderer: Renderer, time: DOMHighResTimeStamp, _: number): boolean { - Random.seed = 42; - - this.resolveRenderer(renderer); - - renderer.setViewArea(vec2.fromValues(0, renderer.canvasSize.y), renderer.canvasSize); - - const topPlanetPosition = vec2.fromValues( - 0.7 * renderer.canvasSize.x, - 0.7 * renderer.canvasSize.y, - ); - - const topPlanet = new PlanetShape( - PlanetBase.createPlanetVertices( - topPlanetPosition, - Random.getRandomInRange(150, 400), - Random.getRandomInRange(150, 400), - Random.getRandomInRange(10, 20), - ), - redPlanet, - ); - - // Fixed terrain phase (no longer animated -> no pulsing); the planet spins - // instead, the same way in-game planets do: PlanetShape's rotation uniform - // turns the whole body, terrain and outline together, in its own frame. - // Speeds sit in the game's per-planet range (~0.05-0.12 rad/s) and - // counter-rotate so the two planets don't drift in lockstep. - topPlanet.randomOffset = Random.getRandom(); - topPlanet.rotation = (time / 1000) * 0.09; - - const bottomPlanetPosition = vec2.fromValues( - 0.3 * renderer.canvasSize.x, - 0.3 * renderer.canvasSize.y, - ); - - const bottomPlanet = new PlanetShape( - PlanetBase.createPlanetVertices( - bottomPlanetPosition, - Random.getRandomInRange(150, 800), - Random.getRandomInRange(150, 400), - Random.getRandomInRange(10, 40), - ), - bluePlanet, - ); - - bottomPlanet.randomOffset = Random.getRandom(); - bottomPlanet.rotation = (time / 1000) * -0.06; - - const planetDistance = vec2.subtract( - vec2.create(), - topPlanetPosition, - bottomPlanetPosition, - ); - const planetDistanceLength = vec2.length(planetDistance); - const planetDirection = vec2.normalize(planetDistance, planetDistance); - const planetAngle = Math.atan2(planetDirection.y, planetDirection.x); - - renderer.addDrawable(topPlanet); - renderer.addDrawable(bottomPlanet); - renderer.addDrawable( - new CircleLight( - this.calculateLightPosition( - renderer, - planetAngle, - planetDistanceLength * 1.2, - -time / 3000, - ), - hsl(25, 75, 60), - 0.75, - ), - ); - - renderer.addDrawable( - new CircleLight( - this.calculateLightPosition( - renderer, - planetAngle, - planetDistanceLength * 1.2, - time / 2000 + Math.PI, - ), - hsl(249, 79, 70), - 0.25, - ), - ); - - return this.isActive; - } - - private calculateLightPosition( - renderer: Renderer, - angle: number, - length: number, - t: number, - ): vec2 { - const lightPosition = vec2.fromValues( - length * Math.sin(t), - length * Math.sin(t) * Math.cos(t), - ); - - const canvasCenter = vec2.scale(vec2.create(), renderer.canvasSize, 0.5); - - vec2.add(lightPosition, lightPosition, canvasCenter); - vec2.rotate(lightPosition, lightPosition, canvasCenter, angle); - return lightPosition; - } - - public destroy() { - this.isActive = false; - } -} diff --git a/frontend/src/scripts/minimap.ts b/frontend/src/scripts/minimap.ts deleted file mode 100644 index 64267e8..0000000 --- a/frontend/src/scripts/minimap.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { CharacterTeam, Id, settings } from 'shared'; - -export interface MinimapBlip { - id: Id; - position: vec2; - team: CharacterTeam; -} - -// Top-down radar of the whole circular arena, pinned to the top-left. The map's -// circular border is the world boundary (its radius maps to worldRadius), so the -// local player's bright dot reads as a true position in the arena and every other -// living player shows as a team-coloured dot. Owns its own , so the Game -// just appends `element` to the overlay and feeds it `update()` each frame. -// Replaces the off-screen chevrons that used to point at other players. -export class Minimap { - public readonly element = document.createElement('canvas'); - - private readonly ctx: CanvasRenderingContext2D; - private readonly colors: Record; - // World-space positions, smoothed per player so the 25 Hz snapshots glide - // rather than step between frames. - private readonly smoothed = new Map(); - private bufferSize = 0; - - constructor() { - this.element.className = 'minimap'; - this.ctx = this.element.getContext('2d')!; - - const theme = getComputedStyle(document.documentElement); - const read = (name: string, fallback: string) => - theme.getPropertyValue(name).trim() || fallback; - this.colors = { - [CharacterTeam.blue]: read('--bright-blue', '#4069a5'), - [CharacterTeam.red]: read('--bright-red', '#d15652'), - [CharacterTeam.neutral]: read('--bright-neutral', '#ccc'), - }; - } - - public update(localPosition: vec2 | undefined, players: Array) { - const size = this.element.clientWidth; - if (size === 0) { - return; // not laid out yet - } - this.syncBufferSize(size); - - const ctx = this.ctx; - ctx.clearRect(0, 0, size, size); - - const center = size / 2; - const innerRadius = center - 5; - const scale = innerRadius / settings.worldRadius; - - const toMap = (world: vec2): { x: number; y: number } => { - let dx = world.x * scale; - let dy = -world.y * scale; // world Y points up, canvas Y points down - const distance = Math.hypot(dx, dy); - if (distance > innerRadius) { - dx = (dx / distance) * innerRadius; - dy = (dy / distance) * innerRadius; - } - return { x: center + dx, y: center + dy }; - }; - - // Faint marker at the world's centre to aid orientation. - ctx.fillStyle = 'rgba(255, 255, 255, 0.12)'; - ctx.beginPath(); - ctx.arc(center, center, 1.5, 0, Math.PI * 2); - ctx.fill(); - - const present = new Set(); - for (const blip of players) { - present.add(blip.id); - let world = this.smoothed.get(blip.id); - if (world) { - vec2.lerp(world, world, blip.position, 0.3); - } else { - world = vec2.clone(blip.position); - this.smoothed.set(blip.id, world); - } - const { x, y } = toMap(world); - this.drawDot(x, y, 3, this.colors[blip.team]); - } - for (const id of this.smoothed.keys()) { - if (!present.has(id)) { - this.smoothed.delete(id); - } - } - - // The local player rides on top, drawn in white with a ring so "you" is - // unmistakable amongst the team-coloured dots. - if (localPosition) { - const { x, y } = toMap(localPosition); - this.drawDot(x, y, 3.5, '#ffffff'); - ctx.beginPath(); - ctx.arc(x, y, 6, 0, Math.PI * 2); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.65)'; - ctx.lineWidth = 1.5; - ctx.stroke(); - } - } - - private drawDot(x: number, y: number, radius: number, color: string) { - const ctx = this.ctx; - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fillStyle = color; - ctx.shadowColor = color; - ctx.shadowBlur = radius * 2; - ctx.fill(); - ctx.shadowBlur = 0; - } - - private syncBufferSize(cssSize: number) { - const dpr = window.devicePixelRatio || 1; - const target = Math.round(cssSize * dpr); - if (this.bufferSize !== target) { - this.bufferSize = target; - this.element.width = target; - this.element.height = target; - } - // Setting the buffer size resets the transform, so (re)establish it every - // frame and draw in CSS pixels regardless of the device pixel ratio. - this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - } -} diff --git a/frontend/src/scripts/objects/camera.ts b/frontend/src/scripts/objects/camera.ts new file mode 100644 index 0000000..46a1989 --- /dev/null +++ b/frontend/src/scripts/objects/camera.ts @@ -0,0 +1,29 @@ +import { vec2 } from 'gl-matrix'; +import { calculateViewArea, CommandExecutors, GameObject } from 'shared'; +import { RenderCommand } from '../commands/types/render'; +import { Game } from '../game'; + +export class Camera extends GameObject { + public center: vec2 = vec2.create(); + + private aspectRatio?: number; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: this.draw.bind(this), + }; + + constructor(private game: Game) { + super(null); + } + + private draw(c: RenderCommand) { + const canvasAspectRatio = c.renderer.canvasSize.x / c.renderer.canvasSize.y; + if (canvasAspectRatio !== this.aspectRatio) { + this.aspectRatio = canvasAspectRatio; + this.game.aspectRatioChanged(canvasAspectRatio); + } + + const viewArea = calculateViewArea(this.center, canvasAspectRatio); + c.renderer.setViewArea(viewArea.topLeft, viewArea.size); + } +} diff --git a/frontend/src/scripts/objects/character-view.ts b/frontend/src/scripts/objects/character-view.ts new file mode 100644 index 0000000..8cb63b6 --- /dev/null +++ b/frontend/src/scripts/objects/character-view.ts @@ -0,0 +1,19 @@ +import { vec2 } from 'gl-matrix'; +import { CharacterBase, CommandExecutors } from 'shared'; +import { RenderCommand } from '../commands/types/render'; +import { BlobShape } from '../shapes/blob-shape'; + +export class CharacterView extends CharacterBase { + private shape = new BlobShape(); + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => { + this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]); + c.renderer.addDrawable(this.shape); + }, + }; + + public get position(): vec2 { + return this.head.center; + } +} diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index 2b03836..053295f 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -1,6 +1,4 @@ -import { vec2 } from 'gl-matrix'; import { - Circle, Command, CommandExecutors, CommandReceiver, @@ -9,164 +7,62 @@ import { DeleteObjectsCommand, GameObject, Id, - PropertyUpdatesForObjects, - RemoteCallsForObjects, - settings, - UpdatePropertyCommand, + StepCommand, + UpdateObjectsCommand, } from 'shared'; -import { BeforeDestroyCommand } from '../commands/types/before-destroy'; -import { StepCommand } from '../commands/types/step'; -import { FeedbackHud } from '../feedback-hud'; import { Game } from '../game'; -import { serverTimeline } from '../helper/server-timeline'; -import { PredictablePlanet } from '../helper/prediction/client-character-world'; -import { localCharacterPredictor } from '../helper/prediction/local-character-predictor'; -import { Camera } from './types/camera'; -import { CharacterView } from './types/character-view'; -import { PlanetView } from './types/planet-view'; +import { Camera } from './camera'; +import { CharacterView } from './character-view'; export class GameObjectContainer extends CommandReceiver { protected objects: Map = new Map(); - public player!: CharacterView; - public camera: Camera = new Camera(this.game); - private wasLocalPlayerAlive = false; + public player: CharacterView; + public camera: Camera; protected commandExecutors: CommandExecutors = { [CreatePlayerCommand.type]: (c: CreatePlayerCommand) => { this.player = c.character as CharacterView; - this.player.isMainCharacter = true; + this.camera = new Camera(this.game); this.addObject(this.player); - // Fresh character (first spawn or respawn at a far planet): drop any - // prediction state so it snaps to the new body instead of gliding across. - localCharacterPredictor.reset(); - // Respawned — clear the elimination overlay. - FeedbackHud.hideElimination(); - this.wasLocalPlayerAlive = true; + this.addObject(this.camera); + }, + + [StepCommand.type]: (_: StepCommand) => { + if (this.player) { + this.camera.center = this.player.position; + } }, [CreateObjectsCommand.type]: (c: CreateObjectsCommand) => - c.objects.forEach((o) => this.addObject(o as GameObject)), + c.objects.forEach((o) => this.addObject(o)), - [StepCommand.type]: (c: StepCommand) => { - this.defaultCommandExecutor(c); + [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => + c.ids.forEach((id: Id) => this.objects.delete(id)), - // The local body is alive only while its object still exists (the server - // deletes it on death) and its health is above zero — `player` keeps - // pointing at the now-stale view after death, so both checks are needed. - const bodyPresent = !!this.player && this.objects.has(this.player.id); - const alive = bodyPresent && this.player.health > 0; - - // Show the elimination overlay on the alive→dead edge; CreatePlayerCommand - // clears it on respawn. - if (this.wasLocalPlayerAlive && !alive) { - FeedbackHud.showElimination(); - } - this.wasLocalPlayerAlive = alive; - - if (bodyPresent) { - // Override the interpolated pose of the local player with the predicted - // one so it responds to input immediately. Suppressed while dead so the - // corpse can't be walked around (the server ignores a dead player's - // input — a moving predicted body would be a pure client-side desync). - // A large correction (respawn / death) snaps inside the predictor. - localCharacterPredictor.setAlive(alive); - localCharacterPredictor.setStrength( - this.player.strengthFraction * settings.playerMaxStrength, - ); - if ( - localCharacterPredictor.update(this.predictablePlanets(), c.deltaTimeInSeconds) - ) { - this.player.head = localCharacterPredictor.head; - this.player.leftFoot = localCharacterPredictor.leftFoot; - this.player.rightFoot = localCharacterPredictor.rightFoot; - } - this.camera.follow(this.player.position, c.deltaTimeInSeconds); - } - }, - - [RemoteCallsForObjects.type]: (c: RemoteCallsForObjects) => - c.callsForObjects.forEach((c) => - this.objects.get(c.id)?.processRemoteCalls(c.calls), - ), - - [PropertyUpdatesForObjects.type]: (c: PropertyUpdatesForObjects) => { - serverTimeline.onSnapshot(c.timestamp); - c.updates.forEach((u) => { - u.updates.forEach((au) => this.objects.get(u.id)?.handleCommand(au)); - if (this.player && u.id === this.player.id) { - this.feedPredictor(u.updates); + [UpdateObjectsCommand.type]: (c: UpdateObjectsCommand) => { + c.objects.forEach((o) => { + this.objects.delete(o.id); + this.addObject(o); + if (o.id === this.player.id) { + this.player = o as CharacterView; } }); }, - - [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => - c.ids.forEach((id: Id) => this.deleteObject(id)), }; constructor(private game: Game) { super(); } - // The local player's world position, but only while the body is alive. On - // death the server deletes the character object (yet `player` keeps pointing - // at the now-stale view), so gate on the object still being present — otherwise - // the minimap would pin the "you" dot at the death spot for the whole respawn. - public get localPlayerPosition(): vec2 | undefined { - return this.player && this.objects.has(this.player.id) - ? this.player.position - : undefined; - } - - public get planets(): Array { - const planets: Array = []; - this.objects.forEach((o) => { - if (o instanceof PlanetView) { - planets.push(o); - } - }); - return planets; - } - protected defaultCommandExecutor(c: Command) { - this.objects.forEach((o) => o.handleCommand(c)); - this.camera.handleCommand(c); + this.objects.forEach((o) => o.sendCommand(c)); } - // Hand the local player's raw authoritative pose to the predictor (the - // interpolated pose would already be ~100 ms stale). The three body parts - // arrive together in one snapshot. - private feedPredictor(updates: Array) { - let head: Circle | undefined; - let leftFoot: Circle | undefined; - let rightFoot: Circle | undefined; - for (const u of updates) { - if (u.propertyKey === 'head') head = u.propertyValue as Circle; - else if (u.propertyKey === 'leftFoot') leftFoot = u.propertyValue as Circle; - else if (u.propertyKey === 'rightFoot') rightFoot = u.propertyValue as Circle; - } - if (head && leftFoot && rightFoot) { - localCharacterPredictor.setAuthoritative(head, leftFoot, rightFoot); - } - } - - private predictablePlanets(): Array { - return this.planets.map((p) => ({ - id: p.id, - vertices: p.vertices, - center: p.center, - radius: p.radius, - rotation: p.predictionRotation, - rotationSpeed: p.predictionRotationSpeed, - })); + public sendCommandToSingleObject(id: Id, e: Command) { + this.objects.get(id)!.sendCommand(e); } private addObject(object: GameObject) { this.objects.set(object.id, object); } - - private deleteObject(id: Id) { - const object = this.objects.get(id); - object?.handleCommand(new BeforeDestroyCommand()); - this.objects.delete(id); - } } diff --git a/frontend/src/scripts/objects/lamp-view.ts b/frontend/src/scripts/objects/lamp-view.ts new file mode 100644 index 0000000..f03ea98 --- /dev/null +++ b/frontend/src/scripts/objects/lamp-view.ts @@ -0,0 +1,17 @@ +import { vec2, vec3 } from 'gl-matrix'; +import { CircleLight } from 'sdf-2d'; +import { CommandExecutors, Id, LampBase } from 'shared'; +import { RenderCommand } from '../commands/types/render'; + +export class LampView extends LampBase { + private light: CircleLight; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.light), + }; + + constructor(id: Id, center: vec2, color: vec3, lightness: number) { + super(id, center, color, lightness); + this.light = new CircleLight(center, color, lightness); + } +} diff --git a/frontend/src/scripts/objects/projectile-view.ts b/frontend/src/scripts/objects/projectile-view.ts new file mode 100644 index 0000000..bc81f73 --- /dev/null +++ b/frontend/src/scripts/objects/projectile-view.ts @@ -0,0 +1,18 @@ +import { vec2 } from 'gl-matrix'; +import { Circle } from 'sdf-2d'; +import { CommandExecutors, Id } from 'shared'; +import { ProjectileBase } from 'shared/src/objects/types/projectile-base'; +import { RenderCommand } from '../commands/types/render'; + +export class ProjectileView extends ProjectileBase { + private circle: Circle; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.circle), + }; + + constructor(id: Id, center: vec2, radius: number) { + super(id, center, radius); + this.circle = new Circle(center, radius); + } +} diff --git a/frontend/src/scripts/objects/tunnel-view.ts b/frontend/src/scripts/objects/tunnel-view.ts new file mode 100644 index 0000000..c51dc5f --- /dev/null +++ b/frontend/src/scripts/objects/tunnel-view.ts @@ -0,0 +1,17 @@ +import { vec2 } from 'gl-matrix'; +import { InvertedTunnel } from 'sdf-2d'; +import { CommandExecutors, Id, TunnelBase } from 'shared'; +import { RenderCommand } from '../commands/types/render'; + +export class TunnelView extends TunnelBase { + private shape: InvertedTunnel; + + protected commandExecutors: CommandExecutors = { + [RenderCommand.type]: (c: RenderCommand) => c.renderer.addDrawable(this.shape), + }; + + constructor(id: Id, from: vec2, to: vec2, fromRadius: number, toRadius: number) { + super(id, from, to, fromRadius, toRadius); + this.shape = new InvertedTunnel(from, to, fromRadius, toRadius); + } +} diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts deleted file mode 100644 index c607c38..0000000 --- a/frontend/src/scripts/objects/types/camera.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - calculateViewArea, - CommandExecutors, - CommandReceiver, - mixRgb, - settings, -} from 'shared'; -import { RenderCommand } from '../../commands/types/render'; -import { Game } from '../../game'; -import { ScreenShake } from '../../screen-shake'; - -export class Camera extends CommandReceiver { - public center: vec2 = vec2.create(); - private aspectRatio?: number; - - // A short exponential lag masks any residual stepping in the followed - // position without the camera noticeably trailing during normal movement. - private static readonly followSeconds = 0.08; - - // Swooshing across half the map after a respawn would be disorienting; - // beyond this distance the camera cuts instead. - private static readonly snapDistance = 1500; - - constructor(private game: Game) { - super(); - } - - public follow(target: vec2, deltaTimeInSeconds: number) { - if (vec2.distance(target, this.center) > Camera.snapDistance) { - vec2.copy(this.center, target); - return; - } - - const q = 1 - Math.exp(-deltaTimeInSeconds / Camera.followSeconds); - vec2.lerp(this.center, this.center, target, q); - } - - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: this.draw.bind(this), - }; - - private draw({ renderer }: RenderCommand) { - const canvasAspectRatio = renderer.canvasSize.x / renderer.canvasSize.y; - if (canvasAspectRatio !== this.aspectRatio) { - this.aspectRatio = canvasAspectRatio; - this.game.aspectRatioChanged(canvasAspectRatio); - } - - // Shake displaces only the rendered view centre and the zoom-punch shrinks - // only the rendered view area — neither touches the followed position, so - // impacts jolt the frame without nudging the camera off the player. Passing - // the zoom as oversizeRatio scales the area about the (shaken) centre. - const shakenCenter = vec2.fromValues( - this.center[0] + ScreenShake.offsetX, - this.center[1] + ScreenShake.offsetY, - ); - const scale = ScreenShake.viewScale; - const viewArea = calculateViewArea(shakenCenter, canvasAspectRatio, scale * scale); - renderer.setViewArea(viewArea.topLeft, viewArea.size); - - renderer.setRuntimeSettings({ - ambientLight: mixRgb( - settings.backgroundGradient[0], - settings.backgroundGradient[1], - vec2.length(this.center) / settings.worldRadius, - ), - }); - } -} diff --git a/frontend/src/scripts/objects/types/character-view.ts b/frontend/src/scripts/objects/types/character-view.ts deleted file mode 100644 index 5c0d751..0000000 --- a/frontend/src/scripts/objects/types/character-view.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { vec2, vec3 } from 'gl-matrix'; -import { CircleLight, Renderer } from 'sdf-2d'; - -import { - Circle, - Id, - CharacterBase, - CharacterTeam, - settings, - clamp, - clamp01, - mix, - CommandExecutors, - UpdatePropertyCommand, -} from 'shared'; -import { BeforeDestroyCommand } from '../../commands/types/before-destroy'; -import { RenderCommand } from '../../commands/types/render'; -import { StepCommand } from '../../commands/types/step'; -import { CircleInterpolator } from '../../helper/interpolators/circle-interpolator'; -import { LinearInterpolator } from '../../helper/interpolators/linear-interpolator'; -import { Pointer } from '../../helper/pointer'; -import { CharacterShape } from '../../shapes/character-shape'; -import { SoundHandler, Sounds } from '../../sound-handler'; -import { VibrationHandler } from '../../vibration-handler'; -import { FeedbackHud } from '../../feedback-hud'; -import { ScreenShake } from '../../screen-shake'; - -const muzzleFlashDecaySeconds = 0.12; -const hitFlashDecaySeconds = 0.15; - -// A white-hot pop of light thrown at the spot a character dies, seen by everyone -// who can see the body. No radius knob on a CircleLight, so the burst is sold by -// a bright (HDR) colour with a fast-decaying intensity envelope. -const deathBurstDecaySeconds = 0.42; -const deathBurstMaxIntensity = 1.7; -const deathBurstColor = vec3.fromValues(2.5, 2.3, 2.1); - -const killIcon = - ''; -const deathIcon = - ''; - -export class CharacterView extends CharacterBase { - private shape: CharacterShape; - private muzzleFlash: CircleLight; - private muzzleFlashIntensity = 0; - private hitFlashIntensity = 0; - private deathBurst: CircleLight; - private deathBurstIntensity = 0; - private strength = settings.playerMaxStrength; - private strengthInterpolator = new LinearInterpolator(settings.playerMaxStrength); - private nameElement: HTMLElement = document.createElement('div'); - private statsElement: HTMLElement = document.createElement('div'); - private killCountElement: HTMLElement = document.createElement('span'); - private deathCountElement: HTMLElement = document.createElement('span'); - private healthElement: HTMLElement = document.createElement('div'); - private chargeElement: HTMLElement = document.createElement('div'); - - public isMainCharacter = false; - - private leftFootInterpolator: CircleInterpolator; - private rightFootInterpolator: CircleInterpolator; - private headInterpolator: CircleInterpolator; - - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: this.draw.bind(this), - [StepCommand.type]: this.step.bind(this), - [BeforeDestroyCommand.type]: this.beforeDestroy.bind(this), - [UpdatePropertyCommand.type]: this.updateProperty.bind(this), - }; - - constructor( - id: Id, - name: string, - killCount: number, - deathCount: number, - team: CharacterTeam, - health: number, - head?: Circle, - leftFoot?: Circle, - rightFoot?: Circle, - ) { - super(id, name, killCount, deathCount, team, health, head, leftFoot, rightFoot); - this.shape = new CharacterShape(settings.colorIndices[team]); - this.muzzleFlash = new CircleLight( - vec2.clone(this.head!.center), - settings.paletteDim[settings.colorIndices[team]], - 0, - ); - this.deathBurst = new CircleLight(vec2.clone(this.head!.center), deathBurstColor, 0); - - this.leftFootInterpolator = new CircleInterpolator(this.leftFoot!); - this.rightFootInterpolator = new CircleInterpolator(this.rightFoot!); - this.headInterpolator = new CircleInterpolator(this.head!); - - this.nameElement.className = 'player-tag ' + this.team; - this.nameElement.innerText = this.name; - this.healthElement.className = 'health'; - this.chargeElement.className = 'charge'; - - this.statsElement.className = 'stats'; - this.killCountElement.className = 'value'; - this.deathCountElement.className = 'value'; - const killStat = document.createElement('span'); - killStat.className = 'stat kills'; - killStat.innerHTML = killIcon; - killStat.appendChild(this.killCountElement); - const deathStat = document.createElement('span'); - deathStat.className = 'stat deaths'; - deathStat.innerHTML = deathIcon; - deathStat.appendChild(this.deathCountElement); - this.statsElement.append(killStat, deathStat); - - this.nameElement.appendChild(this.healthElement); - this.nameElement.appendChild(this.chargeElement); - this.nameElement.appendChild(this.statsElement); - } - - public get position(): vec2 { - return this.head!.center; - } - - public get bodyCenter(): vec2 { - const center = vec2.add(vec2.create(), this.head!.center, this.leftFoot!.center); - vec2.add(center, center, this.rightFoot!.center); - return vec2.scale(center, center, 1 / 3); - } - - public get facingDirection(): vec2 { - const footAverage = vec2.add( - vec2.create(), - this.leftFoot!.center, - this.rightFoot!.center, - ); - vec2.scale(footAverage, footAverage, 0.5); - const forward = vec2.subtract(footAverage, this.head!.center, footAverage); - return vec2.length(forward) > 0 - ? vec2.normalize(forward, forward) - : vec2.fromValues(0, 1); - } - - public get strengthFraction(): number { - return clamp01(this.strength / settings.playerMaxStrength); - } - - private updateProperty({ - propertyKey, - propertyValue, - rateOfChange, - }: UpdatePropertyCommand) { - if (propertyKey === 'head') { - this.headInterpolator.addFrame(propertyValue, rateOfChange); - } - if (propertyKey === 'leftFoot') { - this.leftFootInterpolator.addFrame(propertyValue, rateOfChange); - } - if (propertyKey === 'rightFoot') { - this.rightFootInterpolator.addFrame(propertyValue, rateOfChange); - } - if (propertyKey === 'strength') { - this.strengthInterpolator.addFrame(propertyValue, rateOfChange); - } - } - - public setHealth(health: number) { - const damage = this.health - health; - super.setHealth(health); - - if (damage > 0) { - SoundHandler.play( - Sounds.hit, - Math.min(1, (0.8 * damage) / settings.playerMaxStrength), - ); - this.hitFlashIntensity = Math.min(1, 0.4 + damage / settings.playerMaxStrength); - - if (this.isMainCharacter) { - VibrationHandler.vibrate(Math.min(200, damage * 4)); - // Getting hit jolts the frame too, so taking fire has weight, not just - // dealing it. - ScreenShake.add(clamp01(0.12 + (0.5 * damage) / settings.playerMaxStrength)); - } - } - } - - public onDie() { - if (this.isMainCharacter) { - VibrationHandler.vibrate(150); - } - // Visible to everyone who can see the body: a white-hot flash plus a - // full-body whiteout, so a kill reads as a violent burst rather than the - // character quietly blinking out. - this.deathBurstIntensity = 1; - this.hitFlashIntensity = 1; - } - - public onHitConfirmed(charge = 0) { - if (!this.isMainCharacter) { - return; - } - // Layer a meaty thud under the crisp confirmation tick; a charged hit lands - // lower and harder than a panic tap. - SoundHandler.play(Sounds.hit, mix(0.35, 0.7, charge), mix(1.3, 0.95, charge)); - SoundHandler.play(Sounds.click, mix(0.4, 0.75, charge), mix(1.7, 1.1, charge)); - ScreenShake.add(mix(0.22, 0.5, charge)); - VibrationHandler.vibrate(Math.round(mix(12, 35, charge))); - FeedbackHud.hitMarker(charge); - } - - public onKillConfirmed(victimName?: string, streak = 1, charge = 0) { - if (!this.isMainCharacter) { - return; - } - // A heavy low thud for the kill with a brighter confirmation over the top, - // a hard frame jolt, a zoom-punch toward the action for weight, and a - // double-thump rumble. - SoundHandler.play(Sounds.hit, 1, mix(0.62, 0.5, charge)); - SoundHandler.play(Sounds.click, 0.9, mix(0.6, 0.45, charge)); - ScreenShake.add(mix(0.75, 1, charge)); - ScreenShake.addPunch(mix(0.7, 1, charge)); - VibrationHandler.vibrate([45, 35, Math.round(mix(80, 130, charge))]); - FeedbackHud.killConfirmed(victimName, streak, charge); - } - - public onLeap() { - if (!this.isMainCharacter) { - return; - } - SoundHandler.play(Sounds.shoot, 0.3, 1.5); - } - - private step({ deltaTimeInSeconds }: StepCommand): void { - this.head! = this.headInterpolator.getValue(deltaTimeInSeconds); - this.leftFoot! = this.leftFootInterpolator.getValue(deltaTimeInSeconds); - this.rightFoot! = this.rightFootInterpolator.getValue(deltaTimeInSeconds); - - this.strength = clamp( - this.strengthInterpolator.getValue(deltaTimeInSeconds), - 0, - settings.playerMaxStrength, - ); - - if (this.muzzleFlashIntensity > 0) { - this.muzzleFlashIntensity = Math.max( - 0, - this.muzzleFlashIntensity - deltaTimeInSeconds / muzzleFlashDecaySeconds, - ); - this.muzzleFlash.center = this.head!.center; - this.muzzleFlash.intensity = this.muzzleFlashIntensity; - } - - if (this.hitFlashIntensity > 0) { - this.hitFlashIntensity = Math.max( - 0, - this.hitFlashIntensity - deltaTimeInSeconds / hitFlashDecaySeconds, - ); - } - - if (this.deathBurstIntensity > 0) { - this.deathBurstIntensity = Math.max( - 0, - this.deathBurstIntensity - deltaTimeInSeconds / deathBurstDecaySeconds, - ); - this.deathBurst.center = this.bodyCenter; - // Square the envelope so the flash blooms then drops off sharply rather - // than fading out in a flat ramp. - this.deathBurst.intensity = - deathBurstMaxIntensity * this.deathBurstIntensity * this.deathBurstIntensity; - } - } - - public onShoot(strength: number) { - const q = clamp01( - (strength - settings.chargeShotStrengthMin) / - (settings.chargeShotStrengthMax - settings.chargeShotStrengthMin), - ); - SoundHandler.play(Sounds.shoot, mix(0.55, 1, q), mix(1.15, 0.8, q)); - this.muzzleFlashIntensity = mix(0.35, 1, q); - } - - private beforeDestroy(): void { - this.nameElement.parentElement?.removeChild(this.nameElement); - } - - private draw({ renderer, overlay, shouldChangeLayout }: RenderCommand): void { - if (shouldChangeLayout) { - if (!this.nameElement.parentElement) { - overlay.appendChild(this.nameElement); - } - - const screenPosition = renderer.worldToDisplayCoordinates( - this.calculateTextPosition(), - ); - - this.nameElement.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%) rotate(-15deg)`; - - this.healthElement.style.width = - (50 * this.health) / settings.playerMaxHealth + 'px'; - this.chargeElement.style.width = - (50 * this.strength) / settings.playerMaxStrength + 'px'; - this.killCountElement.innerText = String(this.killCount); - this.deathCountElement.innerText = String(this.deathCount); - } - - this.shape.hitFlash = this.hitFlashIntensity; - this.shape.gazeTarget = this.calculateGazeTarget(renderer); - this.shape.setCircles([this.head!, this.leftFoot!, this.rightFoot!]); - renderer.addDrawable(this.shape); - - if (this.muzzleFlashIntensity > 0) { - renderer.addDrawable(this.muzzleFlash); - } - - if (this.deathBurstIntensity > 0) { - renderer.addDrawable(this.deathBurst); - } - } - - private calculateGazeTarget(renderer: Renderer): vec2 { - const cursor = Pointer.getDisplayPosition(); - if (this.isMainCharacter && cursor) { - return renderer.displayToWorldCoordinates(cursor); - } - - return vec2.scaleAndAdd( - vec2.create(), - this.head!.center, - this.facingDirection, - this.head!.radius * 8, - ); - } - - private calculateTextPosition(): vec2 { - const footAverage = vec2.add( - vec2.create(), - this.leftFoot!.center, - this.rightFoot!.center, - ); - vec2.scale(footAverage, footAverage, 0.5); - - const headFeetDelta = vec2.subtract(footAverage, this.head!.center, footAverage); - vec2.normalize(headFeetDelta, headFeetDelta); - const textOffset = vec2.scale(headFeetDelta, headFeetDelta, this.head!.radius + 80); - return vec2.add(textOffset, this.head!.center, textOffset); - } -} diff --git a/frontend/src/scripts/objects/types/lamp-view.ts b/frontend/src/scripts/objects/types/lamp-view.ts deleted file mode 100644 index e4db4f6..0000000 --- a/frontend/src/scripts/objects/types/lamp-view.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { vec2, vec3 } from 'gl-matrix'; -import { CircleLight } from 'sdf-2d'; -import { CommandExecutors, Id, LampBase, mixRgb, settings } from 'shared'; -import { RenderCommand } from '../../commands/types/render'; -import { StepCommand } from '../../commands/types/step'; - -export class LampView extends LampBase { - private light: CircleLight; - - private targetColor: vec3; - private targetLightness: number; - - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: this.draw.bind(this), - [StepCommand.type]: this.step.bind(this), - }; - - constructor(id: Id, center: vec2, color: vec3, lightness: number) { - super(id, center, color, lightness); - this.light = new CircleLight(vec2.clone(center), vec3.clone(color), lightness); - this.targetColor = vec3.clone(color); - this.targetLightness = lightness; - } - - public setLight(color: vec3, lightness: number) { - this.targetColor = vec3.clone(color); - this.targetLightness = lightness; - } - - private step({ deltaTimeInSeconds }: StepCommand): void { - const t = 1 - Math.exp(-deltaTimeInSeconds / settings.lampLerpSeconds); - this.light.color = mixRgb(this.light.color, this.targetColor, t); - this.light.intensity += (this.targetLightness - this.light.intensity) * t; - } - - private draw({ renderer }: RenderCommand): void { - renderer.addDrawable(this.light); - } -} diff --git a/frontend/src/scripts/objects/types/planet-view.ts b/frontend/src/scripts/objects/types/planet-view.ts deleted file mode 100644 index 06e6880..0000000 --- a/frontend/src/scripts/objects/types/planet-view.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { vec2, vec3 } from 'gl-matrix'; -import { CircleLight } from 'sdf-2d'; -import { - Id, - Random, - PlanetBase, - UpdatePropertyCommand, - CommandExecutors, - CharacterTeam, - settings, -} from 'shared'; -import { BeforeDestroyCommand } from '../../commands/types/before-destroy'; -import { RenderCommand } from '../../commands/types/render'; -import { StepCommand } from '../../commands/types/step'; -import { LinearInterpolator } from '../../helper/interpolators/linear-interpolator'; -import { PlanetShape } from '../../shapes/planet-shape'; - -const fallingPointLifetimeMs = 2000; - -// Global budget for simultaneously-lit capture flares, so a clustered wave of -// captures can never white out the SDF exposure — excess flips still pulse the -// ring and toast, they just skip the extra light. The acquire/release pair keeps -// the count in one place instead of being hand-maintained at every call site. -abstract class FlareBudget { - private static active = 0; - - public static tryAcquire(): boolean { - if (FlareBudget.active >= settings.maxConcurrentFlipFlares) { - return false; - } - FlareBudget.active++; - return true; - } - - public static release(): void { - FlareBudget.active = Math.max(0, FlareBudget.active - 1); - } -} - -export class PlanetView extends PlanetBase { - private shape: PlanetShape; - private ownershipProgress: HTMLElement; - // Rotation is owned by the backend (it drives the collision polygon too) and - // streamed in; the interpolator replays the angle on the shared snapshot - // timeline, in sync with the characters standing on the surface. - private readonly rotationInterpolator = new LinearInterpolator(0); - - private flareLight?: CircleLight; - private flareIntensity = 0; - private holdsFlareSlot = false; - - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: this.draw.bind(this), - [StepCommand.type]: this.step.bind(this), - [BeforeDestroyCommand.type]: this.beforeDestroy.bind(this), - [UpdatePropertyCommand.type]: this.updateProperty.bind(this), - }; - - constructor(id: Id, vertices: Array, ownership = 0.5, isKeystone = false) { - super(id, vertices, ownership, isKeystone); - this.shape = new PlanetShape(vertices, ownership); - this.shape.randomOffset = Random.getRandom(); - - this.ownershipProgress = document.createElement('div'); - this.ownershipProgress.className = 'ownership' + (isKeystone ? ' keystone' : ''); - } - - public setContested(contested: boolean) { - this.ownershipProgress.classList.toggle('contested', contested); - } - - private renderedRotation = 0; - private rotationSpeed = 0; - // Newest streamed rotation VALUE — the server-current angle at the latest - // snapshot — as opposed to renderedRotation, which the interpolator holds - // ~interpolationDelaySeconds in the PAST for drawing. The predictor seeds the - // local body from the same snapshot's pose, so it must collide against the - // planet at THIS (newest) phase and advance forward from it; using the drawn - // lagged angle biases the body off the surface by omega*delay*radius and - // wobbles it whenever the spin or the interpolator's rate cursor varies. - private latestRotation = 0; - public get predictionRotation(): number { - return this.latestRotation; - } - public get predictionRotationSpeed(): number { - return this.rotationSpeed; - } - - private step({ deltaTimeInSeconds }: StepCommand): void { - this.renderedRotation = this.rotationInterpolator.getValue(deltaTimeInSeconds); - this.shape.rotation = this.renderedRotation; - this.shape.colorMixQ = this.ownership; - - if (this.flareIntensity > 0) { - this.flareIntensity = Math.max( - 0, - this.flareIntensity - deltaTimeInSeconds / settings.lampFlareDecaySeconds, - ); - - if (this.flareLight) { - this.flareLight.intensity = - settings.lampFlareIntensity * this.flareIntensity * this.flareIntensity; - } - - if (this.flareIntensity === 0) { - this.releaseFlareSlot(); - } - } - } - - private releaseFlareSlot(): void { - if (this.holdsFlareSlot) { - this.holdsFlareSlot = false; - FlareBudget.release(); - } - } - - private lastGeneratedPoint?: number; - public generatedPoints(value: number) { - this.lastGeneratedPoint = value; - } - - public onFlipped(team: CharacterTeam): void { - const color = settings.palette[settings.colorIndices[team]]; - - if (!this.flareLight) { - this.flareLight = new CircleLight(vec2.clone(this.center), vec3.clone(color), 0); - } else { - this.flareLight.color = vec3.clone(color); - } - - if (!this.holdsFlareSlot) { - if (!FlareBudget.tryAcquire()) { - return; - } - this.holdsFlareSlot = true; - } - - this.flareIntensity = 1; - } - - private beforeDestroy(): void { - this.ownershipProgress.parentElement?.removeChild(this.ownershipProgress); - this.releaseFlareSlot(); - } - - private updateProperty({ - propertyKey, - propertyValue, - rateOfChange, - }: UpdatePropertyCommand): void { - if (propertyKey === 'rotation') { - this.rotationInterpolator.addFrame(propertyValue, rateOfChange); - this.latestRotation = propertyValue; - this.rotationSpeed = rateOfChange; - } else { - this.ownership = propertyValue; - } - } - - private draw({ renderer, overlay, shouldChangeLayout }: RenderCommand): void { - if (shouldChangeLayout) { - if (!this.ownershipProgress.parentElement) { - overlay.appendChild(this.ownershipProgress); - } - - const screenPosition = renderer.worldToDisplayCoordinates(this.center); - - this.ownershipProgress.style.transform = `translateX(${screenPosition.x}px) translateY(${screenPosition.y}px) translateX(-50%) translateY(-50%)`; - this.ownershipProgress.style.background = this.getGradient(); - - if (this.lastGeneratedPoint !== undefined) { - const element = document.createElement('div'); - element.className = 'falling-point ' + (this.ownership < 0.5 ? 'blue' : 'red'); - element.innerText = '+' + this.lastGeneratedPoint; - element.style.left = `${screenPosition.x}px`; - element.style.top = `${screenPosition.y}px`; - overlay.appendChild(element); - setTimeout( - () => element.parentElement?.removeChild(element), - fallingPointLifetimeMs, - ); - - this.lastGeneratedPoint = undefined; - } - } - - renderer.addDrawable(this.shape); - - if (this.flareIntensity > 0 && this.flareLight) { - renderer.addDrawable(this.flareLight); - } - } - - private getGradient(): string { - const sideBlue = this.ownership < 0.5; - // Keep the ring neutral through the same dead-band that gates scoring - // (settings.planetControlThreshold), so "the ring fills" and "this planet - // pays my team" happen together rather than disagreeing. - const control = Math.abs(this.ownership - 0.5); - const t = settings.planetControlThreshold; - const sidePercent = control <= t ? 0 : ((control - t) / (0.5 - t)) * 100; - return sideBlue - ? `conic-gradient( - var(--bright-blue) ${sidePercent}%, - var(--bright-blue) ${sidePercent}%, - rgba(0, 0, 0, 0) ${sidePercent}%, - rgba(0, 0, 0, 0) 100% - )` - : `conic-gradient( - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0) ${100 - sidePercent}%, - var(--bright-red) ${100 - sidePercent}%, - var(--bright-red) 100% - )`; - } -} diff --git a/frontend/src/scripts/objects/types/projectile-view.ts b/frontend/src/scripts/objects/types/projectile-view.ts deleted file mode 100644 index 5843e59..0000000 --- a/frontend/src/scripts/objects/types/projectile-view.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { CircleLight } from 'sdf-2d'; -import { - CharacterTeam, - CommandExecutors, - Id, - ProjectileBase, - settings, - UpdatePropertyCommand, -} from 'shared'; -import { RenderCommand } from '../../commands/types/render'; -import { StepCommand } from '../../commands/types/step'; -import { Vec2Interpolator } from '../../helper/interpolators/vec2-interpolator'; - -export class ProjectileView extends ProjectileBase { - private light: CircleLight; - - private centerInterpolator: Vec2Interpolator; - - protected commandExecutors: CommandExecutors = { - [RenderCommand.type]: this.draw.bind(this), - [StepCommand.type]: this.handleStep.bind(this), - [UpdatePropertyCommand.type]: this.updateProperty.bind(this), - }; - - constructor( - id: Id, - center: vec2, - radius: number, - team: CharacterTeam, - strength: number, - ) { - super(id, center, radius, team, strength); - this.light = new CircleLight( - center, - settings.paletteDim[settings.colorIndices[team]], - 0, - ); - this.centerInterpolator = new Vec2Interpolator(center); - } - - private updateProperty({ propertyValue, rateOfChange }: UpdatePropertyCommand): void { - this.centerInterpolator.addFrame(propertyValue, rateOfChange); - } - - private handleStep({ deltaTimeInSeconds }: StepCommand): void { - this.step(deltaTimeInSeconds); - - this.center = this.centerInterpolator.getValue(deltaTimeInSeconds); - this.light.center = this.center; - this.light.intensity = Math.min( - 0.1, - (0.15 * this.strength) / settings.projectileMaxStrength, - ); - } - - private draw({ renderer }: RenderCommand): void { - renderer.addDrawable(this.light); - } -} diff --git a/frontend/src/scripts/options-handler.ts b/frontend/src/scripts/options-handler.ts deleted file mode 100644 index 8479fb4..0000000 --- a/frontend/src/scripts/options-handler.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { SoundHandler, Sounds } from './sound-handler'; -import { VibrationHandler } from './vibration-handler'; - -interface Options { - vibrationEnabled: boolean; - soundsEnabled: boolean; - musicEnabled: boolean; -} - -export abstract class OptionsHandler { - private static initialized = false; - private static _options: Options = { - vibrationEnabled: true, - soundsEnabled: true, - musicEnabled: true, - }; - - public static initialize(inputElements: { - [k in Extract]: HTMLInputElement; - }) { - if (localStorage.getItem('options')) { - const stored: Partial | null = JSON.parse( - localStorage.getItem('options')!, - ); - this._options = { - ...this._options, - ...stored, - }; - } - - if (this._options.musicEnabled) { - SoundHandler.playAmbient(); - } - - for (const k in inputElements) { - const element = inputElements[k as keyof Options]; - element.checked = OptionsHandler._options[k as keyof Options]; - element.addEventListener('change', function () { - OptionsHandler._options[k as keyof Options] = this.checked; - if (!this.checked && k === 'soundsEnabled') { - OptionsHandler._options.musicEnabled = false; - inputElements.musicEnabled.checked = false; - SoundHandler.stopAmbient(); - } - - if (k === 'musicEnabled') { - if (this.checked) { - SoundHandler.playAmbient(); - } else { - SoundHandler.stopAmbient(); - } - } - - if (this.checked && k === 'vibrationEnabled') { - VibrationHandler.vibrate(100); - } - - SoundHandler.play(Sounds.click); - OptionsHandler.save(); - }); - } - - this.initialized = true; - } - - private static save() { - localStorage.setItem('options', JSON.stringify(this._options)); - } - - public static get options(): Options { - if (!this.initialized) { - throw new Error('Uninitialized'); - } - - return this._options; - } -} diff --git a/frontend/src/scripts/scoreboard.ts b/frontend/src/scripts/scoreboard.ts deleted file mode 100644 index 41b02ba..0000000 --- a/frontend/src/scripts/scoreboard.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { CharacterTeam, UpdateGameState, clamp, settings } from 'shared'; - -// Centred tug-of-war territory bar. The two fills meet at the bar's centre and -// grow outward by each team's share of the score limit, so the meeting point -// reads as the lead at a glance. Numeric scores are overlaid (the leader's is -// emphasised) and a "YOU" tick marks the local team's half. Owns its own DOM so -// the Game just appends `element` to the overlay and feeds it `update()`. -export class Scoreboard { - public readonly element = document.createElement('div'); - - private readonly blueFill = document.createElement('div'); - private readonly redFill = document.createElement('div'); - private readonly blueScore = document.createElement('span'); - private readonly redScore = document.createElement('span'); - private readonly youMarker = document.createElement('div'); - - constructor() { - this.element.className = 'planet-progress'; - - this.blueFill.className = 'fill blue'; - this.redFill.className = 'fill red'; - - this.blueScore.className = 'score blue'; - this.redScore.className = 'score red'; - - this.youMarker.className = 'you-marker'; - this.youMarker.innerText = 'YOU'; - - this.element.append( - this.blueFill, - this.redFill, - this.blueScore, - this.redScore, - this.youMarker, - ); - } - - public update( - { blueCount, redCount, limit }: UpdateGameState, - localTeam: CharacterTeam | undefined, - ) { - const { scoreboardHalfWidthPercent, scoreboardMinFillPercent } = settings; - const fraction = (count: number) => - Math.max( - count > 0 ? scoreboardMinFillPercent : 0, - clamp(count / limit, 0, 1) * scoreboardHalfWidthPercent, - ); - - this.blueFill.style.width = fraction(blueCount) + '%'; - this.redFill.style.width = fraction(redCount) + '%'; - - const isMatchPoint = (count: number) => - count / limit >= settings.matchPointScoreRatio; - this.blueFill.classList.toggle('match-point', isMatchPoint(blueCount)); - this.redFill.classList.toggle('match-point', isMatchPoint(redCount)); - - this.blueScore.innerText = String(Math.round(blueCount)); - this.redScore.innerText = String(Math.round(redCount)); - - this.blueScore.classList.toggle('leading', blueCount > redCount); - this.redScore.classList.toggle('leading', redCount > blueCount); - - if (localTeam === CharacterTeam.blue || localTeam === CharacterTeam.red) { - this.youMarker.style.display = ''; - this.youMarker.classList.toggle('blue', localTeam === CharacterTeam.blue); - this.youMarker.classList.toggle('red', localTeam === CharacterTeam.red); - } else { - this.youMarker.style.display = 'none'; - } - } -} diff --git a/frontend/src/scripts/screen-shake.ts b/frontend/src/scripts/screen-shake.ts deleted file mode 100644 index 75c5bce..0000000 --- a/frontend/src/scripts/screen-shake.ts +++ /dev/null @@ -1,84 +0,0 @@ -// Camera impact effects fed by combat hits, sampled by the camera at render -// time. Kept as a process-wide singleton so any view can feed it without -// threading a camera reference through the deserialized object graph. Both -// effects touch only the *rendered* view (centre offset + view-area zoom), never -// the followed position or the simulation clock — so they can't drift the camera -// off the player, and they're completely decoupled from prediction/netcode. -// -// Two effects: -// - shake: a trauma model (Eiserloh) — the felt shake is trauma SQUARED so -// small taps stay subtle while a kill punches hard; it bleeds off linearly. -// - punch: a brief zoom-in on a kill that snaps back fast, for "weight". -// -// Honours prefers-reduced-motion: callers can fire freely and it simply no-ops -// for players who have asked the OS to minimise motion. -export abstract class ScreenShake { - private static trauma = 0; - private static punch = 0; - - // Peak translation, in world units, at full trauma. The visible world is - // ~3800 units wide, so this tops out at a few percent of the screen. - private static readonly maxTranslation = 150; - // Fraction the view zooms in at full punch (smaller view area = zoomed in). - private static readonly maxZoom = 0.07; - private static readonly traumaDecayPerSecond = 1.7; - // Snaps back quickly so the punch reads as a sharp bite, not a slow drift. - private static readonly punchDecayPerSecond = 6; - - private static offsetXValue = 0; - private static offsetYValue = 0; - - private static get reducedMotion(): boolean { - return ( - typeof window !== 'undefined' && - typeof window.matchMedia === 'function' && - window.matchMedia('(prefers-reduced-motion: reduce)').matches - ); - } - - public static add(trauma: number): void { - if (this.reducedMotion) { - return; - } - this.trauma = Math.min(1, this.trauma + trauma); - } - - public static addPunch(amount: number): void { - if (this.reducedMotion) { - return; - } - this.punch = Math.min(1, this.punch + amount); - } - - public static step(deltaTimeInSeconds: number): void { - this.trauma = Math.max( - 0, - this.trauma - this.traumaDecayPerSecond * deltaTimeInSeconds, - ); - this.punch = Math.max(0, this.punch - this.punchDecayPerSecond * deltaTimeInSeconds); - const shake = this.trauma * this.trauma; - this.offsetXValue = this.maxTranslation * shake * (Math.random() * 2 - 1); - this.offsetYValue = this.maxTranslation * shake * (Math.random() * 2 - 1); - } - - public static get offsetX(): number { - return this.offsetXValue; - } - - public static get offsetY(): number { - return this.offsetYValue; - } - - // Multiplier for the view-area size: <1 zooms in. Squared so the punch bites - // sharply near its peak rather than ramping linearly. - public static get viewScale(): number { - return 1 - this.maxZoom * this.punch * this.punch; - } - - public static reset(): void { - this.trauma = 0; - this.punch = 0; - this.offsetXValue = 0; - this.offsetYValue = 0; - } -} diff --git a/frontend/src/scripts/shapes/blob-shape.ts b/frontend/src/scripts/shapes/blob-shape.ts new file mode 100644 index 0000000..fb5a3fa --- /dev/null +++ b/frontend/src/scripts/shapes/blob-shape.ts @@ -0,0 +1,125 @@ +import { mat2d, vec2 } from 'gl-matrix'; +import { Drawable, DrawableDescriptor } from 'sdf-2d'; +import { Circle } from 'shared'; + +export class BlobShape extends Drawable { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 headCenters[BLOB_COUNT]; + uniform vec2 leftFootCenters[BLOB_COUNT]; + uniform vec2 rightFootCenters[BLOB_COUNT]; + uniform float headRadii[BLOB_COUNT]; + uniform float footRadii[BLOB_COUNT]; + + float blobSmoothMin(float a, float b) + { + const float k = 300.0; + float res = exp2(-k * a) + exp2(-k * b); + return -log2(res) / k; + } + + float circleDistance(vec2 circleCenter, float radius, vec2 target) { + return distance(target, circleCenter) - radius; + } + + float blobMinDistance(vec2 target, out float colorIndex) { + float minDistance = 1000.0; + colorIndex = 3.0; + + for (int i = 0; i < BLOB_COUNT; i++) { + float headDistance = circleDistance(headCenters[i], headRadii[i], target); + float leftFootDistance = circleDistance(leftFootCenters[i], footRadii[i], target); + float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target); + + float res = min( + blobSmoothMin(headDistance, leftFootDistance), + blobSmoothMin(headDistance, rightFootDistance) + ); + + vec2 leftEyeOffset = vec2(-headRadii[i] * 0.35, headRadii[i] * 0.2); + vec2 rightEyeOffset = vec2(headRadii[i] * 0.35, headRadii[i] * 0.2); + + res = max( + res, + -circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target) + ); + + res = max( + res, + -circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target) + ); + + res = min( + res, + circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target) + ); + + res = min( + res, + circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target) + ); + + minDistance = min(minDistance, res); + } + + return minDistance; + } + `, + distanceFunctionName: 'blobMinDistance', + }, + propertyUniformMapping: { + footRadius: 'footRadii', + headRadius: 'headRadii', + rightFootCenter: 'rightFootCenters', + leftFootCenter: 'leftFootCenters', + headCenter: 'headCenters', + }, + uniformCountMacroName: 'BLOB_COUNT', + shaderCombinationSteps: [0, 1, 10], + empty: new BlobShape(), + }; + + protected head: Circle; + protected leftFoot: Circle; + protected rightFoot: Circle; + + public constructor() { + super(); + + const circle = new Circle(vec2.create(), 200); + this.setCircles([circle, circle, circle]); + } + + public setCircles([head, leftFoot, rightFoot]: [Circle, Circle, Circle]) { + this.head = head; + this.leftFoot = leftFoot; + this.rightFoot = rightFoot; + } + + public minDistance(target: vec2): number { + return Math.min( + this.head.distance(target), + this.leftFoot.distance(target), + this.rightFoot.distance(target), + ); + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + return { + headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform2d), + leftFootCenter: vec2.transformMat2d( + vec2.create(), + this.leftFoot.center, + transform2d, + ), + rightFootCenter: vec2.transformMat2d( + vec2.create(), + this.rightFoot.center, + transform2d, + ), + headRadius: this.head.radius * transform1d, + footRadius: this.leftFoot.radius * transform1d, + }; + } +} diff --git a/frontend/src/scripts/shapes/character-shape.ts b/frontend/src/scripts/shapes/character-shape.ts deleted file mode 100644 index 711f70a..0000000 --- a/frontend/src/scripts/shapes/character-shape.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { mat2d, vec2 } from 'gl-matrix'; -import { Drawable, DrawableDescriptor } from 'sdf-2d'; -import { Circle } from 'shared'; - -// A single character silhouette shared by every team (teams differ only by -// colour): a circular head sitting on two thin line-legs that run down to the -// two feet. -export class CharacterShape extends Drawable { - public static descriptor: DrawableDescriptor = { - sdf: { - shader: ` - uniform vec2 characterHeadCenters[CHARACTER_COUNT]; - uniform vec2 characterLeftFeet[CHARACTER_COUNT]; - uniform vec2 characterRightFeet[CHARACTER_COUNT]; - uniform vec2 characterGazeTargets[CHARACTER_COUNT]; - uniform float characterHeadRadii[CHARACTER_COUNT]; - uniform float characterFootRadii[CHARACTER_COUNT]; - uniform int characterColors[CHARACTER_COUNT]; - uniform float characterFlash[CHARACTER_COUNT]; - - float circleDistance(vec2 circleCenter, float radius, vec2 target) { - return distance(target, circleCenter) - radius; - } - - // Distance to the segment a->b; subtract a radius to get a capsule (a - // rounded thick line). - float segmentDistance(vec2 a, vec2 b, vec2 target) { - vec2 pa = target - a; - vec2 ba = b - a; - float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); - return length(pa - ba * h); - } - - float characterMinDistance(vec2 target, out vec4 color) { - float minDistance = 1000.0; - - for (int i = 0; i < CHARACTER_COUNT; i++) { - vec2 head = characterHeadCenters[i]; - vec2 leftFoot = characterLeftFeet[i]; - vec2 rightFoot = characterRightFeet[i]; - float headRadius = characterHeadRadii[i]; - float footRadius = characterFootRadii[i]; - - // The server rotates the whole posture toward travel, so the head - // leads: forward is the facing direction, perp its 90deg rotation, so - // the face sits on the leading side and reads at any rotation. - vec2 footAverage = (leftFoot + rightFoot) * 0.5; - vec2 toHead = head - footAverage; - float toHeadLength = length(toHead); - vec2 forward = toHeadLength > 0.001 ? toHead / toHeadLength : vec2(0.0, 1.0); - vec2 perp = vec2(-forward.y, forward.x); - - // A circular head, drawn a bit smaller than the head->feet gap so the - // legs below it read as two distinct LINES rather than being swallowed - // by the head. - float renderRadius = headRadius * 0.75; - float headDistance = circleDistance(head, renderRadius, target); - - // Legs as thin line-segments (capsules) from the head down to each - // foot, hard-min'd onto the head so the head stays a crisp circle. - float legThickness = footRadius * 0.35; - float leftLeg = segmentDistance(head, leftFoot, target) - legThickness; - float rightLeg = segmentDistance(head, rightFoot, target) - legThickness; - - // Rounded feet at the ends of the lines, kept large enough to stay - // visible even when the leg is short and the head nearly reaches them. - float footRender = footRadius * 0.7; - float leftFootDistance = circleDistance(leftFoot, footRender, target); - float rightFootDistance = circleDistance(rightFoot, footRender, target); - - float body = min( - headDistance, - min(min(leftLeg, rightLeg), min(leftFootDistance, rightFootDistance)) - ); - - // Real eyes painted on the leading face — a bright white sclera with a - // dark pupil — rather than carved holes, so the character reads as - // alive instead of hollow-socketed. - // Big white sclera, small pupil: the eye must read as mostly white so - // it looks like an eye, not a dark socket. (An 8-bit albedo caps the - // sclera at pure white, so its area — not its colour — is what makes - // the eye read brighter against the body.) - vec2 eyeBase = head + forward * (renderRadius * 0.26); - vec2 leftEyeCenter = eyeBase + perp * (renderRadius * 0.38); - vec2 rightEyeCenter = eyeBase - perp * (renderRadius * 0.38); - float scleraRadius = renderRadius * 0.26; - float pupilRadius = renderRadius * 0.11; - - // The pupil slides toward the gaze target (the cursor for the local - // player, otherwise the travel direction), kept well inside the rim so - // a generous ring of white always frames it. The normalize is guarded - // so a cursor resting exactly on an eye can't produce NaNs. - vec2 gazeTarget = characterGazeTargets[i]; - float pupilReach = (scleraRadius - pupilRadius) * 0.6; - vec2 toLeftGaze = gazeTarget - leftEyeCenter; - vec2 toRightGaze = gazeTarget - rightEyeCenter; - vec2 leftGaze = length(toLeftGaze) > 0.001 ? normalize(toLeftGaze) : forward; - vec2 rightGaze = length(toRightGaze) > 0.001 ? normalize(toRightGaze) : forward; - - float sclera = min( - circleDistance(leftEyeCenter, scleraRadius, target), - circleDistance(rightEyeCenter, scleraRadius, target) - ); - float pupil = min( - circleDistance(leftEyeCenter + leftGaze * pupilReach, pupilRadius, target), - circleDistance(rightEyeCenter + rightGaze * pupilReach, pupilRadius, target) - ); - - // Soften each eye edge by at least one screen texel so the colour - // boundary antialiases instead of staircasing when the character is - // small on screen. The fixed world-space term sets a floor on the - // softness when zoomed in; fwidth widens the band to a texel as the - // head shrinks. Computed here in uniform control flow — NOT inside the - // body branch below — so the screen-space derivatives stay defined. - // fwidth needs WebGL2 derivatives (core in GLSL ES 3.00), so the - // WebGL1 fallback keeps the plain world-space band. - float eyeAaBase = renderRadius * 0.025; - #ifdef WEBGL2_IS_AVAILABLE - float scleraAa = max(eyeAaBase, fwidth(sclera)); - float pupilAa = max(eyeAaBase, fwidth(pupil)); - #else - float scleraAa = eyeAaBase; - float pupilAa = eyeAaBase; - #endif - - if (body < minDistance) { - minDistance = body; - // Brief white punch when taking a hit. - color = mix( - readFromPalette(characterColors[i]), - vec4(1.0), - clamp(characterFlash[i], 0.0, 1.0) - ); - - // Paint the eyes over the body colour. The sclera albedo is HDR - // (>> 1): the shading pass multiplies it by the (often dim, reddish) - // scene light before clamping to the screen, so an over-bright white - // reads as a clean white eye everywhere instead of dimming into a - // dark socket. Needs the float colour buffer in sdf-2d; on 8-bit - // fallback it simply clamps back to plain white. The smoothstep - // widths (scleraAa / pupilAa, computed above) antialias the colour - // boundary in a zoom-aware way. - color = mix(color, vec4(10.0, 10.0, 10.0, 1.0), 1.0 - smoothstep(-scleraAa, scleraAa, sclera)); - color = mix(color, vec4(0.04, 0.04, 0.07, 1.0), 1.0 - smoothstep(-pupilAa, pupilAa, pupil)); - } - } - - return minDistance; - } - `, - distanceFunctionName: 'characterMinDistance', - }, - propertyUniformMapping: { - footRadius: 'characterFootRadii', - headRadius: 'characterHeadRadii', - rightFootCenter: 'characterRightFeet', - leftFootCenter: 'characterLeftFeet', - headCenter: 'characterHeadCenters', - gazeTarget: 'characterGazeTargets', - color: 'characterColors', - flash: 'characterFlash', - }, - uniformCountMacroName: 'CHARACTER_COUNT', - shaderCombinationSteps: [0, 1, 2, 8], - empty: new CharacterShape(0), - }; - - protected head!: Circle; - protected leftFoot!: Circle; - protected rightFoot!: Circle; - - // 0..1 transient white flash on taking a hit. Set per frame. - public hitFlash = 0; - // World point the eyes look at; the pupils slide toward it. Set per frame - // (the cursor for the local player, the travel direction for everyone else). - public gazeTarget = vec2.create(); - - public constructor(private readonly color: number) { - super(); - - const circle = new Circle(vec2.create(), 200); - this.setCircles([circle, circle, circle]); - } - - public setCircles([head, leftFoot, rightFoot]: [Circle, Circle, Circle]) { - this.head = head; - this.leftFoot = leftFoot; - this.rightFoot = rightFoot; - } - - public minDistance(target: vec2): number { - return Math.min( - this.head.distance(target), - this.leftFoot.distance(target), - this.rightFoot.distance(target), - ); - } - - protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { - return { - headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform2d), - leftFootCenter: vec2.transformMat2d( - vec2.create(), - this.leftFoot.center, - transform2d, - ), - rightFootCenter: vec2.transformMat2d( - vec2.create(), - this.rightFoot.center, - transform2d, - ), - gazeTarget: vec2.transformMat2d(vec2.create(), this.gazeTarget, transform2d), - headRadius: this.head.radius * transform1d, - footRadius: this.leftFoot.radius * transform1d, - color: this.color, - flash: this.hitFlash, - }; - } -} diff --git a/frontend/src/scripts/shapes/planet-shape.ts b/frontend/src/scripts/shapes/planet-shape.ts deleted file mode 100644 index ceb2e35..0000000 --- a/frontend/src/scripts/shapes/planet-shape.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; -import { PolygonFactory, DrawableDescriptor, Drawable } from 'sdf-2d'; -import { settings } from 'shared'; - -export const colorToString = (v: vec3 | vec4): string => - `vec4(${v[0]}, ${v[1]}, ${v[2]}, ${v.length > 3 ? v[3] : 1})`; - -export class PlanetShape extends PolygonFactory(settings.planetEdgeCount, 0) { - public static descriptor: DrawableDescriptor = { - sdf: { - shader: ` - uniform vec2 planetVertices[PLANET_COUNT * ${settings.planetEdgeCount}]; - uniform vec2 planetCenters[PLANET_COUNT]; - uniform float planetLengths[PLANET_COUNT]; - uniform float planetRandoms[PLANET_COUNT]; - uniform float planetColorMixQ[PLANET_COUNT]; - uniform float planetRotations[PLANET_COUNT]; - - uniform sampler2D noiseTexture; - - #ifdef WEBGL2_IS_AVAILABLE - float planetTerrain(vec2 h) { - return texture(noiseTexture, h)[0] - 0.5; - } - #else - float planetTerrain(vec2 h) { - return texture2D(noiseTexture, h)[0] - 0.5; - } - #endif - - vec2 planetLineDistance(vec2 target, vec2 from, vec2 to) { - vec2 targetFromDelta = target - from; - vec2 toFromDelta = to - from; - float h = clamp( - dot(targetFromDelta, toFromDelta) / dot(toFromDelta, toFromDelta), - 0.0, 1.0 - ); - - vec2 diff = targetFromDelta - toFromDelta * h; - return vec2( - dot(diff, diff), - toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x - ); - } - - float planetMinDistance(vec2 target, out vec4 color) { - float minDistance = 100.0; - - for (int j = 0; j < PLANET_COUNT; j++) { - vec2 startEnd = planetVertices[j * ${settings.planetEdgeCount}]; - vec2 vb = startEnd; - - vec2 center = planetCenters[j]; - float l = planetLengths[j]; - float randomOffset = planetRandoms[j]; - float rotation = planetRotations[j]; - - float cr = cos(rotation); - float sr = sin(rotation); - - // Spin the whole planet: evaluate the SDF in the planet's own - // rotating frame so the polygon outline turns together with its - // terrain, instead of the terrain sliding over a fixed outline. - vec2 targetCenterDelta = target - center; - float targetDistance = length(targetCenterDelta); - vec2 localTarget = center + vec2( - cr * targetCenterDelta.x - sr * targetCenterDelta.y, - sr * targetCenterDelta.x + cr * targetCenterDelta.y - ); - vec2 targetTangent = (localTarget - center) / clamp(targetDistance, 0.01, 1000.0); - - vec2 noisyTarget = localTarget - ( - targetTangent * planetTerrain(vec2( - l * abs(atan(targetTangent.y, targetTangent.x)), - randomOffset - )) / 12.0 - ); - - float d = 10000.0; - float s = 1.0; - for (int k = 1; k < ${settings.planetEdgeCount}; k++) { - vec2 va = vb; - vb = planetVertices[j * ${settings.planetEdgeCount} + k]; - vec2 ds = planetLineDistance(noisyTarget, va, vb); - - bvec3 cond = bvec3(noisyTarget.y >= va.y, noisyTarget.y < vb.y, ds.y > 0.0); - if (all(cond) || all(not(cond))) { - s *= -1.0; - } - - d = min(d, ds.x); - } - - vec2 ds = planetLineDistance(noisyTarget, vb, startEnd); - bvec3 cond = bvec3(noisyTarget.y >= vb.y, noisyTarget.y < startEnd.y, ds.y > 0.0); - if (all(cond) || all(not(cond))) { - s *= -1.0; - } - - d = min(d, ds.x); - float dist = s * sqrt(d); - - if (dist < minDistance) { - minDistance = dist; - color = mix(${colorToString(settings.bluePlanetColor)}, ${colorToString( - settings.redPlanetColor, - )}, planetColorMixQ[j]); - } - } - - return minDistance; - } - `, - distanceFunctionName: 'planetMinDistance', - }, - propertyUniformMapping: { - length: 'planetLengths', - random: 'planetRandoms', - center: 'planetCenters', - vertices: 'planetVertices', - colorMixQ: 'planetColorMixQ', - rotation: 'planetRotations', - }, - uniformCountMacroName: `PLANET_COUNT`, - shaderCombinationSteps: [0, 1, 2, 3], - empty: new PlanetShape(new Array(settings.planetEdgeCount).fill(vec2.create()), 0), - }; - - public randomOffset = 0; - public rotation = 0; - - // Circle about the rotation centre (the vertex centroid, which is what the - // shader spins around — see planetMinDistance above). The vertices never - // change after construction, so cache it once. - private readonly cullCenter: vec2; - private readonly cullRadius: number; - - constructor( - public vertices: Array, - public colorMixQ: number, - ) { - super(vertices); - - this.cullCenter = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create()); - vec2.scale(this.cullCenter, this.cullCenter, 1 / vertices.length); - - this.cullRadius = vertices.reduce( - (max, v) => Math.max(max, vec2.distance(this.cullCenter, v)), - 0, - ); - } - - public minDistance(target: vec2): number { - return vec2.distance(target, this.cullCenter) - this.cullRadius; - } - - protected getObjectToSerialize(transform2d: mat2d, _: number): any { - const transformedVertices = (this as any).actualVertices.map((v: vec2) => - vec2.transformMat2d(vec2.create(), v, transform2d), - ); - - const center = transformedVertices.reduce( - (sum: vec2, v: vec2) => vec2.add(sum, sum, v), - vec2.create(), - ); - vec2.scale(center, center, 1 / transformedVertices.length); - - let length = 0; - for (let i = 1; i < this.vertices.length; i++) { - length += vec2.distance(transformedVertices[i - 1], transformedVertices[i]); - } - - return { - vertices: transformedVertices, - center, - length, - random: this.randomOffset, - colorMixQ: this.colorMixQ, - rotation: this.rotation, - }; - } - - public serializeToUniforms( - uniforms: any, - transform2d: mat2d, - transform1d: number, - ): void { - const { propertyUniformMapping } = (this.constructor as typeof Drawable).descriptor; - - const serialized = this.getObjectToSerialize(transform2d, transform1d); - Object.entries(propertyUniformMapping).forEach(([k, v]) => { - if (!Object.prototype.hasOwnProperty.call(uniforms, v)) { - uniforms[v] = []; - } - - if (k === 'vertices') { - uniforms[v].push(...serialized[k]); - } else { - uniforms[v].push(serialized[k]); - } - }); - } -} diff --git a/frontend/src/scripts/sound-handler.ts b/frontend/src/scripts/sound-handler.ts deleted file mode 100644 index 81607ad..0000000 --- a/frontend/src/scripts/sound-handler.ts +++ /dev/null @@ -1,82 +0,0 @@ -import hitSound from '../../static/hit.mp3'; -import shootSound from '../../static/shoot.mp3'; -import clickSound from '../../static/click.mp3'; -import ambientSound from '../../static/ambient.mp3'; -import { OptionsHandler } from './options-handler'; - -export enum Sounds { - hit = 'hit', - shoot = 'shoot', - click = 'click', -} - -export abstract class SoundHandler { - private static sounds: { [key in Sounds]: HTMLAudioElement }; - private static isAmbientPlaying = false; - - private static ambientSound = new Audio(ambientSound); - - private static initialized = false; - public static async initialize( - onPlayKeypress: () => unknown = () => null, - onPauseKeypress: () => unknown = () => null, - ) { - this.sounds = { - [Sounds.hit]: await this.initializeSound(hitSound), - [Sounds.shoot]: await this.initializeSound(shootSound), - [Sounds.click]: await this.initializeSound(clickSound), - }; - - await this.ambientSound.play(); - this.ambientSound.muted = true; - this.initialized = true; - this.ambientSound.onpause = onPauseKeypress; - this.ambientSound.onplay = onPlayKeypress; - - this.ambientSound.muted = false; - this.ambientSound.volume = 0.5; - this.ambientSound.loop = true; - - if (!this.isAmbientPlaying) { - this.ambientSound.pause(); - } - } - - private static async initializeSound(hitSound: string): Promise { - const sound = new Audio(hitSound); - sound.muted = true; - await sound.play(); - sound.pause(); - sound.muted = false; - sound.currentTime = 0; - return sound; - } - - public static play(sound: Sounds, volume = 1, playbackRate = 1) { - if (!this.initialized || !OptionsHandler.options.soundsEnabled) { - return; - } - - const audio = - this.sounds[sound].currentTime > 0 - ? (this.sounds[sound].cloneNode(true) as HTMLAudioElement) - : this.sounds[sound]; - audio.volume = Math.max(0, Math.min(1, volume)); - audio.playbackRate = playbackRate; - audio.play(); - } - - public static playAmbient() { - this.isAmbientPlaying = true; - if (this.initialized) { - this.ambientSound.play(); - } - } - - public static stopAmbient() { - this.isAmbientPlaying = false; - if (this.initialized) { - this.ambientSound.pause(); - } - } -} diff --git a/frontend/src/scripts/tutorial.ts b/frontend/src/scripts/tutorial.ts deleted file mode 100644 index c0d1155..0000000 --- a/frontend/src/scripts/tutorial.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { - CharacterTeam, - CommandExecutors, - CommandReceiver, - Id, - LeapActionCommand, - MoveActionCommand, - PrimaryActionCommand, -} from 'shared'; -import { GameObjectContainer } from './objects/game-object-container'; -import { PlanetView } from './objects/types/planet-view'; - -type StageTrigger = 'move' | 'shoot' | 'leap' | 'capture'; - -const stages: ReadonlyArray<{ hint: string; clearsOn: StageTrigger }> = [ - { hint: 'WASD / drag to walk', clearsOn: 'move' }, - { hint: 'Click / tap to shoot', clearsOn: 'shoot' }, - { hint: 'Space / leap button to launch off a planet', clearsOn: 'leap' }, - { hint: 'Stand on a planet to capture it', clearsOn: 'capture' }, -]; - -const captureProgressThreshold = 0.05; - -const standingSlack = 200; - -export class Tutorial extends CommandReceiver { - private stage = 0; - private element = document.createElement('div'); - - private characterId?: Id; - - private standingPlanet?: PlanetView; - private standingBaseline = 0; - - protected commandExecutors: CommandExecutors = { - [MoveActionCommand.type]: (c: MoveActionCommand) => { - if (this.clearsOn() === 'move' && vec2.length(c.direction) > 0) { - this.advance(); - } - }, - [PrimaryActionCommand.type]: () => { - if (this.clearsOn() === 'shoot') { - this.advance(); - } - }, - [LeapActionCommand.type]: () => { - if (this.clearsOn() === 'leap') { - this.advance(); - } - }, - }; - - constructor(overlay: HTMLElement) { - super(); - this.element.className = 'tutorial-hint'; - this.element.innerText = stages[0].hint; - overlay.appendChild(this.element); - } - - private clearsOn(): StageTrigger | undefined { - return stages[this.stage]?.clearsOn; - } - - private advance() { - this.setStage(this.stage + 1); - } - - public step(gameObjects: GameObjectContainer) { - if (this.stage >= stages.length) { - return; - } - - const player = gameObjects.player; - if (!player) { - return; - } - - if (this.characterId === undefined) { - this.characterId = player.id; - } else if (player.id !== this.characterId) { - this.finish(); - return; - } - - if (this.clearsOn() === 'capture') { - const standing = gameObjects.planets.find( - (p) => vec2.distance(p.center, player.bodyCenter) < p.radius + standingSlack, - ); - if (standing !== this.standingPlanet) { - this.standingPlanet = standing; - this.standingBaseline = standing?.ownership ?? 0; - } - if (standing) { - const drift = standing.ownership - this.standingBaseline; - const progress = player.team === CharacterTeam.blue ? -drift : drift; - if (progress > captureProgressThreshold) { - this.finish(); - } - } - } - } - - private finish() { - this.setStage(stages.length); - } - - private setStage(stage: number) { - this.stage = stage; - this.element.innerText = stages[stage]?.hint ?? ''; - } -} diff --git a/frontend/src/scripts/vibration-handler.ts b/frontend/src/scripts/vibration-handler.ts deleted file mode 100644 index 3ae8420..0000000 --- a/frontend/src/scripts/vibration-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { OptionsHandler } from './options-handler'; - -export abstract class VibrationHandler { - // Accepts either a single duration or an on/off pattern (e.g. a double-thump - // [40, 30, 90] for a kill). - public static vibrate(pattern: number | number[]): void { - if (OptionsHandler.options.vibrationEnabled && this.isVibrationEnabled) { - navigator.vibrate(pattern); - } - } - - public static get isVibrationEnabled(): boolean { - return 'vibrate' in navigator; - } - - public static get isVibrationEnabledHeuristics(): boolean { - return this.isVibrationEnabled && 'ontouchstart' in window; - } -} diff --git a/frontend/src/styles/_mixins.scss b/frontend/src/styles/_mixins.scss deleted file mode 100644 index d845573..0000000 --- a/frontend/src/styles/_mixins.scss +++ /dev/null @@ -1,11 +0,0 @@ -@mixin square($size) { - width: $size; - height: $size; -} - -@mixin background { - backdrop-filter: blur(24px); - @supports not (backdrop-filter: blur(24px)) { - background-color: rgba(0, 0, 0, 0.5); - } -} diff --git a/frontend/src/styles/_vars.scss b/frontend/src/styles/_vars.scss deleted file mode 100644 index f19825b..0000000 --- a/frontend/src/styles/_vars.scss +++ /dev/null @@ -1,22 +0,0 @@ -$accent: #b33951; -$border-width: 1.5px; -$border-width-focused: 3px; -$border: $border-width solid white; -$small-padding: 12px; -$medium-padding: 24px; -$large-padding: 64px; -$border-radius: 15px; -$animation-time: 200ms ease; -$breakpoint: 700px; -$height-breakpoint: 500px; -$large-icon: 48px; -$small-icon: 32px; -$bright-blue: #4069a5; -$bright-red: #d15652; -$bright-neutral: #ccc; - -:root { - --bright-blue: #{$bright-blue}; - --bright-red: #{$bright-red}; - --bright-neutral: #{$bright-neutral}; -} diff --git a/frontend/src/styles/button.scss b/frontend/src/styles/button.scss deleted file mode 100644 index 13ac5b6..0000000 --- a/frontend/src/styles/button.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use './vars.scss' as *; - -button { - border: none; - background: none; - font-size: 2rem; - font-family: 'Comfortaa', sans-serif; - cursor: pointer; - display: block; - margin: auto; - - $ascend: 6px; - padding: $medium-padding 0 0 0; - margin-bottom: $ascend; - transition: transform $animation-time, padding-bottom $animation-time, - margin-bottom $animation-time; - - &:not(:disabled):hover, - &:not(:disabled):focus { - outline: none; - color: $accent; - transform: translateY(-$ascend); - padding-bottom: $ascend; - margin-bottom: 0; - } - - &:disabled { - color: #777; - cursor: not-allowed; - } -} diff --git a/frontend/src/styles/form.scss b/frontend/src/styles/form.scss deleted file mode 100644 index d096895..0000000 --- a/frontend/src/styles/form.scss +++ /dev/null @@ -1,123 +0,0 @@ -@use 'sass:math'; -@use 'vars' as *; -@use 'mixins' as *; - -form { - @include background; - - padding: math.div($small-padding, 2) $small-padding $small-padding $small-padding; - border-radius: $border-radius; - - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus { - -webkit-text-fill-color: white; - transition: background-color 5000s ease-in-out 0s; - } - - input, - label { - font-size: 1.5rem; - } - - fieldset { - border: $border; - padding: 0 $medium-padding $medium-padding $medium-padding; - border-radius: $border-radius; - - div { - // disable margin collapse - overflow: auto; - } - - legend { - font-family: 'Comfortaa', sans-serif; - font-size: 2rem; - } - - .name-group { - position: relative; - padding: 15px 0 0; - margin-top: 10px; - margin: 10px math.div($small-padding, 2) $small-padding math.div($small-padding, 2); - - input { - font-family: inherit; - border: none; - border-bottom: $border; - color: white; - padding: 10px 0 0px 0; - background: transparent; - margin-bottom: $border-width-focused - $border-width; - width: 100%; - - &::placeholder { - color: transparent; - } - - &:placeholder-shown ~ label { - font-size: 1.3rem; - cursor: text; - top: 20px; - } - } - - label { - position: absolute; - top: 0; - display: block; - transition: $animation-time; - color: $bright-neutral; - font-size: 1.1rem; - } - - input:focus, - input:not(:placeholder-shown) { - outline: none; - margin-bottom: 0; - border-width: $border-width-focused; - border-color: $accent; - - ~ label { - position: absolute; - top: 0; - display: block; - transition: $animation-time; - } - } - } - - input[type='radio'] { - width: 0; - height: 0; - appearance: none; - - + label { - display: block; - border-radius: $border-radius; - padding: $small-padding; - border: $border; - - cursor: pointer; - margin: $border-width-focused - $border-width $border-width-focused - - $border-width + math.div($small-padding, 2); - - .completion { - font-size: 0.7em; - color: $bright-neutral; - } - } - - &:focus { - outline: none; - } - - &:focus + label, - &:checked + label { - border-color: $accent; - border-width: $border-width-focused; - margin: 0 math.div($small-padding, 2); - } - } - } -} diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss new file mode 100644 index 0000000..710ef3a --- /dev/null +++ b/frontend/src/styles/main.scss @@ -0,0 +1,37 @@ +$breakpoint: 800px; + +* { + margin: 0; + box-sizing: border-box; + color: white; +} + +html { + background-color: linear-gradient(45deg, #103783, #9bafd9); + + @media (max-width: $breakpoint) { + font-size: 0.7rem; + } +} + +html, +body, +canvas#main { + height: 100%; + width: 100%; +} + +body { + #overlay { + margin: 0.5rem 1.25rem; + position: absolute; + right: 0; + font-size: 0.75rem; + white-space: pre; + font-family: 'Lucida Console', Monaco, monospace; + + @media (max-width: $breakpoint) { + font-size: 0.6rem; + } + } +} diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss deleted file mode 100644 index 376284a..0000000 --- a/frontend/src/styles/settings.scss +++ /dev/null @@ -1,164 +0,0 @@ -@use 'sass:math'; -@use 'vars' as *; -@use 'mixins' as *; - -#toggle-settings-container, -#settings-container { - position: absolute; - display: flex; - justify-content: center; - top: 0; - right: 0; - font-size: 0; -} - -#toggle-settings-container { - padding: $medium-padding; - - cursor: pointer; - - padding: $medium-padding $medium-padding 0 $medium-padding; - @media (max-width: $breakpoint) { - padding: $medium-padding $medium-padding $medium-padding 0; - } - - #toggle-settings { - animation: spin 32s linear infinite; - - @keyframes spin { - 100% { - transform: rotate(360deg); - } - } - - @include square($large-icon); - @media (max-width: $breakpoint) { - @include square($small-icon); - } - } -} - -#settings-container { - top: $medium-padding + $large-icon + $small-padding; - @media (max-width: $breakpoint) { - top: 0; - right: $medium-padding + $small-icon + $small-padding; - } - pointer-events: none; - overflow: hidden; -} - -#settings { - display: flex; - flex-direction: column; - @include background; - border-radius: 12px; - z-index: 100; - margin-right: $medium-padding - $small-padding; - padding: $small-padding; - - transform: translateY(-100%); - @media (max-width: $breakpoint) { - flex-direction: row-reverse; - transform: translateX(100%); - margin-top: $medium-padding - $small-padding; - margin-right: 0; - } - - opacity: 0; - transition: transform $animation-time, opacity $animation-time; - - &.open { - transform: none; - opacity: 1; - pointer-events: all; - } - - img, - svg { - cursor: pointer; - transition: opacity $animation-time; - @include square($large-icon); - @media (max-width: $breakpoint) { - @include square($small-icon); - } - } - - label { - position: relative; - - input[type='checkbox'] { - width: 0; - height: 0; - appearance: none; - cursor: pointer; - - &:not(:checked) + * { - opacity: 0.4; - } - - &:focus { - outline: none; - } - - &:after { - content: ''; - background: white; - position: absolute; - - $height: 4px; - width: $height; - top: math.div($height, 2); - - right: 0; - transform-origin: top right; - border-radius: 100px; - height: 0; - transform: rotate(45deg) translateY(17%); - transition: height $animation-time; - - @media (max-width: $breakpoint) { - $height: 2px; - width: $height; - top: math.div($height, 2); - } - } - - &:not(:checked):after { - height: $large-icon; - @media (max-width: $breakpoint) { - height: $small-icon; - } - } - } - } - - img, - svg { - margin-top: $small-padding; - @media (max-width: $breakpoint) { - margin-top: 0; - margin-right: $small-padding; - } - } - - label:first-child { - img, - svg { - margin-top: 0; - margin-right: 0; - } - } - - label:not(:first-child) { - input[type='checkbox']:after { - $height: 4px; - top: math.div($height, 2) + $small-padding; - - @media (max-width: $breakpoint) { - right: $small-padding; - top: math.div($height, 2); - } - } - } -} diff --git a/frontend/static/ambient.mp3 b/frontend/static/ambient.mp3 deleted file mode 100644 index 218cc96..0000000 Binary files a/frontend/static/ambient.mp3 and /dev/null differ diff --git a/frontend/static/chevron.svg b/frontend/static/chevron.svg deleted file mode 100644 index 0178fd9..0000000 --- a/frontend/static/chevron.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/static/circle.svg b/frontend/static/circle.svg deleted file mode 100644 index f4fb483..0000000 --- a/frontend/static/circle.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/static/click.mp3 b/frontend/static/click.mp3 deleted file mode 100644 index 50a022c..0000000 Binary files a/frontend/static/click.mp3 and /dev/null differ diff --git a/frontend/static/declared.png b/frontend/static/declared.png new file mode 100644 index 0000000..897278d Binary files /dev/null and b/frontend/static/declared.png differ diff --git a/frontend/static/declared.psd b/frontend/static/declared.psd new file mode 100644 index 0000000..e2f814d --- /dev/null +++ b/frontend/static/declared.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03ed783e268bc05d40997239f6118767c5d5d8b15d23fa7b329321c0a1529c46 +size 437314 diff --git a/frontend/static/favicons/apple-touch-icon.png b/frontend/static/favicons/apple-touch-icon.png deleted file mode 100644 index cabfeb2..0000000 Binary files a/frontend/static/favicons/apple-touch-icon.png and /dev/null differ diff --git a/frontend/static/favicons/favicon-16x16.png b/frontend/static/favicons/favicon-16x16.png deleted file mode 100644 index 5fe28d0..0000000 Binary files a/frontend/static/favicons/favicon-16x16.png and /dev/null differ diff --git a/frontend/static/favicons/favicon-32x32.png b/frontend/static/favicons/favicon-32x32.png deleted file mode 100644 index d007086..0000000 Binary files a/frontend/static/favicons/favicon-32x32.png and /dev/null differ diff --git a/frontend/static/favicons/favicon.ico b/frontend/static/favicons/favicon.ico deleted file mode 100644 index a733520..0000000 Binary files a/frontend/static/favicons/favicon.ico and /dev/null differ diff --git a/frontend/static/flag.svg b/frontend/static/flag.svg deleted file mode 100644 index 20653df..0000000 --- a/frontend/static/flag.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/frontend/static/hit.mp3 b/frontend/static/hit.mp3 deleted file mode 100644 index d088388..0000000 Binary files a/frontend/static/hit.mp3 and /dev/null differ diff --git a/frontend/static/logout.svg b/frontend/static/logout.svg deleted file mode 100644 index ab168e8..0000000 --- a/frontend/static/logout.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/static/mask.svg b/frontend/static/mask.svg deleted file mode 100644 index 8c7ecbf..0000000 --- a/frontend/static/mask.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/frontend/static/maximize.svg b/frontend/static/maximize.svg deleted file mode 100644 index 7118488..0000000 --- a/frontend/static/maximize.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/frontend/static/minimize.svg b/frontend/static/minimize.svg deleted file mode 100644 index 93a212a..0000000 --- a/frontend/static/minimize.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/frontend/static/music.svg b/frontend/static/music.svg deleted file mode 100644 index 5f68ba1..0000000 --- a/frontend/static/music.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/frontend/static/og-image.png b/frontend/static/og-image.png deleted file mode 100644 index 86fa9f9..0000000 Binary files a/frontend/static/og-image.png and /dev/null differ diff --git a/frontend/static/settings.svg b/frontend/static/settings.svg deleted file mode 100644 index 8e99bbe..0000000 --- a/frontend/static/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/static/shoot.mp3 b/frontend/static/shoot.mp3 deleted file mode 100644 index 435a99e..0000000 Binary files a/frontend/static/shoot.mp3 and /dev/null differ diff --git a/frontend/static/shoot2.mp3 b/frontend/static/shoot2.mp3 deleted file mode 100644 index de8c874..0000000 Binary files a/frontend/static/shoot2.mp3 and /dev/null differ diff --git a/frontend/static/shoot3.mp3 b/frontend/static/shoot3.mp3 deleted file mode 100644 index 89face6..0000000 Binary files a/frontend/static/shoot3.mp3 and /dev/null differ diff --git a/frontend/static/shoot4.mp3 b/frontend/static/shoot4.mp3 deleted file mode 100644 index ce505d0..0000000 Binary files a/frontend/static/shoot4.mp3 and /dev/null differ diff --git a/frontend/static/shoot5.mp3 b/frontend/static/shoot5.mp3 deleted file mode 100644 index 0dfa8b9..0000000 Binary files a/frontend/static/shoot5.mp3 and /dev/null differ diff --git a/frontend/static/shoot6.mp3 b/frontend/static/shoot6.mp3 deleted file mode 100644 index 2ce159b..0000000 Binary files a/frontend/static/shoot6.mp3 and /dev/null differ diff --git a/frontend/static/spinner.svg b/frontend/static/spinner.svg deleted file mode 100644 index 5d0cc38..0000000 --- a/frontend/static/spinner.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/static/vibrate.svg b/frontend/static/vibrate.svg deleted file mode 100644 index ce6d594..0000000 --- a/frontend/static/vibrate.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/frontend/static/volume.svg b/frontend/static/volume.svg deleted file mode 100644 index dedc4d8..0000000 --- a/frontend/static/volume.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index b12fbd2..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "target": "es6", - "esModuleInterop": true, - "strict": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "ignoreDeprecations": "6.0", - "skipLibCheck": true, - "module": "commonjs", - "lib": ["dom", "es2017"], - "typeRoots": ["./types", "./node_modules/@types"] - }, - "include": ["src/**/*.ts", "src/*.ts"], - "exclude": ["node_modules", "typings"] -} diff --git a/frontend/types/socket.io-msgpack-parser/index.d.ts b/frontend/types/socket.io-msgpack-parser/index.d.ts deleted file mode 100644 index 4065a1e..0000000 --- a/frontend/types/socket.io-msgpack-parser/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'socket.io-msgpack-parser'; diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 97f6f6b..f2a7f66 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -1,142 +1,120 @@ const path = require('path'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const TerserJSPlugin = require('terser-webpack-plugin'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const HTMLInlineCSSWebpackPlugin = require('html-inline-css-webpack-plugin').default; -// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); const Sass = require('sass'); +const TerserJSPlugin = require('terser-webpack-plugin'); +const { ESBuildPlugin } = require('esbuild-loader'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); -module.exports = (env, argv) => { - const isProduction = argv.mode !== 'development'; +const PATHS = { + entryPoint: path.resolve(__dirname, 'src/index.ts'), + bundles: path.resolve(__dirname, 'dist'), +}; - return { - entry: { - main: path.resolve(__dirname, 'src/index.ts'), - }, - output: { - filename: 'main.js', - path: path.resolve(__dirname, 'dist'), - }, - devServer: { - host: '0.0.0.0', - allowedHosts: 'all', - }, - // webpack-dev-server 5 no longer accepts `watchOptions` under `devServer`; - // polling is configured on the compiler instead (needed for some FS mounts). +module.exports = (env, argv) => ({ + entry: { + index: PATHS.entryPoint, + }, + target: 'web', + output: { + filename: '[name].js', + path: PATHS.bundles, + }, + devtool: argv.mode === 'development' ? 'source-map' : false, + devServer: { + host: '0.0.0.0', + disableHostCheck: true, watchOptions: { poll: true, }, - plugins: [ - new CleanWebpackPlugin(), - new MiniCssExtractPlugin(), - //new BundleAnalyzerPlugin(), - new HtmlWebpackPlugin({ - template: './src/index.html', + }, + optimization: { + minimize: argv.mode !== 'development', + minimizer: [ + new TerserJSPlugin({ + sourceMap: false, + test: /\.js$/, + exclude: /node_modules/, + terserOptions: { + keep_classnames: true, + }, }), - // The SVG UI icons used to be inlined into the HTML by the (abandoned, - // webpack-4-only) html-webpack-inline-svg-plugin. They are now emitted as - // static files and referenced via `static/.svg` from index.html. - new CopyWebpackPlugin({ - patterns: [{ from: 'static/*.svg', to: 'static/[name][ext]' }], - }), - // Inline the extracted CSS into the HTML for production builds (replaces - // the abandoned html-webpack-inline-source-plugin). In development the CSS - // stays a separate, linked file for faster rebuilds. - ...(isProduction ? [new HTMLInlineCSSWebpackPlugin()] : []), + new OptimizeCSSAssetsPlugin({}), ], - optimization: { - minimizer: [ - new TerserJSPlugin({ - exclude: /node_modules/, - // The custom serialization protocol keys on class names, so they must - // survive minification (see shared/src/serialization). - terserOptions: { - keep_classnames: true, + }, + plugins: [ + new ESBuildPlugin(), + new HtmlWebpackPlugin({ + xhtml: true, + template: './src/index.html', + minify: { + collapseWhitespace: true, + removeComments: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + useShortDoctype: true, + }, + inlineSource: '.(js|css)$', + }), + new HtmlWebpackInlineSourcePlugin(), + new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: '[id].css', + }), + new CleanWebpackPlugin({ + protectWebpackAssets: false, + cleanAfterEveryBuildPatterns: ['*.js', '*.css'], + }), + ], + module: { + rules: [ + { + test: /\.ico$/, + use: { + loader: 'file-loader', + query: { + outputPath: '/', + name: '[name].[ext]', }, - }), - ], - }, - module: { - rules: [ - { - test: /\.js$/, - enforce: 'pre', - use: ['source-map-loader'], }, - { - test: /\.ts$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - { - test: /\.scss$/i, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - { - loader: 'sass-loader', - options: { - sourceMap: true, - implementation: Sass, - }, + }, + { + test: /\.scss$/, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + 'postcss-loader', + { + loader: 'resolve-url-loader', + options: { + keepQuery: true, }, - ], - }, - { - // SVGs referenced from CSS (`mask-image`/`background-image`) and JS - // must be inlined as real `data:` URIs. svg-url-loader emits a CJS - // module (`module.exports = "data:..."`) which webpack 5 + css-loader 7 - // write out verbatim as a `.svg` asset file, so the browser fetches JS - // instead of an image and the mask/background silently fails. webpack 5's - // built-in `asset/inline` produces a proper data URI instead. - // (HTML icons are unaffected: they are copied by CopyWebpackPlugin - // and referenced by literal `static/*.svg` paths, not through this rule.) - test: /\.svg$/, - type: 'asset/inline', - }, - { - // Use oneOf so each asset matches exactly one rule (og-image.png would - // otherwise match both the generic png rule and its own rule). - // The directory is encoded in `name` (not `outputPath`) and has no - // leading slash: with the default `publicPath: 'auto'` a leading slash - // produces a doubled slash in the emitted URL (e.g. `//static/x.mp3`). - oneOf: [ - { - test: /og-image\.png$/, - use: { - loader: 'file-loader', - options: { - name: '[name].[ext]', - }, - }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: true, + implementation: Sass, }, - { - test: /\.ico$/, - use: { - loader: 'file-loader', - options: { - name: '[name].[ext]', - }, - }, - }, - { - test: /\.(mp3|png)$/, - use: { - loader: 'file-loader', - options: { - name: 'static/[name].[ext]', - }, - }, - }, - ], + }, + ], + }, + { + test: /\.ts$/, + loader: 'esbuild-loader', + options: { + loader: 'ts', + target: 'es2015', }, - ], - }, - resolve: { - extensions: ['.ts', '.js', '.json'], - }, - }; -}; + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, +}); diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..e3d3386 --- /dev/null +++ b/lerna.json @@ -0,0 +1,8 @@ +{ + "packages": [ + "frontend", + "shared", + "backend" + ], + "version": "0.0.0" +} \ No newline at end of file diff --git a/media/collage-iphone.png b/media/collage-iphone.png deleted file mode 100644 index e2f9f33..0000000 Binary files a/media/collage-iphone.png and /dev/null differ diff --git a/media/game-pc.png b/media/game-pc.png deleted file mode 100644 index 4169164..0000000 Binary files a/media/game-pc.png and /dev/null differ diff --git a/package.json b/package.json index 3e123bd..d90168f 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,44 @@ { "name": "root", "private": true, - "engines": { - "node": ">=20" + "scripts": { + "build": "lerna run build-before && lerna run build", + "initialize": "lerna link && lerna run initialize", + "lint": "eslint './**/src/**/*.{js,ts,json}' --fix", + "start": "lerna run --parallel start", + "try-build": "lerna run --parallel try-build-before && lerna run --parallel try-build" + }, + "dependencies": { + "decla.red-frontend": "file:frontend", + "decla.red-server": "file:backend", + "shared": "file:shared" }, "devDependencies": { - "concurrently": "^10.0.3", - "eslint": "^10.4.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.6", - "eslint-plugin-unused-imports": "^4.4.1", - "prettier": "^3.8.3", - "typescript": "^6.0.3", - "typescript-eslint": "^8.60.1", - "vitest": "^4.1.9" - }, - "scripts": { - "build": "cd shared && npm run build && cd ../frontend && npm run build && cd ../backend && npm run build", - "lint": "eslint ./**/src/**/*.{js,ts} --fix && prettier --write ./**/src/**/*.{js,ts,json}", - "lint:check": "eslint ./**/src/**/*.{js,ts} && prettier --check ./**/src/**/*.{js,ts,json}", - "test": "vitest run", - "test:watch": "vitest", - "init": "cd shared && npm install && cd ../frontend && npm install && cd ../backend && npm install", - "dev": "concurrently --kill-others-on-fail \"cd shared && npm dev\" \"cd backend && npm dev\" \"cd frontend && npm dev\"" + "@types/cors": "^2.8.7", + "@types/express": "^4.17.8", + "@types/node": "^14.11.2", + "@types/socket.io": "^2.1.11", + "@types/socket.io-client": "^1.4.34", + "@types/uuid": "^8.0.0", + "@typescript-eslint/eslint-plugin": "^3.9.1", + "@typescript-eslint/parser": "^3.9.1", + "autoprefixer": "^9.8.5", + "css-loader": "^3.5.2", + "cssnano": "^4.1.10", + "eslint": "^7.2.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-import": "^2.21.2", + "eslint-plugin-json-format": "^2.0.1", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-unused-imports": "^0.1.3", + "file-loader": "^6.1.0", + "firebase": "^7.22.0", + "gl-matrix": "^3.3.0", + "lerna": "^3.22.1", + "prettier": "^2.0.5", + "sdf-2d": "^0.4.0", + "socket.io-client": "^2.3.1", + "typescript": "^4.0.3", + "uuid": "^8.2.0" } } diff --git a/shared/package.json b/shared/package.json index bf2597b..e1c9aa2 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,29 +1,24 @@ { "name": "shared", - "version": "0.0.0", + "private": true, "description": "Shared library between backend and frontend", "main": "lib/main.js", "types": "lib/src/main.d.ts", "files": [ "lib" ], - "engines": { - "node": ">=20" - }, "scripts": { - "build": "npx webpack --mode production", - "dev": "npx webpack --mode development --watch", + "build-before": "npx webpack --mode production", + "initialize": "npm install", + "start": "npx webpack --mode development --watch", "try-build-before": "npx webpack --mode production" }, "devDependencies": { - "clean-webpack-plugin": "^4.0.0", - "file-loader": "^6.2.0", - "gl-matrix": "^3.3.0", - "prettier": "^3.8.3", - "terser-webpack-plugin": "^5.6.1", - "ts-loader": "^9.6.0", - "typescript": "^6.0.3", - "webpack": "^5.107.2", - "webpack-cli": "^7.0.3" + "clean-webpack-plugin": "^3.0.0", + "terser-webpack-plugin": "^2.3.8", + "ts-loader": "^8.0.3", + "typescript": "^4.0.3", + "webpack": "^4.43.0", + "webpack-cli": "^3.3.11" } } diff --git a/shared/src/commands/broadcast-commands.ts b/shared/src/commands/broadcast-commands.ts new file mode 100644 index 0000000..a5da5b5 --- /dev/null +++ b/shared/src/commands/broadcast-commands.ts @@ -0,0 +1,7 @@ +import { CommandReceiver } from './command-receiver'; +import { CommandGenerator } from './command-generator'; + +export const broadcastCommands = ( + commandGenerators: Array, + commandReceivers: Array, +) => commandReceivers.forEach((r) => commandGenerators.forEach((g) => g.subscribe(r))); diff --git a/shared/src/commands/command-generator.ts b/shared/src/commands/command-generator.ts index 27db5f3..b4bf67d 100644 --- a/shared/src/commands/command-generator.ts +++ b/shared/src/commands/command-generator.ts @@ -1,18 +1,14 @@ import { CommandReceiver } from './command-receiver'; import { Command } from './command'; -export abstract class CommandGenerator { +export class CommandGenerator { private subscribers: Array = []; public subscribe(subscriber: CommandReceiver): void { this.subscribers.push(subscriber); } - public clearSubscribers(): void { - this.subscribers = []; - } - - protected sendCommandToSubscribers(command: Command): void { - this.subscribers.forEach((s) => s.handleCommand(command)); + protected sendCommandToSubcribers(command: Command): void { + this.subscribers.forEach((s) => s.sendCommand(command)); } } diff --git a/shared/src/commands/command-receiver.ts b/shared/src/commands/command-receiver.ts index 0eff7c4..6495573 100644 --- a/shared/src/commands/command-receiver.ts +++ b/shared/src/commands/command-receiver.ts @@ -4,13 +4,17 @@ import { Command } from './command'; export abstract class CommandReceiver { protected commandExecutors: CommandExecutors = {}; + public reactsToCommand(commandType: string): boolean { + return Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType); + } + protected defaultCommandExecutor(_: Command) {} - public handleCommand(command: Command) { + public sendCommand(command: Command) { const commandType = command.type; if (Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType)) { - this.commandExecutors[commandType]!(command); + this.commandExecutors[commandType](command); } else { this.defaultCommandExecutor(command); } diff --git a/shared/src/commands/types/actions/leap-action.ts b/shared/src/commands/types/actions/leap-action.ts deleted file mode 100644 index 0115e73..0000000 --- a/shared/src/commands/types/actions/leap-action.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { serializable } from '../../../serialization/serializable'; -import { Command } from '../../command'; - -// Sent client -> server when the player triggers a leap (Space / leap button). -// The launch direction is derived server-side from the character's surface -// normal and current movement input. clientTimeMs is the client's wall-clock at -// the press, echoed back via InputAcknowledgement so the predictor knows which -// leaps the server has already folded into the streamed launch momentum and -// must not replay again. -@serializable -export class LeapActionCommand extends Command { - public constructor(public readonly clientTimeMs: number = 0) { - super(); - } - - public toArray(): Array { - return [this.clientTimeMs]; - } -} diff --git a/shared/src/commands/types/actions/move-action.ts b/shared/src/commands/types/actions/move-action.ts deleted file mode 100644 index 117d3e3..0000000 --- a/shared/src/commands/types/actions/move-action.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { serializable } from '../../../serialization/serializable'; -import { Command } from '../../command'; - -@serializable -export class MoveActionCommand extends Command { - // clientTimeMs is the client's wall-clock (integer ms, survives the - // serializer's toFixed(3)) when the input was generated. The server echoes - // the latest one back via InputAcknowledgement so the client knows how much - // of its input timeline is already baked into a snapshot and can replay the - // rest for prediction. Defaults to 0 for inputs the server itself synthesises. - public constructor( - public readonly direction: vec2, - public readonly clientTimeMs: number = 0, - ) { - super(); - } - - public toArray(): Array { - return [this.direction, this.clientTimeMs]; - } -} diff --git a/shared/src/commands/types/actions/primary-action.ts b/shared/src/commands/types/actions/primary-action.ts deleted file mode 100644 index 54b92c9..0000000 --- a/shared/src/commands/types/actions/primary-action.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { serializable } from '../../../serialization/serializable'; -import { Command } from '../../command'; - -@serializable -export class PrimaryActionCommand extends Command { - public constructor( - public readonly position: vec2, - public readonly charge: number = 0, - ) { - super(); - } - - public toArray(): Array { - return [this.position, this.charge]; - } -} diff --git a/shared/src/commands/types/create-objects.ts b/shared/src/commands/types/create-objects.ts index 108dc06..2579d8c 100644 --- a/shared/src/commands/types/create-objects.ts +++ b/shared/src/commands/types/create-objects.ts @@ -1,5 +1,5 @@ import { GameObject } from '../../objects/game-object'; -import { serializable } from '../../serialization/serializable'; +import { serializable } from '../../transport/serialization/serializable'; import { Command } from '../command'; @serializable diff --git a/shared/src/commands/types/create-player.ts b/shared/src/commands/types/create-player.ts index 35ff556..f811412 100644 --- a/shared/src/commands/types/create-player.ts +++ b/shared/src/commands/types/create-player.ts @@ -1,5 +1,5 @@ import { CharacterBase } from '../../objects/types/character-base'; -import { serializable } from '../../serialization/serializable'; +import { serializable } from '../../transport/serialization/serializable'; import { Command } from '../command'; @serializable diff --git a/shared/src/commands/types/delete-objects.ts b/shared/src/commands/types/delete-objects.ts index 6bf9ecf..8a672e3 100644 --- a/shared/src/commands/types/delete-objects.ts +++ b/shared/src/commands/types/delete-objects.ts @@ -1,5 +1,5 @@ -import { Id } from '../../communication/id'; -import { serializable } from '../../serialization/serializable'; +import { Id } from '../../transport/identity'; +import { serializable } from '../../transport/serialization/serializable'; import { Command } from '../command'; @serializable diff --git a/shared/src/commands/types/game-end.ts b/shared/src/commands/types/game-end.ts deleted file mode 100644 index 32bb84c..0000000 --- a/shared/src/commands/types/game-end.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CharacterTeam } from '../../objects/types/character-base'; -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class GameEndCommand extends Command { - constructor( - public readonly winningTeam: CharacterTeam, - public readonly endCardLengthInSeconds: number, - public readonly shouldReconnect: boolean, - ) { - super(); - } - - public toArray(): Array { - return [this.winningTeam, this.endCardLengthInSeconds, this.shouldReconnect]; - } -} diff --git a/shared/src/commands/types/game-start.ts b/shared/src/commands/types/game-start.ts deleted file mode 100644 index 15f14c8..0000000 --- a/shared/src/commands/types/game-start.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class GameStartCommand extends Command { - constructor() { - super(); - } - - public toArray(): Array { - return []; - } -} diff --git a/shared/src/commands/types/input-acknowledgement.ts b/shared/src/commands/types/input-acknowledgement.ts deleted file mode 100644 index 3f488e7..0000000 --- a/shared/src/commands/types/input-acknowledgement.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -// Sent server -> owning client only, alongside that client's own character -// snapshot. Carries the clientTimeMs of the most recent movement input the -// server had received when the snapshot was taken (so the client's predictor -// can reset to the snapshot and replay just the inputs the server hasn't seen -// yet) and the authoritative launch momentum (so the predictor reproduces a -// leap/slingshot/recoil flight rather than only snapping to it). See -// LocalCharacterPredictor. -@serializable -export class InputAcknowledgement extends Command { - public constructor( - public readonly clientTimeMs: number, - public readonly bodyVelocity: vec2, - // clientTimeMs of the most recent leap the server has received: any leap at - // or before it is already reflected in bodyVelocity, so the predictor must - // not replay it. - public readonly lastLeapClientTimeMs: number, - ) { - super(); - } - - public toArray(): Array { - return [this.clientTimeMs, this.bodyVelocity, this.lastLeapClientTimeMs]; - } -} diff --git a/shared/src/commands/types/move-action.ts b/shared/src/commands/types/move-action.ts new file mode 100644 index 0000000..3e49e4b --- /dev/null +++ b/shared/src/commands/types/move-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class MoveActionCommand extends Command { + public constructor(public readonly direction: vec2) { + super(); + } + + public toArray(): Array { + return [this.direction]; + } +} diff --git a/shared/src/commands/types/primary-action.ts b/shared/src/commands/types/primary-action.ts new file mode 100644 index 0000000..fdc49f4 --- /dev/null +++ b/shared/src/commands/types/primary-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class PrimaryActionCommand extends Command { + public constructor(public readonly position: vec2) { + super(); + } + + public toArray(): Array { + return [this.position]; + } +} diff --git a/shared/src/commands/types/property-updates-for-objects.ts b/shared/src/commands/types/property-updates-for-objects.ts deleted file mode 100644 index bef2d6b..0000000 --- a/shared/src/commands/types/property-updates-for-objects.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PropertyUpdatesForObject } from '../../objects/game-object'; -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class PropertyUpdatesForObjects extends Command { - constructor( - public readonly updates: Array, - // Seconds on the server's monotonic clock at the moment this state was - // captured. Only differences between timestamps are meaningful. - public readonly timestamp: number, - ) { - super(); - } - - public toArray(): Array { - return [this.updates, this.timestamp]; - } -} diff --git a/shared/src/commands/types/remote-calls-for-objects.ts b/shared/src/commands/types/remote-calls-for-objects.ts deleted file mode 100644 index 3a99962..0000000 --- a/shared/src/commands/types/remote-calls-for-objects.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Id, RemoteCall } from '../../main'; -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class RemoteCallsForObject { - constructor( - public readonly id: Id, - public readonly calls: Array, - ) {} - - public toArray(): Array { - return [this.id, this.calls]; - } -} - -@serializable -export class RemoteCallsForObjects extends Command { - constructor(public readonly callsForObjects: Array) { - super(); - } - - public toArray(): Array { - return [this.callsForObjects]; - } -} diff --git a/shared/src/commands/types/secondary-action.ts b/shared/src/commands/types/secondary-action.ts new file mode 100644 index 0000000..0edb39e --- /dev/null +++ b/shared/src/commands/types/secondary-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class SecondaryActionCommand extends Command { + public constructor(public readonly position: vec2) { + super(); + } + + public toArray(): Array { + return [this.position]; + } +} diff --git a/shared/src/commands/types/server-announcement.ts b/shared/src/commands/types/server-announcement.ts deleted file mode 100644 index 49d2be0..0000000 --- a/shared/src/commands/types/server-announcement.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class ServerAnnouncement extends Command { - constructor(public readonly text: string) { - super(); - } - - public toArray(): Array { - return [this.text]; - } -} diff --git a/shared/src/commands/types/actions/set-aspect-ratio-action.ts b/shared/src/commands/types/set-aspect-ratio-action.ts similarity index 66% rename from shared/src/commands/types/actions/set-aspect-ratio-action.ts rename to shared/src/commands/types/set-aspect-ratio-action.ts index f2b0d5c..0806165 100644 --- a/shared/src/commands/types/actions/set-aspect-ratio-action.ts +++ b/shared/src/commands/types/set-aspect-ratio-action.ts @@ -1,5 +1,5 @@ -import { serializable } from '../../../serialization/serializable'; -import { Command } from '../../command'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; @serializable export class SetAspectRatioActionCommand extends Command { diff --git a/shared/src/commands/types/step.ts b/shared/src/commands/types/step.ts new file mode 100644 index 0000000..fb0181c --- /dev/null +++ b/shared/src/commands/types/step.ts @@ -0,0 +1,7 @@ +import { Command } from '../command'; + +export class StepCommand extends Command { + public constructor(public readonly deltaTimeInMiliseconds: number) { + super(); + } +} diff --git a/shared/src/commands/types/ternary-action.ts b/shared/src/commands/types/ternary-action.ts new file mode 100644 index 0000000..547396f --- /dev/null +++ b/shared/src/commands/types/ternary-action.ts @@ -0,0 +1,14 @@ +import { vec2 } from 'gl-matrix'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class TernaryActionCommand extends Command { + public constructor(public readonly position: vec2) { + super(); + } + + public toArray(): Array { + return [this.position]; + } +} diff --git a/shared/src/commands/types/update-game-state.ts b/shared/src/commands/types/update-game-state.ts deleted file mode 100644 index 7e66c19..0000000 --- a/shared/src/commands/types/update-game-state.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class UpdateGameState extends Command { - public constructor( - public readonly blueCount: number, - public readonly redCount: number, - public readonly limit: number, - ) { - super(); - } - - public toArray(): Array { - return [this.blueCount, this.redCount, this.limit]; - } -} diff --git a/shared/src/commands/types/update-minimap.ts b/shared/src/commands/types/update-minimap.ts deleted file mode 100644 index a24e371..0000000 --- a/shared/src/commands/types/update-minimap.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Id } from '../../communication/id'; -import { CharacterTeam } from '../../objects/types/character-base'; -import { serializable } from '../../serialization/serializable'; -import { Command } from '../command'; - -@serializable -export class MinimapPlayer { - public constructor( - public readonly id: Id, - public readonly position: vec2, - public readonly team: CharacterTeam, - ) {} - - public toArray(): Array { - return [this.id, this.position, this.team]; - } -} - -@serializable -export class UpdateMinimap extends Command { - public constructor(public readonly players: Array) { - super(); - } - - public toArray(): Array { - return [this.players]; - } -} diff --git a/shared/src/commands/types/update-objects.ts b/shared/src/commands/types/update-objects.ts new file mode 100644 index 0000000..8469483 --- /dev/null +++ b/shared/src/commands/types/update-objects.ts @@ -0,0 +1,14 @@ +import { GameObject } from '../../objects/game-object'; +import { serializable } from '../../transport/serialization/serializable'; +import { Command } from '../command'; + +@serializable +export class UpdateObjectsCommand extends Command { + public constructor(public readonly objects: Array) { + super(); + } + + public toArray(): Array { + return [this.objects]; + } +} diff --git a/shared/src/communication/player-information.ts b/shared/src/communication/player-information.ts deleted file mode 100644 index 1130b7b..0000000 --- a/shared/src/communication/player-information.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface PlayerInformation { - name: string; -} diff --git a/shared/src/communication/server-information.ts b/shared/src/communication/server-information.ts deleted file mode 100644 index 425ef2a..0000000 --- a/shared/src/communication/server-information.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ServerInformation { - playerLimit: number; - playerCount: number; - serverName: string; - gameStatePercent: number; -} - -export const serverInformationEndpoint = '/state'; diff --git a/shared/src/helper/array.ts b/shared/src/helper/array.ts index 8c86ffa..5a0ff29 100644 --- a/shared/src/helper/array.ts +++ b/shared/src/helper/array.ts @@ -1,7 +1,4 @@ declare global { - // `T` must stay to merge with the global `Array` (otherwise TS2428), but it - // is unused by the x/y aliases below. - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Array { x: number; y: number; diff --git a/shared/src/helper/calculate-view-area.ts b/shared/src/helper/calculate-view-area.ts index 00ebd50..8148975 100644 --- a/shared/src/helper/calculate-view-area.ts +++ b/shared/src/helper/calculate-view-area.ts @@ -14,8 +14,8 @@ export const calculateViewArea = ( ); viewArea.topLeft = vec2.fromValues( - center[0] - viewArea.size[0] / 2, - center[1] + viewArea.size[1] / 2, + center.x - viewArea.size.x / 2, + center.y + viewArea.size.y / 2, ); return viewArea; diff --git a/shared/src/helper/charge.ts b/shared/src/helper/charge.ts deleted file mode 100644 index 1ab6ece..0000000 --- a/shared/src/helper/charge.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { clamp01 } from './clamp'; -import { settings } from '../settings'; - -export const holdDurationToCharge = (heldSeconds: number): number => - clamp01(heldSeconds / settings.chargeShotFullHoldSeconds); diff --git a/shared/src/helper/circle.ts b/shared/src/helper/circle.ts index 854adbb..1fb223b 100644 --- a/shared/src/helper/circle.ts +++ b/shared/src/helper/circle.ts @@ -1,17 +1,18 @@ import { vec2 } from 'gl-matrix'; -import { serializable } from '../serialization/serializable'; +import { serializable } from '../transport/serialization/serializable'; @serializable export class Circle { - constructor( - public center: vec2, - public radius: number, - ) {} + constructor(public center: vec2, public radius: number) {} public distance(target: vec2): number { return vec2.distance(this.center, target) - this.radius; } + public distanceBetween(target: Circle): number { + return vec2.distance(target.center, this.center) - this.radius - target.radius; + } + public toArray(): Array { return [this.center, this.radius]; } diff --git a/shared/src/helper/hsl.ts b/shared/src/helper/hsl.ts deleted file mode 100644 index 5b4aaf0..0000000 --- a/shared/src/helper/hsl.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { vec3 } from 'gl-matrix'; -import { rgb } from './rgb'; - -// source: https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion -export const hsl = (hue: number, saturation: number, lightness: number): vec3 => { - hue /= 360; - saturation /= 100; - lightness /= 100; - let r, g, b; - - if (saturation == 0) { - r = g = b = lightness; - } else { - const hue2rgb = (p: number, q: number, t: number) => { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - }; - - const q = - lightness < 0.5 - ? lightness * (1 + saturation) - : lightness + saturation - lightness * saturation; - const p = 2 * lightness - q; - - r = hue2rgb(p, q, hue + 1 / 3); - g = hue2rgb(p, q, hue); - b = hue2rgb(p, q, hue - 1 / 3); - } - - return rgb(r, g, b); -}; diff --git a/shared/src/helper/last.ts b/shared/src/helper/last.ts index 39a5c1f..5c319b0 100644 --- a/shared/src/helper/last.ts +++ b/shared/src/helper/last.ts @@ -1,3 +1,3 @@ -export function last(a: ArrayLike): T | null { +export function last(a: Array): T | null { return a.length > 0 ? a[a.length - 1] : null; } diff --git a/shared/src/helper/mix-rgb.ts b/shared/src/helper/mix-rgb.ts deleted file mode 100644 index 17195af..0000000 --- a/shared/src/helper/mix-rgb.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec3 } from 'gl-matrix'; -import { clamp01 } from './clamp'; -import { mix } from './mix'; - -export const mixRgb = (a: vec3, b: vec3, q: number): vec3 => { - const clampedQ = clamp01(q); - return vec3.fromValues( - mix(a[0], b[0], clampedQ), - mix(a[1], b[1], clampedQ), - mix(a[2], b[2], clampedQ), - ); -}; diff --git a/shared/src/helper/pretty-print.ts b/shared/src/helper/pretty-print.ts new file mode 100644 index 0000000..704a6fc --- /dev/null +++ b/shared/src/helper/pretty-print.ts @@ -0,0 +1,4 @@ +export const prettyPrint = (o: any): string => + JSON.stringify(o, (_, v) => (v.toFixed ? Number(v.toFixed(3)) : v), ' ') + .replace(/("|,|{|^\n)/g, '') + .replace(/(\W*}\n?)+/g, '\n\n'); diff --git a/shared/src/helper/random.ts b/shared/src/helper/random.ts index d7a0de3..4f800bc 100644 --- a/shared/src/helper/random.ts +++ b/shared/src/helper/random.ts @@ -1,4 +1,4 @@ -// source +// src // https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript // Mulberry32 diff --git a/shared/src/helper/rectangle.ts b/shared/src/helper/rectangle.ts index f1aa8f3..25ea3cb 100644 --- a/shared/src/helper/rectangle.ts +++ b/shared/src/helper/rectangle.ts @@ -1,12 +1,9 @@ import { vec2 } from 'gl-matrix'; -import { serializable } from '../serialization/serializable'; +import { serializable } from '../transport/serialization/serializable'; @serializable export class Rectangle { - constructor( - public topLeft = vec2.create(), - public size = vec2.create(), - ) {} + constructor(public topLeft = vec2.create(), public size = vec2.create()) {} public toArray(): Array { return [this.topLeft, this.size]; diff --git a/shared/src/helper/rotate-90-deg.ts b/shared/src/helper/rotate-90-deg.ts index 2196fa8..1911a63 100644 --- a/shared/src/helper/rotate-90-deg.ts +++ b/shared/src/helper/rotate-90-deg.ts @@ -1,3 +1,3 @@ import { vec2 } from 'gl-matrix'; -export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec[1], vec[0]); +export const rotate90Deg = (vec: vec2): vec2 => vec2.fromValues(-vec.y, vec.x); diff --git a/shared/src/helper/rotate-minus-90-deg.ts b/shared/src/helper/rotate-minus-90-deg.ts deleted file mode 100644 index 3f4773f..0000000 --- a/shared/src/helper/rotate-minus-90-deg.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -export const rotateMinus90Deg = (vec: vec2): vec2 => vec2.fromValues(vec[1], -vec[0]); diff --git a/shared/src/helper/to-percent.ts b/shared/src/helper/to-percent.ts new file mode 100644 index 0000000..0047591 --- /dev/null +++ b/shared/src/helper/to-percent.ts @@ -0,0 +1 @@ +export const toPercent = (value: number) => `${Math.round(value * 100)}%`; diff --git a/shared/src/communication/id.ts b/shared/src/helper/unique.ts similarity index 70% rename from shared/src/communication/id.ts rename to shared/src/helper/unique.ts index c6c1e4d..fdd153e 100644 --- a/shared/src/communication/id.ts +++ b/shared/src/helper/unique.ts @@ -1,5 +1,3 @@ -export type Id = number | null; - let currentId = 0; export const id = (): number => { diff --git a/shared/src/main.ts b/shared/src/main.ts index 7adfcf6..3c47bcf 100644 --- a/shared/src/main.ts +++ b/shared/src/main.ts @@ -2,57 +2,38 @@ export * from './commands/command'; export * from './commands/types/create-objects'; export * from './commands/types/create-player'; export * from './commands/types/delete-objects'; -export * from './commands/types/remote-calls-for-objects'; -export * from './commands/types/server-announcement'; -export * from './commands/types/property-updates-for-objects'; -export * from './commands/types/game-end'; -export * from './commands/types/game-start'; -export * from './commands/types/update-minimap'; -export * from './commands/types/update-game-state'; +export * from './commands/types/update-objects'; +export * from './commands/types/step'; +export * from './commands/types/ternary-action'; +export * from './commands/types/move-action'; +export * from './commands/types/primary-action'; +export * from './commands/types/secondary-action'; export * from './commands/command-receiver'; export * from './commands/command-executors'; export * from './commands/command-generator'; -export * from './commands/types/actions/move-action'; -export * from './commands/types/actions/primary-action'; -export * from './commands/types/actions/leap-action'; -export * from './commands/types/actions/set-aspect-ratio-action'; +export * from './commands/broadcast-commands'; +export * from './commands/types/set-aspect-ratio-action'; export * from './helper/array'; export * from './helper/last'; -export * from './helper/rgb'; -export * from './helper/hsl'; -export * from './helper/rgb255'; -export * from './helper/mix-rgb'; export * from './helper/clamp'; -export * from './helper/charge'; export * from './helper/calculate-view-area'; +export * from './helper/pretty-print'; export * from './helper/circle'; export * from './helper/rectangle'; export * from './helper/mix'; export * from './helper/random'; -export * from './communication/id'; -export * from './communication/server-information'; -export * from './communication/player-information'; +export * from './helper/unique'; export * from './helper/rotate-90-deg'; -export * from './helper/rotate-minus-90-deg'; export * from './objects/game-object'; -export * from './serialization/deserialize'; -export * from './serialization/serialize'; -export * from './serialization/serializes-to'; -export * from './serialization/serializable'; -export * from './serialization/override-deserialization'; +export * from './transport/serialization/deserialize'; +export * from './transport/serialization/serialize'; +export * from './transport/serialization/serializes-to'; +export * from './transport/serialization/serializable'; +export * from './transport/serialization/override-deserialization'; export * from './objects/types/character-base'; export * from './objects/types/lamp-base'; -export * from './objects/types/planet-base'; export * from './objects/types/projectile-base'; +export * from './objects/types/tunnel-base'; export * from './settings'; -export * from './communication/transport-events'; -export * from './commands/types/input-acknowledgement'; -export * from './physics/sdf'; -export * from './physics/evaluate-sdf'; -export * from './physics/sdf-normal'; -export * from './physics/march-circle'; -export * from './physics/depenetrate-circle'; -export * from './physics/resolve-circle-movement'; -export * from './physics/planet-sdf'; -export * from './physics/interpolate-angles'; -export * from './physics/character-movement'; +export * from './transport/transport-events'; +export * from './transport/identity'; diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts index f851137..6192edd 100644 --- a/shared/src/objects/game-object.ts +++ b/shared/src/objects/game-object.ts @@ -1,111 +1,8 @@ -import { Command } from '../commands/command'; import { CommandReceiver } from '../commands/command-receiver'; -import { Id } from '../communication/id'; -import { serializable } from '../serialization/serializable'; - -@serializable -export class RemoteCall { - constructor( - public readonly functionName: string, - public readonly args: Array, - ) {} - - public toArray(): Array { - return [this.functionName, this.args]; - } -} - -// Every object property streamed via UpdatePropertyCommand. A single union means -// a typo or rename on the producing (server *-physical) or consuming (client -// *-view) side is a compile error instead of a silently dropped update that -// just stops a body interpolating. The wire format is unchanged — these remain -// the same strings, only now compiler-checked at both ends. -export type SyncPropertyKey = - | 'head' - | 'leftFoot' - | 'rightFoot' - | 'strength' - | 'center' - | 'ownership' - | 'rotation'; - -@serializable -export class UpdatePropertyCommand extends Command { - constructor( - public readonly propertyKey: SyncPropertyKey, - public readonly propertyValue: any, - public readonly rateOfChange: any, - ) { - super(); - } - - public toArray(): Array { - return [this.propertyKey, this.propertyValue, this.rateOfChange]; - } -} - -@serializable -export class PropertyUpdatesForObject { - constructor( - public readonly id: Id, - public readonly updates: Array, - ) {} - - public toArray(): Array { - return [this.id, this.updates]; - } -} +import { Id } from '../transport/identity'; export abstract class GameObject extends CommandReceiver { - private remoteCalls: Array = []; - - // The only methods a peer may invoke over the wire. processRemoteCalls - // dispatches by a raw string taken straight off the network, so without this - // gate a malformed or hostile packet could call ANY method on the object - // (toArray, resetRemoteCalls, even prototype methods). Keep in sync with the - // remoteCall() emitters on the *-physical classes. - private static readonly allowedRemoteCalls: ReadonlySet = new Set([ - 'onShoot', - 'onLeap', - 'onDie', - 'onHitConfirmed', - 'onKillConfirmed', - 'setHealth', - 'setKillCount', - 'onFlipped', - 'setContested', - 'generatedPoints', - 'setLight', - ]); - constructor(public readonly id: Id) { super(); } - - public processRemoteCalls(remoteCalls: Array) { - remoteCalls.forEach((r) => { - if (!GameObject.allowedRemoteCalls.has(r.functionName)) { - console.warn(`Dropped disallowed remote call: ${r.functionName}`); - return; - } - const fn = this[r.functionName as keyof this]; - if (typeof fn === 'function') { - (fn as (...args: Array) => unknown).apply(this, r.args); - } - }); - } - - public getPropertyUpdates(): PropertyUpdatesForObject | void {} - - public getRemoteCalls(): Array { - return this.remoteCalls; - } - - public resetRemoteCalls() { - this.remoteCalls = []; - } - - protected remoteCall(name: string & keyof this, ...args: Array) { - this.remoteCalls.push(new RemoteCall(name, args)); - } } diff --git a/shared/src/objects/types/character-base.ts b/shared/src/objects/types/character-base.ts index f6cad5c..8475c63 100644 --- a/shared/src/objects/types/character-base.ts +++ b/shared/src/objects/types/character-base.ts @@ -1,36 +1,12 @@ -import { Id } from '../../communication/id'; import { Circle } from '../../helper/circle'; -import { serializable } from '../../serialization/serializable'; -import { toArrayFromFields } from '../../serialization/serialized-fields'; +import { Id } from '../../transport/identity'; +import { serializable } from '../../transport/serialization/serializable'; import { GameObject } from '../game-object'; -export enum CharacterTeam { - blue = 'blue', - neutral = 'neutral', - red = 'red', -} - @serializable export class CharacterBase extends GameObject { - private static readonly serializedFields = [ - 'id', - 'name', - 'killCount', - 'deathCount', - 'team', - 'health', - 'head', - 'leftFoot', - 'rightFoot', - ] as const; - constructor( id: Id, - public name: string, - public killCount: number, - public deathCount: number, - public team: CharacterTeam, - public health: number, public head?: Circle, public leftFoot?: Circle, public rightFoot?: Circle, @@ -38,28 +14,8 @@ export class CharacterBase extends GameObject { super(id); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public onShoot(strength: number) {} - - public onLeap() {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public onHitConfirmed(charge?: number) {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public onKillConfirmed(victimName?: string, streak?: number, charge?: number) {} - - public setHealth(health: number) { - this.health = health; - } - - public onDie() {} - - public setKillCount(killCount: number) { - this.killCount = killCount; - } - public toArray(): Array { - return toArrayFromFields(this, CharacterBase.serializedFields); + const { id, head, leftFoot, rightFoot } = this; + return [id, head, leftFoot, rightFoot]; } } diff --git a/shared/src/objects/types/lamp-base.ts b/shared/src/objects/types/lamp-base.ts index 9f86ca6..3977fce 100644 --- a/shared/src/objects/types/lamp-base.ts +++ b/shared/src/objects/types/lamp-base.ts @@ -1,32 +1,15 @@ import { vec2, vec3 } from 'gl-matrix'; import { Id, serializable } from '../../main'; -import { toArrayFromFields } from '../../serialization/serialized-fields'; import { GameObject } from '../game-object'; @serializable export class LampBase extends GameObject { - private static readonly serializedFields = [ - 'id', - 'center', - 'color', - 'lightness', - ] as const; - - constructor( - id: Id, - public center: vec2, - public color: vec3, - public lightness: number, - ) { + constructor(id: Id, public center: vec2, public color: vec3, public lightness: number) { super(id); } - // Overridden by LampView (which lerps toward the new colour/brightness); a - // no-op on the wire base. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public setLight(color: vec3, lightness: number) {} - public toArray(): Array { - return toArrayFromFields(this, LampBase.serializedFields); + const { id, center, color, lightness } = this; + return [id, center, color, lightness]; } } diff --git a/shared/src/objects/types/planet-base.ts b/shared/src/objects/types/planet-base.ts deleted file mode 100644 index 2f90eb1..0000000 --- a/shared/src/objects/types/planet-base.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Random } from '../../helper/random'; -import { settings } from '../../settings'; -import { serializable } from '../../serialization/serializable'; -import { toArrayFromFields } from '../../serialization/serialized-fields'; -import { GameObject } from '../game-object'; -import { Id } from '../../communication/id'; -import { CharacterTeam } from './character-base'; - -@serializable -export class PlanetBase extends GameObject { - // centre/radius are derived from vertices in the constructor, so they are not - // serialized — only the constructor parameters are. - private static readonly serializedFields = [ - 'id', - 'vertices', - 'ownership', - 'isKeystone', - ] as const; - - public readonly center: vec2; - public readonly radius: number; - - constructor( - id: Id, - public readonly vertices: Array, - public ownership: number = 0.5, - public readonly isKeystone: boolean = false, - ) { - super(id); - this.center = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create()); - vec2.scale(this.center, this.center, 1 / vertices.length); - this.radius = - vertices.reduce((sum, v) => sum + vec2.distance(this.center, v), 0) / - this.vertices.length; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public generatedPoints(value: number) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public onFlipped(team: CharacterTeam) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public setContested(contested: boolean) {} - - public static createPlanetVertices( - center: vec2, - width: number, - height: number, - randomness: number, - vertexCount = settings.planetEdgeCount, - ): Array { - const vertices = []; - - for (let i = 0; i < vertexCount; i++) { - vertices.push( - vec2.fromValues( - center[0] + - (width / 2) * Math.cos((i / vertexCount) * -Math.PI * 2) + - Random.getRandomInRange(-randomness, randomness), - center[1] + - (height / 2) * Math.sin((i / vertexCount) * -Math.PI * 2) + - Random.getRandomInRange(-randomness, randomness), - ), - ); - } - return vertices; - } - - public toArray(): Array { - return toArrayFromFields(this, PlanetBase.serializedFields); - } -} diff --git a/shared/src/objects/types/projectile-base.ts b/shared/src/objects/types/projectile-base.ts index f96fd94..90ef4e2 100644 --- a/shared/src/objects/types/projectile-base.ts +++ b/shared/src/objects/types/projectile-base.ts @@ -1,37 +1,15 @@ import { vec2 } from 'gl-matrix'; -import { settings } from '../../settings'; -import { serializable } from '../../serialization/serializable'; -import { toArrayFromFields } from '../../serialization/serialized-fields'; +import { Id } from '../../transport/identity'; +import { serializable } from '../../transport/serialization/serializable'; import { GameObject } from '../game-object'; -import { Id } from '../../communication/id'; -import { CharacterTeam } from './character-base'; @serializable export class ProjectileBase extends GameObject { - private static readonly serializedFields = [ - 'id', - 'center', - 'radius', - 'team', - 'strength', - ] as const; - - constructor( - id: Id, - public center: vec2, - public radius: number, - public team: CharacterTeam, - public strength: number, - ) { + constructor(id: Id, public center: vec2, public radius: number) { super(id); } - public step(deltaTimeInSeconds: number) { - this.strength -= settings.projectileFadeSpeed * deltaTimeInSeconds; - this.strength = Math.max(0, this.strength); - } - public toArray(): Array { - return toArrayFromFields(this, ProjectileBase.serializedFields); + return [this.id, this.center, this.radius]; } } diff --git a/shared/src/objects/types/tunnel-base.ts b/shared/src/objects/types/tunnel-base.ts new file mode 100644 index 0000000..054a02c --- /dev/null +++ b/shared/src/objects/types/tunnel-base.ts @@ -0,0 +1,22 @@ +import { vec2 } from 'gl-matrix'; +import { Id } from '../../transport/identity'; +import { serializable } from '../../transport/serialization/serializable'; +import { GameObject } from '../game-object'; + +@serializable +export class TunnelBase extends GameObject { + constructor( + id: Id, + public readonly from: vec2, + public readonly to: vec2, + public readonly fromRadius: number, + public readonly toRadius: number, + ) { + super(id); + } + + public toArray(): Array { + const { id, from, to, fromRadius, toRadius } = this; + return [id, from, to, fromRadius, toRadius]; + } +} diff --git a/shared/src/physics/character-movement.ts b/shared/src/physics/character-movement.ts deleted file mode 100644 index 21355a5..0000000 --- a/shared/src/physics/character-movement.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { settings } from '../settings'; -import { GroundSurface, PhysicsBody } from './sdf'; -import { interpolateAngles } from './interpolate-angles'; - -// Body layout, copied verbatim from CharacterPhysical so the predicted body -// matches the authoritative one to the bit. The head sits this far above the -// feet; offsets are measured from the centre of mass. -export const headRadius = 50; -export const feetRadius = 20; - -const desiredHeadOffset = vec2.fromValues(0, 55); -const desiredLeftFootOffset = vec2.fromValues(-20, 0); -const desiredRightFootOffset = vec2.fromValues(20, 0); -const centerOfMass = vec2.scale( - vec2.create(), - vec2.add( - vec2.create(), - vec2.add(vec2.create(), desiredHeadOffset, desiredLeftFootOffset), - desiredRightFootOffset, - ), - 1 / 3, -); -export const headOffset = vec2.subtract(vec2.create(), desiredHeadOffset, centerOfMass); -export const leftFootOffset = vec2.subtract( - vec2.create(), - desiredLeftFootOffset, - centerOfMass, -); -export const rightFootOffset = vec2.subtract( - vec2.create(), - desiredRightFootOffset, - centerOfMass, -); -export const boundRadius = (headRadius + feetRadius * 2) * 2; - -// The character's movement state: the three body parts plus the small amount of -// carried state the step reads and writes. Backend CharacterPhysical and the -// client predictor both expose this shape. -export interface CharacterMovementState { - readonly head: PhysicsBody; - readonly leftFoot: PhysicsBody; - readonly rightFoot: PhysicsBody; - direction: number; - currentPlanet: GroundSurface | undefined; - secondsSinceOnSurface: number; - // Persistent launch momentum (leap / slingshot / recoil / death throw). - // Walking rebuilds and zeroes each body part's velocity every tick, - // so anything that should carry accumulates here, is injected into the parts - // before they step, and decays by friction. Stays zero for ordinary walking. - // The client predictor leaves this zero and relies on the reconciliation snap - // to follow server-side impulses, but the field is here so the server can - // delegate its full movement to stepCharacterMovement unchanged. - bodyVelocity: vec2; -} - -// The collision/gravity world the movement queries. The server backs this with -// its spatial container (planets + dynamics, dispatching collision reactions); -// the client backs it with the planets it knows about, dispatching nothing. -export interface CharacterWorld { - // Planets within `radius` of `center` that exert gravity / can be stood on. - groundsNear(center: vec2, radius: number): Array; - // Resolve one body part's motion this tick; returns the ground it landed on. - stepBody(body: PhysicsBody, deltaTimeInSeconds: number): GroundSurface | undefined; -} - -const applyForce = (body: PhysicsBody, force: vec2, deltaTimeInSeconds: number) => { - vec2.add( - body.velocity, - body.velocity, - vec2.scale(vec2.create(), force, deltaTimeInSeconds), - ); -}; - -// ((head + leftFoot) + rightFoot) / 3, in the exact association the server uses -// everywhere it reads the character centre — do not reassociate. -export const characterCenter = (state: CharacterMovementState): vec2 => { - const center = vec2.add(vec2.create(), state.head.center, state.leftFoot.center); - vec2.add(center, center, state.rightFoot.center); - return vec2.scale(center, center, 1 / 3); -}; - -const setDirection = (state: CharacterMovementState, direction: vec2) => { - state.direction = interpolateAngles( - state.direction, - Math.atan2(direction[1], direction[0]) + Math.PI / 2, - 0.2, - ); -}; - -const springMove = ( - state: CharacterMovementState, - body: PhysicsBody, - center: vec2, - offset: vec2, - stiffness: number, -) => { - const desiredPosition = vec2.add(vec2.create(), center, offset); - vec2.rotate(desiredPosition, desiredPosition, center, state.direction); - const positionDelta = vec2.subtract(vec2.create(), desiredPosition, body.center); - // First-order velocity relaxation toward the desired posture position, added - // to the gravity/movement velocity already accumulated this tick. The dt - // arrives later when the body integrates velocity, so the per-tick - // displacement is positionDelta * stiffness * dt. - vec2.scaleAndAdd(body.velocity, body.velocity, positionDelta, stiffness); -}; - -const keepPosture = (state: CharacterMovementState) => { - const center = characterCenter(state); - springMove( - state, - state.leftFoot, - center, - leftFootOffset, - settings.postureFeetStiffness, - ); - springMove( - state, - state.rightFoot, - center, - rightFootOffset, - settings.postureFeetStiffness, - ); - springMove(state, state.head, center, headOffset, settings.postureHeadStiffness); -}; - -// While standing on a planet, ride its spin: rigidly rotate the whole body -// about the planet centre by the same per-tick angle the collision SDF turns -// by (negative, matching R(-rotation)), so the player is carried with the -// surface instead of sliding across it. -const carryWithRotatingPlanet = ( - state: CharacterMovementState, - deltaTimeInSeconds: number, -) => { - const planet = state.currentPlanet; - if (!planet) { - return; - } - const angle = -planet.angularVelocity * deltaTimeInSeconds; - const center = planet.center; - state.head.center = vec2.rotate(vec2.create(), state.head.center, center, angle); - state.leftFoot.center = vec2.rotate( - vec2.create(), - state.leftFoot.center, - center, - angle, - ); - state.rightFoot.center = vec2.rotate( - vec2.create(), - state.rightFoot.center, - center, - angle, - ); -}; - -// Launch off the current surface: directed by the foot contact normals plus -// the movement input, slingshotted by the planet's spin. Mutates bodyVelocity -// (which the next tick injects into the body) and detaches. Shared so the -// server's leap() and the client's prediction apply the exact same impulse. -// The caller does the gating (strength, cooldown, alive); this is a no-op when -// not on a surface. -export const applyLeapImpulse = (state: CharacterMovementState, moveDirection: vec2) => { - const planet = state.currentPlanet; - if (!planet) { - return; - } - - const up = vec2.add( - vec2.create(), - state.leftFoot.lastNormal, - state.rightFoot.lastNormal, - ); - if (vec2.length(up) === 0) { - vec2.set(up, 0, 1); - } else { - vec2.normalize(up, up); - } - - const launch = vec2.scale(vec2.create(), up, settings.leapUpBias); - if (vec2.length(moveDirection) > 0) { - vec2.scaleAndAdd( - launch, - launch, - vec2.normalize(vec2.create(), moveDirection), - settings.leapMoveBias, - ); - } - vec2.normalize(launch, launch); - vec2.scaleAndAdd(state.bodyVelocity, state.bodyVelocity, launch, settings.leapSpeed); - - // Slingshot: add the tangential velocity of the spinning surface under the - // body (the same motion carryWithRotatingPlanet imparts). - const center = characterCenter(state); - const omega = planet.angularVelocity; - const surfaceVelocity = vec2.fromValues( - omega * (center[1] - planet.center[1]), - -omega * (center[0] - planet.center[0]), - ); - vec2.scaleAndAdd( - state.bodyVelocity, - state.bodyVelocity, - surfaceVelocity, - settings.slingshotScale, - ); - - state.currentPlanet = undefined; - state.secondsSinceOnSurface = settings.planetDetachmentSeconds; -}; - -// Time-based detachment: a grounded body that hasn't touched a surface for a -// while floats free. Kept as its own step because on the server it runs before -// the ownership/scoring blocks; the client calls it at the head of each tick. -export const tickPlanetDetachment = ( - state: CharacterMovementState, - deltaTimeInSeconds: number, -) => { - if ( - (state.secondsSinceOnSurface += deltaTimeInSeconds) > settings.planetDetachmentSeconds - ) { - state.currentPlanet = undefined; - } -}; - -// Inject the persistent launch momentum onto every body part right before they -// step, so the whole body translates rigidly without disturbing the posture -// springs (which only set up relative offsets). No-op while walking. -const applyBodyMomentum = (state: CharacterMovementState) => { - if (vec2.squaredLength(state.bodyVelocity) === 0) { - return; - } - vec2.add(state.leftFoot.velocity, state.leftFoot.velocity, state.bodyVelocity); - vec2.add(state.rightFoot.velocity, state.rightFoot.velocity, state.bodyVelocity); - vec2.add(state.head.velocity, state.head.velocity, state.bodyVelocity); -}; - -// Decay one launch-momentum vector in place by one tick. Stiff on the ground -// (skid to a stop), gentle in the air so a leap or slingshot still carries -// across the gaps. The gentle exponential only asymptotes, though, so on top of -// it a constant deceleration brakes the momentum to a definite stop in a couple -// of seconds instead of leaving a 15+ second drift, and a hard cap stops stacked -// impulses (rapid recoil, a leap into a slingshot) from building without bound. -// Shared so the living body, the client predictor, and the ragdoll corpse all -// brake identically. -export const decayMomentum = ( - bodyVelocity: vec2, - onGround: boolean, - deltaTimeInSeconds: number, -) => { - const speed = vec2.length(bodyVelocity); - if (speed === 0) { - return; - } - const friction = onGround - ? settings.groundMomentumFriction - : settings.airMomentumFriction; - const target = Math.min( - speed * Math.exp(-friction * deltaTimeInSeconds) - - settings.momentumStopDeceleration * deltaTimeInSeconds, - settings.maxBodyMomentum, - ); - if (target <= 1) { - vec2.zero(bodyVelocity); - } else { - vec2.scale(bodyVelocity, bodyVelocity, target / speed); - } -}; - -const decayBodyMomentum = (state: CharacterMovementState, deltaTimeInSeconds: number) => { - decayMomentum(state.bodyVelocity, !!state.currentPlanet, deltaTimeInSeconds); -}; - -const sumGravity = (grounds: Array, position: vec2): vec2 => - grounds.reduce( - (sum, ground) => vec2.add(sum, sum, ground.gravityAt(position)), - vec2.create(), - ); - -const latchGround = ( - state: CharacterMovementState, - ground: GroundSurface | undefined, -) => { - if (ground) { - state.secondsSinceOnSurface = 0; - state.currentPlanet = ground; - } -}; - -// One tick of character movement. This is the exact movement block of the -// authoritative CharacterPhysical.step (gravity gather → movement force → -// on/off-planet branch → posture → step the three body parts), with all -// server-only concerns (scoring, health, shooting, spawn/death animation, -// ownership) left to the caller. `inputDirection` is the already-averaged, -// already-normalized movement direction for this tick and is consumed in place. -export const stepCharacterMovement = ( - state: CharacterMovementState, - world: CharacterWorld, - inputDirection: vec2, - deltaTimeInSeconds: number, -) => { - const center = characterCenter(state); - const grounds = world.groundsNear(center, boundRadius + settings.maxGravityDistance); - - const movementForce = vec2.scale( - inputDirection, - inputDirection, - settings.maxAcceleration, - ); - applyForce(state.leftFoot, movementForce, deltaTimeInSeconds); - applyForce(state.rightFoot, movementForce, deltaTimeInSeconds); - - if (!state.currentPlanet) { - const leftFootGravity = sumGravity(grounds, state.leftFoot.center); - const rightFootGravity = sumGravity(grounds, state.rightFoot.center); - - applyForce(state.leftFoot, leftFootGravity, deltaTimeInSeconds); - applyForce(state.rightFoot, rightFootGravity, deltaTimeInSeconds); - - const sumForce = vec2.subtract(vec2.create(), leftFootGravity, movementForce); - setDirection(state, vec2.length(sumForce) === 0 ? vec2.fromValues(0, -1) : sumForce); - } else { - carryWithRotatingPlanet(state, deltaTimeInSeconds); - - const leftFootGravity = state.currentPlanet.gravityAt(state.leftFoot.center); - const rightFootGravity = state.currentPlanet.gravityAt(state.rightFoot.center); - - vec2.add(leftFootGravity, leftFootGravity, rightFootGravity); - const gravity = vec2.scale(leftFootGravity, leftFootGravity, 0.5); - - if ( - vec2.dot(movementForce, gravity) < - -vec2.length(movementForce) * settings.climbDotThreshold - ) { - vec2.scale(gravity, gravity, settings.climbGravityScale); - } - - const scaledLeftFootGravity = vec2.scale( - vec2.create(), - state.leftFoot.lastNormal, - vec2.dot(state.leftFoot.lastNormal, gravity), - ); - applyForce(state.leftFoot, scaledLeftFootGravity, deltaTimeInSeconds); - - const scaledRightFootGravity = vec2.scale( - vec2.create(), - state.rightFoot.lastNormal, - vec2.dot(state.rightFoot.lastNormal, gravity), - ); - applyForce(state.rightFoot, scaledRightFootGravity, deltaTimeInSeconds); - - if (vec2.length(gravity) <= settings.planetDetachmentForceThreshold) { - state.currentPlanet = undefined; - } - setDirection(state, gravity); - } - - keepPosture(state); - - applyBodyMomentum(state); - - latchGround(state, world.stepBody(state.leftFoot, deltaTimeInSeconds)); - latchGround(state, world.stepBody(state.rightFoot, deltaTimeInSeconds)); - latchGround(state, world.stepBody(state.head, deltaTimeInSeconds)); - - decayBodyMomentum(state, deltaTimeInSeconds); -}; diff --git a/shared/src/physics/depenetrate-circle.ts b/shared/src/physics/depenetrate-circle.ts deleted file mode 100644 index 7232f73..0000000 --- a/shared/src/physics/depenetrate-circle.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { PhysicsBody, Sdf } from './sdf'; -import { evaluateSdf } from './evaluate-sdf'; -import { sdfNormal } from './sdf-normal'; - -// Planet collision outlines rotate (see PlanetPhysical.distance), so a surface -// can sweep into a circle that hasn't itself moved. marchCircle assumes an -// overlap-free start — beginning inside, it registers a zero-distance hit and -// never moves again — so any overlap must be resolved here, before marching. -// Iterating handles concave spots, where leaving one face pushes into another; -// if no overlap-free position exists nearby (a crevice narrower than the -// circle), it gives up and leaves the rest to a later tick. -export const depenetrateCircle = ( - body: PhysicsBody, - possibleIntersectors: Array, -) => { - for (let i = 0; i < 4; i++) { - const distance = evaluateSdf(body.center, possibleIntersectors); - if (distance >= body.radius) { - return; - } - - const normal = sdfNormal(body.center, possibleIntersectors); - if (vec2.squaredLength(normal) === 0) { - return; - } - - vec2.copy(body.lastNormal, normal); - body.center = vec2.scaleAndAdd( - vec2.create(), - body.center, - normal, - body.radius - distance + 0.01, - ); - } -}; diff --git a/shared/src/physics/evaluate-sdf.ts b/shared/src/physics/evaluate-sdf.ts deleted file mode 100644 index f3c60cf..0000000 --- a/shared/src/physics/evaluate-sdf.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Sdf } from './sdf'; - -export const evaluateSdf = (target: vec2, objects: Array) => - objects - .filter((i) => i.canCollide) - .reduce((min, i) => (min = Math.min(min, i.distance(target))), 1000000); diff --git a/shared/src/physics/interpolate-angles.ts b/shared/src/physics/interpolate-angles.ts deleted file mode 100644 index 1fb46d6..0000000 --- a/shared/src/physics/interpolate-angles.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const interpolateAngles = (from: number, to: number, q: number) => { - const max = Math.PI * 2; - const possibleDistance = (to - from) % max; - const shorterDistance = ((2 * possibleDistance) % max) - possibleDistance; - return from + shorterDistance * q; -}; diff --git a/shared/src/physics/march-circle.ts b/shared/src/physics/march-circle.ts deleted file mode 100644 index 0a8f0c8..0000000 --- a/shared/src/physics/march-circle.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { PhysicsBody, Sdf } from './sdf'; -import { evaluateSdf } from './evaluate-sdf'; -import { sdfNormal } from './sdf-normal'; - -export interface MarchResult { - hitSurface: boolean; - normal?: vec2; - hitObject?: Sdf; -} - -// Raymarch a circle by `delta`, stopping at the first surface it would overlap. -// Extracted verbatim from the backend's move-circle so server and client -// resolve motion identically. The collision *reaction* is no longer dispatched -// here: on a real (non-ignored) hit, `onHit(intersecting)` is invoked at the -// exact point the backend used to dispatch its ReactToCollisionCommands, and -// the backend wrapper supplies that callback. The client passes none. -export const marchCircle = ( - body: PhysicsBody, - delta: vec2, - possibleIntersectors: Array, - ignoreCollision = false, - onHit?: (intersecting: Sdf) => void, -): MarchResult => { - const direction = vec2.clone(delta); - - if (vec2.length(delta) > 0) { - vec2.normalize(direction, direction); - } - - const deltaLength = vec2.length(delta); - let travelled = 0; - const rayEnd = vec2.create(); - let prevMinDistance = 0; - while (travelled < deltaLength) { - travelled += prevMinDistance; - vec2.add( - rayEnd, - body.center, - vec2.scale(vec2.create(), direction, Math.min(travelled, deltaLength)), - ); - - const minDistance = evaluateSdf(rayEnd, possibleIntersectors); - - if (minDistance < body.radius) { - const intersecting = possibleIntersectors.find( - (i) => i.distance(rayEnd) <= body.radius, - )!; - - if (ignoreCollision) { - body.center = vec2.add(body.center, body.center, delta); - } else { - onHit?.(intersecting); - } - - vec2.add( - rayEnd, - body.center, - vec2.scale(vec2.create(), direction, travelled - prevMinDistance), - ); - - vec2.copy(body.center, rayEnd); - - const normal = sdfNormal(rayEnd, [intersecting]); - return { - hitSurface: true, - normal, - hitObject: intersecting, - }; - } - - prevMinDistance = minDistance; - } - - vec2.add(body.center, body.center, delta); - - return { - hitSurface: false, - }; -}; diff --git a/shared/src/physics/planet-sdf.ts b/shared/src/physics/planet-sdf.ts deleted file mode 100644 index 24af49d..0000000 --- a/shared/src/physics/planet-sdf.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { clamp, clamp01 } from '../helper/clamp'; -import { settings } from '../settings'; - -// Rotate a world point into the planet's own frame: R(-rotation) about the -// centre, matching the shader's localTarget = center + R(rotation)*(target-center). -// cos/sin are passed in so the server can keep memoising them per angle. -export const toPlanetLocalFrame = ( - target: vec2, - center: vec2, - cos: number, - sin: number, -): vec2 => { - const dx = target[0] - center[0]; - const dy = target[1] - center[1]; - return vec2.fromValues( - center[0] + cos * dx - sin * dy, - center[1] + sin * dx + cos * dy, - ); -}; - -// Signed distance to the (smooth, noise-free) planet polygon, evaluated in the -// planet's rotating frame. Extracted verbatim from PlanetPhysical.distance so -// the client collides against the exact same outline the server does. Indexed -// vector access keeps it independent of the .x/.y prototype plugin. -export const planetDistance = ( - target: vec2, - vertices: Array, - center: vec2, - cos: number, - sin: number, -): number => { - const local = toPlanetLocalFrame(target, center, cos, sin); - - const startEnd = vertices[0]; - let vb = startEnd; - - let d = vec2.dist(local, vb); - let sign = 1; - - for (let i = 1; i <= vertices.length; i++) { - const va = vb; - vb = i === vertices.length ? startEnd : vertices[i]; - const targetFromDelta = vec2.subtract(vec2.create(), local, va); - const toFromDelta = vec2.subtract(vec2.create(), vb, va); - const h = clamp01( - vec2.dot(targetFromDelta, toFromDelta) / vec2.squaredLength(toFromDelta), - ); - - const ds = vec2.fromValues( - vec2.dist(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)), - toFromDelta[0] * targetFromDelta[1] - toFromDelta[1] * targetFromDelta[0], - ); - - if ( - (local[1] >= va[1] && local[1] < vb[1] && ds[1] > 0) || - (local[1] < va[1] && local[1] >= vb[1] && ds[1] <= 0) - ) { - sign *= -1; - } - - d = Math.min(d, ds[0]); - } - - return sign * d; -}; - -// Gravity a planet exerts at a world position. Verbatim from -// PlanetPhysical.getForce. -export const planetGravity = (center: vec2, radius: number, position: vec2): vec2 => { - const diff = vec2.subtract(vec2.create(), center, position); - const dist = Math.max(settings.minGravityDistance, vec2.length(diff) - radius); - vec2.normalize(diff, diff); - const scale = clamp( - settings.maxGravityQ * ((settings.maxGravityDistance / dist) ** 1.5 - 1), - 0, - settings.maxGravityStrength, - ); - return vec2.scale(diff, diff, scale); -}; diff --git a/shared/src/physics/resolve-circle-movement.ts b/shared/src/physics/resolve-circle-movement.ts deleted file mode 100644 index 47f0c1c..0000000 --- a/shared/src/physics/resolve-circle-movement.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { PhysicsBody, Sdf } from './sdf'; -import { depenetrateCircle } from './depenetrate-circle'; -import { marchCircle } from './march-circle'; - -// The position-resolution half of the backend's CirclePhysical.stepManually, -// extracted so server and client integrate a body identically. The caller is -// responsible for the broadphase (gathering `possibleIntersectors`, including -// the swept-radius bump) because that depends on each side's spatial structure; -// everything from depenetration onward lives here. -// -// `onHit` is threaded through to marchCircle so the backend can dispatch its -// collision reactions at the same points as before (including the second, -// post-bounce slide march); the client passes none. Velocity is reset to zero -// at the end — the character re-applies all forces from zero every tick, so a -// body that retained velocity would double-integrate and diverge. -export const resolveCircleMovement = ( - body: PhysicsBody, - deltaTimeInSeconds: number, - possibleIntersectors: Array, - onHit?: (intersecting: Sdf) => void, -): { hitObject?: Sdf; velocity: vec2 } => { - let delta = vec2.scale(vec2.create(), body.velocity, deltaTimeInSeconds); - - depenetrateCircle(body, possibleIntersectors); - - const { normal, hitSurface, hitObject } = marchCircle( - body, - delta, - possibleIntersectors, - false, - onHit, - ); - - if (hitSurface) { - vec2.copy(body.lastNormal, normal!); - - vec2.subtract( - body.velocity, - body.velocity, - vec2.scale( - normal!, - normal!, - (1 + body.restitution) * vec2.dot(normal!, body.velocity), - ), - ); - - if (vec2.length(body.velocity) > 50) { - delta = vec2.scale(vec2.create(), body.velocity, deltaTimeInSeconds); - marchCircle(body, delta, possibleIntersectors, false, onHit); - } - } - - const lastVelocity = vec2.clone(body.velocity); - vec2.zero(body.velocity); - - return { hitObject, velocity: lastVelocity }; -}; diff --git a/shared/src/physics/sdf-normal.ts b/shared/src/physics/sdf-normal.ts deleted file mode 100644 index 507a8eb..0000000 --- a/shared/src/physics/sdf-normal.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { Sdf } from './sdf'; -import { evaluateSdf } from './evaluate-sdf'; - -// Central-difference gradient of the combined SDF. Can be zero where the -// samples cancel out (e.g. on the medial axis of a shape) — callers must -// handle that case. Uses indexed access so it never depends on the vec2 .x/.y -// prototype plugin (identical numerically: .x === [0]). -export const sdfNormal = (target: vec2, objects: Array): vec2 => { - const dx = - evaluateSdf(vec2.fromValues(target[0] + 0.01, target[1]), objects) - - evaluateSdf(vec2.fromValues(target[0] - 0.01, target[1]), objects); - const dy = - evaluateSdf(vec2.fromValues(target[0], target[1] + 0.01), objects) - - evaluateSdf(vec2.fromValues(target[0], target[1] - 0.01), objects); - - const normal = vec2.fromValues(dx, dy); - return vec2.squaredLength(normal) > 0 ? vec2.normalize(normal, normal) : normal; -}; diff --git a/shared/src/physics/sdf.ts b/shared/src/physics/sdf.ts deleted file mode 100644 index 38bb74c..0000000 --- a/shared/src/physics/sdf.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { vec2 } from 'gl-matrix'; - -// Minimal signed-distance collider the geometry functions need. Backend -// Physicals and the client's planet surfaces both satisfy this structurally. -export interface Sdf { - readonly canCollide: boolean; - // Planets set this true so a body landing on one can latch it as ground; - // everything else leaves it falsy. Replaces the server's - // `instanceof PlanetPhysical` check with a structural flag both sides share. - readonly isGround?: boolean; - distance(target: vec2): number; -} - -// A movable circle the geometry resolves in place. Backend CirclePhysical and -// the client predictor's plain bodies both satisfy this. -export interface PhysicsBody { - center: vec2; - radius: number; - velocity: vec2; - lastNormal: vec2; - readonly restitution: number; -} - -// A planet-like surface: collidable, exerts gravity, and spins. Drives the -// character's on-surface movement branch. -export interface GroundSurface extends Sdf { - readonly isGround: true; - readonly center: vec2; - readonly angularVelocity: number; - gravityAt(target: vec2): vec2; -} diff --git a/shared/src/serialization/deserialize.ts b/shared/src/serialization/deserialize.ts deleted file mode 100644 index 6ba3a67..0000000 --- a/shared/src/serialization/deserialize.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { serializableMapping } from './serializable-mapping'; - -export const deserialize = (json: string): any => { - return JSON.parse(json, (_, v) => { - if (v !== null && Object.prototype.hasOwnProperty.call(v, '0')) { - const possibleType = v[0]; - const overridableConstructor = serializableMapping.get(possibleType); - if (overridableConstructor) { - return new overridableConstructor.constructor(...v.slice(1)); - } - return v; - } - return v; - }); -}; diff --git a/shared/src/serialization/mangled-type-key.ts b/shared/src/serialization/mangled-type-key.ts deleted file mode 100644 index 2460725..0000000 --- a/shared/src/serialization/mangled-type-key.ts +++ /dev/null @@ -1 +0,0 @@ -export const mangledTypeKey = '__serializable_type'; diff --git a/shared/src/serialization/serialize.ts b/shared/src/serialization/serialize.ts deleted file mode 100644 index bd72218..0000000 --- a/shared/src/serialization/serialize.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { mangledTypeKey } from './mangled-type-key'; - -export const serialize = (object: any): string => { - return JSON.stringify(object, (_, value) => { - if (value && value[mangledTypeKey]) { - const props = value.toArray() as Array; - props.unshift(value[mangledTypeKey]); - return props; - } - return value?.toFixed ? Number(value.toFixed(3)) : value; - }); -}; diff --git a/shared/src/serialization/serialized-fields.ts b/shared/src/serialization/serialized-fields.ts deleted file mode 100644 index 48733e8..0000000 --- a/shared/src/serialization/serialized-fields.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Derive a serializable object's positional wire array from a single declared -// list of field names, so toArray() and the constructor share ONE source of -// truth for field order instead of a hand-written array that can silently drift -// out of step with the parameters. -// -// deserialize reconstructs via `new Ctor(...array.slice(1))`, so the field list -// MUST name the constructor's parameters in order. Fields a constructor -// recomputes from others (e.g. a planet's centre/radius from its vertices) are -// derived, not serialized, and so are deliberately absent from the list. -export const toArrayFromFields = ( - object: any, - fields: ReadonlyArray, -): Array => fields.map((field) => object[field]); diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 61041a5..3c9935b 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -1,209 +1,19 @@ -import { rgb255 } from './helper/rgb255'; -import { CharacterTeam } from './objects/types/character-base'; - -const blueColor = rgb255(64, 105, 165); -const neutralColor = rgb255(82, 165, 64); -const redColor = rgb255(209, 86, 82); -const q = 2.5; -const blueColorDim = rgb255(64 * q, 105 * q, 165 * q); -const redColorDim = rgb255(209 * q, 86 * q, 82 * q); -const bluePlanetColor = blueColorDim; -const redPlanetColor = redColorDim; +import { vec2 } from 'gl-matrix'; export const settings = { lightCutoffDistance: 600, - lightOverlapReduction: 0.85, - radiusSteps: 500, - spawnDespawnTime: 0.7, - worldRadius: 4000, - objectsOnCircleLength: 0.002, - updateMessageInterval: 1 / 25, - // How far behind the estimated server time remote state is rendered: ~2.5 - // update intervals, so a late packet rarely leaves the client without a - // newer snapshot to interpolate towards. - interpolationDelaySeconds: 0.1, - planetEdgeCount: 7, - playerKillPoint: 25, - takeControlTimeInSeconds: 2.5, - loseControlTimeInSeconds: 16, - planetPointGenerationInterval: 1.5, - planetPointGenerationValue: 1, - planetSizePointMultiplierMax: 4, - captureFlipPointReward: 12, - maxGravityDistance: 800, - minGravityDistance: 1, - maxGravityQ: 5000, - // Half-width of the neutral dead-band around 50% ownership. A planet only - // counts as captured (team(), point generation, flip) once |ownership-0.5| - // exceeds this, and the rendered ownership ring stays neutral until the same - // point — so what you see matches what scores. - planetControlThreshold: 0.12, - playerMaxHealth: 100, - maxGravityStrength: 50000, - planetMinReferenceRadius: 150, - planetMaxReferenceRadius: 1200, - maxAcceleration: 120000, - playerMaxStrength: 80, - endGameDeltaScaling: 2, - playerDiedTimeout: 5, - playerStrengthRegenerationPerSeconds: 80, - playerOutOfCombatDelaySeconds: 5, - playerHealthRegenerationPerSeconds: 8, - playerKillHealthReward: 25, - spawnInvulnerabilityExtraSeconds: 0.4, - spawnSafetyDistance: 2000, - touchAimRange: 1000, - postureHeadStiffness: 12, - postureFeetStiffness: 10, - planetDetachmentSeconds: 0.5, - planetDetachmentForceThreshold: 100, - climbDotThreshold: 0.8, - climbGravityScale: 0.35, - projectileMaxStrength: 40, - projectileMaxBounceCount: 2, - projectileFadeSpeed: 20, - projectileCreationInterval: 0.1, - chargeShotFullHoldSeconds: 0.7, - chargeShotStrengthMin: 40, - chargeShotStrengthMax: 100, - chargeShotRadiusMin: 20, - chargeShotRadiusMax: 32, - chargeShotSpeedMin: 2500, - chargeShotSpeedMax: 3400, - playerColorIndexOffset: 3, - backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)], - blueColor, - bluePlanetColor, - npcNames: [ - 'Adam', - 'Andrew', - 'Blaise', - 'Clarence', - 'Dean', - 'Dustin', - 'Elliot', - 'Ernie', - 'Ethan', - 'Frank', - 'Fred', - 'George', - 'Graham', - 'Harold', - 'Harvey', - 'Henry', - 'Mingan', - 'Irving', - 'Irwin', - 'Jason', - 'Jenssen', - 'Josh', - 'Ladislaus', - 'Larry', - 'Lester', - 'Martin', - 'Marvin', - 'Neil', - 'Nick', - 'Niles', - 'Norm', - 'Oliver', - 'Orin', - 'Pat', - 'Perry', - 'Ron', - 'Ryan', - 'Sisyphus', - 'Tim', - 'Toby', - 'Ulysses', - 'Uri', - 'Waldo', - 'Wally', - 'Walt', - 'Wesley', - 'Will', - 'Wyatt', - ], - redColor, - redPlanetColor, - colorIndices: { - [CharacterTeam.blue]: 0, - [CharacterTeam.neutral]: 1, - [CharacterTeam.red]: 2, - }, - palette: [blueColor, neutralColor, redColor], - paletteDim: [blueColorDim, neutralColor, redColorDim], - targetPhysicsDeltaTimeInSeconds: 1 / 200, - inViewAreaSize: 1920 * 1080 * 4, - scoreboardHalfWidthPercent: 50, - scoreboardMinFillPercent: 1.5, - matchPointScoreRatio: 0.9, - lampLerpSeconds: 0.5, - lampMinLightness: 0.4, - lampMaxLightness: 1, - lampFlareIntensity: 1.2, - lampFlareDecaySeconds: 0.6, - maxConcurrentFlipFlares: 3, - announcementVisibleSeconds: 2, - - chargedHitThreshold: 0.6, - - // Projectiles fall through planetary gravity like a free-falling character, - // so slower (charged) shots arc. Scale kept tiny: near a surface gravity is - // maxGravityStrength=50000, which at full strength would corkscrew a shot into - // the planet — 0.04 gives a readable bend instead. - projectileGravityEnabled: true, - projectileGravityScale: 0.04, - - // Speed the corpse is flung at along the killing shot's direction, lerped by - // that shot's charge. Added to whatever momentum the victim already carried. - deathImpulseMin: 280, - deathImpulseMax: 1300, - - // A planet's net team head-count drives a single capture step per tick; the - // lead multiplier is capped so a zerg can't flip instantly. Equal head-counts - // freeze the planet (contested) instead of silently cancelling. - maxContestLeadMultiplier: 2, - - // Persistent body momentum decays per second by these exponents. Airborne is - // near-frictionless so leaps and slingshots carry across the gaps; grounded is - // stiff so you skid to a stop on landing rather than sliding forever. - airMomentumFriction: 0.4, - - groundMomentumFriction: 7, - - // On top of the exponential frictions above, a constant deceleration (u/s^2) - // applied to body momentum. The exponential alone only asymptotes toward zero, - // so a fast launch keeps a slow tail for 15+ seconds — it reads as drifting - // forever with nothing slowing you. This constant brake brings the momentum to - // a definite stop in a couple of seconds, while the gentle exponential still - // lets the launch cover its distance first. - - momentumStopDeceleration: 400, - // Hard ceiling (u/s) on body momentum, so stacked impulses — rapid charged-shot - // recoil, or a leap chained into a spin slingshot — can't build speed without - // bound. Kept above the overcharge fling (1700) so single launches survive. - - maxBodyMomentum: 2000, - - // Leap: a charged-cost launch off a surface, paid from the shared shooting - // strength pool so it trades against firepower. - leapStrengthCost: 32, - - leapSpeed: 1350, - leapUpBias: 1, - leapMoveBias: 0.65, - leapCooldownSeconds: 0.35, - - // Fraction of the planet's tangential surface velocity you keep when you leave - // it (slingshot). Leap off a fast spinner to be flung far. - slingshotScale: 1, - - // Recoil speed imparted opposite a shot, scaled by its charge (0 for taps). - chargeShotRecoilMax: 650, - - // The central giant is a named, always-contested focus. Its neutral decay is - // slowed so control lingers and teams keep fighting over it; flips are - // announced to everyone and an off-screen arrow points the way. - keystoneLoseControlScale: 2.5, + hitDetectionCirclePointCount: 32, + hitDetectionMaxOverlap: 0.01, + physicsMaxStep: 5, + gravitationalForce: vec2.fromValues(0, -200), + maxVelocityX: 500, + maxVelocityY: 750, + maxAccelerationX: 16000, + maxAccelerationY: 5500, + targetPhysicsDeltaTimeInMilliseconds: 15, + minPhysicsSleepTime: 4, + velocityAttenuation: 0.5, + frictionMinVelocity: 400, + inViewAreaSize: 1920 * 1080 * 3, + defaultJumpEnergy: 0.75, }; diff --git a/shared/src/transport/identity.ts b/shared/src/transport/identity.ts new file mode 100644 index 0000000..53b0c34 --- /dev/null +++ b/shared/src/transport/identity.ts @@ -0,0 +1 @@ +export type Id = number | null; diff --git a/shared/src/serialization/deserializable-class.ts b/shared/src/transport/serialization/deserializable-class.ts similarity index 100% rename from shared/src/serialization/deserializable-class.ts rename to shared/src/transport/serialization/deserializable-class.ts diff --git a/shared/src/transport/serialization/deserialize.ts b/shared/src/transport/serialization/deserialize.ts new file mode 100644 index 0000000..3d9dcfb --- /dev/null +++ b/shared/src/transport/serialization/deserialize.ts @@ -0,0 +1,13 @@ +import { serializableMapping } from './serializable-mapping'; + +export const deserialize = (json: string): any => { + return JSON.parse(json, (k, v) => { + const possibleType = v[0]; + const overridableConstructor = serializableMapping.get(possibleType); + if (overridableConstructor) { + v.shift(); + return new overridableConstructor.constructor(...v); + } + return v; + }); +}; diff --git a/shared/src/serialization/override-deserialization.ts b/shared/src/transport/serialization/override-deserialization.ts similarity index 94% rename from shared/src/serialization/override-deserialization.ts rename to shared/src/transport/serialization/override-deserialization.ts index 7b8d6d9..c59bcee 100644 --- a/shared/src/serialization/override-deserialization.ts +++ b/shared/src/transport/serialization/override-deserialization.ts @@ -8,6 +8,6 @@ export const overrideDeserialization = ( ) => { serializableMapping.set(source.name, { constructor: target, - overridden: true, + overriden: true, }); }; diff --git a/shared/src/serialization/serializable-class.ts b/shared/src/transport/serialization/serializable-class.ts similarity index 100% rename from shared/src/serialization/serializable-class.ts rename to shared/src/transport/serialization/serializable-class.ts diff --git a/shared/src/serialization/serializable-mapping.ts b/shared/src/transport/serialization/serializable-mapping.ts similarity index 78% rename from shared/src/serialization/serializable-mapping.ts rename to shared/src/transport/serialization/serializable-mapping.ts index 5c9d7cb..bb9b731 100644 --- a/shared/src/serialization/serializable-mapping.ts +++ b/shared/src/transport/serialization/serializable-mapping.ts @@ -1,9 +1,12 @@ import { DeserializableClass } from './deserializable-class'; +/** + * @internal + */ export const serializableMapping = new Map< string, { constructor: DeserializableClass; - overridden: boolean; + overriden: boolean; } >(); diff --git a/shared/src/serialization/serializable.ts b/shared/src/transport/serialization/serializable.ts similarity index 59% rename from shared/src/serialization/serializable.ts rename to shared/src/transport/serialization/serializable.ts index 4605f65..967e6cc 100644 --- a/shared/src/serialization/serializable.ts +++ b/shared/src/transport/serialization/serializable.ts @@ -1,4 +1,3 @@ -import { mangledTypeKey } from './mangled-type-key'; import { SerializableClass } from './serializable-class'; import { serializableMapping } from './serializable-mapping'; @@ -6,12 +5,12 @@ export const serializable = (type: SerializableClass): any => { if (!serializableMapping.get(type.name)) { serializableMapping.set(type.name, { constructor: type, - overridden: false, + overriden: false, }); } - Object.defineProperty(type, mangledTypeKey, { value: type.name }); - Object.defineProperty(type.prototype, mangledTypeKey, { value: type.name }); + Object.defineProperty(type, '__serializable_type', { value: type.name }); + Object.defineProperty(type.prototype, '__serializable_type', { value: type.name }); return type; }; diff --git a/shared/src/transport/serialization/serialize.ts b/shared/src/transport/serialization/serialize.ts new file mode 100644 index 0000000..4e93184 --- /dev/null +++ b/shared/src/transport/serialization/serialize.ts @@ -0,0 +1,8 @@ +export const serialize = (object: any): string => { + return JSON.stringify(object, (_, value) => { + if (value?.__serializable_type) { + return [value.__serializable_type, ...value.toArray()]; + } + return value?.toFixed ? Number(value.toFixed(3)) : value; + }); +}; diff --git a/shared/src/serialization/serializes-to.ts b/shared/src/transport/serialization/serializes-to.ts similarity index 66% rename from shared/src/serialization/serializes-to.ts rename to shared/src/transport/serialization/serializes-to.ts index 47030de..1346164 100644 --- a/shared/src/serialization/serializes-to.ts +++ b/shared/src/transport/serialization/serializes-to.ts @@ -1,4 +1,3 @@ -import { mangledTypeKey } from './mangled-type-key'; import { SerializableClass } from './serializable-class'; import { serializableMapping } from './serializable-mapping'; @@ -7,12 +6,12 @@ export const serializesTo = (target: SerializableClass) => { if (!serializableMapping.get(target.name)) { serializableMapping.set(target.name, { constructor: target, - overridden: false, + overriden: false, }); } - Object.defineProperty(actual, mangledTypeKey, { value: target.name }); - Object.defineProperty(actual.prototype, mangledTypeKey, { + Object.defineProperty(actual, '__serializable_type', { value: target.name }); + Object.defineProperty(actual.prototype, '__serializable_type', { value: target.name, }); diff --git a/shared/src/communication/transport-events.ts b/shared/src/transport/transport-events.ts similarity index 61% rename from shared/src/communication/transport-events.ts rename to shared/src/transport/transport-events.ts index 395a564..b254201 100644 --- a/shared/src/communication/transport-events.ts +++ b/shared/src/transport/transport-events.ts @@ -2,8 +2,6 @@ export enum TransportEvents { PlayerJoining = 'PlayerJoining', PlayerToServer = 'PlayerToServer', ServerToPlayer = 'ServerToPlayer', - SubscribeForServerInfoUpdates = 'SubscribeForServerInfoUpdates', - ServerInfoUpdate = 'ServerInfoUpdate', Ping = 'Ping', Pong = 'Pong', } diff --git a/shared/tsconfig.json b/shared/tsconfig.json index 91326be..f85c232 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -8,12 +8,10 @@ "esModuleInterop": true, "strict": true, "experimentalDecorators": true, + "downlevelIteration": true, "moduleResolution": "node", - "ignoreDeprecations": "6.0", "module": "commonjs", - "skipLibCheck": true, "composite": true, "lib": ["dom", "es2017"] - }, - "include": ["src/**/*.ts", "src/*.ts"] + } } diff --git a/test/__snapshots__/physics-determinism.test.mjs.snap b/test/__snapshots__/physics-determinism.test.mjs.snap deleted file mode 100644 index 50ab1c3..0000000 --- a/test/__snapshots__/physics-determinism.test.mjs.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`shared character simulation determinism > matches the pinned reference pose (changes only with intentional physics edits) 1`] = `"{"head":[76.14,123.412],"leftFoot":[83.214,105.036],"rightFoot":[79.974,123.01],"bodyVelocity":[0,0]}"`; diff --git a/test/physics-determinism.test.mjs b/test/physics-determinism.test.mjs deleted file mode 100644 index d0fee1a..0000000 --- a/test/physics-determinism.test.mjs +++ /dev/null @@ -1,83 +0,0 @@ -// Determinism guard for the shared character simulation. The client predictor -// and the authoritative server run the EXACT same stepCharacterMovement; if it -// were non-deterministic (a stray Math.random / Date, or an order-dependent -// reduce) prediction would rubber-band and could never reconcile. This is also -// the regression net for the upcoming GC/scratch-pool perf rewrites: the pinned -// hash must not change unless physics behaviour is intentionally changed. -import { describe, it, expect } from 'vitest'; -import { createRequire } from 'node:module'; - -const require = createRequire(import.meta.url); -const shared = require('../shared/lib/main.js'); -const { vec2 } = require('../shared/node_modules/gl-matrix'); - -const { - stepCharacterMovement, - resolveCircleMovement, - headRadius, - feetRadius, - headOffset, - leftFootOffset, - rightFootOffset, -} = shared; - -const makeBody = (center, radius) => ({ - center: vec2.clone(center), - radius, - velocity: vec2.create(), - lastNormal: vec2.fromValues(0, 1), - restitution: 0, -}); - -// A free-space world (no planets): the body is driven purely by the movement -// input force, posture springs and momentum decay — enough to exercise the -// deterministic core without coupling the test to planet SDF geometry. -const emptyWorld = { - groundsNear: () => [], - stepBody: (body, dt) => { - resolveCircleMovement(body, dt, []); - return undefined; - }, -}; - -const stepSeconds = 1 / 200; - -// Run a fixed, scripted input sequence through the shared sim and return a -// stable string snapshot of the final pose + carried momentum. -const runSimulation = () => { - const start = vec2.fromValues(100, 100); - const state = { - head: makeBody(vec2.add(vec2.create(), start, headOffset), headRadius), - leftFoot: makeBody(vec2.add(vec2.create(), start, leftFootOffset), feetRadius), - rightFoot: makeBody(vec2.add(vec2.create(), start, rightFootOffset), feetRadius), - direction: 0, - currentPlanet: undefined, - secondsSinceOnSurface: 1, - bodyVelocity: vec2.create(), - }; - - for (let i = 0; i < 300; i++) { - const angle = i * 0.1; - const input = vec2.fromValues(Math.cos(angle), Math.sin(angle)); - stepCharacterMovement(state, emptyWorld, input, stepSeconds); - } - - const round = (v) => Math.round(v * 1000) / 1000; - return JSON.stringify({ - head: [round(state.head.center[0]), round(state.head.center[1])], - leftFoot: [round(state.leftFoot.center[0]), round(state.leftFoot.center[1])], - rightFoot: [round(state.rightFoot.center[0]), round(state.rightFoot.center[1])], - bodyVelocity: [round(state.bodyVelocity[0]), round(state.bodyVelocity[1])], - }); -}; - -describe('shared character simulation determinism', () => { - it('produces identical output across independent runs', () => { - expect(runSimulation()).toBe(runSimulation()); - }); - - it('matches the pinned reference pose (changes only with intentional physics edits)', () => { - // Regression pin — update deliberately when physics behaviour changes. - expect(runSimulation()).toMatchSnapshot(); - }); -}); diff --git a/test/prediction-reconciliation.test.ts b/test/prediction-reconciliation.test.ts deleted file mode 100644 index 98ffc1d..0000000 --- a/test/prediction-reconciliation.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Reconciliation guard for the REAL client predictor. It exercises -// LocalCharacterPredictor end-to-end with an injected clock, verifying the two -// properties reconciliation depends on: -// 1. a fully-acknowledged snapshot reproduces the authoritative pose exactly -// (no spurious drift on top of server truth), and -// 2. un-acknowledged input is replayed forward deterministically. -// This is the net for prediction-touching changes (the death-while-dead fix, -// future netcode work) — a regression shows up as drift or non-determinism. -import { describe, it, expect } from 'vitest'; -import { - LocalCharacterPredictor, - setPredictorClockForTesting, -} from '../frontend/src/scripts/helper/prediction/local-character-predictor'; - -const HEAD_RADIUS = 50; -const FEET_RADIUS = 20; - -// A Circle-shaped pose; the predictor only reads .center / .radius, so plain -// objects with array centres are sufficient (and avoid importing gl-matrix). -const poseAt = (cx: number, cy: number) => ({ - head: { center: [cx, cy + 37], radius: HEAD_RADIUS }, - leftFoot: { center: [cx - 33, cy - 18], radius: FEET_RADIUS }, - rightFoot: { center: [cx + 33, cy - 18], radius: FEET_RADIUS }, -}); - -describe('local prediction reconciliation', () => { - it('reproduces the authoritative pose exactly when all input is acknowledged', () => { - let clock = 1000; - setPredictorClockForTesting(() => clock); - - const predictor = new LocalCharacterPredictor(); - const auth = poseAt(500, 500); - - const t = predictor.recordInput([1, 0]); - predictor.acknowledge(t, [0, 0], -Infinity); // server has consumed this input - predictor.setStrength(80); - predictor.setAuthoritative(auth.head as never, auth.leftFoot as never, auth.rightFoot as never); - - // No clock advance → zero replay window → predicted pose == authoritative. - const used = predictor.update([], 1 / 60); - - expect(used).toBe(true); - expect(predictor.head.center[0]).toBeCloseTo(auth.head.center[0], 5); - expect(predictor.head.center[1]).toBeCloseTo(auth.head.center[1], 5); - expect(predictor.leftFoot.center[0]).toBeCloseTo(auth.leftFoot.center[0], 5); - expect(predictor.rightFoot.center[0]).toBeCloseTo(auth.rightFoot.center[0], 5); - }); - - it('replays un-acknowledged input deterministically', () => { - const drive = () => { - let clock = 0; - setPredictorClockForTesting(() => clock); - - const predictor = new LocalCharacterPredictor(); - clock = 1000; - predictor.acknowledge(900, [0, 0], -Infinity); // baseline ack (older than the input below) - predictor.setStrength(80); - const auth = poseAt(0, 0); - predictor.setAuthoritative(auth.head as never, auth.leftFoot as never, auth.rightFoot as never); - predictor.recordInput([1, 0]); // unacked rightward input at t=1000 - - clock = 1100; // replay ~100 ms forward - predictor.update([], 1 / 60); - return [ - predictor.head.center[0], - predictor.head.center[1], - predictor.leftFoot.center[0], - predictor.rightFoot.center[0], - ]; - }; - - const a = drive(); - const b = drive(); - expect(a).toEqual(b); // deterministic replay - expect(a.every((n) => Number.isFinite(n))).toBe(true); - expect(a[0]).toBeGreaterThan(0.5); // rightward input actually moved the body - }); - - it('suppresses prediction while the local player is dead', () => { - let clock = 5000; - setPredictorClockForTesting(() => clock); - - const predictor = new LocalCharacterPredictor(); - const auth = poseAt(200, 200); - const t = predictor.recordInput([1, 0]); - predictor.acknowledge(t, [0, 0], -Infinity); - predictor.setStrength(80); - predictor.setAuthoritative(auth.head as never, auth.leftFoot as never, auth.rightFoot as never); - predictor.setAlive(false); // dead, awaiting respawn - - clock = 5200; // input + elapsed time that would otherwise be replayed forward - // Even with a valid authoritative pose and pending input, a dead body must - // not be predicted/moved — that was the "move while dead" bug. - expect(predictor.update([], 1 / 60)).toBe(false); - }); -}); diff --git a/test/serialization.test.mjs b/test/serialization.test.mjs deleted file mode 100644 index 9c7bc11..0000000 --- a/test/serialization.test.mjs +++ /dev/null @@ -1,159 +0,0 @@ -// Round-trip tests for the custom wire serializer — the single most fragile, -// highest-blast-radius mechanism in the codebase (every networked message goes -// through it, and dispatch is keyed on class name). We exercise the BUILT shared -// bundle (shared/lib/main.js) rather than the TS source, so the decorators are -// already applied exactly as they ship and there is no transform/ESM ambiguity. -import { describe, it, expect } from 'vitest'; -import { createRequire } from 'node:module'; - -const require = createRequire(import.meta.url); -const shared = require('../shared/lib/main.js'); - -const { - serialize, - deserialize, - Circle, - ServerAnnouncement, - MoveActionCommand, - UpdatePropertyCommand, - PropertyUpdatesForObject, - // networked entity bases — their toArray() must mirror their constructor order - CharacterBase, - PlanetBase, - ProjectileBase, - LampBase, - // geometry — single source of truth shared by server & client prediction - headRadius, - feetRadius, - boundRadius, - headOffset, - leftFootOffset, - rightFootOffset, -} = shared; - -describe('serialization round-trip (built shared lib)', () => { - it('reconstructs a ServerAnnouncement as the right class with its payload', () => { - const [out] = deserialize(serialize([new ServerAnnouncement('Hello world')])); - expect(out).toBeInstanceOf(ServerAnnouncement); - expect(out.text).toBe('Hello world'); - }); - - it('round-trips a Circle and rounds floats to 3 decimals', () => { - const out = deserialize(serialize(new Circle([12.34567, -7.1], 5.43219))); - expect(out).toBeInstanceOf(Circle); - // serialize.ts rounds every float via toFixed(3) - expect(out.center[0]).toBeCloseTo(12.346, 6); - expect(out.center[1]).toBeCloseTo(-7.1, 6); - expect(out.radius).toBeCloseTo(5.432, 6); - }); - - it('keeps integer clientTimeMs exact (the predictor matches on it)', () => { - const t = 1718000000123; - const out = deserialize(serialize(new MoveActionCommand([1, 0], t))); - expect(out).toBeInstanceOf(MoveActionCommand); - expect(out.clientTimeMs).toBe(t); - }); - - it('round-trips nested property-update commands', () => { - const out = deserialize( - serialize( - new PropertyUpdatesForObject(42, [new UpdatePropertyCommand('strength', 80, 8)]), - ), - ); - expect(out).toBeInstanceOf(PropertyUpdatesForObject); - expect(out.id).toBe(42); - expect(out.updates[0]).toBeInstanceOf(UpdatePropertyCommand); - expect(out.updates[0].propertyKey).toBe('strength'); - expect(out.updates[0].propertyValue).toBe(80); - }); - - it('preserves per-item class identity across a mixed batch', () => { - // A clobbered name→constructor mapping (two classes sharing a name) would - // surface here as an item deserializing to the wrong class. - const batch = [ - new ServerAnnouncement('a'), - new MoveActionCommand([0, 1], 5), - new Circle([1, 2], 3), - ]; - const out = deserialize(serialize(batch)); - expect(out[0]).toBeInstanceOf(ServerAnnouncement); - expect(out[1]).toBeInstanceOf(MoveActionCommand); - expect(out[2]).toBeInstanceOf(Circle); - }); -}); - -describe('networked entity round-trips (toArray ↔ constructor contract)', () => { - it('round-trips a ProjectileBase', () => { - const out = deserialize(serialize(new ProjectileBase(7, [10, 20], 30, 'blue', 40))); - expect(out).toBeInstanceOf(ProjectileBase); - expect(out.id).toBe(7); - expect(out.center[0]).toBeCloseTo(10, 5); - expect(out.center[1]).toBeCloseTo(20, 5); - expect(out.radius).toBe(30); - expect(out.team).toBe('blue'); - expect(out.strength).toBe(40); - }); - - it('round-trips a CharacterBase with its nested body Circles', () => { - const out = deserialize( - serialize( - new CharacterBase( - 8, - 'Bob', - 2, - 1, - 'red', - 100, - new Circle([1, 2], 50), - new Circle([3, 4], 20), - new Circle([5, 6], 20), - ), - ), - ); - expect(out).toBeInstanceOf(CharacterBase); - expect(out.id).toBe(8); - expect(out.name).toBe('Bob'); - expect(out.killCount).toBe(2); - expect(out.deathCount).toBe(1); - expect(out.team).toBe('red'); - expect(out.health).toBe(100); - expect(out.head).toBeInstanceOf(Circle); - expect(out.head.center[0]).toBeCloseTo(1, 5); - expect(out.head.radius).toBe(50); - }); - - it('round-trips a LampBase', () => { - const out = deserialize(serialize(new LampBase(9, [7, 8], [1, 0.5, 0.2], 0.8))); - expect(out).toBeInstanceOf(LampBase); - expect(out.center[1]).toBeCloseTo(8, 5); - expect(out.color[2]).toBeCloseTo(0.2, 5); - expect(out.lightness).toBeCloseTo(0.8, 5); - }); - - it('round-trips a PlanetBase (derived centre/radius recomputed from vertices)', () => { - const out = deserialize( - serialize(new PlanetBase(10, [[0, 0], [100, 0], [50, 100]], 0.7, true)), - ); - expect(out).toBeInstanceOf(PlanetBase); - expect(out.id).toBe(10); - expect(out.vertices).toHaveLength(3); - expect(out.ownership).toBeCloseTo(0.7, 5); - expect(out.isKeystone).toBe(true); - expect(out.center[0]).toBeCloseTo(50, 5); // recomputed by the constructor - }); -}); - -describe('shared character geometry — single source of truth', () => { - it('matches the known body layout', () => { - expect(headRadius).toBe(50); - expect(feetRadius).toBe(20); - expect(boundRadius).toBe((headRadius + feetRadius * 2) * 2); // 180 - }); - - it('posture offsets are measured from the centre of mass (they sum to ~0)', () => { - const sx = headOffset[0] + leftFootOffset[0] + rightFootOffset[0]; - const sy = headOffset[1] + leftFootOffset[1] + rightFootOffset[1]; - expect(sx).toBeCloseTo(0, 4); - expect(sy).toBeCloseTo(0, 4); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 4b59984..6b26fb4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "sourceMap": true, + "declaration": true, + "declarationMap": true, "target": "es6", "esModuleInterop": true, "strict": true,