more
This commit is contained in:
parent
98c4d63756
commit
82e092ba55
14 changed files with 373 additions and 539 deletions
|
|
@ -1,18 +0,0 @@
|
|||
import { CommandReceiver } from './command-receiver';
|
||||
import { Command } from './command';
|
||||
|
||||
export abstract class CommandGenerator {
|
||||
private subscribers: Array<CommandReceiver> = [];
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ export class PrimaryActionCommand extends Command {
|
|||
public constructor(
|
||||
public readonly position: vec2,
|
||||
public readonly charge: number = 0,
|
||||
// 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.
|
||||
// Client wall-clock at shot release, echoed back via InputAcknowledgement
|
||||
// like the other actions' — it advances the predictor's replay anchor past
|
||||
// this shot. Appended last so the existing wire order is unchanged.
|
||||
public readonly clientTimeMs: number = 0,
|
||||
) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
declare global {
|
||||
// `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;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Float32Array {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
}
|
||||
|
||||
const setIndexAlias = (name: string, index: number, type: any) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(type.prototype, name)) {
|
||||
Object.defineProperty(type.prototype, name, {
|
||||
get() {
|
||||
return this[index];
|
||||
},
|
||||
set(value) {
|
||||
this[index] = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const applyArrayPlugins = () => {
|
||||
setIndexAlias('x', 0, Array);
|
||||
setIndexAlias('y', 1, Array);
|
||||
setIndexAlias('x', 0, Float32Array);
|
||||
setIndexAlias('y', 1, Float32Array);
|
||||
};
|
||||
14
shared/src/helper/mix-rgb.ts
Normal file
14
shared/src/helper/mix-rgb.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { vec3 } from 'gl-matrix';
|
||||
import { clamp01 } from './clamp';
|
||||
import { mix } from './mix';
|
||||
|
||||
// Component-wise lerp of two colours. Kept as a helper because the alternative
|
||||
// is the same clamp-plus-three-mixes expansion repeated at every call site.
|
||||
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),
|
||||
);
|
||||
};
|
||||
|
|
@ -25,6 +25,7 @@ export * from './helper/charge';
|
|||
export * from './helper/calculate-view-area';
|
||||
export * from './helper/circle';
|
||||
export * from './helper/mix';
|
||||
export * from './helper/mix-rgb';
|
||||
export * from './helper/random';
|
||||
export * from './communication/communication';
|
||||
export * from './objects/game-object';
|
||||
|
|
|
|||
|
|
@ -341,6 +341,10 @@ export const stepCharacterMovement = (
|
|||
|
||||
applyBodyMomentum(state);
|
||||
|
||||
// Only the feet latch. The grounded branch above projects gravity onto the
|
||||
// foot contact normals and leap() launches off them, so latching on a head
|
||||
// contact would ground the body against normals no part of it is touching —
|
||||
// a head-first tumble would read as standing.
|
||||
latchGround(state, world.stepBody(state.leftFoot, deltaTimeInSeconds));
|
||||
latchGround(state, world.stepBody(state.rightFoot, deltaTimeInSeconds));
|
||||
world.stepBody(state.head, deltaTimeInSeconds);
|
||||
|
|
|
|||
|
|
@ -49,9 +49,16 @@ export const marchCircle = (
|
|||
)!;
|
||||
|
||||
if (ignoreCollision) {
|
||||
// Pass through, but still report the contact so repeated marches keep progressing.
|
||||
// Pass through, but still report the contact so repeated marches keep
|
||||
// progressing. The normal comes along: `hitSurface: true` is the
|
||||
// contract callers read it under, and one without it would be a
|
||||
// TypeError in whichever caller first enables pass-through.
|
||||
vec2.scaleAndAdd(body.center, body.center, direction, deltaLength);
|
||||
return { hitSurface: true, hitObject: intersecting };
|
||||
return {
|
||||
hitSurface: true,
|
||||
normal: sdfNormal(rayEnd, [intersecting]),
|
||||
hitObject: intersecting,
|
||||
};
|
||||
}
|
||||
|
||||
onHit?.(intersecting);
|
||||
|
|
|
|||
|
|
@ -18,13 +18,17 @@ export const toPlanetLocalFrame = (
|
|||
|
||||
// Innermost leaf — module-level scratch to avoid per-call allocation. Nothing
|
||||
// re-enters planetDistance and none of these outlive the call.
|
||||
const localPoint = vec2.create();
|
||||
const targetFromDelta = vec2.create();
|
||||
const toFromDelta = vec2.create();
|
||||
const closestOnEdge = vec2.create();
|
||||
// Stays a vec2 (Float32Array) because rounding to f32 is part of the outline
|
||||
// the server and the client both have to agree on.
|
||||
const edgeSample = vec2.create();
|
||||
//
|
||||
// Plain array literals, NOT vec2.create(): imports are evaluated before either
|
||||
// entry point reaches its glMatrix.setMatrixArrayType(Array) call, so
|
||||
// vec2.create() here would hand back Float32Array and quietly run the one
|
||||
// outline the server and the client must agree on at f32 while every other
|
||||
// vector in the simulation is f64.
|
||||
const localPoint: vec2 = [0, 0];
|
||||
const targetFromDelta: vec2 = [0, 0];
|
||||
const toFromDelta: vec2 = [0, 0];
|
||||
const closestOnEdge: vec2 = [0, 0];
|
||||
const edgeSample: vec2 = [0, 0];
|
||||
|
||||
// The one and only planet outline: the authoritative server and the client
|
||||
// predictor both collide against it, which is what lets prediction reconcile.
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { Sdf } from './sdf';
|
|||
import { evaluateSdf } from './evaluate-sdf';
|
||||
|
||||
// Reused probe point — sdfNormal is called on every depenetration pass and
|
||||
// raymarch hit.
|
||||
const probe = vec2.create();
|
||||
// raymarch hit. A plain array literal for the same reason as planet-sdf's
|
||||
// scratch: it is allocated before setMatrixArrayType(Array) runs, and the
|
||||
// gradient must be computed at f64 like the rest of the simulation.
|
||||
const probe: vec2 = [0, 0];
|
||||
|
||||
const sampleAt = (x: number, y: number, objects: Array<Sdf>): number => {
|
||||
vec2.set(probe, x, y);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
export type SerializableClass = { new (...args: Array<any>): unknown; name: string };
|
||||
|
||||
// Wire format: the property the decorators stamp on and serialize() looks for
|
||||
// to recognise a decorated class. Defined once on purpose — per-file copies
|
||||
// would have to agree byte-for-byte, and a typo in one of them compiles and
|
||||
// type-checks cleanly while every networked object degrades to a raw array.
|
||||
export const MANGLE = '__serializable_type';
|
||||
|
||||
export const serializableMapping = new Map<string, SerializableClass>();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { SerializableClass, serializableMapping } from './serializable-mapping';
|
||||
|
||||
const MANGLE = '__serializable_type';
|
||||
import { MANGLE, SerializableClass, serializableMapping } from './serializable-mapping';
|
||||
|
||||
export const serializable = (type: SerializableClass): any => {
|
||||
if (!serializableMapping.has(type.name)) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const MANGLE = '__serializable_type';
|
||||
import { MANGLE } from './serializable-mapping';
|
||||
|
||||
export const serialize = (object: any): string => {
|
||||
return JSON.stringify(object, (_, value) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
// deserialize reconstructs via `new Ctor(...array.slice(1))`, so the field list
|
||||
// MUST name the constructor's parameters in order. Derived (non-serialized)
|
||||
// fields are deliberately absent.
|
||||
export const toArrayFromFields = (
|
||||
object: any,
|
||||
fields: ReadonlyArray<string>,
|
||||
): Array<any> => fields.map((field) => object[field]);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { SerializableClass, serializableMapping } from './serializable-mapping';
|
||||
|
||||
const MANGLE = '__serializable_type';
|
||||
|
||||
export const serializesTo = (target: SerializableClass) => {
|
||||
return (actual: SerializableClass): any => {
|
||||
if (!serializableMapping.has(target.name)) {
|
||||
serializableMapping.set(target.name, target);
|
||||
}
|
||||
|
||||
Object.defineProperty(actual, MANGLE, { value: target.name });
|
||||
Object.defineProperty(actual.prototype, MANGLE, { value: target.name });
|
||||
|
||||
return actual;
|
||||
};
|
||||
};
|
||||
Loading…
Reference in a new issue