@jgengine/convex 0.9.0 → 0.11.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 +34 -3
- package/LICENSE +203 -661
- package/README.md +3 -3
- package/dist/convexSaveBackend.d.ts +35 -0
- package/dist/convexSaveBackend.js +24 -0
- package/dist/hostedServer.d.ts +98 -0
- package/dist/hostedServer.js +353 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/occ.d.ts +2 -0
- package/dist/occ.js +2 -0
- package/dist/server.d.ts +63 -2
- package/dist/server.js +74 -12
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-multiplayer/SKILL.md +36 -0
- package/skills/jgengine-multiplayer/api.md +611 -0
- package/skills/jgengine-multiplayer/capabilities.md +9 -0
- package/skills/jgengine-multiplayer/reference.md +69 -0
- package/llms.txt +0 -951
package/dist/server.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { DataModelFromSchemaDefinition, GenericMutationCtx } from "convex/server";
|
|
1
|
+
import type { Auth, DataModelFromSchemaDefinition, GenericMutationCtx } from "convex/server";
|
|
2
2
|
import type { GenericId } from "convex/values";
|
|
3
3
|
import type { ChatMessage } from "@jgengine/core/game/chat";
|
|
4
4
|
import type { ChatSendOutcome } from "@jgengine/core/multiplayer/chatContract";
|
|
5
5
|
import { type PoseSyncRules } from "@jgengine/core/multiplayer/presenceModel";
|
|
6
6
|
import type { GameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
7
|
-
import type { LeaderboardIncrement } from "@jgengine/core/runtime/hostPersistence";
|
|
7
|
+
import type { LeaderboardIncrement, SessionVisibility } from "@jgengine/core/runtime/hostPersistence";
|
|
8
|
+
/** @internal */
|
|
8
9
|
export declare function jgengineTables(): {
|
|
9
10
|
jgGameServers: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
10
11
|
createdAt: number;
|
|
@@ -55,7 +56,9 @@ export declare function jgengineTables(): {
|
|
|
55
56
|
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
56
57
|
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
57
58
|
}, "required", "createdAt" | "dirtyAt" | "gameId" | "joinCode" | "lastSavedAt" | "memberUserIds" | "mode" | "modeConfig" | "revision" | "save" | "save.auto" | "save.scope" | "serverState" | "sessionPlayers" | "slotsPerServer" | "status" | "tickAnchorMs" | "updatedAt" | "visibility" | `modeConfig.${string}` | `serverState.${string}` | `sessionPlayers.${string}`>, {
|
|
59
|
+
by_dirty: ["dirtyAt", "_creationTime"];
|
|
58
60
|
by_game_and_status: ["gameId", "status", "_creationTime"];
|
|
61
|
+
by_status: ["status", "_creationTime"];
|
|
59
62
|
}, {}, {}>;
|
|
60
63
|
jgPlayerProfiles: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
61
64
|
createdAt: number;
|
|
@@ -163,6 +166,38 @@ export declare function jgengineTables(): {
|
|
|
163
166
|
by_server_channel: ["serverId", "channelId", "_creationTime"];
|
|
164
167
|
}, {}, {}>;
|
|
165
168
|
};
|
|
169
|
+
/**
|
|
170
|
+
* Convex table for the hosted GameContext-world path: one row per (gameId, serverId) holding the full
|
|
171
|
+
* `WorldSnapshot` blob, its revision, the member roster, and each member's held input. A sibling of
|
|
172
|
+
* {@link jgengineTables} — spread either (or both) into `defineSchema`.
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export declare function jgengineHostedTables(): {
|
|
176
|
+
jgHostedWorlds: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
177
|
+
createdAt: number;
|
|
178
|
+
gameId: string;
|
|
179
|
+
inputs: any;
|
|
180
|
+
memberUserIds: string[];
|
|
181
|
+
revision: number;
|
|
182
|
+
serverId: string;
|
|
183
|
+
snapshot: any;
|
|
184
|
+
tickAnchorMs: number;
|
|
185
|
+
updatedAt: number;
|
|
186
|
+
}, {
|
|
187
|
+
gameId: import("convex/values").VString<string, "required">;
|
|
188
|
+
serverId: import("convex/values").VString<string, "required">;
|
|
189
|
+
snapshot: import("convex/values").VAny<any, "required", string>;
|
|
190
|
+
revision: import("convex/values").VFloat64<number, "required">;
|
|
191
|
+
memberUserIds: import("convex/values").VArray<string[], import("convex/values").VString<string, "required">, "required">;
|
|
192
|
+
inputs: import("convex/values").VAny<any, "required", string>;
|
|
193
|
+
tickAnchorMs: import("convex/values").VFloat64<number, "required">;
|
|
194
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
195
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
196
|
+
}, "required", "createdAt" | "gameId" | "inputs" | "memberUserIds" | "revision" | "serverId" | "snapshot" | "tickAnchorMs" | "updatedAt" | `inputs.${string}` | `snapshot.${string}`>, {
|
|
197
|
+
by_game_and_server: ["gameId", "serverId", "_creationTime"];
|
|
198
|
+
by_tick_anchor: ["tickAnchorMs", "_creationTime"];
|
|
199
|
+
}, {}, {}>;
|
|
200
|
+
};
|
|
166
201
|
declare const schemaForTypes: import("convex/server").SchemaDefinition<{
|
|
167
202
|
jgGameServers: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
168
203
|
createdAt: number;
|
|
@@ -213,7 +248,9 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
|
|
|
213
248
|
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
214
249
|
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
215
250
|
}, "required", "createdAt" | "dirtyAt" | "gameId" | "joinCode" | "lastSavedAt" | "memberUserIds" | "mode" | "modeConfig" | "revision" | "save" | "save.auto" | "save.scope" | "serverState" | "sessionPlayers" | "slotsPerServer" | "status" | "tickAnchorMs" | "updatedAt" | "visibility" | `modeConfig.${string}` | `serverState.${string}` | `sessionPlayers.${string}`>, {
|
|
251
|
+
by_dirty: ["dirtyAt", "_creationTime"];
|
|
216
252
|
by_game_and_status: ["gameId", "status", "_creationTime"];
|
|
253
|
+
by_status: ["status", "_creationTime"];
|
|
217
254
|
}, {}, {}>;
|
|
218
255
|
jgPlayerProfiles: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
219
256
|
createdAt: number;
|
|
@@ -325,8 +362,28 @@ type JGDataModel = DataModelFromSchemaDefinition<typeof schemaForTypes>;
|
|
|
325
362
|
type JGMutationCtx = GenericMutationCtx<JGDataModel>;
|
|
326
363
|
export type JgAuthMode = "anonymous" | "required";
|
|
327
364
|
export declare const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules;
|
|
365
|
+
/** Resolve the acting user id under a {@link JgAuthMode}: the Convex identity when signed in (rejecting a mismatched claim), the claimed external id under `"anonymous"`, else `null`.
|
|
366
|
+
* @internal
|
|
367
|
+
*/
|
|
368
|
+
export declare function resolveActor(ctx: {
|
|
369
|
+
auth: Auth;
|
|
370
|
+
}, claimedExternalId: string | undefined, mode: JgAuthMode): Promise<string | null>;
|
|
371
|
+
/** True when a server's `visibility` should surface in public listings/browse results.
|
|
372
|
+
* @internal
|
|
373
|
+
*/
|
|
374
|
+
export declare function isListablePublicly(visibility: SessionVisibility | undefined): boolean;
|
|
375
|
+
/** A non-member may enter a private server only by presenting its `joinCode`; members always pass.
|
|
376
|
+
* @internal
|
|
377
|
+
*/
|
|
378
|
+
export declare function canJoinPrivateServer(args: {
|
|
379
|
+
isMember: boolean;
|
|
380
|
+
joinCode: string | undefined;
|
|
381
|
+
suppliedCode: string | undefined;
|
|
382
|
+
}): boolean;
|
|
383
|
+
/** @internal */
|
|
328
384
|
export declare function applyLeaderboardIncrements(ctx: JGMutationCtx, gameId: string, entries: LeaderboardIncrement[]): Promise<void>;
|
|
329
385
|
export declare const JG_RUNTIME_TICK_MS = 1000;
|
|
386
|
+
/** @internal */
|
|
330
387
|
export declare function createGameServerFunctions(options?: {
|
|
331
388
|
runtimes?: GameRuntime[];
|
|
332
389
|
auth?: JgAuthMode;
|
|
@@ -426,6 +483,7 @@ export declare function createGameServerFunctions(options?: {
|
|
|
426
483
|
saved: number;
|
|
427
484
|
}>>;
|
|
428
485
|
};
|
|
486
|
+
/** @internal */
|
|
429
487
|
export declare function createLeaderboardFunctions(options?: {
|
|
430
488
|
auth?: JgAuthMode;
|
|
431
489
|
}): {
|
|
@@ -456,6 +514,7 @@ export declare function createLeaderboardFunctions(options?: {
|
|
|
456
514
|
gameId: string;
|
|
457
515
|
}, Promise<null>>;
|
|
458
516
|
};
|
|
517
|
+
/** @internal */
|
|
459
518
|
export declare function createPresenceFunctions(options?: {
|
|
460
519
|
auth?: JgAuthMode;
|
|
461
520
|
freshWindowMs?: number;
|
|
@@ -491,6 +550,7 @@ export declare function createPresenceFunctions(options?: {
|
|
|
491
550
|
serverId: string;
|
|
492
551
|
}, Promise<null>>;
|
|
493
552
|
};
|
|
553
|
+
/** @internal */
|
|
494
554
|
export declare function createChatFunctions(options?: {
|
|
495
555
|
auth?: JgAuthMode;
|
|
496
556
|
historyLimit?: number;
|
|
@@ -514,5 +574,6 @@ export type JgCronSpec = {
|
|
|
514
574
|
intervalSeconds: number;
|
|
515
575
|
functionKey: "tickActiveServers" | "flushDirtyServers";
|
|
516
576
|
};
|
|
577
|
+
/** @internal */
|
|
517
578
|
export declare function jgengineCronSpecs(): readonly JgCronSpec[];
|
|
518
579
|
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineSchema, defineTable, internalMutationGeneric, mutationGeneric, queryGeneric, } from "convex/server";
|
|
2
2
|
import { ConvexError, v } from "convex/values";
|
|
3
3
|
import { createFeedWriteGate, validateFeedWrite, } from "@jgengine/core/multiplayer/feedWriteGate";
|
|
4
|
+
import { normalizeJoinCode } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
5
|
import { decidePoseSync, spawnPresenceState, } from "@jgengine/core/multiplayer/presenceModel";
|
|
5
6
|
import { createGameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
6
7
|
import { buildHydratePlayers, planServerPersist, shouldAutoSave, trimFeedEntries } from "@jgengine/core/runtime/hostPersistence";
|
|
@@ -10,6 +11,7 @@ const saveConfigValidator = v.union(v.literal("none"), v.object({
|
|
|
10
11
|
auto: v.string(),
|
|
11
12
|
scope: v.union(v.literal("player"), v.literal("chunks"), v.literal("player+chunks")),
|
|
12
13
|
}));
|
|
14
|
+
/** @internal */
|
|
13
15
|
export function jgengineTables() {
|
|
14
16
|
return {
|
|
15
17
|
jgGameServers: defineTable({
|
|
@@ -30,7 +32,10 @@ export function jgengineTables() {
|
|
|
30
32
|
dirtyAt: v.optional(v.number()),
|
|
31
33
|
createdAt: v.number(),
|
|
32
34
|
updatedAt: v.number(),
|
|
33
|
-
})
|
|
35
|
+
})
|
|
36
|
+
.index("by_game_and_status", ["gameId", "status"])
|
|
37
|
+
.index("by_status", ["status"])
|
|
38
|
+
.index("by_dirty", ["dirtyAt"]),
|
|
34
39
|
jgPlayerProfiles: defineTable({
|
|
35
40
|
userId: v.string(),
|
|
36
41
|
gameId: v.string(),
|
|
@@ -87,6 +92,29 @@ export function jgengineTables() {
|
|
|
87
92
|
}).index("by_server_channel", ["serverId", "channelId"]),
|
|
88
93
|
};
|
|
89
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Convex table for the hosted GameContext-world path: one row per (gameId, serverId) holding the full
|
|
97
|
+
* `WorldSnapshot` blob, its revision, the member roster, and each member's held input. A sibling of
|
|
98
|
+
* {@link jgengineTables} — spread either (or both) into `defineSchema`.
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
export function jgengineHostedTables() {
|
|
102
|
+
return {
|
|
103
|
+
jgHostedWorlds: defineTable({
|
|
104
|
+
gameId: v.string(),
|
|
105
|
+
serverId: v.string(),
|
|
106
|
+
snapshot: v.any(),
|
|
107
|
+
revision: v.number(),
|
|
108
|
+
memberUserIds: v.array(v.string()),
|
|
109
|
+
inputs: v.any(),
|
|
110
|
+
tickAnchorMs: v.number(),
|
|
111
|
+
createdAt: v.number(),
|
|
112
|
+
updatedAt: v.number(),
|
|
113
|
+
})
|
|
114
|
+
.index("by_game_and_server", ["gameId", "serverId"])
|
|
115
|
+
.index("by_tick_anchor", ["tickAnchorMs"]),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
90
118
|
const schemaForTypes = defineSchema(jgengineTables());
|
|
91
119
|
const query = queryGeneric;
|
|
92
120
|
const mutation = mutationGeneric;
|
|
@@ -98,7 +126,10 @@ export const DEFAULT_CONVEX_POSE_RULES = {
|
|
|
98
126
|
maxElapsedSec: 0.5,
|
|
99
127
|
keepAliveRefreshMs: 10_000,
|
|
100
128
|
};
|
|
101
|
-
|
|
129
|
+
/** Resolve the acting user id under a {@link JgAuthMode}: the Convex identity when signed in (rejecting a mismatched claim), the claimed external id under `"anonymous"`, else `null`.
|
|
130
|
+
* @internal
|
|
131
|
+
*/
|
|
132
|
+
export async function resolveActor(ctx, claimedExternalId, mode) {
|
|
102
133
|
const identity = await ctx.auth.getUserIdentity();
|
|
103
134
|
if (identity !== null) {
|
|
104
135
|
if (claimedExternalId !== undefined &&
|
|
@@ -113,6 +144,22 @@ async function resolveActor(ctx, claimedExternalId, mode) {
|
|
|
113
144
|
}
|
|
114
145
|
return null;
|
|
115
146
|
}
|
|
147
|
+
/** True when a server's `visibility` should surface in public listings/browse results.
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
export function isListablePublicly(visibility) {
|
|
151
|
+
return (visibility ?? "public") !== "private";
|
|
152
|
+
}
|
|
153
|
+
/** A non-member may enter a private server only by presenting its `joinCode`; members always pass.
|
|
154
|
+
* @internal
|
|
155
|
+
*/
|
|
156
|
+
export function canJoinPrivateServer(args) {
|
|
157
|
+
if (args.isMember)
|
|
158
|
+
return true;
|
|
159
|
+
if (args.joinCode === undefined || args.suppliedCode === undefined)
|
|
160
|
+
return false;
|
|
161
|
+
return normalizeJoinCode(args.suppliedCode) === normalizeJoinCode(args.joinCode);
|
|
162
|
+
}
|
|
116
163
|
async function requireServerMember(ctx, serverId, actorUserId) {
|
|
117
164
|
const server = await ctx.db.get("jgGameServers", serverId);
|
|
118
165
|
if (!server)
|
|
@@ -202,6 +249,7 @@ async function loadServerSnapshot(ctx, server, runtime) {
|
|
|
202
249
|
revision: server.revision,
|
|
203
250
|
});
|
|
204
251
|
}
|
|
252
|
+
/** @internal */
|
|
205
253
|
export async function applyLeaderboardIncrements(ctx, gameId, entries) {
|
|
206
254
|
const now = Date.now();
|
|
207
255
|
for (const entry of entries) {
|
|
@@ -303,6 +351,7 @@ async function flushServerIfDue(ctx, server, now, runtime) {
|
|
|
303
351
|
return true;
|
|
304
352
|
}
|
|
305
353
|
export const JG_RUNTIME_TICK_MS = 1_000;
|
|
354
|
+
/** @internal */
|
|
306
355
|
export function createGameServerFunctions(options) {
|
|
307
356
|
const mode = options?.auth ?? "required";
|
|
308
357
|
const hostSlotsPerServer = options?.slotsPerServer ?? 16;
|
|
@@ -383,8 +432,11 @@ export function createGameServerFunctions(options) {
|
|
|
383
432
|
throw new ConvexError("Server is full");
|
|
384
433
|
}
|
|
385
434
|
if ((server.visibility ?? "public") === "private" &&
|
|
386
|
-
!
|
|
387
|
-
|
|
435
|
+
!canJoinPrivateServer({
|
|
436
|
+
isMember: server.memberUserIds.includes(actorUserId),
|
|
437
|
+
joinCode: server.joinCode,
|
|
438
|
+
suppliedCode: args.joinCode,
|
|
439
|
+
})) {
|
|
388
440
|
throw new ConvexError("Server is private");
|
|
389
441
|
}
|
|
390
442
|
const profile = await ctx.db
|
|
@@ -670,11 +722,15 @@ export function createGameServerFunctions(options) {
|
|
|
670
722
|
updatedAt: v.number(),
|
|
671
723
|
})),
|
|
672
724
|
handler: async (ctx, args) => {
|
|
725
|
+
const limit = args.limit ?? 20;
|
|
673
726
|
const rows = await ctx.db
|
|
674
727
|
.query("jgGameServers")
|
|
675
728
|
.withIndex("by_game_and_status", (q) => q.eq("gameId", args.gameId).eq("status", "running"))
|
|
676
|
-
.take(
|
|
677
|
-
return rows
|
|
729
|
+
.take(Math.min(limit * 4, 200));
|
|
730
|
+
return rows
|
|
731
|
+
.filter((row) => isListablePublicly(row.visibility))
|
|
732
|
+
.slice(0, limit)
|
|
733
|
+
.map((row) => ({
|
|
678
734
|
_id: row._id,
|
|
679
735
|
status: row.status,
|
|
680
736
|
memberCount: row.memberUserIds.length,
|
|
@@ -688,12 +744,13 @@ export function createGameServerFunctions(options) {
|
|
|
688
744
|
args: {},
|
|
689
745
|
handler: async (ctx) => {
|
|
690
746
|
const now = Date.now();
|
|
691
|
-
const servers = await ctx.db
|
|
747
|
+
const servers = await ctx.db
|
|
748
|
+
.query("jgGameServers")
|
|
749
|
+
.withIndex("by_status", (q) => q.eq("status", "running"))
|
|
750
|
+
.collect();
|
|
692
751
|
let ticked = 0;
|
|
693
752
|
let saved = 0;
|
|
694
753
|
for (const server of servers) {
|
|
695
|
-
if (server.status !== "running")
|
|
696
|
-
continue;
|
|
697
754
|
if (server.memberUserIds.length === 0)
|
|
698
755
|
continue;
|
|
699
756
|
const elapsedMs = now - server.tickAnchorMs;
|
|
@@ -717,11 +774,12 @@ export function createGameServerFunctions(options) {
|
|
|
717
774
|
args: {},
|
|
718
775
|
handler: async (ctx) => {
|
|
719
776
|
const now = Date.now();
|
|
720
|
-
const servers = await ctx.db
|
|
777
|
+
const servers = await ctx.db
|
|
778
|
+
.query("jgGameServers")
|
|
779
|
+
.withIndex("by_dirty", (q) => q.gt("dirtyAt", 0))
|
|
780
|
+
.collect();
|
|
721
781
|
let saved = 0;
|
|
722
782
|
for (const server of servers) {
|
|
723
|
-
if (server.dirtyAt === undefined)
|
|
724
|
-
continue;
|
|
725
783
|
const runtime = resolveRuntime(registry, server.gameId);
|
|
726
784
|
const snapshot = await loadServerSnapshot(ctx, server, runtime);
|
|
727
785
|
await persistServerSnapshot(ctx, server, clearDirtyFlags(snapshot), server.save);
|
|
@@ -747,6 +805,7 @@ export function createGameServerFunctions(options) {
|
|
|
747
805
|
}
|
|
748
806
|
const leaderboardScopeValidator = v.union(v.literal("global"), v.literal("server"), v.literal("profile"));
|
|
749
807
|
const MAX_LEADERBOARD_TOP_LIMIT = 100;
|
|
808
|
+
/** @internal */
|
|
750
809
|
export function createLeaderboardFunctions(options) {
|
|
751
810
|
void options;
|
|
752
811
|
const getTop = query({
|
|
@@ -810,6 +869,7 @@ export function createLeaderboardFunctions(options) {
|
|
|
810
869
|
});
|
|
811
870
|
return { getTop, getProfile, incrementMany };
|
|
812
871
|
}
|
|
872
|
+
/** @internal */
|
|
813
873
|
export function createPresenceFunctions(options) {
|
|
814
874
|
const mode = options?.auth ?? "required";
|
|
815
875
|
const freshWindowMs = options?.freshWindowMs ?? 10_000;
|
|
@@ -925,6 +985,7 @@ export function createPresenceFunctions(options) {
|
|
|
925
985
|
});
|
|
926
986
|
return { list, sync, leave };
|
|
927
987
|
}
|
|
988
|
+
/** @internal */
|
|
928
989
|
export function createChatFunctions(options) {
|
|
929
990
|
const mode = options?.auth ?? "required";
|
|
930
991
|
const historyLimit = options?.historyLimit ?? 100;
|
|
@@ -1020,6 +1081,7 @@ const JG_CRON_SPECS = [
|
|
|
1020
1081
|
{ name: "jg tick", intervalSeconds: 1, functionKey: "tickActiveServers" },
|
|
1021
1082
|
{ name: "jg flush", intervalSeconds: 60, functionKey: "flushDirtyServers" },
|
|
1022
1083
|
];
|
|
1084
|
+
/** @internal */
|
|
1023
1085
|
export function jgengineCronSpecs() {
|
|
1024
1086
|
return JG_CRON_SPECS;
|
|
1025
1087
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/convex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Convex adapters for JGengine: game transport, presence transport, and backend wiring over @jgengine/core.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"repository": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
15
|
"CHANGELOG.md",
|
|
16
|
-
"
|
|
16
|
+
"skills"
|
|
17
17
|
],
|
|
18
18
|
"exports": {
|
|
19
19
|
".": {
|
|
@@ -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.11.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"convex": "^1.31.2",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jgengine
|
|
3
|
+
description: Route game work to the smallest useful JGengine guidance set.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JGengine intake and router
|
|
7
|
+
|
|
8
|
+
Read the repository [README](../../../README.md) for package truth, commands, layering, and license. This skill decides what to load; it is not an engine manual.
|
|
9
|
+
|
|
10
|
+
## 1. Establish the target
|
|
11
|
+
|
|
12
|
+
Capture only decisions that change architecture:
|
|
13
|
+
|
|
14
|
+
- player point of view and controls
|
|
15
|
+
- world shape and authored content
|
|
16
|
+
- core loop, interaction, combat, and progression
|
|
17
|
+
- solo, local, hosted, or persistent multiplayer
|
|
18
|
+
- HUD/menu states and art direction
|
|
19
|
+
- one observable completion scenario
|
|
20
|
+
|
|
21
|
+
For a new in-repo game, scaffold with `bun run new:game <id> --name "Title"`. Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
|
|
22
|
+
|
|
23
|
+
## 2. Select domains
|
|
24
|
+
|
|
25
|
+
Load only rows the target needs. Do not preload every domain.
|
|
26
|
+
|
|
27
|
+
| Need | Skill | Owner |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
|
|
30
|
+
| runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
|
|
31
|
+
| game state, systems, inventory, crafting, quests, economy, turns | `jgengine-gameplay` | non-combat rules and state |
|
|
32
|
+
| targeting, stats, damage, effects, abilities, projectiles, loot | `jgengine-combat` | combat resolution |
|
|
33
|
+
| HUD, menus, layout, accessibility, touch presentation | `jgengine-ui` | player-facing interface |
|
|
34
|
+
| models, sprites, materials, audio files, source licensing | `jgengine-assets` | asset discovery and catalogs |
|
|
35
|
+
| authority, transport, replication, sessions, host persistence | `jgengine-multiplayer` | network topology and persistence adapters |
|
|
36
|
+
|
|
37
|
+
Scenes normally require both `jgengine-editor` for authoring and `jgengine-world` for runtime consumption. Combat and multiplayer are opt-in, not default intake.
|
|
38
|
+
|
|
39
|
+
## 3. Discover before designing
|
|
40
|
+
|
|
41
|
+
Within each selected domain:
|
|
42
|
+
|
|
43
|
+
1. Search `capabilities.md` by intent; it maps needs to imports.
|
|
44
|
+
2. Open `api.md` only when exact signatures or export inventory are needed.
|
|
45
|
+
3. Open the linked reference only for a deeper recipe or trap.
|
|
46
|
+
|
|
47
|
+
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; if editor-owned content cannot be expressed, file the editor gap first.
|
|
48
|
+
|
|
49
|
+
## 4. Build and finish
|
|
50
|
+
|
|
51
|
+
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.
|