@korial29/tiny-fps 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +177 -0
- package/README.md +8 -2
- package/dist/cli.js +7 -0
- package/dist/index.js +11 -0
- package/dist/local.js +1 -0
- package/dist/server.js +814 -0
- package/dist/types/client/effects.d.ts +37 -0
- package/dist/types/client/gun.d.ts +19 -0
- package/dist/types/client/index.d.ts +22 -0
- package/dist/types/client/input.d.ts +17 -0
- package/dist/types/client/predict.d.ts +29 -0
- package/dist/types/client/render.d.ts +34 -0
- package/dist/types/client/textures.d.ts +4 -0
- package/dist/types/local/index.d.ts +14 -0
- package/dist/types/server/cli.d.ts +2 -0
- package/dist/types/server/index.d.ts +16 -0
- package/dist/types/shared/constants.d.ts +46 -0
- package/dist/types/shared/level.d.ts +13 -0
- package/dist/types/shared/map.d.ts +6 -0
- package/dist/types/shared/physics.d.ts +20 -0
- package/dist/types/shared/protocol.d.ts +32 -0
- package/dist/types/shared/room.d.ts +59 -0
- package/dist/types/shared/shoot.d.ts +24 -0
- package/dist/types/shared/types.d.ts +156 -0
- package/package.json +87 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PlayerState, Shot } from '../shared/types';
|
|
2
|
+
type Vec3 = [number, number, number];
|
|
3
|
+
export interface Spark {
|
|
4
|
+
p: Vec3;
|
|
5
|
+
v: Vec3;
|
|
6
|
+
/** Seconds left. */
|
|
7
|
+
life: number;
|
|
8
|
+
}
|
|
9
|
+
/** What the renderer and the camera read; update and shot move it. */
|
|
10
|
+
export interface Effects {
|
|
11
|
+
/** Vertical eye offset, metres. */
|
|
12
|
+
bob: number;
|
|
13
|
+
/** Radians, positive leans right (strafing right). */
|
|
14
|
+
roll: number;
|
|
15
|
+
/** Camera yaw and pitch jitter, radians. */
|
|
16
|
+
shakeYaw: number;
|
|
17
|
+
shakePitch: number;
|
|
18
|
+
/** Recoil, 1 at a shot down to 0. */
|
|
19
|
+
kick: number;
|
|
20
|
+
/** Barrel angle, radians. */
|
|
21
|
+
spin: number;
|
|
22
|
+
/** Seconds of own muzzle flash left. */
|
|
23
|
+
flash: number;
|
|
24
|
+
/** Seconds of flash left at each other shooter, by player id. */
|
|
25
|
+
flashes: Map<number, number>;
|
|
26
|
+
sparks: Spark[];
|
|
27
|
+
/** Advances dt seconds: the own player (eased, undefined before the first snapshot) and the held keys. */
|
|
28
|
+
update(me: PlayerState | undefined, keys: number, dt: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* A shot, from the shooter's eye; own ones kick the camera and the gun. No sparks with
|
|
31
|
+
* `sparks` false: a shot that hit the viewer would burst them inside the camera.
|
|
32
|
+
*/
|
|
33
|
+
shot(s: Shot, from: Vec3, own: boolean, sparks?: boolean): void;
|
|
34
|
+
}
|
|
35
|
+
/** rand in [0, 1): injectable so the tests are exact. */
|
|
36
|
+
export declare const createEffects: (rand?: () => number) => Effects;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Vec3 = [number, number, number];
|
|
2
|
+
/** Where the barrels end, on -z: the muzzle flash sits just past it. */
|
|
3
|
+
export declare const MUZZLE = 0.55;
|
|
4
|
+
/**
|
|
5
|
+
* Revolves a profile of [radius, length] points around the axis through `at` along -z: `length`
|
|
6
|
+
* runs forward from `at`. Returns triangles as x, y, z, nx, ny, nz per vertex, `sides` quads of 2
|
|
7
|
+
* triangles per profile segment. The profile goes from back to front and closes on the axis, so
|
|
8
|
+
* the normals (smooth around the ring, flat along the profile) point outward.
|
|
9
|
+
*/
|
|
10
|
+
export declare const lathe: (profile: [number, number][], sides: number, at?: Vec3) => number[];
|
|
11
|
+
/**
|
|
12
|
+
* The chaingun, built once per level of detail: `barrels` turn around the central axis (the caller
|
|
13
|
+
* spins them), the `body` stays. Vertices as in lathe.
|
|
14
|
+
*/
|
|
15
|
+
export declare const chaingun: (sides: number) => {
|
|
16
|
+
barrels: number[];
|
|
17
|
+
body: number[];
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PlayerInfo, SocketLike } from '../shared/types';
|
|
2
|
+
export type { PlayerInfo, SocketLike } from '../shared/types';
|
|
3
|
+
export interface TinyFpsOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
/** WebSocket URL, e.g. ws://localhost:8787/demo (the path picks the room). */
|
|
6
|
+
url?: string;
|
|
7
|
+
/** Any WebSocket-like socket, e.g. localSocket('room') from '@korial29/tiny-fps/local'. */
|
|
8
|
+
socket?: SocketLike;
|
|
9
|
+
}
|
|
10
|
+
export type TinyFpsEvent = 'roster' | 'open' | 'close';
|
|
11
|
+
export interface TinyFps {
|
|
12
|
+
on(ev: TinyFpsEvent, cb: () => void): void;
|
|
13
|
+
/** Everyone in the room, the own player included. */
|
|
14
|
+
players(): PlayerInfo[];
|
|
15
|
+
/** Closes the socket, stops the loop and removes the canvas. */
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mounts the game in a Shadow DOM inside container: one WebGL2 canvas filling it, a click locks the
|
|
20
|
+
* pointer, the mouse looks, WASD (ZQSD on AZERTY) moves, Space jumps.
|
|
21
|
+
*/
|
|
22
|
+
export declare const tinyFps: (container: HTMLElement, opts: TinyFpsOptions) => TinyFps;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The KEY bit of a KeyboardEvent.code, 0 for a key the game ignores. */
|
|
2
|
+
export declare const keyBit: (code: string) => number;
|
|
3
|
+
/** Live look angles and held keys; the loop reads them once per step and per frame. */
|
|
4
|
+
export interface Controls {
|
|
5
|
+
/** Radians, [0, 2π). Mouse right turns right, which lowers yaw. */
|
|
6
|
+
yaw: number;
|
|
7
|
+
/** Radians, ±MAX_PITCH. Positive looks up. */
|
|
8
|
+
pitch: number;
|
|
9
|
+
/** KEY bits held now. */
|
|
10
|
+
keys: number;
|
|
11
|
+
/** The canvas holds the pointer lock. */
|
|
12
|
+
locked(): boolean;
|
|
13
|
+
/** Removes every listener. */
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
16
|
+
/** Click the canvas to lock the pointer; while locked the mouse looks, the keys move, the left button fires. */
|
|
17
|
+
export declare const createInput: (canvas: HTMLCanvasElement) => Controls;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Input, Level, PlayerState, Shot, Snapshot } from '../shared/types';
|
|
2
|
+
export interface Predictor {
|
|
3
|
+
/** The own player id, from the roster. Snapshots before it are only used for the others. */
|
|
4
|
+
you(id: number): void;
|
|
5
|
+
/**
|
|
6
|
+
* One local step: gives the input the next seq, rounds its yaw and pitch to the wire, moves the
|
|
7
|
+
* own player with it, keeps it for replay, and returns it for the caller to send (the very
|
|
8
|
+
* values predicted with, which the host reads back). Until the own player is known (no snapshot
|
|
9
|
+
* shows it yet) it only numbers and keeps the input: the first snapshot replays it.
|
|
10
|
+
* With `now` (ms, performance.now()) the input carries the view tick the others are drawn at.
|
|
11
|
+
*/
|
|
12
|
+
step(i: Omit<Input, 'seq' | 'view'>, now?: number): Input;
|
|
13
|
+
/**
|
|
14
|
+
* The shot the last step fired, resolved with fire() against others(now) of that step; undefined
|
|
15
|
+
* when it did not fire. Replays on a snapshot never fire again.
|
|
16
|
+
*/
|
|
17
|
+
shot(): Shot | undefined;
|
|
18
|
+
/** A decoded snapshot, received at `now` (ms, performance.now()). */
|
|
19
|
+
snapshot(s: Snapshot, now: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* The own player, eased between the state before and after the last step by alpha in [0, 1]
|
|
22
|
+
* (the share of a step elapsed since it), so the camera moves smoothly at any frame rate.
|
|
23
|
+
* Undefined until a snapshot shows the own player.
|
|
24
|
+
*/
|
|
25
|
+
me(alpha: number): PlayerState | undefined;
|
|
26
|
+
/** The other players at now - INTERP_DELAY; a player missing from the latest snapshot is gone. */
|
|
27
|
+
others(now: number): PlayerState[];
|
|
28
|
+
}
|
|
29
|
+
export declare const createPredictor: (lvl: Level) => Predictor;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Edge, Level, PlayerState } from '../shared/types';
|
|
2
|
+
import type { Effects } from './effects';
|
|
3
|
+
type Vec3 = [number, number, number];
|
|
4
|
+
/**
|
|
5
|
+
* Camera axes [right, up, forward]: forward is (-sin yaw cos pitch, sin pitch, -cos yaw cos pitch),
|
|
6
|
+
* y up at roll 0. A positive roll leans the camera right: up tilts toward right.
|
|
7
|
+
*/
|
|
8
|
+
export declare const basis: (yaw: number, pitch: number, roll?: number) => [Vec3, Vec3, Vec3];
|
|
9
|
+
/** Column-major view-projection matrix of basis(yaw, pitch, roll) at eye, 70° vertical field of view. */
|
|
10
|
+
export declare const viewProj: (eye: Vec3, yaw: number, pitch: number, aspect: number, roll?: number) => Float32Array<ArrayBuffer>;
|
|
11
|
+
/** A world point on screen, in [0, 1] from the top left; undefined behind the camera. */
|
|
12
|
+
export declare const project: (m: Float32Array, x: number, y: number, z: number) => [number, number] | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Appends the 36 vertices of a box centred on (cx, cy, cz) with half sizes (hx, hy, hz), turned
|
|
15
|
+
* by yaw about its own vertical axis (local forward, -z, ends up along forward(yaw)).
|
|
16
|
+
*/
|
|
17
|
+
export declare const pushBox: (out: number[], [cx, cy, cz]: Vec3, [hx, hy, hz]: Vec3, yaw: number, color: number[], layer?: number) => void;
|
|
18
|
+
/** Vertical spans [y0, y1] an edge draws on its own side: the wall, or the steps of a portal. */
|
|
19
|
+
export declare const spans: ({ map }: Level, { front, back }: Edge) => [number, number][];
|
|
20
|
+
/** Floors and ceilings as fans, then every edge's walls as quads, in the vertex format above. */
|
|
21
|
+
export declare const levelVerts: (lvl: Level) => number[];
|
|
22
|
+
export interface Renderer {
|
|
23
|
+
/**
|
|
24
|
+
* Draws one frame: the map, the others and the effects as seen from eye, with the view model on
|
|
25
|
+
* top. colors maps player id to CSS.
|
|
26
|
+
*/
|
|
27
|
+
frame(eye: Vec3, yaw: number, pitch: number, others: PlayerState[], colors: ReadonlyMap<number, string>, fx: Effects): void;
|
|
28
|
+
/** A world point on screen as of the last frame, in [0, 1] from the top left. */
|
|
29
|
+
project(x: number, y: number, z: number): [number, number] | undefined;
|
|
30
|
+
destroy(): void;
|
|
31
|
+
}
|
|
32
|
+
/** Throws when the browser has no WebGL2. */
|
|
33
|
+
export declare const createRenderer: (canvas: HTMLCanvasElement, lvl: Level) => Renderer;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rules, SocketLike } from '../shared/types';
|
|
2
|
+
/**
|
|
3
|
+
* Same-browser multiplayer: tabs of one origin play together over a
|
|
4
|
+
* BroadcastChannel. One tab hosts the Room (elected with navigator.locks);
|
|
5
|
+
* when it closes, another tab takes over with a fresh Room (score back to 0-0)
|
|
6
|
+
* and every socket replays its join, so the client only sees a new roster.
|
|
7
|
+
*
|
|
8
|
+
* Without navigator.locks (old browsers, or a non-secure context such as plain
|
|
9
|
+
* http on a LAN address) every tab hosts its own Room and plays alone.
|
|
10
|
+
*
|
|
11
|
+
* `rules` are the match rules of the Room this tab creates when it hosts (missing ones fall
|
|
12
|
+
* back to RULES). Only the host's count: pass the same ones from every tab.
|
|
13
|
+
*/
|
|
14
|
+
export declare const localSocket: (room: string, rules?: Partial<Rules>) => SocketLike;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import type { Rules } from '../shared/types';
|
|
3
|
+
export interface ServeOptions {
|
|
4
|
+
/** Listen on this port (ignored when server is given). */
|
|
5
|
+
port?: number;
|
|
6
|
+
/** Attach to an existing HTTP server instead. */
|
|
7
|
+
server?: Server;
|
|
8
|
+
/** Match rules for every room; missing ones fall back to RULES. */
|
|
9
|
+
rules?: Partial<Rules>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Starts the WebSocket server: one Room per URL path. Returns an idempotent stop
|
|
13
|
+
* function that closes every socket, room, the WebSocketServer, and the HTTP
|
|
14
|
+
* server only when serve created it (`{ port }`).
|
|
15
|
+
*/
|
|
16
|
+
export declare const serve: (opts: ServeOptions) => (() => Promise<void>);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Rules } from './types';
|
|
2
|
+
/** Physics steps per second: one client input per step, the host steps a player per input. */
|
|
3
|
+
export declare const TICK_HZ = 60;
|
|
4
|
+
/** A snapshot goes out every SNAP_EVERY host ticks: 60 / 2 = 30 Hz. */
|
|
5
|
+
export declare const SNAP_EVERY = 2;
|
|
6
|
+
/**
|
|
7
|
+
* Render delay of the other players, interpolated between snapshots, in ms: about three snapshot
|
|
8
|
+
* intervals, so one lost or late snapshot does not freeze them. The own player is predicted.
|
|
9
|
+
*/
|
|
10
|
+
export declare const INTERP_DELAY = 100;
|
|
11
|
+
/** Player cylinder. */
|
|
12
|
+
export declare const PLAYER_R = 0.4;
|
|
13
|
+
export declare const PLAYER_H = 1.8;
|
|
14
|
+
/** Camera height above the feet. */
|
|
15
|
+
export declare const EYE = 1.6;
|
|
16
|
+
/** Metres per second. */
|
|
17
|
+
export declare const RUN_SPEED = 6;
|
|
18
|
+
export declare const JUMP_SPEED = 7;
|
|
19
|
+
/** Metres per second squared. */
|
|
20
|
+
export declare const GRAVITY = 20;
|
|
21
|
+
/** Pitch is clamped to ±MAX_PITCH radians, just short of straight up or down. */
|
|
22
|
+
export declare const MAX_PITCH = 1.55;
|
|
23
|
+
/** A floor at most this much higher is walked onto; a higher one needs a jump. */
|
|
24
|
+
export declare const STEP_H = 0.5;
|
|
25
|
+
/** Steps between two chaingun shots while the trigger is held: 60 / 5 = 12 shots per second. */
|
|
26
|
+
export declare const FIRE_EVERY = 5;
|
|
27
|
+
/** Speed factor while the trigger is held. */
|
|
28
|
+
export declare const FIRE_SLOW = 0.5;
|
|
29
|
+
/** Metres a shot reaches. */
|
|
30
|
+
export declare const RANGE = 100;
|
|
31
|
+
/** Largest angle, in radians, between a shot and the view direction. */
|
|
32
|
+
export declare const SPREAD = 0.02;
|
|
33
|
+
/**
|
|
34
|
+
* Host ticks a shot may look back (0.5 s): the host rewinds the targets to the view tick of the
|
|
35
|
+
* input that fired, clamped to this.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_REWIND = 30;
|
|
38
|
+
/**
|
|
39
|
+
* Host-side speed-hack guard: each player earns one input per host tick and may bank up to
|
|
40
|
+
* INPUT_BURST of them; inputs beyond the bank are dropped (and not acked).
|
|
41
|
+
*/
|
|
42
|
+
export declare const INPUT_BURST = 10;
|
|
43
|
+
export declare const MAX_PLAYERS = 16;
|
|
44
|
+
/** Longest player name, in characters (code points). */
|
|
45
|
+
export declare const MAX_NAME = 16;
|
|
46
|
+
export declare const RULES: Rules;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Level, MapDef, Sector } from './types';
|
|
2
|
+
/** Joins every edge a→b with the sector that has b→a, if any: that edge is a portal. */
|
|
3
|
+
export declare const compile: (map: MapDef) => Level;
|
|
4
|
+
/** Whether (x, z) is inside the sector, its edges included. */
|
|
5
|
+
export declare const inside: ({ pts }: Sector, x: number, z: number) => boolean;
|
|
6
|
+
/** Index of the first sector holding (x, z), -1 outside the map. */
|
|
7
|
+
export declare const sectorAt: ({ map }: Level, x: number, z: number) => number;
|
|
8
|
+
/**
|
|
9
|
+
* Whether a wall, a step or a ceiling hides b from a (both inside the map). Between two edge
|
|
10
|
+
* crossings the segment stays in one sector, and each crossing is checked against the opening of
|
|
11
|
+
* its portal, so floors and ceilings need no test of their own.
|
|
12
|
+
*/
|
|
13
|
+
export declare const blocked: ({ map, edges }: Level, [ax, ay, az]: [number, number, number], [bx, by, bz]: [number, number, number]) => boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MapDef } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The first sector map, a test bed for heights: a start room, a low corridor, a tall hall around a
|
|
4
|
+
* lava pit, stairs up to a balcony. Each spot is reachable on foot; the pit is a 1 m drop.
|
|
5
|
+
*/
|
|
6
|
+
export declare const HALL: MapDef;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Input, Level, PlayerState } from './types';
|
|
2
|
+
export declare const TAU: number;
|
|
3
|
+
/** An angle in [0, 2π). */
|
|
4
|
+
export declare const norm: (a: number) => number;
|
|
5
|
+
/** Unit vector the player faces at yaw, on the floor plane: yaw 0 looks toward -z. */
|
|
6
|
+
export declare const forward: (yaw: number) => [x: number, z: number];
|
|
7
|
+
/**
|
|
8
|
+
* Applies one input for 1 / TICK_HZ s, in place: yaw and pitch from the input (pitch clamped to
|
|
9
|
+
* MAX_PITCH), RUN_SPEED along the held keys (diagonals not faster), a jump from the ground,
|
|
10
|
+
* GRAVITY, then collisions of the cylinder (PLAYER_R, PLAYER_H) with the sectors of the level:
|
|
11
|
+
* it slides along shut edges (see shut and push), stands on the highest floor under the circle
|
|
12
|
+
* (a step up to STEP_H is climbed at once, a higher one needs a jump, a ledge is a fall), and
|
|
13
|
+
* bumps its head on the lowest ceiling.
|
|
14
|
+
* The chaingun: `cool` counts down one per step, RUN_SPEED is scaled by FIRE_SLOW while KEY.FIRE
|
|
15
|
+
* is held, and the step that finds it held with `cool` at 0 re-arms it to FIRE_EVERY and returns
|
|
16
|
+
* true: the caller resolves a shot (one every FIRE_EVERY steps, the first at once).
|
|
17
|
+
*/
|
|
18
|
+
export declare const move: (p: PlayerState, i: Input, lvl: Level) => boolean;
|
|
19
|
+
/** A player standing on the floor at spawn point n of the map (modulo the count). */
|
|
20
|
+
export declare const spawn: (id: number, lvl: Level, n: number) => PlayerState;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Input, PlayerInfo, Snapshot } from './types';
|
|
2
|
+
export type ClientText = {
|
|
3
|
+
t: 'join';
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export type ServerText = {
|
|
7
|
+
t: 'roster';
|
|
8
|
+
you: number;
|
|
9
|
+
players: PlayerInfo[];
|
|
10
|
+
} | {
|
|
11
|
+
t: 'full';
|
|
12
|
+
};
|
|
13
|
+
export declare const MSG_INPUT = 1;
|
|
14
|
+
export declare const MSG_SNAPSHOT = 2;
|
|
15
|
+
export declare const INPUT_BYTES = 11;
|
|
16
|
+
export declare const SNAP_HEAD = 4;
|
|
17
|
+
export declare const SNAP_PLAYER = 24;
|
|
18
|
+
export declare const SNAP_SHOT = 14;
|
|
19
|
+
/**
|
|
20
|
+
* The input as the host will read it after encodeInput and decodeInput: yaw, pitch and view
|
|
21
|
+
* rounded to the wire (an absent view stays absent here, and reads 0 on the host). The client
|
|
22
|
+
* predicts with this, so both sides move with the same values.
|
|
23
|
+
*/
|
|
24
|
+
export declare const onWire: (i: Input) => Input;
|
|
25
|
+
export declare const encodeInput: (i: Input) => ArrayBuffer;
|
|
26
|
+
/** Null on anything that is not a well-formed input frame. */
|
|
27
|
+
export declare const decodeInput: (buf: ArrayBuffer) => Input | null;
|
|
28
|
+
export declare const encodeSnapshot: (s: Snapshot) => ArrayBuffer;
|
|
29
|
+
/** Null on anything that is not a well-formed snapshot frame. */
|
|
30
|
+
export declare const decodeSnapshot: (buf: ArrayBuffer) => Snapshot | null;
|
|
31
|
+
/** Returns null on anything that is not a known text message. */
|
|
32
|
+
export declare const parseText: <T extends ClientText | ServerText>(raw: string) => T | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Conn, Every, Rules } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* One game, transport-agnostic. The Node server and the local host tab both drive a Room: they
|
|
4
|
+
* feed it connections and frames, the room answers through Conn.send.
|
|
5
|
+
*
|
|
6
|
+
* - A peer joins with { t: 'join', name }: unique name (`Bob 2`), unique colour, a spawn point not
|
|
7
|
+
* taken by another player; 'full' beyond rules.maxPlayers. Everyone gets a fresh 'roster' on
|
|
8
|
+
* every join and leave.
|
|
9
|
+
* - Each input frame moves its player at once with move(); the INPUT_BURST bank drops the excess.
|
|
10
|
+
* A step that fires resolves its shot at once with fire(), the other players rewound to the
|
|
11
|
+
* view tick of the input (lag compensation, at most MAX_REWIND ticks back).
|
|
12
|
+
* - Every SNAP_EVERY ticks of TICK_HZ, one snapshot with every player and the shots resolved
|
|
13
|
+
* since the last one goes to every peer.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Room {
|
|
16
|
+
/** Every connected peer; null until it joins. */
|
|
17
|
+
private peers;
|
|
18
|
+
private ticks;
|
|
19
|
+
/** The players as each snapshot of the last MAX_REWIND ticks sent them. */
|
|
20
|
+
private history;
|
|
21
|
+
/** Shots resolved since the last snapshot. */
|
|
22
|
+
private shots;
|
|
23
|
+
private halt;
|
|
24
|
+
private rules;
|
|
25
|
+
/** Round-robin cursor over the spawn points, used once they are all taken. */
|
|
26
|
+
private rr;
|
|
27
|
+
/** Missing rules fall back to RULES. */
|
|
28
|
+
constructor(rules?: Partial<Rules>);
|
|
29
|
+
/** Number of joined players. */
|
|
30
|
+
get size(): number;
|
|
31
|
+
/** A peer opened. Nothing is sent until it joins. */
|
|
32
|
+
connect(conn: Conn): void;
|
|
33
|
+
/** A text (JSON) or binary frame from a peer. */
|
|
34
|
+
receive(conn: Conn, data: string | ArrayBuffer): void;
|
|
35
|
+
/** A peer left: its player goes, the roster goes out again. */
|
|
36
|
+
disconnect(conn: Conn): void;
|
|
37
|
+
/** One host tick. Public so tests can drive the room without a timer. */
|
|
38
|
+
tick(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Drives tick() with a timer. An accumulator on performance.now() keeps the
|
|
41
|
+
* simulation at TICK_HZ whatever the timer jitter; after a stall at most
|
|
42
|
+
* MAX_CATCH_UP ticks are replayed and the rest of the backlog is dropped.
|
|
43
|
+
* `every` replaces setInterval, e.g. with a Worker-backed timer that
|
|
44
|
+
* background tabs do not throttle.
|
|
45
|
+
*/
|
|
46
|
+
start(every?: Every): void;
|
|
47
|
+
stop(): void;
|
|
48
|
+
/**
|
|
49
|
+
* The players other than `shooter` as the shooter drew them at `view` (a u16 tick, fractional,
|
|
50
|
+
* read against the current tick and clamped to the kept history): x, y, z interpolated between
|
|
51
|
+
* the two snapshots around it like the client does. A player the history does not hold stands
|
|
52
|
+
* where it is.
|
|
53
|
+
*/
|
|
54
|
+
private rewind;
|
|
55
|
+
private players;
|
|
56
|
+
private join;
|
|
57
|
+
/** Each joined peer gets the full roster with its own id. */
|
|
58
|
+
private roster;
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Level, PlayerState, Shot } from './types';
|
|
2
|
+
type Vec3 = [number, number, number];
|
|
3
|
+
/**
|
|
4
|
+
* Unit direction of a shot: the view direction at yaw and pitch (yaw 0, pitch 0 is -z; pitch > 0
|
|
5
|
+
* looks up), turned by at most SPREAD in a direction hashed from seq, so the host and the
|
|
6
|
+
* shooter's client agree on it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const aim: (yaw: number, pitch: number, seq: number) => Vec3;
|
|
9
|
+
/**
|
|
10
|
+
* Distance along the ray from o (inside the map) in the unit direction d to the first wall, step
|
|
11
|
+
* face, floor or ceiling, at most max.
|
|
12
|
+
*
|
|
13
|
+
* Walks the sectors: in the current convex one the ray leaves through the edge with the least
|
|
14
|
+
* crossing distance among the edges it goes outward through, or ends earlier on the floor or the
|
|
15
|
+
* ceiling. A solid edge, or a portal whose opening does not hold the crossing height, ends it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const trace: (lvl: Level, o: Vec3, d: Vec3, max: number) => number;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves one shot of `shooter` (state after the step that fired) from its eye (feet + EYE)
|
|
20
|
+
* along aim(yaw, pitch, seq): the nearest of the level (trace) and the upright cylinders
|
|
21
|
+
* (PLAYER_R, PLAYER_H, from the feet) of `targets` other than the shooter, within RANGE.
|
|
22
|
+
*/
|
|
23
|
+
export declare const fire: (lvl: Level, shooter: PlayerState, seq: number, targets: PlayerState[]) => Shot;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export interface PlayerInfo {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
/** CSS colour, unique within the room. */
|
|
5
|
+
color: string;
|
|
6
|
+
}
|
|
7
|
+
/** Bits of Input.keys. Keys are read by KeyboardEvent.code, so WASD is ZQSD on AZERTY. */
|
|
8
|
+
export declare const KEY: {
|
|
9
|
+
readonly FORWARD: 1;
|
|
10
|
+
readonly BACK: 2;
|
|
11
|
+
readonly LEFT: 4;
|
|
12
|
+
readonly RIGHT: 8;
|
|
13
|
+
readonly JUMP: 16;
|
|
14
|
+
/** Left mouse button: the chaingun fires every FIRE_EVERY steps while held. */
|
|
15
|
+
readonly FIRE: 32;
|
|
16
|
+
};
|
|
17
|
+
/** One client step: what the player held during 1 / TICK_HZ s, and where they looked. */
|
|
18
|
+
export interface Input {
|
|
19
|
+
/** Client step counter, u16, wraps. The host acks the last one it applied. */
|
|
20
|
+
seq: number;
|
|
21
|
+
/** KEY bits. */
|
|
22
|
+
keys: number;
|
|
23
|
+
/** Radians, [0, 2π). */
|
|
24
|
+
yaw: number;
|
|
25
|
+
/** Radians, [-MAX_PITCH, MAX_PITCH]. */
|
|
26
|
+
pitch: number;
|
|
27
|
+
/**
|
|
28
|
+
* Host tick the client drew the others at when it sent this input, fractional (interpolation
|
|
29
|
+
* between two snapshots), in [0, 65536): the host rewinds the targets of a shot to it (lag
|
|
30
|
+
* compensation). The wire keeps 1/256 tick. Absent (no snapshot seen yet) goes out as 0: the host
|
|
31
|
+
* clamps it to the oldest tick it keeps.
|
|
32
|
+
*/
|
|
33
|
+
view?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface PlayerState {
|
|
36
|
+
id: number;
|
|
37
|
+
/** Feet position. */
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
z: number;
|
|
41
|
+
/** Vertical speed, m/s. */
|
|
42
|
+
vy: number;
|
|
43
|
+
yaw: number;
|
|
44
|
+
pitch: number;
|
|
45
|
+
/** Standing on a floor: a jump is allowed. */
|
|
46
|
+
ground: boolean;
|
|
47
|
+
/** Steps until the chaingun may fire again, 0 to FIRE_EVERY. */
|
|
48
|
+
cool: number;
|
|
49
|
+
}
|
|
50
|
+
/** One shot, as the host resolved it. */
|
|
51
|
+
export interface Shot {
|
|
52
|
+
/** The shooter. */
|
|
53
|
+
id: number;
|
|
54
|
+
/** Where the shot stopped: a wall, a floor, a ceiling, a target, or RANGE away. */
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
z: number;
|
|
58
|
+
/** Id of the player hit, -1 for the level or nothing. */
|
|
59
|
+
hit: number;
|
|
60
|
+
}
|
|
61
|
+
/** Procedural textures, generated by the client. A sector names one per surface. */
|
|
62
|
+
export declare const TEX: {
|
|
63
|
+
readonly BRICK: 0;
|
|
64
|
+
readonly TILE: 1;
|
|
65
|
+
readonly LAVA: 2;
|
|
66
|
+
readonly STONE: 3;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* A room, Doom style: a convex polygon on the floor plane with a flat floor and a flat ceiling.
|
|
70
|
+
* Two sectors are joined where one has the edge a→b and the other b→a with the very same
|
|
71
|
+
* coordinates (a portal); every other edge is a solid wall. Split an edge with collinear points
|
|
72
|
+
* when several sectors meet it (no T-junctions).
|
|
73
|
+
*/
|
|
74
|
+
export interface Sector {
|
|
75
|
+
/**
|
|
76
|
+
* Corners as (x, z), counter-clockwise: the signed area Σ (x_i z_{i+1} - x_{i+1} z_i) is positive.
|
|
77
|
+
* Collinear points are allowed.
|
|
78
|
+
*/
|
|
79
|
+
pts: [x: number, z: number][];
|
|
80
|
+
/** Heights in metres, ceil > floor. */
|
|
81
|
+
floor: number;
|
|
82
|
+
ceil: number;
|
|
83
|
+
/** TEX ids of the floor, the walls (and steps) on its edges, and the ceiling. */
|
|
84
|
+
ft: number;
|
|
85
|
+
wt: number;
|
|
86
|
+
ct: number;
|
|
87
|
+
/** Light level multiplied into the shading, 0 to 1. Absent: 1. */
|
|
88
|
+
light?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface MapDef {
|
|
91
|
+
sectors: Sector[];
|
|
92
|
+
/** Spawn points: feet x, z (inside a sector, standing on its floor) and the yaw to face. */
|
|
93
|
+
spawns: [x: number, z: number, yaw: number][];
|
|
94
|
+
}
|
|
95
|
+
/** One directed edge of a sector, see compile in level.ts. */
|
|
96
|
+
export interface Edge {
|
|
97
|
+
ax: number;
|
|
98
|
+
az: number;
|
|
99
|
+
bx: number;
|
|
100
|
+
bz: number;
|
|
101
|
+
/** Index of the sector the edge belongs to (the side it faces into). */
|
|
102
|
+
front: number;
|
|
103
|
+
/** Index of the sector across a portal, -1 for a solid wall. */
|
|
104
|
+
back: number;
|
|
105
|
+
}
|
|
106
|
+
/** A map ready for physics and drawing. */
|
|
107
|
+
export interface Level {
|
|
108
|
+
map: MapDef;
|
|
109
|
+
/** Every edge of every sector, in sector then point order. */
|
|
110
|
+
edges: Edge[];
|
|
111
|
+
}
|
|
112
|
+
/** One player in a snapshot. */
|
|
113
|
+
export interface SnapPlayer {
|
|
114
|
+
id: number;
|
|
115
|
+
x: number;
|
|
116
|
+
y: number;
|
|
117
|
+
z: number;
|
|
118
|
+
vy: number;
|
|
119
|
+
yaw: number;
|
|
120
|
+
pitch: number;
|
|
121
|
+
ground: boolean;
|
|
122
|
+
cool: number;
|
|
123
|
+
/** Seq of the last input of this player the host applied: the client replays the ones after. */
|
|
124
|
+
ack: number;
|
|
125
|
+
}
|
|
126
|
+
export interface Snapshot {
|
|
127
|
+
/** Host tick, u16, wraps. */
|
|
128
|
+
tick: number;
|
|
129
|
+
players: SnapPlayer[];
|
|
130
|
+
/** Shots fired since the previous snapshot, in order. */
|
|
131
|
+
shots: Shot[];
|
|
132
|
+
}
|
|
133
|
+
/** Room rules, set by the host. Defaults in constants.ts (RULES). */
|
|
134
|
+
export interface Rules {
|
|
135
|
+
maxPlayers: number;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The subset of WebSocket the client relies on. A real WebSocket (with
|
|
139
|
+
* binaryType 'arraybuffer') and the local BroadcastChannel socket both fit.
|
|
140
|
+
*/
|
|
141
|
+
export interface SocketLike {
|
|
142
|
+
binaryType: string;
|
|
143
|
+
onopen: (() => void) | null;
|
|
144
|
+
onclose: (() => void) | null;
|
|
145
|
+
onmessage: ((ev: {
|
|
146
|
+
data: string | ArrayBuffer;
|
|
147
|
+
}) => void) | null;
|
|
148
|
+
send(data: string | ArrayBuffer): void;
|
|
149
|
+
close(): void;
|
|
150
|
+
}
|
|
151
|
+
/** Calls cb every ms milliseconds until the returned function is called. */
|
|
152
|
+
export type Every = (cb: () => void, ms: number) => () => void;
|
|
153
|
+
/** One connected peer, as the room sees it. Transport-agnostic. */
|
|
154
|
+
export interface Conn {
|
|
155
|
+
send(data: string | ArrayBuffer): void;
|
|
156
|
+
}
|