@jgengine/ws 0.9.0 → 0.10.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/CHANGELOG.md +21 -1
- package/dist/host.d.ts +5 -0
- package/dist/host.js +2 -0
- package/dist/hostRouter.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/worldHost.d.ts +26 -0
- package/dist/worldHost.js +119 -0
- package/llms.txt +31 -18
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,7 +11,27 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
11
11
|
same data as typed values, so an updater can diff its installed version against
|
|
12
12
|
the latest and surface the migration steps.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## 0.10.0
|
|
15
|
+
|
|
16
|
+
### Migrate
|
|
17
|
+
|
|
18
|
+
- **Nothing to change for existing games** — the shared mobile-composition system (viewport allocation, region collision, `--jg-*` viewport vars) is wired automatically by the shell for every game; `HudPanel`s inside a `HudCanvas` register themselves. Legacy `orientation: "landscape" | "portrait"` keeps its advisory dismissible-hint behavior unchanged.
|
|
19
|
+
- **Opt a driving/landscape game into the strict gate**: replace the advisory `orientation` with the contract form — `orientation: { mobile: "landscape-required" }` (rules: `any` · `portrait`/`landscape` advisory · `portrait-required`/`landscape-required` · `unsupported`). In portrait the engine now shows a polished `RotateDeviceScreen`, suppresses input, freezes the sim, and unmounts the HUD/controls until the device is landscape.
|
|
20
|
+
- **Declare HUD intent** where useful: `HudPanel` gains `priority` (`critical`/`secondary`/`tertiary`), `mobileBehavior` (`hidden` unmounts on phones, `transient` softens collision, …), `allowOverlapWith`, and `collisionGroup`. Optional — omitting them keeps prior behavior.
|
|
21
|
+
- **Mobile validation now also fails on overlap**: `bun run shoot <game> --device mobile|mobile-landscape|both` exits non-zero on forbidden inter-element collisions (not just viewport overflow), naming both regions.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Shared mobile layout composition — `@jgengine/core/ui/gameLayout` (pure geometry: `resolveLayoutMode`, `intersects`, `overlapArea`, `detectLayoutCollisions`, `computeGameplayRect`, `LayoutRegion`, `GameViewportLayout`) and `@jgengine/core/ui/orientation` (`resolveOrientationRequirement`, `orientationGateActive`, `MobileOrientationRule`). `@jgengine/react` adds `GameViewportProvider` (mounted by the shell), hooks (`useGameViewportLayout`, `useGameLayoutMode`, `useGameOrientation`, `useReservedControlZones`, `useLayoutCollisions`, `useRegisterLayoutRegion`), and a headless `RotateDeviceScreen`. The provider tracks `window.visualViewport`, publishes `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS vars, resolves the explicit layout mode, and detects forbidden region overlaps (dev `data-jg-layout-collision` + console diagnostic).
|
|
26
|
+
- Mandatory orientation contract — `PlayableGame.orientation` accepts `{ mobile: <rule> }`; `landscape-required`/`portrait-required` render an engine-owned rotate gate that blocks gameplay (input suppressed, simulation frozen, HUD/controls unmounted), self-correcting when the device is turned. Reduced-motion and safe-area respected; `visualViewport`-sized for mobile Safari/PWA.
|
|
27
|
+
- Reserved touch-control zones — the touch dock registers its joystick / action-cluster / utility rectangles (runtime-measured) as `control` regions, so HUD placement and collision validation know exactly where controls sit.
|
|
28
|
+
- HUD placement metadata — `HudPanel` gains `priority`, `mobileBehavior`, `allowOverlapWith`, `collisionGroup`, and registers as a collision-tracked layout region.
|
|
29
|
+
- Mobile-landscape shoot device — `bun run shoot --device mobile-landscape` (844×390) plus collision-gate reads that fail mobile validation on forbidden overlaps.
|
|
30
|
+
- Canyon Chase migrated as the reference landscape-required game — declares `orientation: { mobile: "landscape-required" }`, composes its HUD through coordinated `HudPanel` regions (pursuit distance critical, border secondary, survey map hidden on mobile, radio transient) instead of independent fixed corners.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- First-person projectile tracers now originate from the weapon muzzle instead of the camera/eye centerline. Hit detection is unchanged (still crosshair-accurate) — only the drawn tracer's start point moved to the viewmodel muzzle, which tracks full aim yaw and pitch. Non-viewmodel games and enemy tracers are unaffected.
|
|
15
35
|
|
|
16
36
|
## 0.9.0
|
|
17
37
|
|
package/dist/host.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { GameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
|
2
2
|
import type { HostPersistence, ServerListing, SessionAttributes } from "@jgengine/core/runtime/hostPersistence";
|
|
3
3
|
import type { MatchFilter, SessionListing } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
4
|
import type { GameRuntimePlayerView, GameRuntimeServerView, JoinServerResult, TransportRunCommandResult } from "@jgengine/core/runtime/transport";
|
|
5
|
+
/** A change notification emitted by a `GameHost` for a server, player, or feed mutation. */
|
|
5
6
|
export type HostChangeEvent = {
|
|
6
7
|
type: "server";
|
|
7
8
|
serverId: string;
|
|
@@ -14,6 +15,7 @@ export type HostChangeEvent = {
|
|
|
14
15
|
serverId: string;
|
|
15
16
|
action: string;
|
|
16
17
|
};
|
|
18
|
+
/** Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes. */
|
|
17
19
|
export type GameHostOptions = {
|
|
18
20
|
runtimes?: GameRuntime[];
|
|
19
21
|
persistence: HostPersistence;
|
|
@@ -23,6 +25,7 @@ export type GameHostOptions = {
|
|
|
23
25
|
createServerId?: () => string;
|
|
24
26
|
allowedFeedActions?: readonly string[];
|
|
25
27
|
};
|
|
28
|
+
/** A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence. */
|
|
26
29
|
export type GameHost = {
|
|
27
30
|
joinServer: (args: {
|
|
28
31
|
userId: string;
|
|
@@ -86,5 +89,7 @@ export type GameHost = {
|
|
|
86
89
|
stop: () => Promise<void>;
|
|
87
90
|
subscribe: (listener: (event: HostChangeEvent) => void) => () => void;
|
|
88
91
|
};
|
|
92
|
+
/** Creates a `GameHost` that runs game servers over the given persistence and runtimes. */
|
|
89
93
|
export declare function createGameHost(options: GameHostOptions): GameHost;
|
|
94
|
+
/** Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts. */
|
|
90
95
|
export declare function memoryPersistence(now?: () => number): HostPersistence;
|
package/dist/host.js
CHANGED
|
@@ -9,6 +9,7 @@ const builtinCommands = {
|
|
|
9
9
|
apply: (snapshot) => snapshot,
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
|
+
/** Creates a `GameHost` that runs game servers over the given persistence and runtimes. */
|
|
12
13
|
export function createGameHost(options) {
|
|
13
14
|
const now = options.now ?? Date.now;
|
|
14
15
|
const tickMs = options.tickMs ?? 1_000;
|
|
@@ -389,6 +390,7 @@ export function createGameHost(options) {
|
|
|
389
390
|
};
|
|
390
391
|
return host;
|
|
391
392
|
}
|
|
393
|
+
/** Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts. */
|
|
392
394
|
export function memoryPersistence(now = Date.now) {
|
|
393
395
|
const servers = new Map();
|
|
394
396
|
const profiles = new Map();
|
package/dist/hostRouter.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HostedWorldSession } from "@jgengine/core/runtime/hostedWorldSession";
|
|
2
|
+
import type { GameHost } from "./host.js";
|
|
3
|
+
/** Config for {@link createWorldGameHost}: how to resolve a hosted world's authoritative session per server. */
|
|
4
|
+
export interface WorldGameHostOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Resolve (or lazily build) the authoritative {@link HostedWorldSession} for a server — called once per new
|
|
7
|
+
* `serverId`. Return `null` for an unknown game. Bind the game's definition + content here.
|
|
8
|
+
*/
|
|
9
|
+
session(args: {
|
|
10
|
+
gameId: string;
|
|
11
|
+
serverId: string;
|
|
12
|
+
}): HostedWorldSession | null;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
/** A {@link GameHost} whose worlds run on `HostedWorldSession`s; `tick` advances them and re-broadcasts on change. */
|
|
16
|
+
export interface WorldGameHost extends GameHost {
|
|
17
|
+
/** Advance every live world by `dtSeconds` and emit a `server` change for each whose revision moved. */
|
|
18
|
+
tick(dtSeconds: number): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The GameContext-loop counterpart of the reducer `createGameHost`: a structural {@link GameHost} that serves each
|
|
22
|
+
* world's full `WorldSnapshot` as `serverState`, so the existing ws router, `createWsBackend`, and the shell's
|
|
23
|
+
* `attachWorldSync` carry host-authoritative GameContext worlds with zero changes to any of them. The harness owns
|
|
24
|
+
* the tick cadence (call {@link WorldGameHost.tick} on an interval); commands and joins broadcast immediately.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createWorldGameHost(options: WorldGameHostOptions): WorldGameHost;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
|
|
2
|
+
/**
|
|
3
|
+
* The GameContext-loop counterpart of the reducer `createGameHost`: a structural {@link GameHost} that serves each
|
|
4
|
+
* world's full `WorldSnapshot` as `serverState`, so the existing ws router, `createWsBackend`, and the shell's
|
|
5
|
+
* `attachWorldSync` carry host-authoritative GameContext worlds with zero changes to any of them. The harness owns
|
|
6
|
+
* the tick cadence (call {@link WorldGameHost.tick} on an interval); commands and joins broadcast immediately.
|
|
7
|
+
*/
|
|
8
|
+
export function createWorldGameHost(options) {
|
|
9
|
+
const live = new Map();
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
const now = options.now ?? (() => Date.now());
|
|
12
|
+
function emit(event) {
|
|
13
|
+
for (const listener of listeners)
|
|
14
|
+
listener(event);
|
|
15
|
+
}
|
|
16
|
+
function ensure(gameId, serverId) {
|
|
17
|
+
const existing = live.get(serverId);
|
|
18
|
+
if (existing !== undefined)
|
|
19
|
+
return existing;
|
|
20
|
+
const session = options.session({ gameId, serverId });
|
|
21
|
+
if (session === null)
|
|
22
|
+
return null;
|
|
23
|
+
const entry = { gameId, session };
|
|
24
|
+
live.set(serverId, entry);
|
|
25
|
+
return entry;
|
|
26
|
+
}
|
|
27
|
+
function tickAll(dtSeconds) {
|
|
28
|
+
for (const [serverId, entry] of live) {
|
|
29
|
+
const before = entry.session.revision();
|
|
30
|
+
entry.session.tick(dtSeconds);
|
|
31
|
+
if (entry.session.revision() !== before)
|
|
32
|
+
emit({ type: "server", serverId });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
async joinServer({ userId, gameId, serverId }) {
|
|
37
|
+
const id = serverId ?? gameId;
|
|
38
|
+
const entry = ensure(gameId, id);
|
|
39
|
+
if (entry === null)
|
|
40
|
+
throw new Error(`no hosted world for game "${gameId}"`);
|
|
41
|
+
const isNew = !entry.session.members().includes(userId);
|
|
42
|
+
entry.session.join(userId, isNew);
|
|
43
|
+
emit({ type: "server", serverId: id });
|
|
44
|
+
emit({ type: "player", serverId: id, userId });
|
|
45
|
+
return { serverId: id, isNew };
|
|
46
|
+
},
|
|
47
|
+
async leaveServer({ userId, serverId }) {
|
|
48
|
+
const entry = live.get(serverId);
|
|
49
|
+
if (entry === undefined)
|
|
50
|
+
return;
|
|
51
|
+
entry.session.leave(userId);
|
|
52
|
+
emit({ type: "server", serverId });
|
|
53
|
+
},
|
|
54
|
+
async runCommand({ userId, serverId, command, input }) {
|
|
55
|
+
const entry = live.get(serverId);
|
|
56
|
+
if (entry === undefined)
|
|
57
|
+
return { ok: false, reason: "no-server" };
|
|
58
|
+
if (command === INPUT_COMMAND) {
|
|
59
|
+
entry.session.input(userId, input);
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
const before = entry.session.revision();
|
|
63
|
+
const result = entry.session.command(userId, command, input);
|
|
64
|
+
if (result.status === "rejected")
|
|
65
|
+
return { ok: false, reason: result.reason };
|
|
66
|
+
if (result.status === "unknown-command")
|
|
67
|
+
return { ok: false, reason: "unknown-command" };
|
|
68
|
+
if (entry.session.revision() !== before)
|
|
69
|
+
emit({ type: "server", serverId });
|
|
70
|
+
return { ok: true };
|
|
71
|
+
},
|
|
72
|
+
async isMember({ userId, serverId }) {
|
|
73
|
+
return live.get(serverId)?.session.members().includes(userId) ?? false;
|
|
74
|
+
},
|
|
75
|
+
async getServerView({ serverId }) {
|
|
76
|
+
const entry = live.get(serverId);
|
|
77
|
+
if (entry === undefined)
|
|
78
|
+
return null;
|
|
79
|
+
return {
|
|
80
|
+
serverId,
|
|
81
|
+
gameId: entry.gameId,
|
|
82
|
+
revision: entry.session.revision(),
|
|
83
|
+
memberUserIds: [...entry.session.members()],
|
|
84
|
+
serverState: entry.session.runner().snapshot(),
|
|
85
|
+
updatedAt: now(),
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
async getPlayerView() {
|
|
89
|
+
return null;
|
|
90
|
+
},
|
|
91
|
+
async getFeed() {
|
|
92
|
+
return [];
|
|
93
|
+
},
|
|
94
|
+
async pushFeedEntry() { },
|
|
95
|
+
async browseServers() {
|
|
96
|
+
return [];
|
|
97
|
+
},
|
|
98
|
+
async joinByCode() {
|
|
99
|
+
return null;
|
|
100
|
+
},
|
|
101
|
+
async listOpenServers() {
|
|
102
|
+
return [];
|
|
103
|
+
},
|
|
104
|
+
tick: tickAll,
|
|
105
|
+
async tickOnce() {
|
|
106
|
+
tickAll(0);
|
|
107
|
+
return { ticked: live.size, saved: 0 };
|
|
108
|
+
},
|
|
109
|
+
async flushAll() {
|
|
110
|
+
return 0;
|
|
111
|
+
},
|
|
112
|
+
start() { },
|
|
113
|
+
async stop() { },
|
|
114
|
+
subscribe(listener) {
|
|
115
|
+
listeners.add(listener);
|
|
116
|
+
return () => listeners.delete(listener);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @jgengine/ws
|
|
2
2
|
> Browser-safe WebSocket client backend for JGengine: protocol codec, createWsBackend, and HTTP read helpers.
|
|
3
3
|
|
|
4
|
-
Version: 0.
|
|
4
|
+
Version: 0.10.0
|
|
5
5
|
License: AGPL-3.0-only
|
|
6
6
|
Repository: https://github.com/Noisemaker111/jgengine
|
|
7
7
|
Docs: https://jgengine.com
|
|
@@ -20,11 +20,11 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
20
20
|
|
|
21
21
|
### @jgengine/ws/host
|
|
22
22
|
|
|
23
|
-
- GameHost (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<SessionListing[]>; joinByCode: (args: { userId: strin…
|
|
24
|
-
- GameHostOptions (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; }
|
|
25
|
-
- HostChangeEvent (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string }
|
|
26
|
-
- createGameHost (function): function createGameHost(options: GameHostOptions): GameHost
|
|
27
|
-
- memoryPersistence (function): function memoryPersistence(now: () => number = Date.now): HostPersistence
|
|
23
|
+
- GameHost (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<SessionListing[]>; joinByCode: (args: { userId: strin… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence.
|
|
24
|
+
- GameHostOptions (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes.
|
|
25
|
+
- HostChangeEvent (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string } — A change notification emitted by a `GameHost` for a server, player, or feed mutation.
|
|
26
|
+
- createGameHost (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes.
|
|
27
|
+
- memoryPersistence (function): function memoryPersistence(now: () => number = Date.now): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts.
|
|
28
28
|
|
|
29
29
|
### @jgengine/ws/hostRouter
|
|
30
30
|
|
|
@@ -34,7 +34,7 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
34
34
|
- HostRouterConnection (type): type HostRouterConnection = { handleRaw: (raw: unknown) => void; close: () => void; }
|
|
35
35
|
- HostRouterOptions (type): type HostRouterOptions = { host: GameHost; authenticate?: HostRouterAuthenticate; allowAnonymous?: boolean; singleSession?: boolean; poseRules?: PoseSyncRules; positionHistoryMs?: number; chatRateLimit?: ChatRateLimit; chatHistoryLimit?: number; chatMaxBodyLength?: number; now?: () => number; }
|
|
36
36
|
- HostRouterTransport (type): type HostRouterTransport = { send: (data: string) => void; close: () => void; }
|
|
37
|
-
- RewoundPosition (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; }
|
|
37
|
+
- RewoundPosition (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; } — A player's interpolated position sampled from history at a past timestamp.
|
|
38
38
|
- createHostRouter (function): function createHostRouter(options: HostRouterOptions): HostRouter
|
|
39
39
|
- loopbackPipe (function): function loopbackPipe(router: HostRouter): TransportPipeFactory
|
|
40
40
|
|
|
@@ -47,9 +47,9 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
47
47
|
### @jgengine/ws/index
|
|
48
48
|
|
|
49
49
|
- DEFAULT_POSE_RULES (const): const DEFAULT_POSE_RULES: PoseSyncRules
|
|
50
|
-
- GameHost (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<SessionListing[]>; joinByCode: (args: { userId: strin…
|
|
51
|
-
- GameHostOptions (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; }
|
|
52
|
-
- HostChangeEvent (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string }
|
|
50
|
+
- GameHost (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<SessionListing[]>; joinByCode: (args: { userId: strin… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence.
|
|
51
|
+
- GameHostOptions (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes.
|
|
52
|
+
- HostChangeEvent (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string } — A change notification emitted by a `GameHost` for a server, player, or feed mutation.
|
|
53
53
|
- HostRouter (type): type HostRouter = { connect: (transport: HostRouterTransport) => HostRouterConnection; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => void; }
|
|
54
54
|
- HostRouterAuthenticate (type): type HostRouterAuthenticate = (args: { userId: string; token?: string; }) => Promise<string | null> | string | null
|
|
55
55
|
- HostRouterConnection (type): type HostRouterConnection = { handleRaw: (raw: unknown) => void; close: () => void; }
|
|
@@ -68,7 +68,7 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
68
68
|
- ReadsHandler (type): type ReadsHandler = (request: Request) => Promise<Response>
|
|
69
69
|
- ReadsHandlerOptions (type): type ReadsHandlerOptions = { persistence: ReadsPersistence | (() => Promise<ReadsPersistence>); basePath?: string; listOpenServers?: (args: { gameId: string; limit?: number }) => Promise<ServerListing[]>; authenticate?: ReadsAuthenticate; allowPublicProfiles?: boolean; }
|
|
70
70
|
- ReadsPersistence (type): type ReadsPersistence = Pick< HostPersistence, "listServers" | "loadProfile" | "getLeaderboardTop" | "getLeaderboardProfile" >
|
|
71
|
-
- RewoundPosition (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; }
|
|
71
|
+
- RewoundPosition (type): type RewoundPosition = { userId: string; x: number; y: number; z: number; } — A player's interpolated position sampled from history at a past timestamp.
|
|
72
72
|
- SocketIoBackendOptions (type): type SocketIoBackendOptions = Omit<WsBackendOptions, "url" | "pipe" | "webSocketFactory"> & { socket: SocketIoLikeSocket; }
|
|
73
73
|
- SocketIoLikeSocket (type): type SocketIoLikeSocket = { connected: boolean; on: (event: string, listener: (payload: string) => void) => unknown; off: (event: string, listener: (payload: string) => void) => unknown; send: (data: string) => unknown; disconnect: () => unknown; }
|
|
74
74
|
- TransportPipe (type): type TransportPipe = { send: (data: string) => void; close: () => void; }
|
|
@@ -81,6 +81,8 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
81
81
|
- VoicePosition (interface): interface VoicePosition
|
|
82
82
|
- VoiceRoute (interface): interface VoiceRoute
|
|
83
83
|
- WS_PROTOCOL_VERSION (const): const WS_PROTOCOL_VERSION: 1
|
|
84
|
+
- WorldGameHost (interface): interface WorldGameHost extends GameHost — A {@link GameHost} whose worlds run on `HostedWorldSession`s; `tick` advances them and re-broadcasts on change.
|
|
85
|
+
- WorldGameHostOptions (interface): interface WorldGameHostOptions — Config for {@link createWorldGameHost}: how to resolve a hosted world's authoritative session per server.
|
|
84
86
|
- WsAppearance (type): type WsAppearance = Record<string, string | number | boolean> — Client-set cosmetic/state tags carried alongside a pose (skin, mount, emote, ...). Primitive values only.
|
|
85
87
|
- WsBackend (type): type WsBackend = GameBackend & { pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; browse: (args: { gameId: string; filter?: MatchFilter; limit?: number }) => Promise<SessionListing[]>; joinByCode: (args: { gameId: string; code: string }) => Promise<JoinSe…
|
|
86
88
|
- WsBackendOptions (type): type WsBackendOptions = { url?: string; pipe?: TransportPipeFactory; userId: string; token?: string; webSocketFactory?: (url: string) => WebSocket; reconnectDelayMs?: number; maxReconnectDelayMs?: number; rpcTimeoutMs?: number; poseTuning?: PoseSyncTuning; now?: () => number; setTimeoutFn?: typeof s…
|
|
@@ -103,7 +105,7 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
103
105
|
- announcePeerHost (function): function announcePeerHost(host: PeerHost, signaling: PeerSignaling): () => void
|
|
104
106
|
- broadcastChannelSignaling (function): function broadcastChannelSignaling(room: string): PeerSignaling
|
|
105
107
|
- computeVoiceGain (function): function computeVoiceGain(def: VoiceChannelDef, distance: number | null): number
|
|
106
|
-
- createGameHost (function): function createGameHost(options: GameHostOptions): GameHost
|
|
108
|
+
- createGameHost (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes.
|
|
107
109
|
- createHostRouter (function): function createHostRouter(options: HostRouterOptions): HostRouter
|
|
108
110
|
- createHttpReads (function): function createHttpReads(options: HttpReadsOptions): HttpReads
|
|
109
111
|
- createPeerGuest (function): function createPeerGuest(options: PeerGuestOptions): PeerGuest
|
|
@@ -111,6 +113,7 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
111
113
|
- createReadsHandler (function): function createReadsHandler(options: ReadsHandlerOptions): ReadsHandler
|
|
112
114
|
- createSocketIoBackend (function): function createSocketIoBackend(options: SocketIoBackendOptions): WsBackend
|
|
113
115
|
- createVoiceChannelRouter (function): function createVoiceChannelRouter(channels?: readonly VoiceChannelDef[]): VoiceChannelRouter
|
|
116
|
+
- createWorldGameHost (function): function createWorldGameHost(options: WorldGameHostOptions): WorldGameHost — The GameContext-loop counterpart of the reducer `createGameHost`: a structural {@link GameHost} that serves each world's full `WorldSnapshot` as `serverState`, so the existing ws router, `createWsBackend`, and the shell's `attachWorldSync` carry host-authoritative GameContext worlds with zero changes to any of them. The harness owns the tick cadence (call {@link WorldGameHost.tick} on an interval); commands and joins broadcast immediately.
|
|
114
117
|
- createWsBackend (function): function createWsBackend(options: WsBackendOptions): WsBackend
|
|
115
118
|
- decodePeerSignal (function): function decodePeerSignal(code: string): PeerSignalPayload | null
|
|
116
119
|
- decodeWsClientMessage (function): function decodeWsClientMessage(raw: unknown): WsClientMessage | null
|
|
@@ -120,7 +123,7 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
120
123
|
- inspectWsDecodeFailure (function): function inspectWsDecodeFailure(raw: unknown): WsDecodeFailure
|
|
121
124
|
- joinPeerSession (function): function joinPeerSession(guest: PeerGuest, signaling: PeerSignaling): Promise<WsBackend>
|
|
122
125
|
- loopbackPipe (function): function loopbackPipe(router: HostRouter): TransportPipeFactory
|
|
123
|
-
- memoryPersistence (function): function memoryPersistence(now: () => number = Date.now): HostPersistence
|
|
126
|
+
- memoryPersistence (function): function memoryPersistence(now: () => number = Date.now): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts.
|
|
124
127
|
- socketIoPipe (function): function socketIoPipe(socket: SocketIoLikeSocket): TransportPipeFactory
|
|
125
128
|
- subscriptionKey (function): function subscriptionKey(channel: WsChannel, serverId: string, action?: string): string
|
|
126
129
|
- webSocketPipe (function): function webSocketPipe(url: string, webSocketFactory: (url: string) => WebSocket = (target) => new WebSocket(target)): TransportPipeFactory
|
|
@@ -198,6 +201,12 @@ Imports use deep paths: `@jgengine/ws/<path>`.
|
|
|
198
201
|
- computeVoiceGain (function): function computeVoiceGain(def: VoiceChannelDef, distance: number | null): number
|
|
199
202
|
- createVoiceChannelRouter (function): function createVoiceChannelRouter(channels?: readonly VoiceChannelDef[]): VoiceChannelRouter
|
|
200
203
|
|
|
204
|
+
### @jgengine/ws/worldHost
|
|
205
|
+
|
|
206
|
+
- WorldGameHost (interface): interface WorldGameHost extends GameHost — A {@link GameHost} whose worlds run on `HostedWorldSession`s; `tick` advances them and re-broadcasts on change.
|
|
207
|
+
- WorldGameHostOptions (interface): interface WorldGameHostOptions — Config for {@link createWorldGameHost}: how to resolve a hosted world's authoritative session per server.
|
|
208
|
+
- createWorldGameHost (function): function createWorldGameHost(options: WorldGameHostOptions): WorldGameHost — The GameContext-loop counterpart of the reducer `createGameHost`: a structural {@link GameHost} that serves each world's full `WorldSnapshot` as `serverState`, so the existing ws router, `createWsBackend`, and the shell's `attachWorldSync` carry host-authoritative GameContext worlds with zero changes to any of them. The harness owns the tick cadence (call {@link WorldGameHost.tick} on an interval); commands and joins broadcast immediately.
|
|
209
|
+
|
|
201
210
|
## Guides
|
|
202
211
|
|
|
203
212
|
# JGengine
|
|
@@ -298,7 +307,9 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
298
307
|
| Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
|
|
299
308
|
| Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
|
|
300
309
|
| Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
|
|
301
|
-
| World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
|
|
310
|
+
| World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building`, `road` |
|
|
311
|
+
| Street layout | `world/streets` | `laneCenters`, `sidewalkPaths`, `furnitureSpots`, `parkingSpots`, `sidewalkPoint`, `offsetPath`, `sidewalkWidthOf`, `StreetLane`, `FurnitureSpot`, `ParkingSpot` — where things belong on a `road()`: lanes for traffic, sidewalks for peds, curb anchors for furniture |
|
|
312
|
+
| Road ribbons | `world/roads` | `buildRoadRibbon`, `dashSegments`, `nearestOnPath`, `isOnRoad`, `pathLength`, `RoadRibbon`, `RoadSample`, `RoadPoint` — geometry + queries behind the `road()` environment feature |
|
|
302
313
|
| Voxel field | `world/voxelField` | `createVoxelField`, `VoxelField`, `VoxelCell`, `VoxelHit`, `VoxelBounds`, `VoxelFieldSummary`, `VoxelFace`, `VOXEL_FACES`, `VOXEL_FACE_NORMALS` — a chunked block lattice, distinct from the `voxel()` `WorldFeature` descriptor |
|
|
303
314
|
| Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
|
|
304
315
|
| Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
|
|
@@ -590,7 +601,7 @@ A voxel block is an object. A rack is an object with a slot inventory. A GPU is
|
|
|
590
601
|
|
|
591
602
|
## Game repo layout
|
|
592
603
|
|
|
593
|
-
Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. Dense files — one `catalog.ts` per domain, never one file per entry.
|
|
604
|
+
Every game is one shape, enforced by `check-game-shape` (part of `check-types`): the top of `src/` holds only the skeleton, and every game-specific module, UI component, and test lives under `src/game/`. In this repo the gate also requires a root `package.json` script `"games:<id>": "bun run --cwd Games/<id> dev"` for every `Games/*` directory — add it with the scaffold, or the very first `check-types` fails before the compiler even runs. Dense files — one `catalog.ts` per domain, never one file per entry.
|
|
594
605
|
|
|
595
606
|
```
|
|
596
607
|
src/
|
|
@@ -621,6 +632,8 @@ src/
|
|
|
621
632
|
|
|
622
633
|
**Smart defaults** — omit any of these and the call still resolves: `multiplayer` → `offline()`; `assets` → an empty asset catalog; `loop` hooks (`onInit`/`onNewPlayer`/`onTick`) → no-ops; `content` → `{}`; `GameUI` → an empty component; `camera` → third-person orbit; `feed` → 20-entry ring buffers per action; a `world` of kind `environment()` auto-renders as the backdrop with no `environment` component supplied — a non-`environment()` world (`flat()`, `voxel()`, …) still needs the game to hand it one.
|
|
623
634
|
|
|
635
|
+
**Opt-in `ctx.game.*` subsystems (`features`)** — core is genre-agnostic: the always-on base is `commands` / `events` / `store` / `feed` (plus `audio`), and genre subsystems are opt-in via `defineGame({ features: { roster, cards, turn, race, leaderboard, social, chat } })`. Omit one and `ctx.game.<name>` is `undefined` — a puzzle game isn't handed a card pile, race state, or party/chat it never asked for. Declare only what the game uses (`chat` implies `social`). (The content cluster — economy/quest/loot/trade — joins this manifest in a later slim-core phase.)
|
|
636
|
+
|
|
624
637
|
```ts
|
|
625
638
|
// game.config.ts — imports only, nothing inline
|
|
626
639
|
import { defineGame } from "@jgengine/shell/defineGame";
|
|
@@ -682,7 +695,7 @@ export const physics: PhysicsConfig = { gravity: -32 };
|
|
|
682
695
|
|
|
683
696
|
- `PhysicsConfig.gravity`/`jumpVelocity` tune the shell's built-in walk controller's fall/jump feel (defaults ~`-24`/`7.1`) — the only two levers on gravity and jump height; everything else about movement (speed, poses) stays catalog `movement` fields.
|
|
684
697
|
- Input bindings are string arrays (hold semantics) or `{ hold, toggle, repeatMs? }` for the same verb. `repeatMs` turns a held action into an auto-repeat fire (build-mode place-on-drag, rapid-fire without a separate `wasPressed`/interval combo in game code) — the shell refires the command every `repeatMs` while the binding stays down, on top of its normal press-edge fire.
|
|
685
|
-
- **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback).
|
|
698
|
+
- **Keybind → command convention.** The shell fires a command for any bound action that isn't reserved: pressing an action runs a command of the **same name** if one is defined, else a `ui.<action>` fallback (so `openBackpack` → `ui.openBackpack`). Just declare the binding and a matching command — no per-game `keydown` listener. Reserved actions the shell consumes natively and never routes to a command: `moveForward/moveBack/moveLeft/moveRight`, `turnLeft/turnRight`, `sprint`, `jump`, `tabTarget`, `clearTarget`, `useAbility`, `interact`, and any `hotbarSlotN`/`slotN`. `tabTarget`/`clearTarget` run `target.cycle`/`target.clear` (native `cycleTarget`/`setTarget` fallback). Because `hotbarSlotN` never reaches a command, a game that drives selection from game code binds its own `selectSlot1..N` actions, defines matching commands that write a store key, and reads it back through `hotbarSelection: () => ...` on `defineGame` (see `loot-shooter`).
|
|
686
699
|
- **`interact`** is special: pressing it resolves the active proximity prompt from the `prompts` field of `defineGame({...})` and runs that prompt's `invoke` command. A prompt with `invoke: null` is display-only and does nothing on the key.
|
|
687
700
|
- UI keybind badges derive from `keybinds` via `actionLabel(keybinds, "openBackpack")` — `bindingLabel` maps codes to short labels (`Digit1`→`1`, `KeyB`→`B`, `mouse0`→`LMB`, `Escape`→`Esc`). Never hardcode label strings; they drift the moment a binding changes.
|
|
688
701
|
- `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
|
|
@@ -767,7 +780,7 @@ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})`
|
|
|
767
780
|
The shell ships a **rig library**; a game picks and tunes one through `camera` config, never by writing camera positions from `onTick`. Select with `camera.rig` (or the `perspective: "third" | "first"` shorthand) — or by config block alone (#207.8): supplying `camera.<rig>` selects that rig with no redundant `rig` field, checked in the table's order; an explicit `rig` wins when several blocks are present:
|
|
768
781
|
| `rig` | For | Key config (`camera.<rig>`) |
|
|
769
782
|
|-------|-----|------------------------------|
|
|
770
|
-
| `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
|
|
783
|
+
| `orbit` (default) | Third-person chase | Top-level fields on `camera` itself — `initialDistance`, `minDistance`/`maxDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom. There is **no `camera.orbit` block**; orbit is the one rig tuned at the top level |
|
|
771
784
|
| `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
|
|
772
785
|
| `topDown` | ARPG iso / top-down (Diablo IV, Hades II) | `topDown: { height, pitch, yaw, followSmoothing, zoom }` — decoupled follow; `pitch` is camera elevation (PI/2 = straight down, near 0 = grazing and boom-distance blows up past `frustum.far`) |
|
|
773
786
|
| `rts` | Free-pan / edge-scroll (The Sims, Manor Lords) | `rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start, pan }` — `pan: false` turns it into a static backdrop camera: no WASD/arrow pan, no edge-scroll, no Q/E rotate, no wheel zoom, still re-centers on `followEntityId` if one resolves |
|
|
@@ -826,7 +839,7 @@ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
|
|
|
826
839
|
ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
|
|
827
840
|
```
|
|
828
841
|
|
|
829
|
-
`content.itemById(id)` supplies `{ use?, weapon?, trade? }
|
|
842
|
+
`content.itemById(id)` supplies `{ use?, weapon?, trade? }` — exact shapes: `use` is the handler **name string** (not an object), `weapon` is a flat `Record<string, number | Record<string, number>>` built from your catalog stats (`damage`, `projectile.{...}`, `explosion.{...}`), and every lookup returns **`null`** (not `undefined`) for an unknown id; `content.entityById(id)` supplies `{ stats?, receive?, onDeath?, movement?, role? }`; `content.objectById(id)` supplies `GameContextObjectEntry` `{ proximityPrompt?, breakable?, slotInventory? }`. Build all three from your catalogs in `content.ts`. Call-shape gotchas that cost typecheck loops: `ctx.item.use.use(input)` takes **one** argument on the ctx surface (the two-arg `use(state, input)` is the raw factory shape); `ctx.scene.entity.moveToward(id, target, { speed, dt, stopDistance? })` — the third argument is an options object, never a bare `dt`; `nav/pathFollow`'s `createPathFollow(config)` returns the initial `PathFollowState` directly and `advancePathFollow(config, state, dt)` returns the **next state** (with `position: [x,y,z]`, `heading`, `done`) — there is no wrapper object, and `Waypoint` is a `[x, y, z]` tuple. A placed object resolves its catalog entry via `ctx.scene.object.catalog(instanceId)`; `ctx.scene.object.at(x, y, z, tolerance?)` finds placed objects near a point (grid interaction, click resolution beyond the pointer service). `ctx.scene.entity.update(id, patch)` writes a shallow patch onto a spawned entity's mutable fields (e.g. `movement.walkSpeed`) without a full respawn — `scene/movementSpeed`'s `applyStatDrivenSpeed(deps, id, { baseSpeed, multiplierStat?, flatBonusStat? })` is the catalog-driven helper that recomputes and writes `movement.walkSpeed` from a stat-modifier pair each time a buff changes.
|
|
830
843
|
|
|
831
844
|
### Two tiers: `ctx` runtime vs pure factories
|
|
832
845
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/ws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Browser-safe WebSocket client backend for JGengine: protocol codec, createWsBackend, and HTTP read helpers.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "bun test src"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@jgengine/core": "^0.
|
|
34
|
+
"@jgengine/core": "^0.10.0"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|