From c074bc2645fa8cf67f72740b654348ef77491d3c Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Mon, 14 Sep 2026 08:04:19 +0100 Subject: [PATCH] claude --- backend/src/game-events.ts | 13 ++++++++++ backend/src/main.ts | 9 +------ backend/src/objects/character-physical.ts | 24 +++++++------------ backend/src/objects/circle-physical.ts | 12 ++-------- backend/src/objects/planet-physical.ts | 16 ++++++++----- backend/src/objects/projectile-physical.ts | 9 +++---- backend/src/physics/bounding-box.ts | 8 +++++++ backend/src/physics/physical.ts | 7 ++---- backend/src/players/npc.ts | 14 +++++++---- backend/src/players/player-base.ts | 14 ++++++----- backend/src/players/player-container.ts | 15 ++++++++---- backend/src/players/player.ts | 22 +++++++---------- frontend/src/scripts/charge-indicator.ts | 4 ++-- .../src/scripts/commands/input-generator.ts | 2 +- .../src/scripts/commands/keyboard-listener.ts | 3 ++- .../src/scripts/commands/mouse-listener.ts | 9 +++---- .../src/scripts/commands/touch-listener.ts | 13 ++++++---- frontend/src/scripts/feedback-hud.ts | 4 ++-- frontend/src/scripts/game.ts | 21 +++++++++------- .../src/scripts/helper/centered-transform.ts | 2 ++ frontend/src/scripts/helper/pointer.ts | 8 ++++++- .../prediction/local-character-predictor.ts | 4 ++-- .../scripts/objects/game-object-container.ts | 13 +++++----- frontend/src/scripts/objects/types/camera.ts | 2 +- .../scripts/objects/types/character-view.ts | 11 ++++++--- .../src/scripts/objects/types/planet-view.ts | 6 ++++- frontend/src/scripts/tutorial.ts | 6 ++--- shared/src/commands/command-receiver.ts | 4 ++-- shared/src/commands/command.ts | 10 +------- .../types/actions/client-heartbeat.ts | 2 +- .../src/commands/types/actions/leap-action.ts | 2 +- .../src/commands/types/actions/move-action.ts | 2 +- .../commands/types/actions/primary-action.ts | 4 ++-- shared/src/helper/charge.ts | 5 +--- shared/src/objects/game-object.ts | 4 +--- shared/src/objects/types/planet-base.ts | 13 ++++++---- shared/src/physics/evaluate-sdf.ts | 2 +- shared/src/settings.ts | 2 -- test/backpressure.test.ts | 5 ++-- 39 files changed, 178 insertions(+), 148 deletions(-) create mode 100644 frontend/src/scripts/helper/centered-transform.ts diff --git a/backend/src/game-events.ts b/backend/src/game-events.ts index 58475fc..e4ae22b 100644 --- a/backend/src/game-events.ts +++ b/backend/src/game-events.ts @@ -1,4 +1,17 @@ +import { CharacterTeam } from 'shared'; + export interface GameEvents { addPoints(blue: number, red: number): void; announce(text: string): void; } + +export const addPointsForTeam = ( + game: GameEvents, + team: CharacterTeam, + points: number, +): void => { + game.addPoints( + team === CharacterTeam.blue ? points : 0, + team === CharacterTeam.red ? points : 0, + ); +}; diff --git a/backend/src/main.ts b/backend/src/main.ts index 1f7115e..60a92fe 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -32,14 +32,7 @@ const io = new IoServer(server, { const gameServer = new GameServer(io, options); -app.use( - cors({ - origin: (_, callback) => { - callback(null, true); - }, - credentials: true, - }), -); +app.use(cors({ origin: true, credentials: true })); app.get(serverInformationEndpoint, (_, res) => { res.json(gameServer.serverInfo); diff --git a/backend/src/objects/character-physical.ts b/backend/src/objects/character-physical.ts index fd870d0..4d847e6 100644 --- a/backend/src/objects/character-physical.ts +++ b/backend/src/objects/character-physical.ts @@ -13,6 +13,7 @@ import { clamp01, strengthToCharge, stepCharacterMovement, + applyForce, applyLeapImpulse, decayMomentum, tickPlanetDetachment, @@ -27,12 +28,13 @@ import { rightFootOffset, boundRadius, } from 'shared'; +import { addPointsForTeam } from '../game-events'; import { Physical } from '../physics/physical'; import { CirclePhysical } from './circle-physical'; import { PhysicalContainer } from '../physics/physical-container'; import { BoundingBox } from '../physics/bounding-box'; import { ProjectilePhysical } from './projectile-physical'; -import { PlanetPhysical, planetsIn } from './planet-physical'; +import { PlanetPhysical, planetsNear } from './planet-physical'; const placeholder = new Circle(vec2.create(), 0); @@ -99,8 +101,7 @@ export class CharacterPhysical extends CharacterBase implements Physical { } private readonly movementWorld: CharacterWorld = { - groundsNear: (center, radius) => - planetsIn(this.container.findIntersecting(BoundingBox.ofCircle(center, radius))), + groundsNear: (center, radius) => planetsNear(this.container, center, radius), stepBody: (body, deltaTimeInSeconds) => { const hit = (body as CirclePhysical).stepManually(deltaTimeInSeconds); return hit instanceof PlanetPhysical ? hit : undefined; @@ -139,12 +140,7 @@ export class CharacterPhysical extends CharacterBase implements Physical { } public get boundingBox(): BoundingBox { - const center = this.center; - this.box.xMin = center[0] - boundRadius; - this.box.xMax = center[0] + boundRadius; - this.box.yMin = center[1] - boundRadius; - this.box.yMax = center[1] + boundRadius; - return this.box; + return this.box.setCircle(this.center, boundRadius); } public get gameObject(): this { @@ -309,11 +305,9 @@ export class CharacterPhysical extends CharacterBase implements Physical { this.remoteCall('onDie'); if (killedInCombat) { - const points = settings.playerKillPoint; - this.container.game.addPoints( - this.team === CharacterTeam.blue ? 0 : points, - this.team === CharacterTeam.red ? 0 : points, - ); + const opposingTeam = + this.team === CharacterTeam.blue ? CharacterTeam.red : CharacterTeam.blue; + addPointsForTeam(this.container.game, opposingTeam, settings.playerKillPoint); } } @@ -427,7 +421,7 @@ export class CharacterPhysical extends CharacterBase implements Physical { let grounded = false; for (const part of this.parts) { vec2.copy(part.velocity, this.bodyVelocity); - part.applyForce(sumGravity(planets, part.center), deltaTime); + applyForce(part, sumGravity(planets, part.center), deltaTime); if (part.stepManually(deltaTime) instanceof PlanetPhysical) { grounded = true; } diff --git a/backend/src/objects/circle-physical.ts b/backend/src/objects/circle-physical.ts index 7cccc47..1b70122 100644 --- a/backend/src/objects/circle-physical.ts +++ b/backend/src/objects/circle-physical.ts @@ -1,5 +1,5 @@ import { vec2 } from 'gl-matrix'; -import { applyForce, Circle, GameObject, resolveCircleMovement } from 'shared'; +import { Circle, GameObject, resolveCircleMovement } from 'shared'; import { BoundingBox } from '../physics/bounding-box'; import { PhysicalContainer } from '../physics/physical-container'; import { Physical } from '../physics/physical'; @@ -22,11 +22,7 @@ export class CirclePhysical extends Circle implements Physical { } public get boundingBox(): BoundingBox { - this.box.xMin = this.center[0] - this.radius; - this.box.xMax = this.center[0] + this.radius; - this.box.yMin = this.center[1] - this.radius; - this.box.yMax = this.center[1] + this.radius; - return this.box; + return this.box.setCircle(this.center, this.radius); } public get gameObject(): GameObject { @@ -37,10 +33,6 @@ export class CirclePhysical extends Circle implements Physical { this.owner.onCollision?.(other); } - public applyForce(force: vec2, timeInSeconds: number) { - applyForce(this, force, timeInSeconds); - } - public stepManually( deltaTimeInSeconds: number, possibleIntersectors?: Array, diff --git a/backend/src/objects/planet-physical.ts b/backend/src/objects/planet-physical.ts index 950a548..7afdc3d 100644 --- a/backend/src/objects/planet-physical.ts +++ b/backend/src/objects/planet-physical.ts @@ -10,15 +10,23 @@ import { PropertyUpdatesForObject, UpdatePropertyCommand, } from 'shared'; -import { GameEvents } from '../game-events'; +import { addPointsForTeam, GameEvents } from '../game-events'; import { BoundingBox } from '../physics/bounding-box'; import { Physical } from '../physics/physical'; +import { PhysicalContainer } from '../physics/physical-container'; import { LampPhysical } from './lamp-physical'; import type { CharacterPhysical } from './character-physical'; export const planetsIn = (objects: Array): Array => objects.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical); +export const planetsNear = ( + container: PhysicalContainer, + center: vec2, + radius: number, +): Array => + planetsIn(container.findIntersecting(BoundingBox.ofCircle(center, radius))); + export class PlanetPhysical extends PlanetBase implements Physical { public readonly boundingBox: BoundingBox; public readonly sizePointMultiplier: number; @@ -80,11 +88,7 @@ export class PlanetPhysical extends PlanetBase implements Physical { } private awardToOwner(base: number) { - const value = Math.round(base * this.sizePointMultiplier); - this.game.addPoints( - this.team === CharacterTeam.blue ? value : 0, - this.team === CharacterTeam.red ? value : 0, - ); + addPointsForTeam(this.game, this.team, Math.round(base * this.sizePointMultiplier)); } private resolveCapture(deltaTime: number) { diff --git a/backend/src/objects/projectile-physical.ts b/backend/src/objects/projectile-physical.ts index fd8496c..ce0e2bd 100644 --- a/backend/src/objects/projectile-physical.ts +++ b/backend/src/objects/projectile-physical.ts @@ -84,10 +84,11 @@ export class ProjectilePhysical extends ProjectileBase implements Physical { } public onCollision(other: GameObject) { - if ( - !(other instanceof CharacterPhysical && other.team === this.team) && - this.bounceCount++ === settings.projectileMaxBounceCount - ) { + if (other instanceof CharacterPhysical && other.team === this.team) { + return; + } + this.bounceCount++; + if (this.bounceCount > settings.projectileMaxBounceCount) { this.destroy(); } } diff --git a/backend/src/physics/bounding-box.ts b/backend/src/physics/bounding-box.ts index 0cc6477..feb0d7a 100644 --- a/backend/src/physics/bounding-box.ts +++ b/backend/src/physics/bounding-box.ts @@ -17,6 +17,14 @@ export class BoundingBox { ); } + public setCircle(center: vec2, radius: number): BoundingBox { + this.xMin = center[0] - radius; + this.xMax = center[0] + radius; + this.yMin = center[1] - radius; + this.yMax = center[1] + radius; + return this; + } + public intersects(other: BoundingBox): boolean { return ( this.xMin < other.xMax && diff --git a/backend/src/physics/physical.ts b/backend/src/physics/physical.ts index 4e3fc1a..65d6cb2 100644 --- a/backend/src/physics/physical.ts +++ b/backend/src/physics/physical.ts @@ -1,13 +1,10 @@ -import { vec2 } from 'gl-matrix'; -import { GameObject } from 'shared'; +import { GameObject, Sdf } from 'shared'; import { BoundingBox } from './bounding-box'; -export interface Physical { - readonly canCollide: boolean; +export interface Physical extends Sdf { readonly boundingBox: BoundingBox; readonly gameObject: GameObject; - distance(target: vec2): number; step?(deltaTimeInSeconds: number): void; onCollision?(other: GameObject): void; } diff --git a/backend/src/players/npc.ts b/backend/src/players/npc.ts index 3fa8bef..d5ee0b8 100644 --- a/backend/src/players/npc.ts +++ b/backend/src/players/npc.ts @@ -1,5 +1,12 @@ import { vec2 } from 'gl-matrix'; -import { PlayerInformation, settings, Random, CharacterTeam, Id } from 'shared'; +import { + PlayerInformation, + settings, + Random, + CharacterTeam, + Id, + evaluateSdf, +} from 'shared'; import { PhysicalContainer } from '../physics/physical-container'; import { BoundingBox } from '../physics/bounding-box'; import { PlayerContainer } from './player-container'; @@ -321,10 +328,7 @@ export class NPC extends PlayerBase { let traveled = tuning.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)); - } + const sdf = evaluateSdf(position, planets); if (sdf < tuning.lineOfSightClearance) { return false; } diff --git a/backend/src/players/player-base.ts b/backend/src/players/player-base.ts index 25a6751..ff3312d 100644 --- a/backend/src/players/player-base.ts +++ b/backend/src/players/player-base.ts @@ -12,7 +12,7 @@ import { import { PhysicalContainer } from '../physics/physical-container'; import { BoundingBox } from '../physics/bounding-box'; import { CharacterPhysical } from '../objects/character-physical'; -import { PlanetPhysical, planetsIn } from '../objects/planet-physical'; +import { PlanetPhysical, planetsNear } from '../objects/planet-physical'; import { PlayerContainer } from './player-container'; const maximumNameLength = 40; @@ -65,7 +65,9 @@ export abstract class PlayerBase extends CommandReceiver { return this.character; } - public abstract step(deltaTimeInSeconds: number): void; + public step(deltaTimeInSeconds: number) { + this.stepLifecycle(deltaTimeInSeconds); + } protected stepLifecycle(deltaTimeInSeconds: number): CharacterPhysical | undefined { if (this.character) { @@ -89,10 +91,10 @@ export abstract class PlayerBase extends CommandReceiver { } private findSpawnCenter(): vec2 { - const planets = planetsIn( - this.objectContainer.findIntersecting( - BoundingBox.ofCircle(vec2.create(), settings.worldRadius * 2), - ), + const planets = planetsNear( + this.objectContainer, + vec2.create(), + settings.worldRadius * 2, ); const friendly = planets.filter((p) => p.team === this.team); diff --git a/backend/src/players/player-container.ts b/backend/src/players/player-container.ts index d82497f..7b5bac9 100644 --- a/backend/src/players/player-container.ts +++ b/backend/src/players/player-container.ts @@ -36,9 +36,7 @@ export class PlayerContainer { ); 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)), - ); + this.npcs.push(new NPC({ name }, this, this.objects, this.getTeamOfNextNpc())); } } @@ -102,8 +100,15 @@ export class PlayerContainer { this._players.forEach((p) => p.sendQueuedCommandsToClient()); } - private getTeamOfNextPlayer(isNpc = false): CharacterTeam { - const players = isNpc ? this.players : this._players; + private getTeamOfNextPlayer(): CharacterTeam { + return this.teamOfNext(this._players); + } + + private getTeamOfNextNpc(): CharacterTeam { + return this.teamOfNext(this.players); + } + + private teamOfNext(players: Array): CharacterTeam { const blueCount = players.filter((p) => p.team === CharacterTeam.blue).length; const redCount = players.filter((p) => p.team === CharacterTeam.red).length; diff --git a/backend/src/players/player.ts b/backend/src/players/player.ts index baa9f1a..e29951d 100644 --- a/backend/src/players/player.ts +++ b/backend/src/players/player.ts @@ -42,9 +42,9 @@ const minimumAspectRatio = 0.2; const maximumAspectRatio = 8; const sheddableWhenBackedUp: ReadonlyArray = [ - PropertyUpdatesForObjects.type, - UpdateMinimap.type, - InputAcknowledgement.type, + PropertyUpdatesForObjects.name, + UpdateMinimap.name, + InputAcknowledgement.name, ]; export class Player extends PlayerBase { @@ -68,7 +68,7 @@ export class Player extends PlayerBase { // Input is untrusted: one non-finite value reaching the simulation leaves a character that never dies. protected commandExecutors: CommandExecutors = { - [SetAspectRatioActionCommand.type]: (v: SetAspectRatioActionCommand) => { + [SetAspectRatioActionCommand.name]: (v: SetAspectRatioActionCommand) => { this.aspectRatio = finiteInRange( v.aspectRatio, minimumAspectRatio, @@ -76,7 +76,7 @@ export class Player extends PlayerBase { this.aspectRatio, ); }, - [MoveActionCommand.type]: (c: MoveActionCommand) => { + [MoveActionCommand.name]: (c: MoveActionCommand) => { const direction = finiteVec2(c.direction, 1); if (!direction) { return; @@ -84,7 +84,7 @@ export class Player extends PlayerBase { this.observeClientTime(c.clientTimeMs); this.character?.setMoveDirection(direction); }, - [PrimaryActionCommand.type]: (c: PrimaryActionCommand) => { + [PrimaryActionCommand.name]: (c: PrimaryActionCommand) => { const position = finiteVec2(c.position, settings.maxClientPositionMagnitude); if (!position) { return; @@ -92,7 +92,7 @@ export class Player extends PlayerBase { this.observeClientTime(c.clientTimeMs); this.character?.shootTowards(position, finiteInRange(c.charge, 0, 1, 0)); }, - [LeapActionCommand.type]: (c: LeapActionCommand) => { + [LeapActionCommand.name]: (c: LeapActionCommand) => { if (!isFiniteNumber(c.clientTimeMs)) { return; } @@ -100,7 +100,7 @@ export class Player extends PlayerBase { this.observeClientTime(c.clientTimeMs); this.character?.leap(); }, - [ClientHeartbeatCommand.type]: (c: ClientHeartbeatCommand) => + [ClientHeartbeatCommand.name]: (c: ClientHeartbeatCommand) => this.observeClientTime(c.clientTimeMs), }; @@ -150,10 +150,6 @@ export class Player extends PlayerBase { this.winnerTeam = winnerTeam; } - public step(deltaTimeInSeconds: number) { - this.stepLifecycle(deltaTimeInSeconds); - } - public queueCommandSend(command: Command) { this.commandsToBeSent.push(command); } @@ -273,7 +269,7 @@ export class Player extends PlayerBase { // Only state the next snapshot regenerates may be dropped; create/delete are one-shot. if (this.bufferedBytes > settings.maxBufferedBytesPerClient) { this.commandsToBeSent = this.commandsToBeSent.filter( - (c) => !sheddableWhenBackedUp.includes(c.type), + (c) => !sheddableWhenBackedUp.includes(c.constructor.name), ); } if (this.commandsToBeSent.length === 0) { diff --git a/frontend/src/scripts/charge-indicator.ts b/frontend/src/scripts/charge-indicator.ts index 1bad137..e068eb6 100644 --- a/frontend/src/scripts/charge-indicator.ts +++ b/frontend/src/scripts/charge-indicator.ts @@ -1,5 +1,5 @@ import { chargeHeldSince } from 'shared'; -import { pointer } from './helper/pointer'; +import { getDisplayPosition } from './helper/pointer'; let element: HTMLElement | undefined; let heldSince = 0; @@ -42,7 +42,7 @@ function updateChargeIndicator() { element.classList.toggle('full', charge >= 1); if (isFollowingPointer) { - const cursor = pointer.displayPosition; + const cursor = getDisplayPosition(); if (cursor) { element.style.left = `${cursor[0]}px`; element.style.top = `${cursor[1]}px`; diff --git a/frontend/src/scripts/commands/input-generator.ts b/frontend/src/scripts/commands/input-generator.ts index 6c8ef50..39c7759 100644 --- a/frontend/src/scripts/commands/input-generator.ts +++ b/frontend/src/scripts/commands/input-generator.ts @@ -10,7 +10,7 @@ import { localCharacterPredictor, } from '../helper/prediction/local-character-predictor'; -export abstract class InputGenerator { +export class InputGenerator { constructor(protected readonly onCommand: (command: Command) => void) {} protected sendMove(direction: vec2) { diff --git a/frontend/src/scripts/commands/keyboard-listener.ts b/frontend/src/scripts/commands/keyboard-listener.ts index 476396e..7674247 100644 --- a/frontend/src/scripts/commands/keyboard-listener.ts +++ b/frontend/src/scripts/commands/keyboard-listener.ts @@ -1,10 +1,11 @@ import { vec2 } from 'gl-matrix'; +import type { Command } from 'shared'; import { InputGenerator } from './input-generator'; export class KeyboardListener extends InputGenerator { private keysDown: Set = new Set(); - constructor(onCommand: ConstructorParameters[0]) { + constructor(onCommand: (command: Command) => void) { super(onCommand); addEventListener('keydown', this.keyDownListener); addEventListener('keyup', this.keyUpListener); diff --git a/frontend/src/scripts/commands/mouse-listener.ts b/frontend/src/scripts/commands/mouse-listener.ts index 5747a14..5055c67 100644 --- a/frontend/src/scripts/commands/mouse-listener.ts +++ b/frontend/src/scripts/commands/mouse-listener.ts @@ -1,8 +1,9 @@ import { vec2 } from 'gl-matrix'; -import { chargeHeldSince, Command } from 'shared'; -import { Game } from '../game'; +import { chargeHeldSince } from 'shared'; +import type { Command } from 'shared'; +import type { Game } from '../game'; import { ChargeIndicator } from '../charge-indicator'; -import { pointer } from '../helper/pointer'; +import { setDisplayPosition } from '../helper/pointer'; import { InputGenerator } from './input-generator'; export class MouseListener extends InputGenerator { @@ -22,7 +23,7 @@ export class MouseListener extends InputGenerator { } private mouseMoveListener = (event: MouseEvent) => { - pointer.displayPosition = vec2.fromValues(event.clientX, event.clientY); + setDisplayPosition(vec2.fromValues(event.clientX, event.clientY)); }; private mouseDownListener = (event: MouseEvent) => { diff --git a/frontend/src/scripts/commands/touch-listener.ts b/frontend/src/scripts/commands/touch-listener.ts index 5b933ab..31aa502 100644 --- a/frontend/src/scripts/commands/touch-listener.ts +++ b/frontend/src/scripts/commands/touch-listener.ts @@ -1,7 +1,9 @@ import { vec2 } from 'gl-matrix'; -import { chargeHeldSince, Command, settings } from 'shared'; -import { Game } from '../game'; +import { chargeHeldSince, settings } from 'shared'; +import type { Command } from 'shared'; +import type { Game } from '../game'; import { ChargeIndicator } from '../charge-indicator'; +import { centeredTransform } from '../helper/centered-transform'; import { InputGenerator } from './input-generator'; const deadZone = 8; @@ -104,11 +106,14 @@ export class TouchListener extends InputGenerator { this.primaryDownAt = null; ChargeIndicator.end(); this.overlay.appendChild(this.joystick); - this.joystick.style.transform = `translateX(${this.touchStartPosition[0]}px) translateY(${this.touchStartPosition[1]}px) translateX(-50%) translateY(-50%)`; + this.joystick.style.transform = centeredTransform( + this.touchStartPosition[0], + this.touchStartPosition[1], + ); } vec2.scale(delta, delta, Math.min(1, joystickMaxLength / deltaLength)); - this.joystickButton.style.transform = `translateX(${delta[0]}px) translateY(${delta[1]}px) translateX(-50%) translateY(-50%)`; + this.joystickButton.style.transform = centeredTransform(delta[0], delta[1]); if (deltaLength > deadZone) { vec2.set(delta, delta[0], -delta[1]); diff --git a/frontend/src/scripts/feedback-hud.ts b/frontend/src/scripts/feedback-hud.ts index ef7b251..4d59359 100644 --- a/frontend/src/scripts/feedback-hud.ts +++ b/frontend/src/scripts/feedback-hud.ts @@ -1,5 +1,5 @@ import { settings } from 'shared'; -import { pointer } from './helper/pointer'; +import { getDisplayPosition } from './helper/pointer'; let root: HTMLElement | undefined; let killfeed: HTMLElement | undefined; @@ -20,7 +20,7 @@ function ensureRoot(): { root: HTMLElement; killfeed: HTMLElement } { } function focusPoint(): { x: number; y: number } { - const cursor = pointer.displayPosition; + const cursor = getDisplayPosition(); if (cursor) { return { x: cursor[0], y: cursor[1] }; } diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index d4bc384..2267d64 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -34,6 +34,7 @@ import parser from 'socket.io-msgpack-parser'; import { CharacterShape } from './shapes/character-shape'; import { PlanetShape } from './shapes/planet-shape'; import { serverTimeline } from './helper/server-timeline'; +import { centeredTransform } from './helper/centered-transform'; import { localCharacterPredictor, setFrameTimeMs, @@ -79,21 +80,21 @@ export class Game extends CommandReceiver { private framesSinceLastLayoutUpdate = 0; protected commandExecutors: CommandExecutors = { - [ServerAnnouncement.type]: (c: ServerAnnouncement) => { + [ServerAnnouncement.name]: (c: ServerAnnouncement) => { this.lastAnnouncementText = c.text; this.timeSinceLastAnnouncement = 0; }, - [UpdateGameState.type]: (c: UpdateGameState) => (this.lastGameState = c), - [InputAcknowledgement.type]: (c: InputAcknowledgement) => + [UpdateGameState.name]: (c: UpdateGameState) => (this.lastGameState = c), + [InputAcknowledgement.name]: (c: InputAcknowledgement) => localCharacterPredictor.acknowledge( c.clientTimeMs, c.movement, c.lastLeapClientTimeMs, c.ackAgeMs, ), - [GameEndCommand.type]: () => (this.isEnding = true), - [UpdateMinimap.type]: (c: UpdateMinimap) => (this.lastMinimap = c), - [GameStartCommand.type]: () => this.initialize(), + [GameEndCommand.name]: () => (this.isEnding = true), + [UpdateMinimap.name]: (c: UpdateMinimap) => (this.lastMinimap = c), + [GameStartCommand.name]: () => this.initialize(), }; constructor( @@ -220,6 +221,7 @@ export class Game extends CommandReceiver { public async start(): Promise { this.initialize(); + this.resolveStarted(); const noiseTexture = await renderNoise([256, 256], 2, 1); await runAnimation( @@ -311,7 +313,6 @@ export class Game extends CommandReceiver { deltaTime: DOMHighResTimeStamp, ): boolean { this.renderer = renderer; - this.resolveStarted(); // The frame timestamp, not performance.now(): dispatch jitter would land in the replay window. setFrameTimeMs(currentTime); deltaTime /= 1000; @@ -408,6 +409,10 @@ export class Game extends CommandReceiver { deltaX = (deltaY * dx) / dy; } - this.keystoneArrow.style.transform = `translateX(${width / 2 + deltaX}px) translateY(${height / 2 + deltaY}px) translateX(-50%) translateY(-50%) rotate(${angle + Math.PI / 2}rad)`; + this.keystoneArrow.style.transform = centeredTransform( + width / 2 + deltaX, + height / 2 + deltaY, + ` rotate(${angle + Math.PI / 2}rad)`, + ); } } diff --git a/frontend/src/scripts/helper/centered-transform.ts b/frontend/src/scripts/helper/centered-transform.ts new file mode 100644 index 0000000..9496fc2 --- /dev/null +++ b/frontend/src/scripts/helper/centered-transform.ts @@ -0,0 +1,2 @@ +export const centeredTransform = (x: number, y: number, extra = ''): string => + `translateX(${x}px) translateY(${y}px) translateX(-50%) translateY(-50%)${extra}`; diff --git a/frontend/src/scripts/helper/pointer.ts b/frontend/src/scripts/helper/pointer.ts index cda73ee..e126c41 100644 --- a/frontend/src/scripts/helper/pointer.ts +++ b/frontend/src/scripts/helper/pointer.ts @@ -1,3 +1,9 @@ import { vec2 } from 'gl-matrix'; -export const pointer = { displayPosition: null as vec2 | null }; +let displayPosition: vec2 | null = null; + +export const setDisplayPosition = (position: vec2): void => { + displayPosition = position; +}; + +export const getDisplayPosition = (): vec2 | null => displayPosition; diff --git a/frontend/src/scripts/helper/prediction/local-character-predictor.ts b/frontend/src/scripts/helper/prediction/local-character-predictor.ts index 97c37cc..b3e290f 100644 --- a/frontend/src/scripts/helper/prediction/local-character-predictor.ts +++ b/frontend/src/scripts/helper/prediction/local-character-predictor.ts @@ -12,7 +12,7 @@ import { headRadius, } from 'shared'; import { ClientCharacterWorld } from './client-character-world'; -import { PlanetView } from '../../objects/types/planet-view'; +import type { PlanetView } from '../../objects/types/planet-view'; import { InputHistory } from './input-history'; const stepSeconds = settings.targetPhysicsDeltaTimeInSeconds; @@ -32,7 +32,7 @@ const makeBody = (center: vec2, radius: number, normal: vec2): PhysicsBody => ({ center: vec2.clone(center), radius, velocity: vec2.create(), - lastNormal: vec2.fromValues(normal[0], normal[1]), + lastNormal: vec2.clone(normal), restitution: 0, }); diff --git a/frontend/src/scripts/objects/game-object-container.ts b/frontend/src/scripts/objects/game-object-container.ts index f28848f..20cc31b 100644 --- a/frontend/src/scripts/objects/game-object-container.ts +++ b/frontend/src/scripts/objects/game-object-container.ts @@ -13,7 +13,7 @@ import { UpdatePropertyCommand, } from 'shared'; import { FeedbackHud } from '../feedback-hud'; -import { Game } from '../game'; +import type { Game } from '../game'; import { serverTimeline } from '../helper/server-timeline'; import { localCharacterPredictor } from '../helper/prediction/local-character-predictor'; import { Camera } from './types/camera'; @@ -36,7 +36,7 @@ export class GameObjectContainer extends CommandReceiver { private awaitingDeleteStamp: Array = []; protected commandExecutors: CommandExecutors = { - [CreatePlayerCommand.type]: (c: CreatePlayerCommand) => { + [CreatePlayerCommand.name]: (c: CreatePlayerCommand) => { this.player = c.character as CharacterView; this.player.isMainCharacter = true; this.addObject(this.player); @@ -45,18 +45,18 @@ export class GameObjectContainer extends CommandReceiver { this.wasLocalPlayerAlive = true; }, - [CreateObjectsCommand.type]: (c: CreateObjectsCommand) => + [CreateObjectsCommand.name]: (c: CreateObjectsCommand) => c.objects.forEach((o) => { this.addObject(o as View); this.awaitingCreateStamp.push(o.id); }), - [RemoteCallsForObjects.type]: (c: RemoteCallsForObjects) => + [RemoteCallsForObjects.name]: (c: RemoteCallsForObjects) => c.callsForObjects.forEach((c) => this.objects.get(c.id)?.processRemoteCalls(c.calls), ), - [PropertyUpdatesForObjects.type]: (c: PropertyUpdatesForObjects) => { + [PropertyUpdatesForObjects.name]: (c: PropertyUpdatesForObjects) => { serverTimeline.onSnapshot(c.timestamp); this.stampPending(c.timestamp); c.updates.forEach((u) => { @@ -68,7 +68,7 @@ export class GameObjectContainer extends CommandReceiver { }); }, - [DeleteObjectsCommand.type]: (c: DeleteObjectsCommand) => + [DeleteObjectsCommand.name]: (c: DeleteObjectsCommand) => c.ids.forEach((id: Id) => this.awaitingDeleteStamp.push(id)), }; @@ -80,6 +80,7 @@ export class GameObjectContainer extends CommandReceiver { public reset() { this.objects.forEach((o) => o.beforeDestroy?.()); this.objects.clear(); + this.player = undefined; this.planets = []; this.visibleFrom.clear(); this.deleteAt.clear(); diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts index ea8ee4d..ca768e4 100644 --- a/frontend/src/scripts/objects/types/camera.ts +++ b/frontend/src/scripts/objects/types/camera.ts @@ -1,7 +1,7 @@ import { vec2 } from 'gl-matrix'; import { Renderer } from 'sdf-2d'; import { calculateViewArea, followVec2, mixRgb, settings } from 'shared'; -import { Game } from '../../game'; +import type { Game } from '../../game'; import { ScreenShake } from '../../screen-shake'; const followSeconds = 0.08; diff --git a/frontend/src/scripts/objects/types/character-view.ts b/frontend/src/scripts/objects/types/character-view.ts index 2efa0ca..18e5434 100644 --- a/frontend/src/scripts/objects/types/character-view.ts +++ b/frontend/src/scripts/objects/types/character-view.ts @@ -13,9 +13,10 @@ import { strengthToCharge, UpdatePropertyCommand, } from 'shared'; +import { centeredTransform } from '../../helper/centered-transform'; import { CircleInterpolator } from '../../helper/interpolators/circle-interpolator'; import { LinearInterpolator } from '../../helper/interpolators/linear-interpolator'; -import { pointer } from '../../helper/pointer'; +import { getDisplayPosition } from '../../helper/pointer'; import { CharacterShape } from '../../shapes/character-shape'; import { SoundHandler, Sounds } from '../../sound-handler'; import { VibrationHandler } from '../../vibration-handler'; @@ -264,7 +265,11 @@ export class CharacterView extends CharacterBase implements View { this.head.radius + 80, ), ); - this.nameElement.style.transform = `translateX(${screenPosition[0]}px) translateY(${screenPosition[1]}px) translateX(-50%) translateY(-50%) rotate(-15deg)`; + this.nameElement.style.transform = centeredTransform( + screenPosition[0], + screenPosition[1], + ' rotate(-15deg)', + ); this.healthElement.style.width = (50 * this.health) / settings.playerMaxHealth + 'px'; this.chargeElement.style.width = @@ -287,7 +292,7 @@ export class CharacterView extends CharacterBase implements View { } private calculateGazeTarget(renderer: Renderer): vec2 { - const cursor = pointer.displayPosition; + const cursor = getDisplayPosition(); if (this.isMainCharacter && cursor) { return renderer.displayToWorldCoordinates(cursor); } diff --git a/frontend/src/scripts/objects/types/planet-view.ts b/frontend/src/scripts/objects/types/planet-view.ts index 28e0d47..a0ded36 100644 --- a/frontend/src/scripts/objects/types/planet-view.ts +++ b/frontend/src/scripts/objects/types/planet-view.ts @@ -8,6 +8,7 @@ import { CharacterTeam, settings, } from 'shared'; +import { centeredTransform } from '../../helper/centered-transform'; import { LinearInterpolator } from '../../helper/interpolators/linear-interpolator'; import { PlanetShape } from '../../shapes/planet-shape'; import { View } from '../view'; @@ -118,7 +119,10 @@ export class PlanetView extends PlanetBase implements View { } const screenPosition = renderer.worldToDisplayCoordinates(this.center); - this.ownershipProgress.style.transform = `translateX(${screenPosition[0]}px) translateY(${screenPosition[1]}px) translateX(-50%) translateY(-50%)`; + this.ownershipProgress.style.transform = centeredTransform( + screenPosition[0], + screenPosition[1], + ); const gradient = this.getGradient(); if (gradient !== this.lastGradient) { diff --git a/frontend/src/scripts/tutorial.ts b/frontend/src/scripts/tutorial.ts index ba047ef..e227c71 100644 --- a/frontend/src/scripts/tutorial.ts +++ b/frontend/src/scripts/tutorial.ts @@ -34,17 +34,17 @@ export class Tutorial extends CommandReceiver { private standingBaseline = 0; protected commandExecutors: CommandExecutors = { - [MoveActionCommand.type]: (c: MoveActionCommand) => { + [MoveActionCommand.name]: (c: MoveActionCommand) => { if (this.clearsOn() === 'move' && vec2.length(c.direction) > 0) { this.advance(); } }, - [PrimaryActionCommand.type]: () => { + [PrimaryActionCommand.name]: () => { if (this.clearsOn() === 'shoot') { this.advance(); } }, - [LeapActionCommand.type]: () => { + [LeapActionCommand.name]: () => { if (this.clearsOn() === 'leap') { this.advance(); } diff --git a/shared/src/commands/command-receiver.ts b/shared/src/commands/command-receiver.ts index 51f8600..447df6e 100644 --- a/shared/src/commands/command-receiver.ts +++ b/shared/src/commands/command-receiver.ts @@ -10,9 +10,9 @@ export abstract class CommandReceiver { protected defaultCommandExecutor(_: Command) {} public handleCommand(command: Command) { - const commandType = command.type; + const commandType = command.constructor.name; - if (Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType)) { + if (commandType in this.commandExecutors) { this.commandExecutors[commandType]!(command); } else { this.defaultCommandExecutor(command); diff --git a/shared/src/commands/command.ts b/shared/src/commands/command.ts index 7670416..9074445 100644 --- a/shared/src/commands/command.ts +++ b/shared/src/commands/command.ts @@ -1,9 +1 @@ -export abstract class Command { - public static get type(): string { - return this.name; - } - - public get type(): string { - return this.constructor.name; - } -} +export abstract class Command {} diff --git a/shared/src/commands/types/actions/client-heartbeat.ts b/shared/src/commands/types/actions/client-heartbeat.ts index e2e0644..af4ef01 100644 --- a/shared/src/commands/types/actions/client-heartbeat.ts +++ b/shared/src/commands/types/actions/client-heartbeat.ts @@ -3,7 +3,7 @@ import { Command } from '../../command'; @serializable export class ClientHeartbeatCommand extends Command { - public constructor(public readonly clientTimeMs: number = 0) { + public constructor(public readonly clientTimeMs: number) { super(); } diff --git a/shared/src/commands/types/actions/leap-action.ts b/shared/src/commands/types/actions/leap-action.ts index 3681de2..1cc1f54 100644 --- a/shared/src/commands/types/actions/leap-action.ts +++ b/shared/src/commands/types/actions/leap-action.ts @@ -3,7 +3,7 @@ import { Command } from '../../command'; @serializable export class LeapActionCommand extends Command { - public constructor(public readonly clientTimeMs: number = 0) { + public constructor(public readonly clientTimeMs: number) { super(); } diff --git a/shared/src/commands/types/actions/move-action.ts b/shared/src/commands/types/actions/move-action.ts index 7f2a455..ea82c43 100644 --- a/shared/src/commands/types/actions/move-action.ts +++ b/shared/src/commands/types/actions/move-action.ts @@ -6,7 +6,7 @@ import { Command } from '../../command'; export class MoveActionCommand extends Command { public constructor( public readonly direction: vec2, - public readonly clientTimeMs: number = 0, + public readonly clientTimeMs: number, ) { super(); } diff --git a/shared/src/commands/types/actions/primary-action.ts b/shared/src/commands/types/actions/primary-action.ts index ea60b20..ea78ae7 100644 --- a/shared/src/commands/types/actions/primary-action.ts +++ b/shared/src/commands/types/actions/primary-action.ts @@ -6,8 +6,8 @@ import { Command } from '../../command'; export class PrimaryActionCommand extends Command { public constructor( public readonly position: vec2, - public readonly charge: number = 0, - public readonly clientTimeMs: number = 0, + public readonly charge: number, + public readonly clientTimeMs: number, ) { super(); } diff --git a/shared/src/helper/charge.ts b/shared/src/helper/charge.ts index 80cd5c8..9842cc9 100644 --- a/shared/src/helper/charge.ts +++ b/shared/src/helper/charge.ts @@ -1,11 +1,8 @@ import { clamp01 } from './clamp'; import { settings } from '../settings'; -export const holdDurationToCharge = (heldSeconds: number): number => - clamp01(heldSeconds / settings.chargeShotFullHoldSeconds); - export const chargeHeldSince = (heldSinceMs: number): number => - holdDurationToCharge((performance.now() - heldSinceMs) / 1000); + clamp01((performance.now() - heldSinceMs) / 1000 / settings.chargeShotFullHoldSeconds); export const strengthToCharge = (strength: number): number => clamp01( diff --git a/shared/src/objects/game-object.ts b/shared/src/objects/game-object.ts index 54afd7e..6c8890c 100644 --- a/shared/src/objects/game-object.ts +++ b/shared/src/objects/game-object.ts @@ -92,9 +92,7 @@ export abstract class GameObject { } public resetRemoteCalls() { - if (this.remoteCalls.length > 0) { - this.remoteCalls = []; - } + this.remoteCalls = []; } protected remoteCall(name: string & keyof this, ...args: Array) { diff --git a/shared/src/objects/types/planet-base.ts b/shared/src/objects/types/planet-base.ts index b193100..f2ef614 100644 --- a/shared/src/objects/types/planet-base.ts +++ b/shared/src/objects/types/planet-base.ts @@ -27,11 +27,16 @@ export class PlanetBase extends GameObject implements GroundSurface { public readonly isKeystone: boolean = false, ) { super(id); - this.center = vertices.reduce((sum, v) => vec2.add(sum, sum, v), vec2.create()); + this.center = vec2.create(); + for (const vertex of vertices) { + vec2.add(this.center, this.center, vertex); + } vec2.scale(this.center, this.center, 1 / vertices.length); - this.radius = - vertices.reduce((sum, v) => sum + vec2.distance(this.center, v), 0) / - vertices.length; + let totalRadius = 0; + for (const vertex of vertices) { + totalRadius += vec2.distance(this.center, vertex); + } + this.radius = totalRadius / vertices.length; } public get team(): CharacterTeam { diff --git a/shared/src/physics/evaluate-sdf.ts b/shared/src/physics/evaluate-sdf.ts index e8f3cb5..3a51c65 100644 --- a/shared/src/physics/evaluate-sdf.ts +++ b/shared/src/physics/evaluate-sdf.ts @@ -2,7 +2,7 @@ import { vec2 } from 'gl-matrix'; import { Sdf } from './sdf'; export const evaluateSdf = (target: vec2, objects: Array): number => { - let minimum = 1000000; + let minimum = Infinity; for (let i = 0; i < objects.length; i++) { const object = objects[i]; if (object.canCollide) { diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 1a6a0fb..0227d43 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -62,7 +62,6 @@ export const settings = { chargeShotSpeedMin: 2500, chargeShotSpeedMax: 3400, backgroundGradient: [rgb255(90, 38, 43), rgb255(43, 39, 73)], - blueColor, bluePlanetColor: blueColorDim, npcNames: [ 'Adam', @@ -114,7 +113,6 @@ export const settings = { 'Will', 'Wyatt', ], - redColor, redPlanetColor: redColorDim, colorIndices: { [CharacterTeam.blue]: 0, diff --git a/test/backpressure.test.ts b/test/backpressure.test.ts index 921bf88..b82fa1d 100644 --- a/test/backpressure.test.ts +++ b/test/backpressure.test.ts @@ -33,7 +33,7 @@ class FakeSocket { this.sent.push({ event, payload }); } - public get lastBatch(): Array<{ type: string }> { + public get lastBatch(): Array<{ constructor: { name: string } }> { const last = [...this.sent] .reverse() .find((m) => m.event === TransportEvents.ServerToPlayer); @@ -61,7 +61,8 @@ const queueOneSnapshot = (player: ReturnType['player']) => { player.queueCommandSend(new PropertyUpdatesForObjects([], 1)); }; -const typesIn = (batch: Array<{ type: string }>) => batch.map((c) => c.type); +const typesIn = (batch: Array<{ constructor: { name: string } }>) => + batch.map((c) => c.constructor.name); describe('slow-client backpressure', () => { beforeEach(() => {