@jgengine/node 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/worldServer.d.ts +34 -0
- package/dist/worldServer.js +51 -0
- package/llms.txt +21 -6
- package/package.json +3 -3
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/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GameDefinition } from "@jgengine/core/game/defineGame";
|
|
2
|
+
import type { GameContextContent } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
import type { ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
|
|
4
|
+
import { type WorldGameHost } from "@jgengine/ws/worldHost";
|
|
5
|
+
import { type GameWsServer, type GameWsServerOptions } from "./wsServer.js";
|
|
6
|
+
/** A game the world server can host — its authoritative {@link GameDefinition} and the content lookup a `GameContext` reads. */
|
|
7
|
+
export interface HostedGameDefinition {
|
|
8
|
+
game: GameDefinition<ModelAssetRef, unknown>;
|
|
9
|
+
content: GameContextContent;
|
|
10
|
+
}
|
|
11
|
+
/** Config for {@link createWorldGameServer}: how to resolve a game by id, the tick cadence, and the underlying ws-server/router options (minus `host`, which the server builds). */
|
|
12
|
+
export interface WorldGameServerOptions extends Omit<GameWsServerOptions, "host"> {
|
|
13
|
+
/** Resolve a game to host by id — `null` for an unknown id, which rejects the join. */
|
|
14
|
+
resolveGame(gameId: string): HostedGameDefinition | null;
|
|
15
|
+
/** Ticks per second for the real-clock loop {@link WorldGameServer.start} drives; default 30. Tests call {@link WorldGameServer.tick} manually instead. */
|
|
16
|
+
tickHz?: number;
|
|
17
|
+
}
|
|
18
|
+
/** A runnable ws host for GameContext worlds: {@link createWorldGameHost} + {@link createGameWsServer} + a tick loop, with a manual `tick(dt)` seam so a fake clock can drive it in tests. */
|
|
19
|
+
export interface WorldGameServer {
|
|
20
|
+
host: WorldGameHost;
|
|
21
|
+
ws: GameWsServer;
|
|
22
|
+
/** Advance every live world by `dtSeconds` — the manual/test drive point; {@link start} calls it on a real-clock interval. */
|
|
23
|
+
tick(dtSeconds: number): void;
|
|
24
|
+
/** Begin driving {@link tick} on a real-clock interval at `tickHz` (idempotent). */
|
|
25
|
+
start(): void;
|
|
26
|
+
/** Stop the tick interval (idempotent). */
|
|
27
|
+
stop(): void;
|
|
28
|
+
/** Stop the tick loop and tear down the ws server. */
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
/** The bound ws port. */
|
|
31
|
+
port(): number;
|
|
32
|
+
}
|
|
33
|
+
/** Build a {@link WorldGameServer} — one process hosting authoritative GameContext worlds over ws, ready for two-client play once {@link WorldGameServer.start} runs. */
|
|
34
|
+
export declare function createWorldGameServer(options: WorldGameServerOptions): WorldGameServer;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHostedWorldSession } from "@jgengine/core/runtime/hostedWorldSession";
|
|
2
|
+
import { createWorldGameHost } from "@jgengine/ws/worldHost";
|
|
3
|
+
import { createGameWsServer } from "./wsServer.js";
|
|
4
|
+
const DEFAULT_TICK_HZ = 30;
|
|
5
|
+
/** Build a {@link WorldGameServer} — one process hosting authoritative GameContext worlds over ws, ready for two-client play once {@link WorldGameServer.start} runs. */
|
|
6
|
+
export function createWorldGameServer(options) {
|
|
7
|
+
const { resolveGame, tickHz = DEFAULT_TICK_HZ, ...serverOptions } = options;
|
|
8
|
+
const clock = options.now ?? (() => Date.now());
|
|
9
|
+
const host = createWorldGameHost({
|
|
10
|
+
now: clock,
|
|
11
|
+
session: ({ gameId }) => {
|
|
12
|
+
const resolved = resolveGame(gameId);
|
|
13
|
+
if (resolved === null)
|
|
14
|
+
return null;
|
|
15
|
+
return createHostedWorldSession({ definition: resolved.game, content: resolved.content, now: clock });
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const ws = createGameWsServer({ ...serverOptions, host });
|
|
19
|
+
let interval = null;
|
|
20
|
+
let last = clock();
|
|
21
|
+
function tick(dtSeconds) {
|
|
22
|
+
host.tick(dtSeconds);
|
|
23
|
+
}
|
|
24
|
+
function stop() {
|
|
25
|
+
if (interval !== null) {
|
|
26
|
+
clearInterval(interval);
|
|
27
|
+
interval = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
host,
|
|
32
|
+
ws,
|
|
33
|
+
tick,
|
|
34
|
+
stop,
|
|
35
|
+
port: ws.port,
|
|
36
|
+
start() {
|
|
37
|
+
if (interval !== null)
|
|
38
|
+
return;
|
|
39
|
+
last = clock();
|
|
40
|
+
interval = setInterval(() => {
|
|
41
|
+
const current = clock();
|
|
42
|
+
tick((current - last) / 1000);
|
|
43
|
+
last = current;
|
|
44
|
+
}, 1000 / tickHz);
|
|
45
|
+
},
|
|
46
|
+
async close() {
|
|
47
|
+
stop();
|
|
48
|
+
await ws.close();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @jgengine/node
|
|
2
2
|
> Standalone authoritative game host for JGengine: in-memory server snapshots, tick loop, save-cadence flush, WebSocket server, memory/file persistence.
|
|
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
|
|
@@ -15,13 +15,17 @@ Imports use deep paths: `@jgengine/node/<path>`.
|
|
|
15
15
|
- GameSocketIoServerOptions (type): type GameSocketIoServerOptions = HostRouterOptions & { io: SocketIoLikeServer }
|
|
16
16
|
- GameWsServer (type): type GameWsServer = { wss: WebSocketServer; port: () => number; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => Promise<void>; }
|
|
17
17
|
- GameWsServerOptions (type): type GameWsServerOptions = HostRouterOptions & { server?: HttpServer; port?: number; path?: string; }
|
|
18
|
+
- HostedGameDefinition (interface): interface HostedGameDefinition — A game the world server can host — its authoritative {@link GameDefinition} and the content lookup a `GameContext` reads.
|
|
18
19
|
- NodeHandler (type): type NodeHandler = (req: IncomingMessage, res: ServerResponse) => void
|
|
19
20
|
- SocketIoLikeServer (type): type SocketIoLikeServer = { on: (event: "connection", listener: (socket: SocketIoLikeServerSocket) => void) => unknown; }
|
|
20
21
|
- SocketIoLikeServerSocket (type): type SocketIoLikeServerSocket = { on: (event: string, listener: (payload: string) => void) => unknown; send: (data: string) => unknown; disconnect: (close?: boolean) => unknown; }
|
|
21
22
|
- WebHandler (type): type WebHandler = (request: Request) => Promise<Response>
|
|
23
|
+
- WorldGameServer (interface): interface WorldGameServer — A runnable ws host for GameContext worlds: {@link createWorldGameHost} + {@link createGameWsServer} + a tick loop, with a manual `tick(dt)` seam so a fake clock can drive it in tests.
|
|
24
|
+
- WorldGameServerOptions (interface): interface WorldGameServerOptions extends Omit<GameWsServerOptions, "host"> — Config for {@link createWorldGameServer}: how to resolve a game by id, the tick cadence, and the underlying ws-server/router options (minus `host`, which the server builds).
|
|
22
25
|
- attachGameSocketIoServer (function): function attachGameSocketIoServer(options: GameSocketIoServerOptions): GameSocketIoServer
|
|
23
26
|
- clearFilePersistence (function): function clearFilePersistence(dir: string): Promise<void>
|
|
24
27
|
- createGameWsServer (function): function createGameWsServer(options: GameWsServerOptions): GameWsServer
|
|
28
|
+
- createWorldGameServer (function): function createWorldGameServer(options: WorldGameServerOptions): WorldGameServer — Build a {@link WorldGameServer} — one process hosting authoritative GameContext worlds over ws, ready for two-client play once {@link WorldGameServer.start} runs.
|
|
25
29
|
- filePersistence (function): function filePersistence(dir: string, now: () => number = Date.now): HostPersistence
|
|
26
30
|
- toNodeHandler (function): function toNodeHandler(handler: WebHandler): NodeHandler
|
|
27
31
|
- toWebRequest (function): function toWebRequest(req: IncomingMessage): Promise<Request>
|
|
@@ -51,6 +55,13 @@ Imports use deep paths: `@jgengine/node/<path>`.
|
|
|
51
55
|
- toNodeHandler (function): function toNodeHandler(handler: WebHandler): NodeHandler
|
|
52
56
|
- toWebRequest (function): function toWebRequest(req: IncomingMessage): Promise<Request>
|
|
53
57
|
|
|
58
|
+
### @jgengine/node/worldServer
|
|
59
|
+
|
|
60
|
+
- HostedGameDefinition (interface): interface HostedGameDefinition — A game the world server can host — its authoritative {@link GameDefinition} and the content lookup a `GameContext` reads.
|
|
61
|
+
- WorldGameServer (interface): interface WorldGameServer — A runnable ws host for GameContext worlds: {@link createWorldGameHost} + {@link createGameWsServer} + a tick loop, with a manual `tick(dt)` seam so a fake clock can drive it in tests.
|
|
62
|
+
- WorldGameServerOptions (interface): interface WorldGameServerOptions extends Omit<GameWsServerOptions, "host"> — Config for {@link createWorldGameServer}: how to resolve a game by id, the tick cadence, and the underlying ws-server/router options (minus `host`, which the server builds).
|
|
63
|
+
- createWorldGameServer (function): function createWorldGameServer(options: WorldGameServerOptions): WorldGameServer — Build a {@link WorldGameServer} — one process hosting authoritative GameContext worlds over ws, ready for two-client play once {@link WorldGameServer.start} runs.
|
|
64
|
+
|
|
54
65
|
### @jgengine/node/wsServer
|
|
55
66
|
|
|
56
67
|
- GameWsServer (type): type GameWsServer = { wss: WebSocketServer; port: () => number; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => Promise<void>; }
|
|
@@ -157,7 +168,9 @@ Exact import paths and export names — **do not invent paths**; every row below
|
|
|
157
168
|
| Loadout | `game/loadout` | `LoadoutDef`, `LoadoutItemEntry`, `Loadouts` |
|
|
158
169
|
| Cosmetic loadout | `game/cosmetics` | `createCosmetics`, `Cosmetics`, `CosmeticLoadoutDef` |
|
|
159
170
|
| Quest | `game/quest` | `QuestDef`, `QuestRewards`, `QuestObjective`, `QuestJournal` |
|
|
160
|
-
| World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building` |
|
|
171
|
+
| World features | `world/features` | `WorldFeature`, `biomes`, `voxel`, `plots`, `tilemap`, `flat`, `environment`, `terrain`, `rain`, `snow`, `grass`, `ocean`, `building`, `road` |
|
|
172
|
+
| 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 |
|
|
173
|
+
| Road ribbons | `world/roads` | `buildRoadRibbon`, `dashSegments`, `nearestOnPath`, `isOnRoad`, `pathLength`, `RoadRibbon`, `RoadSample`, `RoadPoint` — geometry + queries behind the `road()` environment feature |
|
|
161
174
|
| 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 |
|
|
162
175
|
| Terrain field | `world/terrain` | `TerrainField`, `noiseField`, `resolveTerrainField`, `rollingField`, `fractalNoise`, `valueNoise`, `withNormal`, `arenaField`, `flatField`, `resolveGroundStep`, `snapToGround`, `snapEntityToGround`, `resolveTerrainPalette`, `TERRAIN_MATERIAL_PALETTES` |
|
|
163
176
|
| Seeded RNG | `random/rng` | `seededRng`, `seededStreams` |
|
|
@@ -449,7 +462,7 @@ A voxel block is an object. A rack is an object with a slot inventory. A GPU is
|
|
|
449
462
|
|
|
450
463
|
## Game repo layout
|
|
451
464
|
|
|
452
|
-
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.
|
|
465
|
+
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.
|
|
453
466
|
|
|
454
467
|
```
|
|
455
468
|
src/
|
|
@@ -480,6 +493,8 @@ src/
|
|
|
480
493
|
|
|
481
494
|
**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.
|
|
482
495
|
|
|
496
|
+
**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.)
|
|
497
|
+
|
|
483
498
|
```ts
|
|
484
499
|
// game.config.ts — imports only, nothing inline
|
|
485
500
|
import { defineGame } from "@jgengine/shell/defineGame";
|
|
@@ -541,7 +556,7 @@ export const physics: PhysicsConfig = { gravity: -32 };
|
|
|
541
556
|
|
|
542
557
|
- `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.
|
|
543
558
|
- 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.
|
|
544
|
-
- **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).
|
|
559
|
+
- **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`).
|
|
545
560
|
- **`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.
|
|
546
561
|
- 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.
|
|
547
562
|
- `server.mode` is a string your loop/commands interpret — the engine ships no gamemode presets.
|
|
@@ -626,7 +641,7 @@ Catalog-first, no per-game audio glue. The `audio` field of `defineGame({...})`
|
|
|
626
641
|
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:
|
|
627
642
|
| `rig` | For | Key config (`camera.<rig>`) |
|
|
628
643
|
|-------|-----|------------------------------|
|
|
629
|
-
| `orbit` (default) | Third-person chase | `initialDistance`, `targetHeight`, `min/maxPolarAngle`, drag/zoom |
|
|
644
|
+
| `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 |
|
|
630
645
|
| `first` | FPS mouse-look | `firstPerson: { eyeHeight, sensitivity, maxPitch, reticle, viewmodel }` |
|
|
631
646
|
| `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`) |
|
|
632
647
|
| `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 |
|
|
@@ -685,7 +700,7 @@ ctx.time advance, now, calendar, snapshot; pause, play, toggle,
|
|
|
685
700
|
ctx.subscribe / ctx.version change signal — UI layers bind via useSyncExternalStore
|
|
686
701
|
```
|
|
687
702
|
|
|
688
|
-
`content.itemById(id)` supplies `{ use?, weapon?, trade? }
|
|
703
|
+
`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.
|
|
689
704
|
|
|
690
705
|
### Two tiers: `ctx` runtime vs pure factories
|
|
691
706
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Standalone authoritative game host for JGengine: in-memory server snapshots, tick loop, save-cadence flush, WebSocket server, memory/file persistence.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"test": "bun test src"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@jgengine/core": "^0.
|
|
35
|
-
"@jgengine/ws": "^0.
|
|
34
|
+
"@jgengine/core": "^0.10.0",
|
|
35
|
+
"@jgengine/ws": "^0.10.0",
|
|
36
36
|
"ws": "^8.18.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|