@jgengine/convex 0.50.0 → 0.52.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 +1363 -0
- package/dist/createConvexGameTransport.js +13 -0
- package/dist/hostedServer.d.ts +1 -1
- package/dist/hostedServer.js +3 -2
- package/dist/server.d.ts +23 -0
- package/dist/server.js +31 -8
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +43 -63
- package/skills/jgengine/api.md +26 -9
- package/skills/jgengine/capabilities.md +8 -0
- package/skills/jgengine/recipes/cookbook.md +752 -0
- package/skills/jgengine-multiplayer/SKILL.md +9 -0
- package/skills/jgengine-multiplayer/api.md +37 -20
- package/skills/jgengine-multiplayer/references/peer-worlds.md +1 -1
|
@@ -104,6 +104,14 @@ export function createConvexPresenceSync(client, api, config, tuning) {
|
|
|
104
104
|
const viewerChunks = new Map();
|
|
105
105
|
const refreshers = new Map();
|
|
106
106
|
const lastSentAt = new Map();
|
|
107
|
+
// A pose row outlives its tab unless something ends it; the last unsubscribe and an unload both do.
|
|
108
|
+
const leave = (serverId) => {
|
|
109
|
+
void client.mutation(api.presence.leave, { serverId, ...(config.userId === undefined ? {} : { externalId: config.userId }) }).catch(() => undefined);
|
|
110
|
+
};
|
|
111
|
+
const onPageHide = () => {
|
|
112
|
+
for (const serverId of refreshers.keys())
|
|
113
|
+
leave(serverId);
|
|
114
|
+
};
|
|
107
115
|
return {
|
|
108
116
|
subscribe(serverId, onChange) {
|
|
109
117
|
let unsubscribe;
|
|
@@ -115,6 +123,8 @@ export function createConvexPresenceSync(client, api, config, tuning) {
|
|
|
115
123
|
let listeners = refreshers.get(serverId);
|
|
116
124
|
if (listeners === undefined) {
|
|
117
125
|
listeners = new Set();
|
|
126
|
+
if (refreshers.size === 0)
|
|
127
|
+
globalThis.addEventListener?.("pagehide", onPageHide);
|
|
118
128
|
refreshers.set(serverId, listeners);
|
|
119
129
|
}
|
|
120
130
|
listeners.add(refresh);
|
|
@@ -127,6 +137,9 @@ export function createConvexPresenceSync(client, api, config, tuning) {
|
|
|
127
137
|
viewerChunks.delete(serverId);
|
|
128
138
|
gates.delete(serverId);
|
|
129
139
|
lastSentAt.delete(serverId);
|
|
140
|
+
leave(serverId);
|
|
141
|
+
if (refreshers.size === 0)
|
|
142
|
+
globalThis.removeEventListener?.("pagehide", onPageHide);
|
|
130
143
|
}
|
|
131
144
|
};
|
|
132
145
|
},
|
package/dist/hostedServer.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface HostedWorldOutcome<T> {
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function invokeHostedWorld<T>(invocation: HostedWorldInvocation<T>): Promise<HostedWorldOutcome<T>>;
|
|
41
41
|
/** Default minimum elapsed ms before `tickHostedWorlds` advances a world — override via the factory's `tickMs`. */
|
|
42
|
-
export declare const JG_HOSTED_TICK_MS =
|
|
42
|
+
export declare const JG_HOSTED_TICK_MS = 50;
|
|
43
43
|
/**
|
|
44
44
|
* The Convex counterpart of ws's `createWorldGameHost`: serves GameContext worlds statelessly by
|
|
45
45
|
* reconstructing a hosted session from the persisted `jgHostedWorlds` row on every invocation via
|
package/dist/hostedServer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { defineSchema, internalMutationGeneric, mutationGeneric, queryGeneric, }
|
|
|
2
2
|
import { ConvexError, v } from "convex/values";
|
|
3
3
|
import { createHostedGameRunner, INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
|
|
4
4
|
import { createHostedWorldSessionAsync, } from "@jgengine/core/runtime/hostedWorldSession";
|
|
5
|
+
import { DEFAULT_HOST_TICK_MS, resolveHostTickMs } from "@jgengine/core/runtime/hostPolicy";
|
|
5
6
|
import { commitIfRevisionMatch } from "./occ.js";
|
|
6
7
|
import { jgengineHostedTables, resolveActor } from "./server.js";
|
|
7
8
|
/**
|
|
@@ -40,7 +41,7 @@ const query = queryGeneric;
|
|
|
40
41
|
const mutation = mutationGeneric;
|
|
41
42
|
const internalMutation = internalMutationGeneric;
|
|
42
43
|
/** Default minimum elapsed ms before `tickHostedWorlds` advances a world — override via the factory's `tickMs`. */
|
|
43
|
-
export const JG_HOSTED_TICK_MS =
|
|
44
|
+
export const JG_HOSTED_TICK_MS = DEFAULT_HOST_TICK_MS;
|
|
44
45
|
async function getWorldRow(ctx, gameId, serverId) {
|
|
45
46
|
return await ctx.db
|
|
46
47
|
.query("jgHostedWorlds")
|
|
@@ -100,7 +101,7 @@ async function persistWorldRow(ctx, args) {
|
|
|
100
101
|
*/
|
|
101
102
|
export function createHostedGameServerFunctions(options) {
|
|
102
103
|
const mode = options.auth ?? "anonymous";
|
|
103
|
-
const tickMs = options.tickMs ?? JG_HOSTED_TICK_MS;
|
|
104
|
+
const tickMs = resolveHostTickMs(options.tickMs ?? JG_HOSTED_TICK_MS);
|
|
104
105
|
function resolveGame(gameId) {
|
|
105
106
|
const game = options.games[gameId];
|
|
106
107
|
if (game === undefined)
|
package/dist/server.d.ts
CHANGED
|
@@ -662,6 +662,10 @@ export type LoadedServerSnapshot = {
|
|
|
662
662
|
*/
|
|
663
663
|
export type GameServerHelpers = {
|
|
664
664
|
ensureServer: (ctx: JGMutationCtx, gameId: string) => Promise<ServerDoc>;
|
|
665
|
+
/** The shared singleton world `ensureServer` would return, read without creating it, so a query can scope reads to it. `null` until a player or host mutation creates it. */
|
|
666
|
+
findServer: (ctx: {
|
|
667
|
+
db: Pick<JGQueryCtx["db"], "query">;
|
|
668
|
+
}, gameId: string) => Promise<ServerDoc | null>;
|
|
665
669
|
loadSnapshot: (ctx: JGMutationCtx, serverId: string, scope?: LoadSnapshotScope) => Promise<LoadedServerSnapshot | null>;
|
|
666
670
|
applyCommand: (args: {
|
|
667
671
|
gameId: string;
|
|
@@ -904,6 +908,17 @@ export type PresenceSyncResult = {
|
|
|
904
908
|
lastSeenAt: number;
|
|
905
909
|
displaced: boolean;
|
|
906
910
|
};
|
|
911
|
+
/** A live pose row handed to a `checkpoint` writer, so a host can persist its own last-position state. */
|
|
912
|
+
export type PresenceCheckpoint = {
|
|
913
|
+
userId: string;
|
|
914
|
+
homeGameId?: string;
|
|
915
|
+
position: {
|
|
916
|
+
x: number;
|
|
917
|
+
y: number;
|
|
918
|
+
z: number;
|
|
919
|
+
};
|
|
920
|
+
rotationY: number;
|
|
921
|
+
};
|
|
907
922
|
/**
|
|
908
923
|
* The pose lane of a hosted server: the backend behind `PresenceSync`, which replicates where each
|
|
909
924
|
* member of one `jgGameServers` server is standing. It is not the backend for `PresenceTransport` —
|
|
@@ -936,6 +951,14 @@ export declare function createPresenceFunctions(options?: {
|
|
|
936
951
|
y: number;
|
|
937
952
|
z: number;
|
|
938
953
|
} | null;
|
|
954
|
+
/**
|
|
955
|
+
* Persist host-owned last-position state from a live row: on a sync at most once per `intervalMs`
|
|
956
|
+
* (default 30 s), and always when `leave` ends the row, so a returning player spawns where they stood.
|
|
957
|
+
*/
|
|
958
|
+
checkpoint?: {
|
|
959
|
+
intervalMs?: number;
|
|
960
|
+
write: (ctx: JGMutationCtx, row: PresenceCheckpoint, nowMs: number) => Promise<void>;
|
|
961
|
+
};
|
|
939
962
|
}): {
|
|
940
963
|
list: import("convex/server").RegisteredQuery<"public", {
|
|
941
964
|
externalId?: string;
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ConvexError, v } from "convex/values";
|
|
|
3
3
|
import { validateChatMessage } from "@jgengine/core/chat/chat";
|
|
4
4
|
import { definedOnly } from "@jgengine/core/data/definedOnly";
|
|
5
5
|
import { createFeedWriteGate, validateFeedWrite, } from "@jgengine/core/multiplayer/feedWriteGate";
|
|
6
|
-
import { DEFAULT_POSE_SYNC_RULES, decidePoseSync, isPresenceExpired, pickReusablePresence, resolveActivePresence, spawnPresenceState, } from "@jgengine/core/multiplayer/presenceModel";
|
|
6
|
+
import { DEFAULT_POSE_SYNC_RULES, decidePoseSync, isPresenceExpired, pickReusablePresence, resolveActivePresence, shouldPersistWorldSnapshot, spawnPresenceState, } from "@jgengine/core/multiplayer/presenceModel";
|
|
7
7
|
import { SERVER_ONLY_COMMAND_REASON, commandScopeEscape, scopeWithActor } from "@jgengine/core/commands/commandRunner";
|
|
8
8
|
import { createGameRuntime, initialPlayerState } from "@jgengine/core/runtime/gameRuntime";
|
|
9
9
|
import { buildHydratePlayers, planServerPersist, shouldAutoSave, trimFeedEntries, } from "@jgengine/core/runtime/hostPersistence";
|
|
@@ -774,14 +774,22 @@ export function createGameServerFunctions(options) {
|
|
|
774
774
|
}
|
|
775
775
|
return joinAs(ctx, args.userId, { gameId: args.gameId, serverId }, undefined);
|
|
776
776
|
},
|
|
777
|
-
async
|
|
777
|
+
async findServer(ctx, gameId) {
|
|
778
778
|
if (!shared || matchmaking !== "singleton")
|
|
779
|
-
throw new Error("
|
|
779
|
+
throw new Error("findServer requires a shared singleton host");
|
|
780
780
|
for (const status of ["running", "open"]) {
|
|
781
781
|
const existing = await ctx.db.query("jgGameServers").withIndex("by_game_and_status", q => q.eq("gameId", gameId).eq("status", status)).first();
|
|
782
782
|
if (existing)
|
|
783
783
|
return existing;
|
|
784
784
|
}
|
|
785
|
+
return null;
|
|
786
|
+
},
|
|
787
|
+
async ensureServer(ctx, gameId) {
|
|
788
|
+
if (!shared || matchmaking !== "singleton")
|
|
789
|
+
throw new Error("ensureServer requires a shared singleton host");
|
|
790
|
+
const existing = await helpers.findServer(ctx, gameId);
|
|
791
|
+
if (existing)
|
|
792
|
+
return existing;
|
|
785
793
|
const runtime = resolveRuntime(registry, gameId);
|
|
786
794
|
const now = Date.now();
|
|
787
795
|
const id = await ctx.db.insert("jgGameServers", {
|
|
@@ -1287,6 +1295,10 @@ export function createLeaderboardFunctions(options) {
|
|
|
1287
1295
|
*/
|
|
1288
1296
|
export function createPresenceFunctions(options) {
|
|
1289
1297
|
const mode = options?.auth ?? "required";
|
|
1298
|
+
const checkpointIntervalMs = options?.checkpoint?.intervalMs ?? 30_000;
|
|
1299
|
+
if (!Number.isFinite(checkpointIntervalMs) || checkpointIntervalMs < 0)
|
|
1300
|
+
throw new Error("Invalid presence checkpoint interval");
|
|
1301
|
+
const checkpointOf = (row) => definedOnly({ userId: row.userId, homeGameId: row.homeGameId, position: { x: row.x, y: row.y, z: row.z }, rotationY: row.rotationY });
|
|
1290
1302
|
const freshWindowMs = options?.freshWindowMs ?? 10_000;
|
|
1291
1303
|
const idleCutoffMs = options?.idleCutoffMs ?? 60_000;
|
|
1292
1304
|
const maxReapPerRun = options?.maxReapPerRun ?? 256;
|
|
@@ -1412,9 +1424,15 @@ export function createPresenceFunctions(options) {
|
|
|
1412
1424
|
...(args.label === undefined ? {} : { label: args.label }),
|
|
1413
1425
|
};
|
|
1414
1426
|
const displaced = existing?.revokedAt !== undefined;
|
|
1427
|
+
const checkpoint = options?.checkpoint !== undefined
|
|
1428
|
+
&& shouldPersistWorldSnapshot(existing?.lastWorldSnapshotAt, now, checkpointIntervalMs);
|
|
1429
|
+
if (checkpoint) {
|
|
1430
|
+
await options?.checkpoint?.write(ctx, checkpointOf({ userId: actorUserId, homeGameId: existing?.homeGameId, ...pose }), now);
|
|
1431
|
+
}
|
|
1432
|
+
const stamp = checkpoint ? { lastWorldSnapshotAt: now } : {};
|
|
1415
1433
|
if (existing) {
|
|
1416
|
-
if (decision.changed || decision.refreshKeepAlive || displaced || Object.keys(identity).length > 0) {
|
|
1417
|
-
await ctx.db.patch(existing._id, { ...pose, ...identity, chunkKey: chunkKeyAt([pose.x, pose.y, pose.z]), updatedAt: now, revokedAt: undefined });
|
|
1434
|
+
if (decision.changed || decision.refreshKeepAlive || displaced || checkpoint || Object.keys(identity).length > 0) {
|
|
1435
|
+
await ctx.db.patch(existing._id, { ...pose, ...identity, ...stamp, chunkKey: chunkKeyAt([pose.x, pose.y, pose.z]), updatedAt: now, revokedAt: undefined });
|
|
1418
1436
|
}
|
|
1419
1437
|
}
|
|
1420
1438
|
else {
|
|
@@ -1424,6 +1442,7 @@ export function createPresenceFunctions(options) {
|
|
|
1424
1442
|
...pose,
|
|
1425
1443
|
chunkKey: chunkKeyAt([pose.x, pose.y, pose.z]),
|
|
1426
1444
|
...identity,
|
|
1445
|
+
...stamp,
|
|
1427
1446
|
updatedAt: now,
|
|
1428
1447
|
});
|
|
1429
1448
|
}
|
|
@@ -1456,8 +1475,10 @@ export function createPresenceFunctions(options) {
|
|
|
1456
1475
|
for (const row of rows) {
|
|
1457
1476
|
if (args.sessionId !== undefined && row.sessionId !== args.sessionId)
|
|
1458
1477
|
continue;
|
|
1459
|
-
if (row.revokedAt
|
|
1460
|
-
|
|
1478
|
+
if (row.revokedAt !== undefined)
|
|
1479
|
+
continue;
|
|
1480
|
+
await options?.checkpoint?.write(ctx, checkpointOf(row), now);
|
|
1481
|
+
await ctx.db.patch(row._id, { revokedAt: now });
|
|
1461
1482
|
}
|
|
1462
1483
|
return null;
|
|
1463
1484
|
},
|
|
@@ -1622,6 +1643,7 @@ export function createChatFunctions(options) {
|
|
|
1622
1643
|
id: v.string(),
|
|
1623
1644
|
channelId: v.string(),
|
|
1624
1645
|
fromUserId: v.string(),
|
|
1646
|
+
fromLabel: v.optional(v.string()),
|
|
1625
1647
|
body: v.string(),
|
|
1626
1648
|
at: v.number(),
|
|
1627
1649
|
})),
|
|
@@ -1634,10 +1656,11 @@ export function createChatFunctions(options) {
|
|
|
1634
1656
|
return rows
|
|
1635
1657
|
.slice()
|
|
1636
1658
|
.reverse()
|
|
1637
|
-
.map((row) => ({
|
|
1659
|
+
.map((row) => definedOnly({
|
|
1638
1660
|
id: row._id,
|
|
1639
1661
|
channelId: row.channelId,
|
|
1640
1662
|
fromUserId: row.userId,
|
|
1663
|
+
fromLabel: row.authorName,
|
|
1641
1664
|
body: row.body,
|
|
1642
1665
|
at: row.at,
|
|
1643
1666
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/convex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Convex adapters for JGengine: game transport, presence transport, and backend wiring over @jgengine/core.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"check-types": "tsgo --noEmit -p tsconfig.json"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@jgengine/core": "^0.
|
|
33
|
+
"@jgengine/core": "^0.52.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"convex": "^1.31.2",
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -3,82 +3,62 @@ name: jgengine
|
|
|
3
3
|
description: Route game work to the smallest useful JGengine guidance set.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# JGengine
|
|
6
|
+
# JGengine router
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
JGengine is a TypeScript SDK of small general systems that combine into any game. Rules live in [AGENTS.md](../../../AGENTS.md); packages, layering and commands in [README.md](../../../README.md) (jgengine.com from a created project). Load only the domain a task needs.
|
|
9
9
|
|
|
10
|
-
A created project
|
|
10
|
+
A created project ships `jgengine`, `create-game-jg`, `jgengine-editor`, `jgengine-verify`, `game-design` and `jgengine-ui`; `npx jgengine skills -p --all` adds the rest, with no `api.md` (read `node_modules/@jgengine/<pkg>/dist/*.d.ts`). Upgrading a project, `doctor` and `sync`: `jgengine-editor`.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Start
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
A new game or big feature starts with `create-game-jg`: it owns the create flow, `PLAN.md` and milestones. Never copy `games/*` or reach for a genre kit or preset. Pitch to testable promise: `game-design`; authored spaces and encounters: `level-design`.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
[AGENTS.md](../../../AGENTS.md): discover the installed
|
|
18
|
-
capability, adopt it, or improve its shared owner before adding reusable game-local code.
|
|
19
|
-
Games keep unique rules, content, assets and presentation. Custom markup is not a reason
|
|
20
|
-
to copy focus handling, input tracking, storage or model lifecycle.
|
|
16
|
+
## The parts
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
the same commit, so a fix that exists is adopted by importing it. After adoption, remove
|
|
24
|
-
superseded local machinery and verify the actual game. Outside this repository, report
|
|
25
|
-
missing SDK contracts upstream rather than treating a local engine implementation as the
|
|
26
|
-
finished solution.
|
|
18
|
+
Break every feature into these before looking anything up: "supply drop" is a Schedule, Placement, Ladder, Requirement, Table and Motion track. One home each; `@jgengine/core/parts` re-exports their model layers.
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
| Part | Home |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| Meter | `stats/statPool.ts` |
|
|
23
|
+
| Ladder | `threshold/thresholds.ts` |
|
|
24
|
+
| Phases | `time/stateSchedule.ts` |
|
|
25
|
+
| Schedule | `time/pacedSchedule.ts` |
|
|
26
|
+
| Slots | `time/refillSlots.ts` |
|
|
27
|
+
| Placement | `world/spatialSample.ts`, `game/spawnPoints.ts` |
|
|
28
|
+
| Table | `random/pick.ts` |
|
|
29
|
+
| Requirement | `rules/predicate.ts` |
|
|
30
|
+
| Modifiers | `progression/statGraph.ts` |
|
|
31
|
+
| Inheritance | `inheritance/inheritance.ts` |
|
|
32
|
+
| Motion track | `movement/motionTrack.ts` |
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
The cookbook shows each common feature built from these in a few lines: [recipes/cookbook.md](recipes/cookbook.md).
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
## Domains
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
38
|
+
| Load | When |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `jgengine-editor` | anything visible in the world: placement, terrain, paths, assets; project upgrades |
|
|
41
|
+
| `jgengine-world` | runtime movement, input, interaction, AI, navigation, audio |
|
|
42
|
+
| `jgengine-gameplay` | state, systems, inventory, crafting, quests, economy, turns |
|
|
43
|
+
| `jgengine-combat` | targeting, stats, damage, effects, abilities, loot |
|
|
44
|
+
| `jgengine-ui` | HUD, menus, layout, accessibility, touch |
|
|
45
|
+
| `jgengine-assets` | models, sprites, audio files, licensing, catalogs |
|
|
46
|
+
| `jgengine-multiplayer` | authority, replication, sessions, persistence |
|
|
47
|
+
| `game-improvement` | deepening an existing game |
|
|
48
|
+
| `jgengine-verify` | before any claim that a change works |
|
|
49
|
+
| `workflow` | landing a change (monorepo) |
|
|
50
|
+
| `improve` | backlog passes |
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
World content is editor-first: anything that adds, moves or restyles something visible starts in `jgengine-editor`, and `jgengine-world` consumes the document.
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
## Discover before writing
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
1. `npx jgengine has "<feature>" ...` answers many features at once (covered, partial or gap, with imports). `npx jgengine find <intent>` names the home folder, primitives, recipes and assets. Search the moment you would hand-roll a HUD window, inventory grid, hotkey, minimap, stat bar or character motion.
|
|
57
|
+
2. `npx jgengine recipe <name>` prints a typechecked snippet; `npx jgengine recipe` lists them. [recipes/minimal-game.md](recipes/minimal-game.md) is the whole-game walkthrough; each domain has `recipes/`.
|
|
58
|
+
3. Open `api.md` or a `.d.ts` only for exact signatures, with grep.
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
If nothing fits, name the shared owner first (AGENTS.md, "Reusable capability goes upstream first"); report SDK gaps upstream outside this repo.
|
|
48
61
|
|
|
49
|
-
##
|
|
62
|
+
## Finish
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| Need | Skill | Owner |
|
|
54
|
-
| --- | --- | --- |
|
|
55
|
-
| player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
|
|
56
|
-
| substantial existing-game improvement across depth, world and presentation | `game-improvement` | ordered improvement plan and per-game completion |
|
|
57
|
-
| metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
|
|
58
|
-
| scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
|
|
59
|
-
| runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
|
|
60
|
-
| game state, systems, inventory, crafting, quests, economy, turns | `jgengine-gameplay` | non-combat rules and state |
|
|
61
|
-
| targeting, stats, damage, effects, abilities, projectiles, loot | `jgengine-combat` | combat resolution |
|
|
62
|
-
| HUD, menus, layout, accessibility, touch presentation | `jgengine-ui` | player-facing interface |
|
|
63
|
-
| models, sprites, materials, audio files, source licensing | `jgengine-assets` | asset discovery and catalogs |
|
|
64
|
-
| authority, transport, replication, sessions, host persistence | `jgengine-multiplayer` | network topology and persistence adapters |
|
|
65
|
-
|
|
66
|
-
World content routes editor-first: any request that adds, moves, restyles, or removes something visible in the world — however phrased ("design this world", "place the enemies", "make it look better") — starts in `jgengine-editor`; the other domains consume the authored document. Scenes normally require both `jgengine-editor` for authoring and `jgengine-world` for runtime consumption. Combat and multiplayer are opt-in, not default intake.
|
|
67
|
-
|
|
68
|
-
## 3. Discover before designing
|
|
69
|
-
|
|
70
|
-
Within each selected domain:
|
|
71
|
-
|
|
72
|
-
0. Fastest start for a common composition: `npx jgengine recipe <name>` prints a vetted, SDK-typechecked snippet (imports + wired code) (`combat-loop`, `loot`, `world-drops`, `click-target`, `ability-bar`, `hitscan-weapon`, `rolled-gear`, `enter-vehicle` and more); `npx jgengine recipe` lists them. Copy that instead of reconning per-file.
|
|
73
|
-
1. Search capabilities by intent; do not read `capabilities.md` top to bottom. Several features at once: `npx jgengine has torpor riding "snap building"` answers each (covered, partial or gap, with folders and imports) as one JSON document. `npx jgengine find <intent>` names the concept's home folder in each layer (read from the engine source or the installed packages), searches every domain's index and every recipe at once, and prints the primitive and its import (or the recipe to open) and matching assets; agents can use `--json --limit 5` for ranked imports and recipes with total, partial-match, and truncation fields. In the monorepo, `rg -i "<intent>" .claude/skills/*/capabilities.md` does the same search. Search the moment you are tempted to hand-roll a HUD window, inventory grid, paperdoll, hotkey listener, minimap, stat bar, or character motion. A `z-index`, a `keydown` for a panel, a `<div>` inventory, or a limb bob almost certainly has a shipped drop-in (`usePanels`/`PanelHost`, `InventoryGrid`, `CharacterSheet`/`Paperdoll`, `PartMotionRig`, `EntityPreview`); games own the *skin*, not the re-derivation.
|
|
74
|
-
2. Open `api.md` (monorepo) or the package `.d.ts` only when exact signatures are needed; grep it, never read it whole.
|
|
75
|
-
3. Open the linked reference only for a deeper recipe or trap.
|
|
76
|
-
4. For how several primitives wire into a running loop, read the domain's `recipes/` — connected, genre-free walkthroughs organized by composition seam; this skill's own [recipes/minimal-game.md](recipes/minimal-game.md) is the default whole-game walkthrough. Prefer a recipe over reading a game's source; the games under `games/*` are not templates or references, and a hybrid game is just a different composition of the same primitives.
|
|
77
|
-
|
|
78
|
-
If no capability fits, identify the upstream package seam before writing game-local code. In this repository, implement a reusable primitive with its first adopter and a reviewed consumer cleanup; coordinate a differently composed second consumer when applicable. Recipes wire SDK APIs, not copied implementations. If editor-owned content cannot be expressed, file the editor gap first; track any fallback and its removal path.
|
|
79
|
-
|
|
80
|
-
## 4. Build and finish
|
|
81
|
-
|
|
82
|
-
Keep scene coordinates in the editor document, state serializable, randomness injected, and hot-path work bounded. Use `jgengine-verify` for evidence appropriate to the change. Inside this monorepo, `workflow` owns issue/PR completion. Visual polish work also uses the `jgengine-ui` quality reference.
|
|
83
|
-
|
|
84
|
-
**Required asset pass — completion is gated on real models.** A game is not done while its entities and props are still primitive markers or box placeholders when real models are available for them. Before claiming completion, run an asset pass: every placed entity has its `entityModels` set and every prop its `objectModels` set to real, resolved model refs — not primitive-shape markers. Scaffolds ship with `jgengine-assets` and `npx jgengine assets pull` installed but never auto-triggered, so this step is on you to run explicitly. `jgengine-assets` owns the how-to (sourcing, licensing, `assets pull`, catalog indexing, and resolving refs); load it for the workflow. If a game genuinely wants primitive shapes as its art direction, say so — otherwise unresolved markers/box props are unfinished, not a style.
|
|
64
|
+
Keep scene coordinates in the editor document, state serializable, randomness injected. A game is not done while its entities and props are box markers: run the asset pass in `jgengine-assets`. Prove it with `jgengine-verify`.
|
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.52.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
- `SceneOwnershipManifest` (interface): interface SceneOwnershipManifest — Serializable record of a world's ownership declarations — persisted next to the scene document or shipped by a game.
|
|
33
33
|
- `SceneProvenance` (type): type SceneProvenance = | AuthoredProvenance | GeneratedProvenance | RuntimeProvenance | TransientProvenance — The provenance of one editor-visible object — authored, generated, runtime, or transient.
|
|
34
34
|
- `SceneProvenanceKind` (type): type SceneProvenanceKind = "authored" | "generated" | "runtime" | "transient" — Scene ownership boundary — gives every editor-visible object an explicit provenance and a single verdict (expose / bake / reject) so the editor never presents unauthored content as broken authored content.
|
|
35
|
-
- `
|
|
35
|
+
- `SpawnPointBiasStrength` (type): type SpawnPointBiasStrength = "subtle" | "moderate" | "strong" — How strongly the distance preference weighs candidates — a named intent, not a weighting exponent.
|
|
36
|
+
- `SpawnPointDistanceBias` (type): type SpawnPointDistanceBias = "near" | "far" | "none" — Which way a spawn's chance leans, relative to `avoid`: closer, farther, or unweighted.
|
|
36
37
|
- `SpawnPointSelectionOptions` (interface): interface SpawnPointSelectionOptions — Semantic options for selecting a spawn point without exposing weighting internals.
|
|
37
38
|
- `TransientProvenance` (interface): interface TransientProvenance — An ephemeral simulation object — a spawned agent, projectile, or particle that never persists.
|
|
38
39
|
- `WorldFeature` (type): type WorldFeature = | PlaceWorldFeature | ({ kind: "biomes" } & BiomesWorldConfig) | ({ kind: "voxel" } & VoxelWorldConfig) | ({ kind: "plots" } & PlotsWorldConfig) | ({ kind: "tilemap" } & TilemapWorldConfig) | EnvironmentWorldFeature | { kind: "flat" } — A declared world shape passed to `defineGame`. The preferred model is the place feature from `world()` (`@jgengine/core/placement/place`): substrate + laws, with all dressing authored in the editor. The remaining members — biomes, voxel grid, plots, tilemap, environment, flat — are the legacy code-declared shapes kept for existing games.
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
- `road` (function): function road(config: RoadEnvironmentConfig): RoadEnvironmentDescriptor — Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain.
|
|
55
56
|
- `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
56
57
|
- `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name.
|
|
58
|
+
- `selectSpawnPoint` (function): function selectSpawnPoint(options: SpawnPointSelectionOptions): NavPoint | null — Select a spawn point by game intent, keeping the weighting in the placement part's candidate table.
|
|
57
59
|
- `snow` (function): function snow(config: SnowEnvironmentConfig = {}): SnowEnvironmentDescriptor — Declares a snowfall weather effect for `environment()` — area, density, drift, wind, and flake opacity.
|
|
58
60
|
- `terrain` (function): function terrain(config: TerrainEnvironmentConfig = {}): TerrainEnvironmentDescriptor — Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks.
|
|
59
61
|
- `tilemap` (function): function tilemap(config: TilemapWorldConfig): WorldFeature — Declares a 2D tilemap world from a map string.
|
|
@@ -273,6 +275,7 @@
|
|
|
273
275
|
|
|
274
276
|
- `FrameModule` (interface): interface FrameModule — Frame subsystem exposing the frame-timing, profiling, and render-sample facades.
|
|
275
277
|
- `createFrameModule` (const): const createFrameModule: (deps: { readProbes: () => Record<string, unknown>; }) => FrameModule — Create the frame subsystem that records frame/sim/phase timings, computes stats, and captures long-frame events.
|
|
278
|
+
- `percentile` (function): function percentile(sorted: readonly number[], fraction: number): number — Value at `fraction` (0-1) of an ascending-sorted list; 0 when empty.
|
|
276
279
|
|
|
277
280
|
## @jgengine/core/devtools/frameBreakdown
|
|
278
281
|
|
|
@@ -331,6 +334,12 @@
|
|
|
331
334
|
|
|
332
335
|
- `diagnose` (function): function diagnose(frame: NonNullable<ReturnType<typeof devtools.frame.stats>>, longs: readonly LongFrameEvent[]): string | null — ⚠ undocumented
|
|
333
336
|
|
|
337
|
+
## @jgengine/core/devtools/perfOverlay.draw
|
|
338
|
+
|
|
339
|
+
- `PERF_OVERLAY_FRAMES` (const): const PERF_OVERLAY_FRAMES: 120 — Frames the FPS graph keeps.
|
|
340
|
+
- `PerfOverlay` (function): function PerfOverlay({ sample }: { sample: PerfOverlaySample }): ReactElement — The F3 performance overlay every game gets: an FPS graph of the last frames, draws, triangles, scene nodes, linked programs and the costliest subsystems from the devtools frame breakdown. Pair it with {@link PerfOverlayProbe} inside the Canvas, sharing one sample.
|
|
341
|
+
- `PerfOverlaySample` (interface): interface PerfOverlaySample — What the overlay draws, filled in place by {@link PerfOverlayProbe} and read by {@link PerfOverlay}.
|
|
342
|
+
|
|
334
343
|
## @jgengine/core/devtools/report
|
|
335
344
|
|
|
336
345
|
- `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
|
|
@@ -436,7 +445,7 @@
|
|
|
436
445
|
|
|
437
446
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
438
447
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
439
|
-
- `VERSION` (const): const VERSION: "0.
|
|
448
|
+
- `VERSION` (const): const VERSION: "0.52.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
|
|
440
449
|
|
|
441
450
|
## @jgengine/core/runtime/adapter
|
|
442
451
|
|
|
@@ -476,7 +485,7 @@
|
|
|
476
485
|
- `CameraDirector` (interface): interface CameraDirector — Runtime camera policy at `ctx.camera`: follow target, rig overlay, cinematic, chase tuning, FOV kicks and RTS pan jumps.
|
|
477
486
|
- `CameraDirectorSnapshot` (type): type CameraDirectorSnapshot = CameraSerialized<CameraDirectorState> — JSON-safe camera policy; documented instant chase responses use an explicit Infinity marker.
|
|
478
487
|
- `CameraDirectorState` (interface): interface CameraDirectorState — Camera policy state before the JSON-safe snapshot number encoding.
|
|
479
|
-
- `CameraRigConfig` (type): type CameraRigConfig = Omit<GameCameraConfig, "rig" | "
|
|
488
|
+
- `CameraRigConfig` (type): type CameraRigConfig = Omit<GameCameraConfig, "rig" | "lens" | "follow"> & { follow?: Omit<GameCameraFollowConfig, "weapon" | "onCameraFollow"> } — Data overlay for an existing rig; callbacks and Canvas projection/preferences remain configured by the game.
|
|
480
489
|
- `CameraRigOverride` (interface): interface CameraRigOverride — Runtime rig selection and detached data overlay over the configured camera.
|
|
481
490
|
- `ChaseCameraTuning` (type): type ChaseCameraTuning = Partial< Pick< ChaseCameraConfig, | "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse" | "headingSource" | "distanceBySpeed" | "pitchFollow" | "fovKick" | "lookBackAction" | "collision" | "view" … — Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), a whole driving-feel overlay applied only while a vehicle is piloted (#1299), or a `view` switch between chase and seat cameras.
|
|
482
491
|
- `nextChaseView` (function): function nextChaseView(current: ChaseView, views: readonly ChaseView[] = CHASE_VIEWS): ChaseView — The view after `current` in `views`, wrapping at the end; a view missing from the list starts the cycle over. Pair with `setChaseTuning` to bind a "change camera" key: `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })`.
|
|
@@ -497,7 +506,7 @@
|
|
|
497
506
|
- `GameContext` (interface): interface GameContext — The live engine handle a game's loop, systems, commands, and UI read and mutate — entities, objects, the reactive store, opted-in `game.*` subsystems, world queries, and the sim clock. One context = one running world.
|
|
498
507
|
- `GameContextCards` (interface): interface GameContextCards { pile(id: string, config?: CardPileConfig): CardPile } — ⚠ undocumented
|
|
499
508
|
- `GameContextCommands` (interface): interface GameContextCommands { define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void; has(name: string): boolean; names(): string[]; run(name: string, input: unknown): CommandResult<GameContext>; runAs(actorUserId: string, name: string,… — ⚠ undocumented
|
|
500
|
-
- `GameContextContent` (interface): interface GameContextContent { itemById?(itemId: string): GameContextItemEntry | null | undefined; entityById?(catalogId: string): GameContextEntityEntry | null | undefined; objectById?(catalogId: string): GameContextObjectEntry | null |
|
|
509
|
+
- `GameContextContent` (interface): interface GameContextContent { defaultRarity?: string; itemById?(itemId: string): GameContextItemEntry | null | undefined; entityById?(catalogId: string): GameContextEntityEntry | null | undefined; objectById?(catalogId: string): GameContextObjectEntry | null | undefin… — ⚠ undocumented
|
|
501
510
|
- `GameContextEconomy` (interface): interface GameContextEconomy { balance(userId: string, currencyId: string | CurrencyDefinition): number; grant(userId: string, currencyId: string | CurrencyDefinition, amount: number): void; charge(userId: string, currencyId: string | CurrencyDefinition, amount: number… — ⚠ undocumented
|
|
502
511
|
- `GameContextEntityEntry` (interface): interface GameContextEntityEntry { stats?: StatCatalog; receive?: ReceiveMap; onDeath?: OnDeathSpec; movement?: PoseAllowedStates & {walkSpeed?: number}; role?: CatalogEntityRole; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; scale?: number } — ⚠ undocumented · used by `entityEntryFromCatalog` (@jgengine/core/editor): Turns an authored entity row into the runtime {@link GameContextEntityEntry} a spawn consumes: looks up `entityId` in the document's {@link …
|
|
503
512
|
- `GameContextFeed` (interface): interface GameContextFeed extends Omit<GameFeed, "bind"> { bind(action: keyof GameEventMap): () => void } — ⚠ undocumented
|
|
@@ -528,7 +537,7 @@
|
|
|
528
537
|
- `GameContext` (interface): interface GameContext — The live engine handle a game's loop, systems, commands, and UI read and mutate — entities, objects, the reactive store, opted-in `game.*` subsystems, world queries, and the sim clock. One context = one running world.
|
|
529
538
|
- `GameContextCards` (interface): interface GameContextCards { pile(id: string, config?: CardPileConfig): CardPile } — ⚠ undocumented
|
|
530
539
|
- `GameContextCommands` (interface): interface GameContextCommands { define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void; has(name: string): boolean; names(): string[]; run(name: string, input: unknown): CommandResult<GameContext>; runAs(actorUserId: string, name: string,… — ⚠ undocumented
|
|
531
|
-
- `GameContextContent` (interface): interface GameContextContent { itemById?(itemId: string): GameContextItemEntry | null | undefined; entityById?(catalogId: string): GameContextEntityEntry | null | undefined; objectById?(catalogId: string): GameContextObjectEntry | null |
|
|
540
|
+
- `GameContextContent` (interface): interface GameContextContent { defaultRarity?: string; itemById?(itemId: string): GameContextItemEntry | null | undefined; entityById?(catalogId: string): GameContextEntityEntry | null | undefined; objectById?(catalogId: string): GameContextObjectEntry | null | undefin… — ⚠ undocumented
|
|
532
541
|
- `GameContextEconomy` (interface): interface GameContextEconomy { balance(userId: string, currencyId: string | CurrencyDefinition): number; grant(userId: string, currencyId: string | CurrencyDefinition, amount: number): void; charge(userId: string, currencyId: string | CurrencyDefinition, amount: number… — ⚠ undocumented
|
|
533
542
|
- `GameContextEntityEntry` (interface): interface GameContextEntityEntry { stats?: StatCatalog; receive?: ReceiveMap; onDeath?: OnDeathSpec; movement?: PoseAllowedStates & {walkSpeed?: number}; role?: CatalogEntityRole; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; scale?: number } — ⚠ undocumented · used by `entityEntryFromCatalog` (@jgengine/core/editor): Turns an authored entity row into the runtime {@link GameContextEntityEntry} a spawn consumes: looks up `entityId` in the document's {@link …
|
|
534
543
|
- `GameContextFeed` (interface): interface GameContextFeed extends Omit<GameFeed, "bind"> { bind(action: keyof GameEventMap): () => void } — ⚠ undocumented
|
|
@@ -594,9 +603,12 @@
|
|
|
594
603
|
|
|
595
604
|
## @jgengine/core/runtime/hostPolicy
|
|
596
605
|
|
|
606
|
+
- `DEFAULT_HOST_SAVE_INTERVAL_MS` (const): const DEFAULT_HOST_SAVE_INTERVAL_MS: 1000 — Milliseconds between auto-saves of a hosted world that changed; 0 saves after every changed tick.
|
|
607
|
+
- `DEFAULT_HOST_TICK_MS` (const): const DEFAULT_HOST_TICK_MS: 50 — Authoritative host tick interval every host uses unless the game sets one: 20 Hz.
|
|
597
608
|
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Room-topology ceiling on `slotsPerServer`; shared membership uses indexed rows instead.
|
|
598
609
|
- `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.
|
|
599
610
|
- `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.
|
|
611
|
+
- `MAX_HOST_TICK_MS` (const): const MAX_HOST_TICK_MS: 1000 — Longest tick interval a host accepts; 1 Hz is the floor for a world that still feels live.
|
|
600
612
|
- `MAX_SESSION_ID_LENGTH` (const): const MAX_SESSION_ID_LENGTH: 128 — Longest accepted client join/leave `sessionId`, in UTF-16 code units.
|
|
601
613
|
- `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
|
|
602
614
|
- `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.
|
|
@@ -605,6 +617,7 @@
|
|
|
605
617
|
- `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.
|
|
606
618
|
- `isServerFull` (function): function isServerFull(memberCount: number, slotsPerServer: number, isMember: boolean): 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.
|
|
607
619
|
- `isServerMember` (function): function isServerMember(memberUserIds: readonly string[], userId: string): boolean — Whether `userId` is already on the server's member roster.
|
|
620
|
+
- `resolveHostTickMs` (function): function resolveHostTickMs(tickMs: number | undefined): number — Validate a host tick interval in milliseconds (greater than zero, at most {@link MAX_HOST_TICK_MS}); `undefined` yields {@link DEFAULT_HOST_TICK_MS}.
|
|
608
621
|
- `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.
|
|
609
622
|
- `statusAfterLeave` (function): function statusAfterLeave(remainingMemberCount: number, currentStatus: GameServerStatus): GameServerStatus — Status after a leave: empty rooms reopen; non-empty rooms keep their current status.
|
|
610
623
|
- `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number, topology: "rooms" | "shared" = "rooms"): { ok: true } | { ok: false; reason: string } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
|
|
@@ -707,11 +720,12 @@
|
|
|
707
720
|
|
|
708
721
|
## @jgengine/core/runtime/simLoop
|
|
709
722
|
|
|
723
|
+
- `DEFAULT_SIM_HZ` (const): const DEFAULT_SIM_HZ: 60 — Steps per second a loop runs at when `hz` is not set: the physics backends' own substep rate, so one sim step is one solver step.
|
|
710
724
|
- `SimAdvanceResult` (interface): interface SimAdvanceResult — What one {@link SimLoop.advance} did.
|
|
711
725
|
- `SimLoop` (interface): interface SimLoop — The stepper handle: advance with real time, read the tick and interpolation alpha, retune the rate, snapshot and restore.
|
|
712
726
|
- `SimLoopState` (interface): interface SimLoopState — Serializable loop position: the tick count and the real seconds accumulated toward the next fixed step.
|
|
713
727
|
- `SimulationConfig` (interface): interface SimulationConfig — How the simulation is stepped: a fixed rate with a catch-up cap, or once per frame with the frame's dt.
|
|
714
|
-
- `createSimLoop` (function): function createSimLoop(initial?: SimulationConfig): SimLoop — The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in equal slices with a catch-up cap; variable mode steps once per advance.
|
|
728
|
+
- `createSimLoop` (function): function createSimLoop(initial?: SimulationConfig): SimLoop — The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in equal slices with a catch-up cap (60 Hz unless the game picks another rate); variable mode steps once per advance.
|
|
715
729
|
|
|
716
730
|
## @jgengine/core/runtime/simSnapshot
|
|
717
731
|
|
|
@@ -791,9 +805,12 @@
|
|
|
791
805
|
|
|
792
806
|
## @jgengine/core/runtime/worldReplication
|
|
793
807
|
|
|
808
|
+
- `ReplicatorView` (interface): interface ReplicatorView — Narrows {@link WorldReplicator.diff} to one viewer's interest.
|
|
794
809
|
- `WorldDiff` (interface): interface WorldDiff — A revision-stamped delta over a {@link WorldSnapshot}. The host sends one per tick to each client, carrying only what changed since that client's last acknowledged revision — entity/stat/store deltas plus whole snapshots of any other opted-in module (feed, leaderboard, chat, …) that changed. Fold it onto a prior baseline with {@link applyWorldDiff}.
|
|
795
810
|
- `WorldReplicator` (type): type WorldReplicator = ReturnType<typeof createWorldReplicator> — The stateful diff tracker returned by {@link createWorldReplicator}: `commit()`, `diff(sinceRevision)`, `revision()`.
|
|
796
811
|
- `WorldReplicatorOptions` (interface): interface WorldReplicatorOptions — Optional acceleration for {@link createWorldReplicator}: a monotone world-dirty counter (aggregated from each {@link SnapshotModule.version}). When it hasn't advanced since the last commit nothing mutated, so the replicator skips re-reading and re-serializing the whole world — the change-detection short-circuit item #28 asks for. Omit it (or pass a snapshot-only source) to keep the original full-re-serialize-per-commit behavior.
|
|
812
|
+
- `WorldViewState` (interface): interface WorldViewState — What one viewer has been sent, held by whoever serves that viewer (one per connection and world). `sent` is the entity ids the viewer's mirror holds; `null` means no baseline has been served yet. `all` marks a viewer that holds every entity (no interest radius applied to it), so narrowing later knows what to drop.
|
|
813
|
+
- `createWorldViewState` (function): function createWorldViewState(): WorldViewState — A fresh {@link WorldViewState}: the next pull is a baseline.
|
|
797
814
|
|
|
798
815
|
## @jgengine/core/runtime/worldSnapshot
|
|
799
816
|
|
|
@@ -831,8 +848,8 @@
|
|
|
831
848
|
- `HudCanvas` (function): function HudCanvas({ layout, editChord, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Opt-in play-phase gate: render the HUD only during these phases (e.g. `["playing"]`). Omit for always-visible (default). */ showDuring?: readonly GamePha… — Full-viewport HUD surface. Each `HudPanel` names an anchor and a priority; the canvas measures every panel and places them all at once so none lands on another, at any screen size. Panels sharing an anchor stack outward from that edge; a panel that would cover a neighbour slides along its edge to the nearest free spot; when the screen is too small, the lowest-priority panels fold into a toggle, and only then does the whole HUD shrink. Device safe areas and every touch control (any region registered as kind `control` or `system`) are kept clear. Games declare panels, never place them per size.
|
|
832
849
|
- `HudFade` (function): function HudFade({ watch, idleMs = 3500, dimTo = 0.18, hold = false, children, className, style }: HudFadeProps): ReactNode — A HUD group that steps back when nothing is happening: it fades after a quiet spell and comes back the moment a watched value changes, so the world fills the screen and the HUD speaks only when it has news. Hovering it also brings it back.
|
|
833
850
|
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack position within the anchor, asc… — A HUD block anchored to a corner, edge or the centre. `HudCanvas` places every panel together: panels sharing an anchor stack outward from the screen edge in ascending `order`, and a panel that would cover another slides to the nearest free spot. When the screen is too small, lower-`priority` panels fold into a toggle first. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement.
|
|
834
|
-
- `HudSlots` (function): function HudSlots({ storageKey, slots, overlay, showDuring = DEFAULT_SLOT_PHASES }: HudSlotsProps): ReactNode — The play HUD in one line: a few small panels pinned to the screen's edges, scaled from a 1600x900 design to any window and movable by the player (the layout is remembered per `storageKey`). Keep it to a handful of slots; the world is the picture.
|
|
835
|
-
- `PlayableGame` (type): type PlayableGame<TProbe extends ProbeReadings = ProbeReadings> = PlayableGameContract< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps>, TProbe > & { /** HUD theme the shell applies at the `
|
|
851
|
+
- `HudSlots` (function): function HudSlots({ storageKey, slots = {}, panels = {}, overlay, showDuring = DEFAULT_SLOT_PHASES, className }: HudSlotsProps): ReactNode — The play HUD in one line: a few small panels pinned to the screen's edges, scaled from a 1600x900 design to any window and movable by the player (the layout is remembered per `storageKey`). Keep it to a handful of slots; the world is the picture.
|
|
852
|
+
- `PlayableGame` (type): type PlayableGame<TProbe extends ProbeReadings = ProbeReadings> = PlayableGameContract< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps>, TProbe, ComponentType<RemotePlayersProps> > & { /** HUD theme the shell applies at the `G… — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle. `TProbe` is what the game's `capture.probe` returns, so a proof flow over `typeof game` names only real probes.
|
|
836
853
|
- `StoreHandle` (interface): interface StoreHandle<T> — A typed, cast-free handle onto one slot of the reactive game store (`ctx.game.store`). The single type parameter is fixed at definition; `read`/`write`/`update` never widen to `unknown`, so no call site repeats `store.get(key) as T`. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `store.get`/`store.set` pair gives up the moment it forks run state into a module-level singleton.
|
|
837
854
|
- `SystemDefinition` (interface): interface SystemDefinition — A reusable game capability — lifecycle, timing, events, and optional save / replication / reset / disposal. Pass instances via `defineGame({ systems })`. Prefer one system per meaningful capability (`combat`, `quests`), not per micro-tick.
|
|
838
855
|
- `authoredSpawnPosition` (function): function authoredSpawnPosition(document: AuthoredSpawnDocumentLike, options: AuthoredSpawnOptions = {}): [number, number, number] | null — Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the document has none. Reads the first `player_spawn` marker by default, so dragging the marker in the editor moves where players spawn — no coordinates copied into game code.
|
|
@@ -168,6 +168,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
168
168
|
|
|
169
169
|
- `SceneObjectSlots` (interface) · `import { SceneObjectSlots } from "@jgengine/core/runtime/context/objectSlots"`
|
|
170
170
|
|
|
171
|
+
## perf-overlay — F3 in-game performance overlay — FPS graph, draws, nodes, programs and the costliest subsystems
|
|
172
|
+
|
|
173
|
+
- `PerfOverlay` (function) · `import { PerfOverlay } from "@jgengine/core/devtools/perfOverlay.draw"`
|
|
174
|
+
|
|
171
175
|
## placed-object-rehydrate — rebuild live placed objects, with their state and slot contents, from persisted snapshot rows
|
|
172
176
|
|
|
173
177
|
- `fromRuntimeObjectRow` (function) · `import { fromRuntimeObjectRow } from "@jgengine/core/runtime/objectRows"`
|
|
@@ -214,6 +218,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
214
218
|
|
|
215
219
|
- `createSimSnapshotRegistry` (function) · `import { createSimSnapshotRegistry } from "@jgengine/core/runtime/simSnapshot"`
|
|
216
220
|
|
|
221
|
+
## spawn-point-select — select a spawn location by its distance preference from avoided positions
|
|
222
|
+
|
|
223
|
+
- `selectSpawnPoint` (function) · `import { selectSpawnPoint } from "@jgengine/core/authoring"`
|
|
224
|
+
|
|
217
225
|
## tune-control-labels — find a Tune panel field by its control name (drive --fill, screen readers)
|
|
218
226
|
|
|
219
227
|
- `tuneControlLabels` (function) · `import { tuneControlLabels } from "@jgengine/core/devtools/TunePanel.view"`
|