ai
Some checks failed
Build & deploy / Build & publish server image (pull_request) Has been skipped
Build & deploy / Build & deploy website (pull_request) Failing after 4m40s

This commit is contained in:
Andras Schmelczer 2026-06-20 11:30:49 +01:00
parent b6db7e8dc7
commit d9b80b92ca
22 changed files with 563 additions and 62 deletions

View file

@ -15,10 +15,24 @@ export class RemoteCall {
}
}
// 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: string,
public readonly propertyKey: SyncPropertyKey,
public readonly propertyValue: any,
public readonly rateOfChange: any,
) {

View file

@ -1,6 +1,7 @@
import { Id } from '../../communication/id';
import { Circle } from '../../helper/circle';
import { serializable } from '../../serialization/serializable';
import { toArrayFromFields } from '../../serialization/serialized-fields';
import { GameObject } from '../game-object';
export enum CharacterTeam {
@ -11,6 +12,18 @@ export enum CharacterTeam {
@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,
@ -26,37 +39,27 @@ export class CharacterBase extends GameObject {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onShoot(strength: number) { }
public onShoot(strength: number) {}
public onLeap() { }
public onLeap() {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onHitConfirmed(charge?: number) { }
public onHitConfirmed(charge?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onKillConfirmed(victimName?: string, streak?: number, charge?: number) { }
public onKillConfirmed(victimName?: string, streak?: number, charge?: number) {}
public setHealth(health: number) {
this.health = health;
}
public onDie() { }
public onDie() {}
public setKillCount(killCount: number) {
this.killCount = killCount;
}
public toArray(): Array<any> {
return [
this.id,
this.name,
this.killCount,
this.deathCount,
this.team,
this.health,
this.head,
this.leftFoot,
this.rightFoot,
];
return toArrayFromFields(this, CharacterBase.serializedFields);
}
}

View file

@ -1,9 +1,17 @@
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,
@ -19,7 +27,6 @@ export class LampBase extends GameObject {
public setLight(color: vec3, lightness: number) {}
public toArray(): Array<any> {
const { id, center, color, lightness } = this;
return [id, center, color, lightness];
return toArrayFromFields(this, LampBase.serializedFields);
}
}

View file

@ -2,12 +2,22 @@ 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;
@ -57,6 +67,6 @@ export class PlanetBase extends GameObject {
}
public toArray(): Array<any> {
return [this.id, this.vertices, this.ownership, this.isKeystone];
return toArrayFromFields(this, PlanetBase.serializedFields);
}
}

View file

@ -1,12 +1,21 @@
import { vec2 } from 'gl-matrix';
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 ProjectileBase extends GameObject {
private static readonly serializedFields = [
'id',
'center',
'radius',
'team',
'strength',
] as const;
constructor(
id: Id,
public center: vec2,
@ -23,6 +32,6 @@ export class ProjectileBase extends GameObject {
}
public toArray(): Array<any> {
return [this.id, this.center, this.radius, this.team, this.strength];
return toArrayFromFields(this, ProjectileBase.serializedFields);
}
}

View file

@ -107,7 +107,13 @@ const springMove = (
const keepPosture = (state: CharacterMovementState) => {
const center = characterCenter(state);
springMove(state, state.leftFoot, center, leftFootOffset, settings.postureFeetStiffness);
springMove(
state,
state.leftFoot,
center,
leftFootOffset,
settings.postureFeetStiffness,
);
springMove(
state,
state.rightFoot,
@ -133,7 +139,12 @@ const carryWithRotatingPlanet = (
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.leftFoot.center = vec2.rotate(
vec2.create(),
state.leftFoot.center,
center,
angle,
);
state.rightFoot.center = vec2.rotate(
vec2.create(),
state.rightFoot.center,
@ -148,10 +159,7 @@ const carryWithRotatingPlanet = (
// 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,
) => {
export const applyLeapImpulse = (state: CharacterMovementState, moveDirection: vec2) => {
const planet = state.currentPlanet;
if (!planet) {
return;
@ -206,7 +214,9 @@ export const tickPlanetDetachment = (
state: CharacterMovementState,
deltaTimeInSeconds: number,
) => {
if ((state.secondsSinceOnSurface += deltaTimeInSeconds) > settings.planetDetachmentSeconds) {
if (
(state.secondsSinceOnSurface += deltaTimeInSeconds) > settings.planetDetachmentSeconds
) {
state.currentPlanet = undefined;
}
};
@ -255,10 +265,7 @@ export const decayMomentum = (
}
};
const decayBodyMomentum = (
state: CharacterMovementState,
deltaTimeInSeconds: number,
) => {
const decayBodyMomentum = (state: CharacterMovementState, deltaTimeInSeconds: number) => {
decayMomentum(state.bodyVelocity, !!state.currentPlanet, deltaTimeInSeconds);
};
@ -293,7 +300,11 @@ export const stepCharacterMovement = (
const center = characterCenter(state);
const grounds = world.groundsNear(center, boundRadius + settings.maxGravityDistance);
const movementForce = vec2.scale(inputDirection, inputDirection, settings.maxAcceleration);
const movementForce = vec2.scale(
inputDirection,
inputDirection,
settings.maxAcceleration,
);
applyForce(state.leftFoot, movementForce, deltaTimeInSeconds);
applyForce(state.rightFoot, movementForce, deltaTimeInSeconds);

View file

@ -0,0 +1,13 @@
// 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<string>,
): Array<any> => fields.map((field) => object[field]);