@jgengine/core 0.1.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.
Files changed (119) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +11 -0
  3. package/dist/assets/spriteSheet.d.ts +46 -0
  4. package/dist/assets/spriteSheet.js +86 -0
  5. package/dist/combat/death.d.ts +61 -0
  6. package/dist/combat/death.js +61 -0
  7. package/dist/combat/effects.d.ts +61 -0
  8. package/dist/combat/effects.js +108 -0
  9. package/dist/combat/projectiles.d.ts +45 -0
  10. package/dist/combat/projectiles.js +156 -0
  11. package/dist/commands/commandRegistry.d.ts +23 -0
  12. package/dist/commands/commandRegistry.js +26 -0
  13. package/dist/economy/currency.d.ts +26 -0
  14. package/dist/economy/currency.js +28 -0
  15. package/dist/economy/wallet.d.ts +16 -0
  16. package/dist/economy/wallet.js +49 -0
  17. package/dist/game/defineGame.d.ts +41 -0
  18. package/dist/game/defineGame.js +7 -0
  19. package/dist/game/events.d.ts +87 -0
  20. package/dist/game/events.js +24 -0
  21. package/dist/game/feed.d.ts +19 -0
  22. package/dist/game/feed.js +47 -0
  23. package/dist/game/leaderboard.d.ts +46 -0
  24. package/dist/game/leaderboard.js +60 -0
  25. package/dist/game/loadout.d.ts +47 -0
  26. package/dist/game/loadout.js +36 -0
  27. package/dist/game/lootTable.d.ts +27 -0
  28. package/dist/game/lootTable.js +78 -0
  29. package/dist/game/playableGame.d.ts +55 -0
  30. package/dist/game/playableGame.js +1 -0
  31. package/dist/game/quest.d.ts +93 -0
  32. package/dist/game/quest.js +228 -0
  33. package/dist/game/social.d.ts +78 -0
  34. package/dist/game/social.js +227 -0
  35. package/dist/game/trade.d.ts +35 -0
  36. package/dist/game/trade.js +53 -0
  37. package/dist/game/unlocks.d.ts +13 -0
  38. package/dist/game/unlocks.js +34 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +6 -0
  41. package/dist/input/actionBindings.d.ts +41 -0
  42. package/dist/input/actionBindings.js +115 -0
  43. package/dist/input/lookChannel.d.ts +29 -0
  44. package/dist/input/lookChannel.js +44 -0
  45. package/dist/input/touchGestures.d.ts +31 -0
  46. package/dist/input/touchGestures.js +72 -0
  47. package/dist/interaction/proximityPrompt.d.ts +51 -0
  48. package/dist/interaction/proximityPrompt.js +76 -0
  49. package/dist/inventory/inventoryModel.d.ts +57 -0
  50. package/dist/inventory/inventoryModel.js +208 -0
  51. package/dist/item/use.d.ts +25 -0
  52. package/dist/item/use.js +40 -0
  53. package/dist/item/weapon.d.ts +8 -0
  54. package/dist/item/weapon.js +18 -0
  55. package/dist/movement/avatarGait.d.ts +14 -0
  56. package/dist/movement/avatarGait.js +20 -0
  57. package/dist/movement/cameraRig.d.ts +15 -0
  58. package/dist/movement/cameraRig.js +43 -0
  59. package/dist/movement/movementModel.d.ts +96 -0
  60. package/dist/movement/movementModel.js +171 -0
  61. package/dist/movement/poseState.d.ts +23 -0
  62. package/dist/movement/poseState.js +61 -0
  63. package/dist/multiplayer/poseSyncGate.d.ts +18 -0
  64. package/dist/multiplayer/poseSyncGate.js +30 -0
  65. package/dist/multiplayer/presenceContract.d.ts +60 -0
  66. package/dist/multiplayer/presenceContract.js +19 -0
  67. package/dist/multiplayer/presenceModel.d.ts +60 -0
  68. package/dist/multiplayer/presenceModel.js +51 -0
  69. package/dist/runtime/adapter.d.ts +24 -0
  70. package/dist/runtime/adapter.js +12 -0
  71. package/dist/runtime/commandRunner.d.ts +15 -0
  72. package/dist/runtime/commandRunner.js +25 -0
  73. package/dist/runtime/gameContext.d.ts +138 -0
  74. package/dist/runtime/gameContext.js +371 -0
  75. package/dist/runtime/gameRuntime.d.ts +42 -0
  76. package/dist/runtime/gameRuntime.js +95 -0
  77. package/dist/runtime/hostPersistence.d.ts +105 -0
  78. package/dist/runtime/hostPersistence.js +117 -0
  79. package/dist/runtime/save.d.ts +9 -0
  80. package/dist/runtime/save.js +29 -0
  81. package/dist/runtime/snapshot.d.ts +87 -0
  82. package/dist/runtime/snapshot.js +75 -0
  83. package/dist/runtime/transport.d.ts +60 -0
  84. package/dist/runtime/transport.js +1 -0
  85. package/dist/scene/assetCatalog.d.ts +10 -0
  86. package/dist/scene/assetCatalog.js +17 -0
  87. package/dist/scene/assetPreload.d.ts +7 -0
  88. package/dist/scene/assetPreload.js +22 -0
  89. package/dist/scene/behaviors.d.ts +19 -0
  90. package/dist/scene/behaviors.js +18 -0
  91. package/dist/scene/entityStats.d.ts +38 -0
  92. package/dist/scene/entityStats.js +65 -0
  93. package/dist/scene/entityStore.d.ts +52 -0
  94. package/dist/scene/entityStore.js +84 -0
  95. package/dist/scene/objectStore.d.ts +28 -0
  96. package/dist/scene/objectStore.js +69 -0
  97. package/dist/scene/spatial.d.ts +34 -0
  98. package/dist/scene/spatial.js +96 -0
  99. package/dist/scene/targeting.d.ts +20 -0
  100. package/dist/scene/targeting.js +56 -0
  101. package/dist/stats/statModifiers.d.ts +18 -0
  102. package/dist/stats/statModifiers.js +56 -0
  103. package/dist/store/changeSignal.d.ts +7 -0
  104. package/dist/store/changeSignal.js +32 -0
  105. package/dist/store/engineStore.d.ts +22 -0
  106. package/dist/store/engineStore.js +50 -0
  107. package/dist/store/features.d.ts +10 -0
  108. package/dist/store/features.js +23 -0
  109. package/dist/store/observableKeyedStore.d.ts +10 -0
  110. package/dist/store/observableKeyedStore.js +42 -0
  111. package/dist/time/calendarClock.d.ts +17 -0
  112. package/dist/time/calendarClock.js +42 -0
  113. package/dist/time/gameClock.d.ts +9 -0
  114. package/dist/time/gameClock.js +20 -0
  115. package/dist/time/serverTick.d.ts +16 -0
  116. package/dist/time/serverTick.js +18 -0
  117. package/dist/world/features.d.ts +39 -0
  118. package/dist/world/features.js +15 -0
  119. package/package.json +32 -0
@@ -0,0 +1,23 @@
1
+ export type MovementPose = "standing" | "crouch" | "prone" | "running";
2
+ export type AimMode = "hip" | "ads";
3
+ export interface PoseHitbox {
4
+ height: number;
5
+ eyeHeight: number;
6
+ speedMultiplier: number;
7
+ }
8
+ export declare const POSE_HITBOX: Record<MovementPose, PoseHitbox>;
9
+ export interface PoseAllowedStates {
10
+ poses?: readonly MovementPose[];
11
+ aim?: readonly AimMode[];
12
+ }
13
+ export interface PoseRejection {
14
+ reason: string;
15
+ }
16
+ export interface PoseState {
17
+ getPose(instanceId: string): MovementPose;
18
+ setPose(instanceId: string, pose: MovementPose): PoseRejection | null;
19
+ getAim(instanceId: string): AimMode;
20
+ setAim(instanceId: string, mode: AimMode): PoseRejection | null;
21
+ clear(instanceId: string): void;
22
+ }
23
+ export declare function createPoseState(resolveAllowed: (instanceId: string) => PoseAllowedStates | null | undefined): PoseState;
@@ -0,0 +1,61 @@
1
+ import { MOVEMENT_TUNING } from "./movementModel.js";
2
+ const STANDING_HEIGHT = 1.8;
3
+ const CROUCH_HEIGHT = STANDING_HEIGHT * (MOVEMENT_TUNING.crouchEyeHeight / MOVEMENT_TUNING.standEyeHeight);
4
+ export const POSE_HITBOX = {
5
+ standing: {
6
+ height: STANDING_HEIGHT,
7
+ eyeHeight: MOVEMENT_TUNING.standEyeHeight,
8
+ speedMultiplier: 1,
9
+ },
10
+ crouch: {
11
+ height: CROUCH_HEIGHT,
12
+ eyeHeight: MOVEMENT_TUNING.crouchEyeHeight,
13
+ speedMultiplier: MOVEMENT_TUNING.crouchSpeedMultiplier,
14
+ },
15
+ prone: {
16
+ height: CROUCH_HEIGHT * 0.5,
17
+ eyeHeight: MOVEMENT_TUNING.crouchEyeHeight * 0.4,
18
+ speedMultiplier: MOVEMENT_TUNING.crouchSpeedMultiplier * 0.5,
19
+ },
20
+ running: {
21
+ height: STANDING_HEIGHT,
22
+ eyeHeight: MOVEMENT_TUNING.standEyeHeight,
23
+ speedMultiplier: MOVEMENT_TUNING.runSpeedMultiplier,
24
+ },
25
+ };
26
+ export function createPoseState(resolveAllowed) {
27
+ const poses = new Map();
28
+ const aims = new Map();
29
+ function allowedPoses(instanceId) {
30
+ return resolveAllowed(instanceId)?.poses ?? ["standing"];
31
+ }
32
+ function allowedAims(instanceId) {
33
+ return resolveAllowed(instanceId)?.aim ?? ["hip"];
34
+ }
35
+ return {
36
+ getPose(instanceId) {
37
+ return poses.get(instanceId) ?? "standing";
38
+ },
39
+ setPose(instanceId, pose) {
40
+ if (!allowedPoses(instanceId).includes(pose)) {
41
+ return { reason: `Entity "${instanceId}" does not allow pose "${pose}".` };
42
+ }
43
+ poses.set(instanceId, pose);
44
+ return null;
45
+ },
46
+ getAim(instanceId) {
47
+ return aims.get(instanceId) ?? "hip";
48
+ },
49
+ setAim(instanceId, mode) {
50
+ if (!allowedAims(instanceId).includes(mode)) {
51
+ return { reason: `Entity "${instanceId}" does not allow aim mode "${mode}".` };
52
+ }
53
+ aims.set(instanceId, mode);
54
+ return null;
55
+ },
56
+ clear(instanceId) {
57
+ poses.delete(instanceId);
58
+ aims.delete(instanceId);
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,18 @@
1
+ export interface PlayerPose {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ rotationY: number;
6
+ rotationPitch: number;
7
+ }
8
+ export interface PoseSyncTuning {
9
+ minIntervalMs: number;
10
+ heartbeatMs: number;
11
+ positionEpsilon: number;
12
+ verticalEpsilon: number;
13
+ rotationEpsilon: number;
14
+ }
15
+ export interface PoseSyncGate {
16
+ evaluate(pose: PlayerPose, nowMs: number): boolean;
17
+ }
18
+ export declare function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate;
@@ -0,0 +1,30 @@
1
+ function poseChanged(a, b, tuning) {
2
+ return (Math.abs(a.x - b.x) > tuning.positionEpsilon
3
+ || Math.abs(a.y - b.y) > tuning.verticalEpsilon
4
+ || Math.abs(a.z - b.z) > tuning.positionEpsilon
5
+ || Math.abs(a.rotationY - b.rotationY) > tuning.rotationEpsilon
6
+ || Math.abs(a.rotationPitch - b.rotationPitch) > tuning.rotationEpsilon);
7
+ }
8
+ export function createPoseSyncGate(tuning) {
9
+ let lastSentPose = null;
10
+ let lastSentAt = 0;
11
+ return {
12
+ evaluate(pose, nowMs) {
13
+ if (lastSentPose === null) {
14
+ lastSentPose = pose;
15
+ lastSentAt = nowMs;
16
+ return true;
17
+ }
18
+ const changed = poseChanged(lastSentPose, pose, tuning);
19
+ const intervalElapsed = nowMs - lastSentAt >= tuning.minIntervalMs;
20
+ const heartbeatDue = nowMs - lastSentAt >= tuning.heartbeatMs;
21
+ if (!changed && !heartbeatDue)
22
+ return false;
23
+ if (!intervalElapsed && !heartbeatDue)
24
+ return false;
25
+ lastSentPose = pose;
26
+ lastSentAt = nowMs;
27
+ return true;
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,60 @@
1
+ export interface PresencePosition {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ }
6
+ export interface PresencePose {
7
+ position: {
8
+ x: number;
9
+ y?: number;
10
+ z: number;
11
+ };
12
+ rotationY?: number;
13
+ rotationPitch?: number;
14
+ externalId?: string;
15
+ }
16
+ export interface PresenceSession<TGameId extends string = string> {
17
+ homeGameId: TGameId;
18
+ externalId: string;
19
+ }
20
+ export interface EnsurePresenceArgs<TGameId extends string = string> {
21
+ gameId: TGameId;
22
+ externalId: string;
23
+ }
24
+ export interface EnsurePresenceResult {
25
+ presenceId: string;
26
+ position: PresencePosition;
27
+ rotationY: number;
28
+ }
29
+ export interface LeavePresenceArgs<TGameId extends string = string> {
30
+ gameId: TGameId;
31
+ externalId: string;
32
+ }
33
+ export interface PresenceFeeds<TRow, TLocation> {
34
+ myPresenceLocation: TLocation | null | undefined;
35
+ onlinePresences: readonly TRow[] | undefined;
36
+ dormantPresences: readonly TRow[] | undefined;
37
+ }
38
+ export interface PresenceActions<TGameId extends string = string> {
39
+ ensurePresence(args: EnsurePresenceArgs<TGameId>): Promise<EnsurePresenceResult | null>;
40
+ leavePresence(args: LeavePresenceArgs<TGameId>): Promise<{
41
+ left: boolean;
42
+ }>;
43
+ syncPose(pose: PresencePose): void;
44
+ }
45
+ /**
46
+ * Backend seam for multiplayer presence. Feeds are reactive data and change
47
+ * identity whenever any player's pose updates; actions MUST be identity-stable
48
+ * for the lifetime of a mounted session so join/leave lifecycle effects can
49
+ * depend on them without re-running per pose tick. The use* members are called
50
+ * as React hooks by consumers, so a mounted transport must never change
51
+ * identity — remount the subtree to switch backends.
52
+ */
53
+ export interface PresenceTransport<TRow, TLocation, TGameId extends string = string> {
54
+ useFeeds(session: PresenceSession<TGameId> | "skip"): PresenceFeeds<TRow, TLocation>;
55
+ useActions(): PresenceActions<TGameId>;
56
+ }
57
+ export declare function createLocalPresenceTransport<TRow, TLocation, TGameId extends string = string>(): {
58
+ transport: PresenceTransport<TRow, TLocation, TGameId>;
59
+ actions: PresenceActions<TGameId>;
60
+ };
@@ -0,0 +1,19 @@
1
+ export function createLocalPresenceTransport() {
2
+ const feeds = {
3
+ myPresenceLocation: null,
4
+ onlinePresences: [],
5
+ dormantPresences: [],
6
+ };
7
+ const actions = {
8
+ ensurePresence: async () => null,
9
+ leavePresence: async () => ({ left: true }),
10
+ syncPose: () => undefined,
11
+ };
12
+ return {
13
+ transport: {
14
+ useFeeds: () => feeds,
15
+ useActions: () => actions,
16
+ },
17
+ actions,
18
+ };
19
+ }
@@ -0,0 +1,60 @@
1
+ export interface PresencePoseState {
2
+ position: {
3
+ x: number;
4
+ y: number;
5
+ z: number;
6
+ };
7
+ rotationY: number;
8
+ rotationPitch?: number;
9
+ lastSeenAtMs?: number;
10
+ }
11
+ export interface IncomingPose {
12
+ position: {
13
+ x: number;
14
+ z: number;
15
+ y?: number;
16
+ };
17
+ rotationY?: number;
18
+ rotationPitch?: number;
19
+ }
20
+ export interface PoseSyncRules {
21
+ /** Speed cap (units/sec) for client-authoritative movement. */
22
+ maxSpeed: number;
23
+ /** Vertical offset clamp (e.g. peak jump height). */
24
+ maxVerticalOffset: number;
25
+ /** Elapsed-time clamp so stale or bursty clients cannot teleport. */
26
+ minElapsedSec: number;
27
+ maxElapsedSec: number;
28
+ /** Unchanged poses refresh the keep-alive stamp at most this often. */
29
+ keepAliveRefreshMs: number;
30
+ }
31
+ export interface PoseSyncDecision {
32
+ position: {
33
+ x: number;
34
+ y: number;
35
+ z: number;
36
+ };
37
+ rotationY: number;
38
+ rotationPitch: number;
39
+ /** True when the pose differs and a pose write is needed. */
40
+ changed: boolean;
41
+ /** True when only the keep-alive stamp should be written. */
42
+ refreshKeepAlive: boolean;
43
+ }
44
+ export declare function decidePoseSync(current: PresencePoseState, incoming: IncomingPose, rules: PoseSyncRules, nowMs: number): PoseSyncDecision;
45
+ export declare function shouldRefreshKeepAlive(lastSeenAtMs: number | undefined, nowMs: number, rules: Pick<PoseSyncRules, "keepAliveRefreshMs">): boolean;
46
+ export declare function shouldPersistWorldSnapshot(lastSavedAtMs: number | undefined, nowMs: number, intervalMs: number): boolean;
47
+ export interface ActivePresenceResolution<T> {
48
+ active: T | null;
49
+ /** Other unrevoked rows for the same actor — a session may only have one. */
50
+ extras: T[];
51
+ }
52
+ export declare function resolveActivePresence<T extends {
53
+ revokedAt?: number;
54
+ lastSeenAt?: number;
55
+ }>(rows: readonly T[]): ActivePresenceResolution<T>;
56
+ export declare function isPresenceExpired(lastSeenAtMs: number, nowMs: number, idleCutoffMs: number): boolean;
57
+ /** Most-recently-seen row across an actor's rows, to reuse instead of inserting a new one. */
58
+ export declare function pickReusablePresence<T extends {
59
+ lastSeenAt?: number;
60
+ }>(rows: readonly T[]): T | undefined;
@@ -0,0 +1,51 @@
1
+ export function decidePoseSync(current, incoming, rules, nowMs) {
2
+ const elapsedSec = Math.max(rules.minElapsedSec, Math.min(rules.maxElapsedSec, (nowMs - (current.lastSeenAtMs ?? nowMs)) / 1000));
3
+ const maxDist = rules.maxSpeed * elapsedSec;
4
+ const maxDistSq = maxDist * maxDist;
5
+ let targetX = incoming.position.x;
6
+ let targetZ = incoming.position.z;
7
+ const dx = targetX - current.position.x;
8
+ const dz = targetZ - current.position.z;
9
+ const magSq = dx * dx + dz * dz;
10
+ if (magSq > maxDistSq) {
11
+ const scale = Math.sqrt(maxDistSq / magSq);
12
+ targetX = current.position.x + dx * scale;
13
+ targetZ = current.position.z + dz * scale;
14
+ }
15
+ const nextY = incoming.position.y === undefined
16
+ ? current.position.y
17
+ : Math.max(0, Math.min(rules.maxVerticalOffset, incoming.position.y));
18
+ const nextRotationY = incoming.rotationY ?? current.rotationY;
19
+ const nextRotationPitch = incoming.rotationPitch ?? current.rotationPitch ?? 0;
20
+ const moved = targetX !== current.position.x || targetZ !== current.position.z;
21
+ const jumped = nextY !== current.position.y;
22
+ const rotated = nextRotationY !== current.rotationY || nextRotationPitch !== (current.rotationPitch ?? 0);
23
+ const changed = moved || jumped || rotated;
24
+ return {
25
+ position: { x: targetX, y: nextY, z: targetZ },
26
+ rotationY: nextRotationY,
27
+ rotationPitch: nextRotationPitch,
28
+ changed,
29
+ refreshKeepAlive: !changed && shouldRefreshKeepAlive(current.lastSeenAtMs, nowMs, rules),
30
+ };
31
+ }
32
+ export function shouldRefreshKeepAlive(lastSeenAtMs, nowMs, rules) {
33
+ return nowMs - (lastSeenAtMs ?? 0) >= rules.keepAliveRefreshMs;
34
+ }
35
+ export function shouldPersistWorldSnapshot(lastSavedAtMs, nowMs, intervalMs) {
36
+ return lastSavedAtMs === undefined || nowMs - lastSavedAtMs >= intervalMs;
37
+ }
38
+ export function resolveActivePresence(rows) {
39
+ const actives = rows.filter((row) => row.revokedAt === undefined);
40
+ if (actives.length === 0)
41
+ return { active: null, extras: [] };
42
+ const sorted = [...actives].sort((a, b) => (b.lastSeenAt ?? 0) - (a.lastSeenAt ?? 0));
43
+ return { active: sorted[0] ?? null, extras: sorted.slice(1) };
44
+ }
45
+ export function isPresenceExpired(lastSeenAtMs, nowMs, idleCutoffMs) {
46
+ return nowMs - lastSeenAtMs >= idleCutoffMs;
47
+ }
48
+ /** Most-recently-seen row across an actor's rows, to reuse instead of inserting a new one. */
49
+ export function pickReusablePresence(rows) {
50
+ return [...rows].sort((a, b) => (b.lastSeenAt ?? 0) - (a.lastSeenAt ?? 0))[0];
51
+ }
@@ -0,0 +1,24 @@
1
+ export type MultiplayerTopology = "shared" | "lobbies" | "private";
2
+ export type MultiplayerAdapterConfig = {
3
+ kind: "convex";
4
+ topology?: MultiplayerTopology;
5
+ } | {
6
+ kind: "ws";
7
+ topology?: MultiplayerTopology;
8
+ } | {
9
+ kind: "offline";
10
+ };
11
+ export declare function convex(config?: {
12
+ topology?: MultiplayerTopology;
13
+ }): MultiplayerAdapterConfig;
14
+ export declare function ws(config?: {
15
+ topology?: MultiplayerTopology;
16
+ }): MultiplayerAdapterConfig;
17
+ export declare function offline(): MultiplayerAdapterConfig;
18
+ export type ServersPoolConfig = {
19
+ maxServers: number;
20
+ slotsPerServer: number;
21
+ minPlayersToStart?: number;
22
+ adapter: MultiplayerAdapterConfig;
23
+ };
24
+ export declare function servers(config: ServersPoolConfig): ServersPoolConfig;
@@ -0,0 +1,12 @@
1
+ export function convex(config) {
2
+ return { kind: "convex", topology: config?.topology ?? "shared" };
3
+ }
4
+ export function ws(config) {
5
+ return { kind: "ws", topology: config?.topology ?? "shared" };
6
+ }
7
+ export function offline() {
8
+ return { kind: "offline" };
9
+ }
10
+ export function servers(config) {
11
+ return config;
12
+ }
@@ -0,0 +1,15 @@
1
+ export type CommandValidationError = {
2
+ reason: string;
3
+ };
4
+ export type CommandDef<TInput = unknown> = {
5
+ validate: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput) => CommandValidationError | null;
6
+ apply: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput) => import("./snapshot.js").GameRuntimeSnapshot;
7
+ };
8
+ export type RunCommandResult = {
9
+ ok: true;
10
+ snapshot: import("./snapshot.js").GameRuntimeSnapshot;
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
15
+ export declare function runCommand<TInput>(snapshot: import("./snapshot.js").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): RunCommandResult;
@@ -0,0 +1,25 @@
1
+ export function runCommand(snapshot, commands, commandName, input, actorUserId) {
2
+ const command = commands[commandName];
3
+ if (!command) {
4
+ return { ok: false, reason: `Unknown command: ${commandName}` };
5
+ }
6
+ const validationError = command.validate(snapshot, input);
7
+ if (validationError) {
8
+ return { ok: false, reason: validationError.reason };
9
+ }
10
+ const next = command.apply(snapshot, input);
11
+ return {
12
+ ok: true,
13
+ snapshot: {
14
+ ...next,
15
+ revision: snapshot.revision + 1,
16
+ dirty: {
17
+ server: true,
18
+ players: next.dirty.players.includes(actorUserId)
19
+ ? next.dirty.players
20
+ : [...next.dirty.players, actorUserId],
21
+ chunks: next.dirty.chunks,
22
+ },
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,138 @@
1
+ import { type OnDeathSpec } from "../combat/death.js";
2
+ import { type EffectInput, type EffectResult, type ReceiveMap, type SingleTargetEffectInput } from "../combat/effects.js";
3
+ import { type ProjectileSystem } from "../combat/projectiles.js";
4
+ import { type CommandDefinition, type CommandResult } from "../commands/commandRegistry.js";
5
+ import type { GameDefinition } from "../game/defineGame.js";
6
+ import { type GameEventMap, type GameEvents } from "../game/events.js";
7
+ import { type GameFeed } from "../game/feed.js";
8
+ import { type Leaderboard } from "../game/leaderboard.js";
9
+ import { type Loadouts } from "../game/loadout.js";
10
+ import { type Drop, type LootTableDef } from "../game/lootTable.js";
11
+ import { type QuestJournal } from "../game/quest.js";
12
+ import { type Social } from "../game/social.js";
13
+ import { type TradeField, type TradeSystem } from "../game/trade.js";
14
+ import { type Unlocks } from "../game/unlocks.js";
15
+ import { type InventorySet } from "../inventory/inventoryModel.js";
16
+ import { type ItemUseHandler, type ItemUseInput, type ItemUseRejection, type ItemUseResult } from "../item/use.js";
17
+ import { type WeaponStats } from "../item/weapon.js";
18
+ import { type PoseAllowedStates, type PoseState } from "../movement/poseState.js";
19
+ import type { ModelAssetRef } from "../scene/assetCatalog.js";
20
+ import { type EntityStatsApi, type PoolStatCatalog } from "../scene/entityStats.js";
21
+ import type { EntityPose, SceneEntity, SpawnOptions } from "../scene/entityStore.js";
22
+ import { type ObjectStore } from "../scene/objectStore.js";
23
+ import { type SpatialApi } from "../scene/spatial.js";
24
+ import { type CycleTargetOptions } from "../scene/targeting.js";
25
+ import { type Stats } from "../stats/statModifiers.js";
26
+ export interface GameContextItemEntry {
27
+ use?: string;
28
+ weapon?: Record<string, unknown>;
29
+ trade?: TradeField;
30
+ }
31
+ export interface GameContextEntityEntry {
32
+ stats?: PoolStatCatalog;
33
+ receive?: ReceiveMap;
34
+ onDeath?: OnDeathSpec;
35
+ movement?: PoseAllowedStates & {
36
+ walkSpeed?: number;
37
+ };
38
+ role?: string;
39
+ }
40
+ export interface GameContextContent {
41
+ itemById?(itemId: string): GameContextItemEntry | null | undefined;
42
+ entityById?(catalogId: string): GameContextEntityEntry | null | undefined;
43
+ }
44
+ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> {
45
+ definition: GameDefinition<TAssetRef, TMultiplayer>;
46
+ content: GameContextContent;
47
+ player: {
48
+ userId: string;
49
+ isNew: boolean;
50
+ };
51
+ now?: () => number;
52
+ }
53
+ export interface SceneEntityContext {
54
+ spawn(name: string, options?: SpawnOptions): string;
55
+ despawn(instanceId: string): boolean;
56
+ setPose(instanceId: string, pose: EntityPose): boolean;
57
+ get(instanceId: string): SceneEntity | null;
58
+ list(): readonly SceneEntity[];
59
+ stats: EntityStatsApi;
60
+ setTarget(fromId: string, toId: string | null): void;
61
+ getTarget(fromId: string): string | null;
62
+ cycleTarget(fromId: string, options?: CycleTargetOptions): string | null;
63
+ canReceive(instanceId: string, effect: string): string | null;
64
+ preview(input: SingleTargetEffectInput): number;
65
+ effect(input: EffectInput): EffectResult[];
66
+ willHitProjectile: ProjectileSystem["willHitProjectile"];
67
+ fireProjectile: ProjectileSystem["fireProjectile"];
68
+ settleProjectile: ProjectileSystem["settleProjectile"];
69
+ distance: SpatialApi["distance"];
70
+ inRadius: SpatialApi["inRadius"];
71
+ hasLineOfSight: SpatialApi["hasLineOfSight"];
72
+ queryArc: SpatialApi["queryArc"];
73
+ moveToward: SpatialApi["moveToward"];
74
+ }
75
+ export interface GameContextCommands {
76
+ define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void;
77
+ has(name: string): boolean;
78
+ names(): string[];
79
+ run(name: string, input: unknown): CommandResult<GameContext>;
80
+ }
81
+ export interface GameContextFeed extends Omit<GameFeed, "bind"> {
82
+ bind(action: keyof GameEventMap): () => void;
83
+ }
84
+ export interface GameContextLoot {
85
+ register(def: LootTableDef): void;
86
+ has(id: string): boolean;
87
+ roll(id: string, rng?: () => number): Drop[];
88
+ grantToPlayer(userId: string, drops: Drop[], source?: string): void;
89
+ }
90
+ export interface GameContextEconomy {
91
+ balance(userId: string, currencyId: string): number;
92
+ grant(userId: string, currencyId: string, amount: number): void;
93
+ charge(userId: string, currencyId: string, amount: number): {
94
+ reason: string;
95
+ } | null;
96
+ }
97
+ export interface GameContextItemUse {
98
+ register(handlers: Record<string, ItemUseHandler<GameContext>>): void;
99
+ registered(): string[];
100
+ can(input: ItemUseInput): ItemUseRejection | null;
101
+ use(input: ItemUseInput): ItemUseResult<GameContext>;
102
+ }
103
+ export interface GameContext {
104
+ scene: {
105
+ object: ObjectStore;
106
+ entity: SceneEntityContext;
107
+ };
108
+ game: {
109
+ commands: GameContextCommands;
110
+ events: GameEvents;
111
+ feed: GameContextFeed;
112
+ loot: GameContextLoot;
113
+ trade: TradeSystem;
114
+ quest: QuestJournal;
115
+ social: Social;
116
+ unlocks: Unlocks;
117
+ economy: GameContextEconomy;
118
+ leaderboard: Leaderboard;
119
+ };
120
+ player: {
121
+ userId: string;
122
+ isNew: boolean;
123
+ inventory: InventorySet<string>;
124
+ stats: Stats<string>;
125
+ loadout: Loadouts;
126
+ applyLoadout(userId: string, loadoutId: string): {
127
+ reason: string;
128
+ } | null;
129
+ movement: PoseState;
130
+ };
131
+ item: {
132
+ use: GameContextItemUse;
133
+ weapon: WeaponStats;
134
+ };
135
+ subscribe(listener: () => void): () => void;
136
+ version(): number;
137
+ }
138
+ export declare function createGameContext<TAssetRef extends ModelAssetRef, TMultiplayer>(options: GameContextOptions<TAssetRef, TMultiplayer>): GameContext;