GLM
This commit is contained in:
parent
b9274a42c7
commit
5017ff168a
53 changed files with 1004 additions and 2007 deletions
|
|
@ -1,3 +0,0 @@
|
|||
export type CommandExecutors = {
|
||||
[type: string]: (command: any) => unknown;
|
||||
};
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import { CommandExecutors } from './command-executors';
|
||||
import { Command } from './command';
|
||||
|
||||
export type CommandExecutors = {
|
||||
[type: string]: (command: any) => unknown;
|
||||
};
|
||||
|
||||
export abstract class CommandReceiver {
|
||||
protected commandExecutors: CommandExecutors = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { serializable } from '../../../serialization/serializable';
|
||||
import { Command } from '../../command';
|
||||
|
||||
// Appended to every client -> server batch. Movement is only transmitted when it
|
||||
// changes, so on its own the server's "newest input I have applied" marker
|
||||
// freezes whenever a key is simply held down — which pins the client predictor's
|
||||
// replay window and stops reconciliation from covering the un-acknowledged
|
||||
// input. This carries the client's wall-clock for the batch, so the marker keeps
|
||||
// advancing at the send rate whether or not the input changed.
|
||||
// Appended to every client -> server batch. Movement is only sent on change,
|
||||
// so without this the server's "newest input applied" marker freezes while a
|
||||
// key is held, pinning the predictor's replay window. This carries the client's
|
||||
// wall-clock so the marker advances at the send rate regardless of input change.
|
||||
@serializable
|
||||
export class ClientHeartbeatCommand extends Command {
|
||||
public constructor(public readonly clientTimeMs: number = 0) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
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.
|
||||
// clientTimeMs is 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) {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ 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.
|
||||
// clientTimeMs (integer ms, survives the serializer's toFixed(3)) is echoed
|
||||
// back via InputAcknowledgement so the client knows how much of its input
|
||||
// timeline is baked into a snapshot and can replay the rest. Defaults to 0
|
||||
// for inputs the server itself synthesises.
|
||||
public constructor(
|
||||
public readonly direction: vec2,
|
||||
public readonly clientTimeMs: number = 0,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,9 @@ export class PrimaryActionCommand extends Command {
|
|||
public constructor(
|
||||
public readonly position: vec2,
|
||||
public readonly charge: number = 0,
|
||||
// Client wall-clock at the moment the shot was released. Appended last so
|
||||
// the existing wire order is unchanged. The server uses it (together with
|
||||
// the measured RTT) to fast-forward the projectile by the time the command
|
||||
// spent in flight, so the shooter does not have to lead by their own
|
||||
// latency on top of the projectile's travel time.
|
||||
// Client wall-clock at shot release. Appended last so the existing wire
|
||||
// order is unchanged. The server fast-forwards the projectile by its
|
||||
// in-flight time so the shooter need not lead by their own latency.
|
||||
public readonly clientTimeMs: number = 0,
|
||||
) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Id } from '../../communication/id';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
// "The round is over" — a bare signal, like GameStartCommand. The winner is
|
||||
// announced through ServerAnnouncement and the end-card length is the server's
|
||||
// own restart timer, so neither has to travel here.
|
||||
@serializable
|
||||
export class GameEndCommand extends Command {
|
||||
public toArray(): Array<any> {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,25 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Id } from '../../communication/id';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
/**
|
||||
* Everything the character's movement simulation carries from one tick to the
|
||||
* next that is *not* in the pose.
|
||||
* Movement state carried tick-to-tick that is NOT in the pose. Together with
|
||||
* the pose (which travels as ordinary property updates) this is the complete
|
||||
* state `stepCharacterMovement` needs, so the predictor resumes the server's
|
||||
* simulation rather than approximating it.
|
||||
*
|
||||
* The pose (head/feet circles) travels as ordinary property updates because
|
||||
* every viewer needs it; this travels to the owning client only. Together they
|
||||
* are the complete state `stepCharacterMovement` needs, so the predictor can
|
||||
* resume the server's simulation rather than approximate it.
|
||||
*
|
||||
* It all has to be here. `direction` in particular is not recoverable from the
|
||||
* pose: the body is sprung *towards* the posture that direction implies and
|
||||
* lags it while walking, so deriving the angle back out of a walking pose is
|
||||
* off by tens of degrees — and every replay then spent its first ticks
|
||||
* twisting the body back, by an amount that varied with the replay length.
|
||||
* `direction` is not recoverable from the pose: the body is sprung *towards*
|
||||
* the posture that direction implies and lags it while walking, so deriving the
|
||||
* angle back out is off by tens of degrees.
|
||||
*/
|
||||
@serializable
|
||||
export class CharacterMovementSnapshot {
|
||||
public constructor(
|
||||
public readonly direction: number,
|
||||
// Persistent launch momentum (leap / slingshot / recoil), so the predictor
|
||||
// reproduces a flight rather than only snapping to it.
|
||||
public readonly bodyVelocity: vec2,
|
||||
// Foot contact normals: the on-surface branch projects gravity onto these,
|
||||
// so a body standing on a slope needs the real ones.
|
||||
public readonly leftFootNormal: vec2,
|
||||
public readonly rightFootNormal: vec2,
|
||||
// The planet being stood on, or null while airborne.
|
||||
public readonly groundPlanetId: Id,
|
||||
public readonly secondsSinceOnSurface: number,
|
||||
) {}
|
||||
|
|
@ -46,28 +36,21 @@ export class CharacterMovementSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
// Sent server -> owning client only, alongside that client's own character
|
||||
// snapshot. Carries the clientTimeMs of the most recent 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 movement state the replay resumes from. See
|
||||
// LocalCharacterPredictor.
|
||||
// Sent server -> owning client only. Carries the clientTimeMs of the most
|
||||
// recent input the server had received when the snapshot was taken (so the
|
||||
// predictor replays only the inputs the server hasn't seen) and the movement
|
||||
// state the replay resumes from. See LocalCharacterPredictor.
|
||||
@serializable
|
||||
export class InputAcknowledgement extends Command {
|
||||
public constructor(
|
||||
public readonly clientTimeMs: number,
|
||||
public readonly movement: CharacterMovementSnapshot,
|
||||
// clientTimeMs of the most recent leap the server has received: any leap at
|
||||
// or before it is already reflected in movement.bodyVelocity, so the
|
||||
// predictor must not replay it.
|
||||
// or before it is already in movement.bodyVelocity and must not be replayed.
|
||||
public readonly lastLeapClientTimeMs: number,
|
||||
// How long before this snapshot the acknowledged input arrived. The client
|
||||
// sends at a fixed cadence, so by the time a snapshot is taken the newest
|
||||
// input it has is already 0..1 send interval old — and that age varies with
|
||||
// where the client's sends happened to fall between two snapshots. Adding
|
||||
// it back places the replay anchor at the snapshot instant itself instead
|
||||
// of at whichever input happened to land last, which is what keeps the
|
||||
// predicted pose free of the send-cadence beat. See LocalCharacterPredictor.
|
||||
// Age of the acknowledged input at snapshot time. Adding it back places the
|
||||
// replay anchor at the snapshot instant instead of whichever input landed
|
||||
// last, keeping the predicted pose free of the send-cadence beat.
|
||||
public readonly ackAgeMs: number = 0,
|
||||
) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import { Command } from '../command';
|
|||
export class PropertyUpdatesForObjects extends Command {
|
||||
constructor(
|
||||
public readonly updates: Array<PropertyUpdatesForObject>,
|
||||
// Seconds on the server's monotonic clock at the moment this state was
|
||||
// captured. Only differences between timestamps are meaningful.
|
||||
// Only differences between timestamps are meaningful.
|
||||
public readonly timestamp: number,
|
||||
) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Id } from '../../communication/id';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { CharacterTeam } from '../../objects/types/character-base';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
|
|
|||
20
shared/src/communication/communication.ts
Normal file
20
shared/src/communication/communication.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export type Id = number | null;
|
||||
|
||||
let currentId = 0;
|
||||
export const id = (): number => {
|
||||
return currentId++;
|
||||
};
|
||||
|
||||
export interface PlayerInformation {
|
||||
name: string;
|
||||
reconnectToken?: string;
|
||||
}
|
||||
|
||||
export interface ServerInformation {
|
||||
playerLimit: number;
|
||||
playerCount: number;
|
||||
serverName: string;
|
||||
gameStatePercent: number;
|
||||
}
|
||||
|
||||
export const serverInformationEndpoint = '/state';
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export type Id = number | null;
|
||||
|
||||
let currentId = 0;
|
||||
|
||||
export const id = (): number => {
|
||||
return currentId++;
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export interface PlayerInformation {
|
||||
name: string;
|
||||
// Issued by the server on a previous join. Presented on reconnect to reclaim
|
||||
// the team and score held during the grace window; absent on a first join.
|
||||
reconnectToken?: string;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export interface ServerInformation {
|
||||
playerLimit: number;
|
||||
playerCount: number;
|
||||
serverName: string;
|
||||
gameStatePercent: number;
|
||||
}
|
||||
|
||||
export const serverInformationEndpoint = '/state';
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
declare global {
|
||||
// `T` must stay to merge with the global `Array<T>` (otherwise TS2428), but it
|
||||
// is unused by the x/y aliases below.
|
||||
// `T` must stay to merge with the global `Array<T>` (otherwise TS2428).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Array<T> {
|
||||
x: number;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Rectangle, settings } from '../main';
|
||||
import { settings } from '../main';
|
||||
|
||||
export const calculateViewArea = (
|
||||
center: vec2,
|
||||
aspectRatio: number,
|
||||
oversizeRatio = 1,
|
||||
): Rectangle => {
|
||||
const viewArea = new Rectangle();
|
||||
|
||||
viewArea.size = vec2.fromValues(
|
||||
) => {
|
||||
const size = vec2.fromValues(
|
||||
Math.sqrt(settings.inViewAreaSize * oversizeRatio * aspectRatio),
|
||||
Math.sqrt((settings.inViewAreaSize * oversizeRatio) / aspectRatio),
|
||||
);
|
||||
|
||||
viewArea.topLeft = vec2.fromValues(
|
||||
center[0] - viewArea.size[0] / 2,
|
||||
center[1] + viewArea.size[1] / 2,
|
||||
);
|
||||
const topLeft = vec2.fromValues(center[0] - size[0] / 2, center[1] + size[1] / 2);
|
||||
|
||||
return viewArea;
|
||||
return { topLeft, size };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import { settings } from '../settings';
|
|||
export const holdDurationToCharge = (heldSeconds: number): number =>
|
||||
clamp01(heldSeconds / settings.chargeShotFullHoldSeconds);
|
||||
|
||||
// The charge a shot of this strength represents — the inverse of the mix() the
|
||||
// server uses to size a projectile. The server needs it to scale radius, speed
|
||||
// and recoil; the client needs the same number to pitch the sound and the
|
||||
// muzzle flash, so it lives here rather than being derived twice.
|
||||
// Inverse of the server's projectile mix(); both sides need this number, so it
|
||||
// lives here rather than being derived twice.
|
||||
export const strengthToCharge = (strength: number): number =>
|
||||
clamp01(
|
||||
(strength - settings.chargeShotStrengthMin) /
|
||||
|
|
|
|||
|
|
@ -48,10 +48,8 @@ export const finiteVec2 = (value: unknown, maxMagnitude: number): vec2 | undefin
|
|||
};
|
||||
|
||||
/**
|
||||
* A display name that is safe to store and re-serialize. Anything that is not a
|
||||
* string is coerced, because a non-string survives `.slice()` — an array name
|
||||
* round-trips through the serializer and is revived as a class on every peer
|
||||
* that receives it, throwing inside the deserializer's reviver.
|
||||
* Coerces non-strings: a non-string survives `.slice()` and round-trips through
|
||||
* the serializer revived as a class on every peer, throwing in the reviver.
|
||||
*/
|
||||
export const sanitizeName = (value: unknown, maxLength: number): string => {
|
||||
const text = typeof value === 'string' ? value : String(value ?? '');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
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;
|
||||
|
|
@ -31,5 +29,5 @@ export const hsl = (hue: number, saturation: number, lightness: number): vec3 =>
|
|||
b = hue2rgb(p, q, hue - 1 / 3);
|
||||
}
|
||||
|
||||
return rgb(r, g, b);
|
||||
return vec3.fromValues(r, g, b);
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue