@jgengine/core 0.16.0 → 0.18.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 +46 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +45 -1
- package/dist/movement/playerMovement.js +6 -172
- package/dist/movement/solidObstacles.d.ts +87 -0
- package/dist/movement/solidObstacles.js +208 -0
- package/dist/runtime/commandRunner.d.ts +36 -0
- package/dist/runtime/commandRunner.js +36 -0
- package/dist/runtime/context/scene.js +11 -0
- package/dist/runtime/gameContext.js +4 -1
- package/dist/runtime/gameRuntime.d.ts +20 -1
- package/dist/runtime/gameRuntime.js +10 -1
- package/dist/runtime/hostPersistence.d.ts +27 -2
- package/dist/runtime/hostPersistence.js +59 -7
- package/dist/runtime/hostPolicy.d.ts +54 -0
- package/dist/runtime/hostPolicy.js +53 -0
- package/dist/runtime/transport.d.ts +6 -0
- package/dist/runtime/worldChunks.d.ts +39 -0
- package/dist/runtime/worldChunks.js +83 -0
- package/dist/runtime/worldProjection.d.ts +16 -2
- package/dist/runtime/worldProjection.js +20 -0
- package/dist/scene/behaviorRuntime.js +25 -3
- package/dist/scene/spatial.d.ts +16 -0
- package/dist/scene/spatial.js +7 -4
- package/package.json +1 -1
- package/skills/jgengine/api.md +29 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine-world/api.md +6 -0
- package/skills/jgengine-world/capabilities.md +8 -0
|
@@ -41,6 +41,26 @@ export function projectByVisibleIds(byId, visible) {
|
|
|
41
41
|
out[id] = value;
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
|
+
/** Where a viewer is standing, read off the replicated entity set; `undefined` when it has no entity yet.
|
|
45
|
+
* @internal host projection plumbing
|
|
46
|
+
*/
|
|
47
|
+
export function viewerOrigin(world, viewer) {
|
|
48
|
+
const entities = (world["entities"] ?? []);
|
|
49
|
+
return entities.find((entity) => entity.id === viewer.userId)?.position;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cull placed objects to a viewer's area of interest. Placed objects are the one large spatial set a
|
|
53
|
+
* world grows without bound — a datacenter of racks, a city of props — so without this every viewer
|
|
54
|
+
* downloads every placement. Fails open (full list) when the viewer has no locatable entity, matching
|
|
55
|
+
* {@link projectEntitiesForViewer}.
|
|
56
|
+
* @internal host projection plumbing
|
|
57
|
+
*/
|
|
58
|
+
export function projectObjectsForViewer(objects, origin, radius) {
|
|
59
|
+
if (origin === undefined)
|
|
60
|
+
return objects;
|
|
61
|
+
const radiusSquared = radius * radius;
|
|
62
|
+
return objects.filter((object) => distanceSquared(object.position, origin) <= radiusSquared);
|
|
63
|
+
}
|
|
44
64
|
/**
|
|
45
65
|
* Narrow a `userId → state` record to only the viewer's own entry — the projection for private
|
|
46
66
|
* per-user state (inventory, wallets) so one client never receives another player's private data.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { seededRng } from "../random/rng.js";
|
|
2
2
|
import { advancePathFollow, createPathFollow, pathFollowSeek, } from "../nav/pathFollow.js";
|
|
3
|
+
import { resolveWalkerStep } from "../movement/solidObstacles.js";
|
|
3
4
|
import { perContext } from "../runtime/perContext.js";
|
|
4
5
|
const DEFAULT_WANDER_SPEED = 1.5;
|
|
5
6
|
const WANDER_ARRIVAL = 0.6;
|
|
7
|
+
/** Below this share of its intended step a wanderer counts as blocked and re-rolls its target. */
|
|
8
|
+
const WANDER_BLOCKED_FRACTION = 0.25;
|
|
6
9
|
function patrolOf(entity) {
|
|
7
10
|
return entity.behaviors.find((b) => b.kind === "patrol") ?? null;
|
|
8
11
|
}
|
|
@@ -72,18 +75,37 @@ function stepWander(ctx, id, nav, dt) {
|
|
|
72
75
|
if (dist < 1e-6)
|
|
73
76
|
return;
|
|
74
77
|
const step = Math.min(dist, speed * dt);
|
|
75
|
-
const
|
|
76
|
-
const
|
|
78
|
+
const desired = resolveWalkerStep(ctx, entity.position, (dx / dist) * step, (dz / dist) * step);
|
|
79
|
+
const nx = px + desired.stepX;
|
|
80
|
+
const nz = pz + desired.stepZ;
|
|
81
|
+
// A wanderer pinned against a solid picks a fresh target next tick instead of grinding into it.
|
|
82
|
+
if (Math.hypot(desired.stepX, desired.stepZ) < step * WANDER_BLOCKED_FRACTION)
|
|
83
|
+
nav.target = null;
|
|
77
84
|
ctx.scene.entity.setPose(id, {
|
|
78
85
|
position: [nx, ctx.world.groundHeightAt(nx, nz), nz],
|
|
79
86
|
rotationY: Math.atan2(dx, dz),
|
|
80
87
|
dt,
|
|
81
88
|
});
|
|
82
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Pose a patroller at its path point, with the *move* to that point slid against solids. The path state
|
|
92
|
+
* still advances on its own clock — the route is authored, and rewinding it on contact would stall the
|
|
93
|
+
* whole patrol behind one prop — but the entity can no longer end a tick inside geometry the player
|
|
94
|
+
* cannot walk through. A route drawn through a building shows up as an NPC pressed against its wall,
|
|
95
|
+
* which is the authoring bug made visible rather than hidden.
|
|
96
|
+
*/
|
|
83
97
|
function posePatrol(ctx, id, nav, dt) {
|
|
84
98
|
const [x, y, z] = nav.state.position;
|
|
99
|
+
const entity = ctx.scene.entity.get(id);
|
|
100
|
+
let px = x;
|
|
101
|
+
let pz = z;
|
|
102
|
+
if (entity !== null) {
|
|
103
|
+
const step = resolveWalkerStep(ctx, entity.position, x - entity.position[0], z - entity.position[2]);
|
|
104
|
+
px = entity.position[0] + step.stepX;
|
|
105
|
+
pz = entity.position[2] + step.stepZ;
|
|
106
|
+
}
|
|
85
107
|
ctx.scene.entity.setPose(id, {
|
|
86
|
-
position: nav.groundClamp ? [
|
|
108
|
+
position: nav.groundClamp ? [px, ctx.world.groundHeightAt(px, pz), pz] : [px, y, pz],
|
|
87
109
|
rotationY: nav.state.heading,
|
|
88
110
|
dt,
|
|
89
111
|
});
|
package/dist/scene/spatial.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface MoveTowardOptions {
|
|
|
17
17
|
speed: number;
|
|
18
18
|
stopDistance?: number;
|
|
19
19
|
dt: number;
|
|
20
|
+
/**
|
|
21
|
+
* Slide the step against the solid geometry that blocks the player instead of lerping through it
|
|
22
|
+
* (default `true` wherever the runtime wires a resolver). Set `false` for a mover that is meant to
|
|
23
|
+
* ignore the world — a flier, a camera rig, a decorative drift.
|
|
24
|
+
*/
|
|
25
|
+
avoidSolids?: boolean;
|
|
20
26
|
}
|
|
21
27
|
export interface SpatialGridOptions {
|
|
22
28
|
cellSize: number;
|
|
@@ -37,6 +43,16 @@ export interface SpatialApiOptions {
|
|
|
37
43
|
grid?: SpatialGridOptions | false;
|
|
38
44
|
/** When this number changes between queries, the grid rebuilds without an explicit `invalidate()`. */
|
|
39
45
|
getVersion?: () => number;
|
|
46
|
+
/**
|
|
47
|
+
* Slide a horizontal step against the scene's solid geometry, returning the step actually allowed.
|
|
48
|
+
* Wired by the runtime to the same blocking-collider query the player resolver reads, so a chaser
|
|
49
|
+
* calling `moveToward` stops at the wall the player stops at. Omitted (tests, bare indexes) leaves
|
|
50
|
+
* `moveToward` a pure lerp.
|
|
51
|
+
*/
|
|
52
|
+
resolveStep?: (position: EntityPosition, stepX: number, stepZ: number) => {
|
|
53
|
+
stepX: number;
|
|
54
|
+
stepZ: number;
|
|
55
|
+
};
|
|
40
56
|
}
|
|
41
57
|
export interface SpatialApi {
|
|
42
58
|
distance(aInstanceId: string, bInstanceId: string): number | null;
|
package/dist/scene/spatial.js
CHANGED
|
@@ -23,7 +23,7 @@ function packCell(cx, cz) {
|
|
|
23
23
|
}
|
|
24
24
|
const DEFAULT_CELL_SIZE = 8;
|
|
25
25
|
export function createSpatialApi(options) {
|
|
26
|
-
const { resolvePosition, candidates, occluder, getVersion } = options;
|
|
26
|
+
const { resolvePosition, candidates, occluder, getVersion, resolveStep } = options;
|
|
27
27
|
const gridConfig = options.grid === false ? undefined : (options.grid ?? { cellSize: DEFAULT_CELL_SIZE });
|
|
28
28
|
const cellSize = gridConfig?.cellSize;
|
|
29
29
|
let gridIndex = null;
|
|
@@ -198,7 +198,7 @@ export function createSpatialApi(options) {
|
|
|
198
198
|
invalidate() {
|
|
199
199
|
gridDirty = true;
|
|
200
200
|
},
|
|
201
|
-
moveToward(instanceId, target, { speed, stopDistance = 0, dt }) {
|
|
201
|
+
moveToward(instanceId, target, { speed, stopDistance = 0, dt, avoidSolids = true }) {
|
|
202
202
|
const current = resolvePosition(instanceId);
|
|
203
203
|
const destination = resolveTarget(target);
|
|
204
204
|
if (current === undefined || destination === undefined)
|
|
@@ -209,10 +209,13 @@ export function createSpatialApi(options) {
|
|
|
209
209
|
return current;
|
|
210
210
|
const step = Math.min(speed * dt, remaining);
|
|
211
211
|
const scale = step / total;
|
|
212
|
+
const stepX = (destination[0] - current[0]) * scale;
|
|
213
|
+
const stepZ = (destination[2] - current[2]) * scale;
|
|
214
|
+
const slid = avoidSolids && resolveStep !== undefined ? resolveStep(current, stepX, stepZ) : { stepX, stepZ };
|
|
212
215
|
return [
|
|
213
|
-
current[0] +
|
|
216
|
+
current[0] + slid.stepX,
|
|
214
217
|
current[1] + (destination[1] - current[1]) * scale,
|
|
215
|
-
current[2] +
|
|
218
|
+
current[2] + slid.stepZ,
|
|
216
219
|
];
|
|
217
220
|
},
|
|
218
221
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "jgengine core — pure-TypeScript game framework SDK (npm @jgengine/core). Entity stores, commands, combat, inventory, multiplayer seams. Zero dependencies. Not an ECS. Agent skills ship in the tarball. https://jgengine.com",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jgengine",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.18.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
|
|
229
229
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
230
230
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
231
|
-
- `VERSION` (const): const VERSION: "0.
|
|
231
|
+
- `VERSION` (const): const VERSION: "0.18.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
232
232
|
|
|
233
233
|
## @jgengine/core/runtime/adapter
|
|
234
234
|
|
|
@@ -260,7 +260,8 @@
|
|
|
260
260
|
|
|
261
261
|
## @jgengine/core/runtime/commandRunner
|
|
262
262
|
|
|
263
|
-
- `CommandDef` (type): type CommandDef<TInput = unknown> = {
|
|
263
|
+
- `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * What this command reads and writes, derived from its own input. A host that hydrates through a * scope loads only this slice instead of the whole world, and refuses the command if `apply` then * dirties a player or chunk the scope did not name — writing an… — ⚠ undocumented
|
|
264
|
+
- `CommandScope` (type): type CommandScope = { /** Member ids to hydrate; omit for the whole roster. */ players?: readonly string[]; /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */ chunkKeys?: readonly string[]; } — Which slice of a server has to be hydrated before some work runs against it. Both fields default to "everything", which costs a read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch.
|
|
264
265
|
- `CommandValidationError` (type): type CommandValidationError = { reason: string } — ⚠ undocumented
|
|
265
266
|
- `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string } — ⚠ undocumented
|
|
266
267
|
|
|
@@ -334,13 +335,13 @@
|
|
|
334
335
|
|
|
335
336
|
## @jgengine/core/runtime/gameRuntime
|
|
336
337
|
|
|
337
|
-
- `GameRuntime` (type): type GameRuntime = { gameId: string; save: SaveConfig;
|
|
338
|
+
- `GameRuntime` (type): type GameRuntime = { gameId: string; save: SaveConfig; /** * Whether this runtime declares `loop.onTick`. A host's tick cron reads it to skip hydrating and * persisting a server whose `tick` is a no-op by construction — the difference between a world that * costs a full read/write every second while… — ⚠ undocumented
|
|
338
339
|
- `GameRuntimeDefinition` (type): type GameRuntimeDefinition = { gameId: string; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; } — ⚠ undocumented
|
|
339
340
|
- `HydrateInput` (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; revision?: number; } — ⚠ undocumented
|
|
340
341
|
- `RuntimeInitContext` (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; } — ⚠ undocumented
|
|
341
342
|
- `RuntimeLoopContext` (type): type RuntimeLoopContext = RuntimeInitContext & { player: { userId: string; isNew: boolean; }; } — ⚠ undocumented
|
|
342
343
|
- `RuntimeWorldContext` (type): type RuntimeWorldContext = RuntimeInitContext & { playerIds: readonly string[]; } — ⚠ undocumented
|
|
343
|
-
- `ServerLoopHooks` (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void;
|
|
344
|
+
- `ServerLoopHooks` (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void; /** * What `onNewPlayer` touches, so a join hydrates that instead of the whole world. Only consulted * when `onNewPlay… — ⚠ undocumented
|
|
344
345
|
|
|
345
346
|
## @jgengine/core/runtime/headlessRunner
|
|
346
347
|
|
|
@@ -365,21 +366,28 @@
|
|
|
365
366
|
- `OPEN_SERVER_LISTING_MAX` (const): const OPEN_SERVER_LISTING_MAX: 100 — ⚠ undocumented
|
|
366
367
|
- `PlayerProfileRecord` (type): type PlayerProfileRecord = { userId: string; gameId: string; playerState: RuntimePlayerRow; revision: number; updatedAt: number; } — ⚠ undocumented
|
|
367
368
|
- `ServerListing` (type): type ServerListing = { serverId: string; status: GameServerStatus; memberCount: number; slotsPerServer: number; mode?: string; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; updatedAt: number; } — ⚠ undocumented
|
|
368
|
-
- `ServerPersistPlan` (type): type ServerPersistPlan = { server: GameServerRecord; profiles: PlayerProfileRecord[]; chunks: WorldChunkRecord[]; deletedChunks: string[]; leaderboard: LeaderboardIncrement[];
|
|
369
|
+
- `ServerPersistPlan` (type): type ServerPersistPlan = { server: GameServerRecord; profiles: PlayerProfileRecord[]; chunks: WorldChunkRecord[]; deletedChunks: string[]; leaderboard: LeaderboardIncrement[]; /** * Which fields of the server row the plan actually moved. A store that pays per written field — * or whose subscribers r… — ⚠ undocumented
|
|
369
370
|
- `SessionAttributes` (type): type SessionAttributes = { label?: string; mode?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; } — ⚠ undocumented
|
|
370
371
|
- `SessionVisibility` (type): type SessionVisibility = "public" | "private" — ⚠ undocumented
|
|
371
372
|
- `ToServerListingOptions` (type): type ToServerListingOptions = { includeJoinCode?: boolean; } — ⚠ undocumented
|
|
372
373
|
- `WorldChunkRecord` (type): type WorldChunkRecord = { serverId: string; chunkKey: string; snapshot: RuntimeChunkRow; updatedAt: number; } — ⚠ undocumented
|
|
374
|
+
- `isSnapshotClean` (function): function isSnapshotClean(snapshot: GameRuntimeSnapshot): boolean — True when a snapshot carries no unwritten mutation — nothing for a persist to do.
|
|
373
375
|
|
|
374
376
|
## @jgengine/core/runtime/hostPolicy
|
|
375
377
|
|
|
378
|
+
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Hard ceiling on `slotsPerServer` for a single hosted server row.
|
|
379
|
+
- `JoinCandidate` (type): type JoinCandidate = { memberUserIds: readonly string[]; slotsPerServer: number; visibility?: SessionVisibility | undefined; createdAt: number; } — A row `selectJoinTarget` can choose between — the matchmaking-relevant fields of a server record.
|
|
380
|
+
- `JoinTarget` (type): type JoinTarget<T extends JoinCandidate> = | { kind: "join"; row: T } | { kind: "create" } | { kind: "refuse"; reason: string } — What a join should do with the candidate rows it found.
|
|
381
|
+
- `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
|
|
376
382
|
- `canJoinPrivateServer` (function): function canJoinPrivateServer(args: { isMember: boolean; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Private-server join-code gate. Existing members always pass; non-members must present a matching `joinCode` (loose-normalized via {@link normalizeJoinCode}). Callers still decide whether the server is private — this only answers the code/membership half.
|
|
377
383
|
- `isAutoJoinCandidate` (function): function isAutoJoinCandidate(args: { memberUserIds: readonly string[]; slotsPerServer: number; visibility: SessionVisibility | undefined; userId: string; }): boolean — Auto-match candidate when no `serverId` is supplied: already a member, or a public room with free capacity. Private rooms are never auto-picked (join-by-code / direct id only).
|
|
378
384
|
- `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
|
|
379
385
|
- `isPrivateJoinBlocked` (function): function isPrivateJoinBlocked(args: { visibility: SessionVisibility | undefined; memberUserIds: readonly string[]; userId: string; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Whether a private-visibility server blocks this join (non-member without a matching code). Public / undefined visibility never blocks.
|
|
380
386
|
- `isServerFull` (function): function isServerFull(memberUserIds: readonly string[], slotsPerServer: number, userId: string): boolean — True when the server has no free slots for a non-member. Existing members never count as "full" so rejoin/leave cycles keep working.
|
|
381
387
|
- `isServerMember` (function): function isServerMember(memberUserIds: readonly string[], userId: string): boolean — Whether `userId` is already on the server's member roster.
|
|
388
|
+
- `selectJoinTarget` (function): function selectJoinTarget<T extends JoinCandidate>(rows: readonly T[], args: { userId: string; mode?: MatchmakingMode; /** Capacity to judge rows against — the host's current option, when it supersedes the stored value. */ slotsPerServer?: number; }): JoinTarget<T> — Resolve an auto-match join against the game's joinable rows under a {@link MatchmakingMode}. Reach for it instead of hand-rolling the "find a room with space, else make one" scan, which is where a singleton world silently shards into per-player copies.
|
|
382
389
|
- `statusAfterLeave` (function): function statusAfterLeave(remainingMemberCount: number, currentStatus: GameServerStatus): GameServerStatus — Status after a leave: empty rooms reopen; non-empty rooms keep their current status.
|
|
390
|
+
- `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number): { ok: true } | { ok: false; reason: string } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
|
|
383
391
|
- `withJoinedMember` (function): function withJoinedMember(memberUserIds: readonly string[], userId: string): string[] — Roster after a successful join: unchanged when already a member, else appended.
|
|
384
392
|
- `withoutMember` (function): function withoutMember(memberUserIds: readonly string[], userId: string): string[] — Roster after a leave: `userId` removed; order of remaining members preserved.
|
|
385
393
|
|
|
@@ -470,7 +478,7 @@
|
|
|
470
478
|
- `JoinServerResult` (type): type JoinServerResult = { serverId: string; isNew: boolean; } — ⚠ undocumented
|
|
471
479
|
- `LiveGameBackend` (type): type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & { presenceSync: PresenceSync; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; chatSyncFor… — ⚠ undocumented
|
|
472
480
|
- `MultiplayerSession` (type): type MultiplayerSession = { gameId: string; userId: string; backend: LiveGameBackend; feedActions: string[]; } — ⚠ undocumented
|
|
473
|
-
- `PresencePoseRow` (type): type PresencePoseRow = { userId: string;
|
|
481
|
+
- `PresencePoseRow` (type): type PresencePoseRow = { userId: string; /** Set when the host tracks presence per session, so one user can hold two rows. */ sessionId?: string; /** Actor class, e.g. `"player"` / `"agent"` — a host may clamp each differently. */ kind?: string; /** Display name carried on the row, so a nameplate ne… — ⚠ undocumented
|
|
474
482
|
- `PresenceSync` (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; } — ⚠ undocumented
|
|
475
483
|
- `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; } — ⚠ undocumented
|
|
476
484
|
- `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string } — ⚠ undocumented
|
|
@@ -495,6 +503,20 @@
|
|
|
495
503
|
- `WorldHostConnection` (interface): interface WorldHostConnection — One client's link on the host side: routes its upstream frames into the shared session, pushes it sync frames.
|
|
496
504
|
- `WorldServerFrame` (type): type WorldServerFrame = | { t: "baseline"; revision: number; snapshot: WorldSnapshot } | { t: "diff"; diff: WorldDiff } — Host→client frame: the full baseline a joiner needs, then per-tick diffs. What a transport marshals downstream.
|
|
497
505
|
|
|
506
|
+
## @jgengine/core/runtime/worldChunks
|
|
507
|
+
|
|
508
|
+
- `ChunkCoord` (type): type ChunkCoord = { cx: number; cz: number } — Integer cell coordinates of a chunk on the ground plane.
|
|
509
|
+
- `DEFAULT_CHUNK_SIZE` (const): const DEFAULT_CHUNK_SIZE: 64 — Default edge length, in world units, of one persisted world chunk.
|
|
510
|
+
- `chunkCoordAt` (function): function chunkCoordAt(position: readonly [number, number, number], size: number = DEFAULT_CHUNK_SIZE): ChunkCoord — Chunk coordinates containing a world position at `size` units per cell.
|
|
511
|
+
- `chunkKeyAt` (function): function chunkKeyAt(position: readonly [number, number, number], size: number = DEFAULT_CHUNK_SIZE): string — Chunk key containing a world position at `size` units per cell.
|
|
512
|
+
- `chunkKeyOf` (function): function chunkKeyOf(coord: ChunkCoord): string — Chunk key for a cell — the canonical `"cx,cz"` string a `jgWorldChunks` row is stored under. Games used to invent this mapping each time, which made two systems in one game disagree about which row a position belongs to.
|
|
513
|
+
- `chunkKeysInRadius` (function): function chunkKeysInRadius(center: readonly [number, number, number], radius: number, size: number = DEFAULT_CHUNK_SIZE): string[] — Every chunk key whose cell intersects a radius around a world position — the key list a viewer's bounded hydration or client chunk query asks for, instead of loading a whole world.
|
|
514
|
+
- `createEmptyChunkRow` (function): function createEmptyChunkRow(chunkKey: string): RuntimeChunkRow — An empty chunk row for `chunkKey` — the starting value for a cell nothing has been placed in yet.
|
|
515
|
+
- `deleteChunk` (function): function deleteChunk(snapshot: GameRuntimeSnapshot, chunkKey: string): GameRuntimeSnapshot — Drop a chunk from a snapshot and mark the key dirty so the host deletes its stored row.
|
|
516
|
+
- `parseChunkKey` (function): function parseChunkKey(key: string): ChunkCoord | null — Parse a `chunkKeyOf` string back to coordinates; `null` for anything not in that form.
|
|
517
|
+
- `setChunk` (function): function setChunk(snapshot: GameRuntimeSnapshot, chunkKey: string, chunk: RuntimeChunkRow): GameRuntimeSnapshot — Write a chunk into a snapshot and mark it dirty, so the next persist actually stores it. The engine never writes `dirty.chunks` for a raw `snapshot.chunks[key] = …` assignment, so a command that mutated the map by hand silently never saved that chunk.
|
|
518
|
+
- `updateChunk` (function): function updateChunk(snapshot: GameRuntimeSnapshot, chunkKey: string, update: (chunk: RuntimeChunkRow) => RuntimeChunkRow): GameRuntimeSnapshot — Read-modify-write a chunk (creating it empty when absent), marking it dirty.
|
|
519
|
+
|
|
498
520
|
## @jgengine/core/runtime/worldMirror
|
|
499
521
|
|
|
500
522
|
- `WorldMirror` (interface): interface WorldMirror — The client end of host-authoritative replication: folds a host's baseline + {@link WorldDiff} stream onto a local {@link GameContext}. It keeps the last full {@link WorldSnapshot}, advances it with each diff, and pushes the result through `ctx.hydrate` — so the client mirrors exactly the subsystems its own game opted into and silently ignores host modules it lacks. This is the inverse of a {@link HostedWorldSession}; the transport in between (loopback, ws, Convex) is irrelevant.
|
|
@@ -48,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
48
48
|
|
|
49
49
|
- `canJoinPrivateServer` (function) · `import { canJoinPrivateServer } from "@jgengine/core/runtime/hostPolicy"`
|
|
50
50
|
|
|
51
|
+
## host-join-target — choose or refuse an auto-matched server under a matchmaking mode
|
|
52
|
+
|
|
53
|
+
- `selectJoinTarget` (function) · `import { selectJoinTarget } from "@jgengine/core/runtime/hostPolicy"`
|
|
54
|
+
|
|
51
55
|
## host-listing-filter — exclude private sessions from public browse results
|
|
52
56
|
|
|
53
57
|
- `isListablePublicly` (function) · `import { isListablePublicly } from "@jgengine/core/runtime/hostPolicy"`
|
|
@@ -146,6 +150,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
146
150
|
|
|
147
151
|
- `defineStore` (function) · `import { defineStore } from "@jgengine/shell/gameKit"`
|
|
148
152
|
|
|
153
|
+
## world-chunk-key — map world positions to persisted chunk keys at a declared cell size
|
|
154
|
+
|
|
155
|
+
- `chunkKeyOf` (function) · `import { chunkKeyOf } from "@jgengine/core/runtime/worldChunks"`
|
|
156
|
+
|
|
157
|
+
## world-chunk-write — mutate a snapshot's chunk and have the host persist it
|
|
158
|
+
|
|
159
|
+
- `setChunk` (function) · `import { setChunk } from "@jgengine/core/runtime/worldChunks"`
|
|
160
|
+
|
|
149
161
|
## world-convergence — derive environment coordinate content from the scene document
|
|
150
162
|
|
|
151
163
|
- `environmentContentFromDocument` (function) · `import { environmentContentFromDocument } from "@jgengine/shell/gameKit"`
|
|
@@ -587,6 +587,12 @@
|
|
|
587
587
|
- `PoseState` (interface): interface PoseState — ⚠ undocumented
|
|
588
588
|
- `createPoseState` (function): function createPoseState(resolveAllowed: (instanceId: string) => PoseAllowedStates | null | undefined): PoseState — Stance/pose transitions — stand, crouch, prone — that change the hitbox and movement.
|
|
589
589
|
|
|
590
|
+
## @jgengine/core/movement/solidObstacles
|
|
591
|
+
|
|
592
|
+
- `ObstacleReach` (interface): interface ObstacleReach — Largest blocking-collider extent in the scene, split so tower height cannot inflate the XZ query.
|
|
593
|
+
- `resolveWalkerStep` (function): function resolveWalkerStep(ctx: GameContext, position: EntityPosition, stepX: number, stepZ: number, options: { radius?: number; stepUpHeight?: number } = {}): { stepX: number; stepZ: number } — Slide a walker's horizontal step against the same solid geometry that stops the player, returning the X/Z it may actually take this tick. A step into a wall is cut on the blocked axis and preserved on the other, exactly as {@link stepPlayerMovement} resolves the player's.
|
|
594
|
+
- `solidObstaclesNear` (function): function solidObstaclesNear(ctx: GameContext, position: EntityPosition, reachX: number, reachZ: number, height = WALKER_HEIGHT): CollisionObstacle[] — Every blocking physical obstacle overlapping the box `position` ± `reachX`/`reachZ`, read from the scene's resolved colliders.
|
|
595
|
+
|
|
590
596
|
## @jgengine/core/movement/steering
|
|
591
597
|
|
|
592
598
|
- `YawVectorXZ` (type): type YawVectorXZ = readonly [number, number] — Yaw-frame steering math. The engine's heading convention is `forward = (sin yaw, cos yaw)` on the XZ plane with +Y up, which makes a positive yaw increment rotate counterclockwise seen from above — a turn to the entity's LEFT on screen. Every hand-written `heading += steer * rate * dt` therefore turns the wrong way; integrate steering through `steerYaw` instead.
|
|
@@ -496,6 +496,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
496
496
|
|
|
497
497
|
- `SOIL_KIND` (const) · `import { SOIL_KIND } from "@jgengine/core/world"`
|
|
498
498
|
|
|
499
|
+
## solid-obstacles — the one blocking-geometry query the player resolver and NPC movers share
|
|
500
|
+
|
|
501
|
+
- `solidObstaclesNear` (function) · `import { solidObstaclesNear } from "@jgengine/core/movement/solidObstacles"`
|
|
502
|
+
|
|
499
503
|
## spatial-region-box — sample uniformly within an axis-aligned 3D box
|
|
500
504
|
|
|
501
505
|
- `boxRegion` (function) · `import { boxRegion } from "@jgengine/core/world"`
|
|
@@ -593,6 +597,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
593
597
|
|
|
594
598
|
- `VolumetricCloudsConfig` (interface) · `import { VolumetricCloudsConfig } from "@jgengine/core/world"`
|
|
595
599
|
|
|
600
|
+
## walker-solid-step — slide an NPC's step against the geometry that blocks the player
|
|
601
|
+
|
|
602
|
+
- `resolveWalkerStep` (function) · `import { resolveWalkerStep } from "@jgengine/core/movement/solidObstacles"`
|
|
603
|
+
|
|
596
604
|
## wave-runner — observable serializable wave/spawn runner over the seeded spawn director — WAVE N, wave progress, budget/alert readout, onSpawn sink, snapshot/restore
|
|
597
605
|
|
|
598
606
|
- `createWaveRunner` (function) · `import { createWaveRunner } from "@jgengine/core/world"`
|