@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.
- package/LICENSE +661 -0
- package/README.md +11 -0
- package/dist/assets/spriteSheet.d.ts +46 -0
- package/dist/assets/spriteSheet.js +86 -0
- package/dist/combat/death.d.ts +61 -0
- package/dist/combat/death.js +61 -0
- package/dist/combat/effects.d.ts +61 -0
- package/dist/combat/effects.js +108 -0
- package/dist/combat/projectiles.d.ts +45 -0
- package/dist/combat/projectiles.js +156 -0
- package/dist/commands/commandRegistry.d.ts +23 -0
- package/dist/commands/commandRegistry.js +26 -0
- package/dist/economy/currency.d.ts +26 -0
- package/dist/economy/currency.js +28 -0
- package/dist/economy/wallet.d.ts +16 -0
- package/dist/economy/wallet.js +49 -0
- package/dist/game/defineGame.d.ts +41 -0
- package/dist/game/defineGame.js +7 -0
- package/dist/game/events.d.ts +87 -0
- package/dist/game/events.js +24 -0
- package/dist/game/feed.d.ts +19 -0
- package/dist/game/feed.js +47 -0
- package/dist/game/leaderboard.d.ts +46 -0
- package/dist/game/leaderboard.js +60 -0
- package/dist/game/loadout.d.ts +47 -0
- package/dist/game/loadout.js +36 -0
- package/dist/game/lootTable.d.ts +27 -0
- package/dist/game/lootTable.js +78 -0
- package/dist/game/playableGame.d.ts +55 -0
- package/dist/game/playableGame.js +1 -0
- package/dist/game/quest.d.ts +93 -0
- package/dist/game/quest.js +228 -0
- package/dist/game/social.d.ts +78 -0
- package/dist/game/social.js +227 -0
- package/dist/game/trade.d.ts +35 -0
- package/dist/game/trade.js +53 -0
- package/dist/game/unlocks.d.ts +13 -0
- package/dist/game/unlocks.js +34 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/input/actionBindings.d.ts +41 -0
- package/dist/input/actionBindings.js +115 -0
- package/dist/input/lookChannel.d.ts +29 -0
- package/dist/input/lookChannel.js +44 -0
- package/dist/input/touchGestures.d.ts +31 -0
- package/dist/input/touchGestures.js +72 -0
- package/dist/interaction/proximityPrompt.d.ts +51 -0
- package/dist/interaction/proximityPrompt.js +76 -0
- package/dist/inventory/inventoryModel.d.ts +57 -0
- package/dist/inventory/inventoryModel.js +208 -0
- package/dist/item/use.d.ts +25 -0
- package/dist/item/use.js +40 -0
- package/dist/item/weapon.d.ts +8 -0
- package/dist/item/weapon.js +18 -0
- package/dist/movement/avatarGait.d.ts +14 -0
- package/dist/movement/avatarGait.js +20 -0
- package/dist/movement/cameraRig.d.ts +15 -0
- package/dist/movement/cameraRig.js +43 -0
- package/dist/movement/movementModel.d.ts +96 -0
- package/dist/movement/movementModel.js +171 -0
- package/dist/movement/poseState.d.ts +23 -0
- package/dist/movement/poseState.js +61 -0
- package/dist/multiplayer/poseSyncGate.d.ts +18 -0
- package/dist/multiplayer/poseSyncGate.js +30 -0
- package/dist/multiplayer/presenceContract.d.ts +60 -0
- package/dist/multiplayer/presenceContract.js +19 -0
- package/dist/multiplayer/presenceModel.d.ts +60 -0
- package/dist/multiplayer/presenceModel.js +51 -0
- package/dist/runtime/adapter.d.ts +24 -0
- package/dist/runtime/adapter.js +12 -0
- package/dist/runtime/commandRunner.d.ts +15 -0
- package/dist/runtime/commandRunner.js +25 -0
- package/dist/runtime/gameContext.d.ts +138 -0
- package/dist/runtime/gameContext.js +371 -0
- package/dist/runtime/gameRuntime.d.ts +42 -0
- package/dist/runtime/gameRuntime.js +95 -0
- package/dist/runtime/hostPersistence.d.ts +105 -0
- package/dist/runtime/hostPersistence.js +117 -0
- package/dist/runtime/save.d.ts +9 -0
- package/dist/runtime/save.js +29 -0
- package/dist/runtime/snapshot.d.ts +87 -0
- package/dist/runtime/snapshot.js +75 -0
- package/dist/runtime/transport.d.ts +60 -0
- package/dist/runtime/transport.js +1 -0
- package/dist/scene/assetCatalog.d.ts +10 -0
- package/dist/scene/assetCatalog.js +17 -0
- package/dist/scene/assetPreload.d.ts +7 -0
- package/dist/scene/assetPreload.js +22 -0
- package/dist/scene/behaviors.d.ts +19 -0
- package/dist/scene/behaviors.js +18 -0
- package/dist/scene/entityStats.d.ts +38 -0
- package/dist/scene/entityStats.js +65 -0
- package/dist/scene/entityStore.d.ts +52 -0
- package/dist/scene/entityStore.js +84 -0
- package/dist/scene/objectStore.d.ts +28 -0
- package/dist/scene/objectStore.js +69 -0
- package/dist/scene/spatial.d.ts +34 -0
- package/dist/scene/spatial.js +96 -0
- package/dist/scene/targeting.d.ts +20 -0
- package/dist/scene/targeting.js +56 -0
- package/dist/stats/statModifiers.d.ts +18 -0
- package/dist/stats/statModifiers.js +56 -0
- package/dist/store/changeSignal.d.ts +7 -0
- package/dist/store/changeSignal.js +32 -0
- package/dist/store/engineStore.d.ts +22 -0
- package/dist/store/engineStore.js +50 -0
- package/dist/store/features.d.ts +10 -0
- package/dist/store/features.js +23 -0
- package/dist/store/observableKeyedStore.d.ts +10 -0
- package/dist/store/observableKeyedStore.js +42 -0
- package/dist/time/calendarClock.d.ts +17 -0
- package/dist/time/calendarClock.js +42 -0
- package/dist/time/gameClock.d.ts +9 -0
- package/dist/time/gameClock.js +20 -0
- package/dist/time/serverTick.d.ts +16 -0
- package/dist/time/serverTick.js +18 -0
- package/dist/world/features.d.ts +39 -0
- package/dist/world/features.js +15 -0
- package/package.json +32 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { isSaveEnabled, parseSaveAutoMs, saveScopeIncludesChunks, saveScopeIncludesPlayer, } from "./save.js";
|
|
2
|
+
import { createEmptyPlayerRow, splitProfilePlayer } from "./snapshot.js";
|
|
3
|
+
export const LEADERBOARD_PENDING_KEY = "leaderboardPending";
|
|
4
|
+
function isLeaderboardIncrement(value) {
|
|
5
|
+
if (typeof value !== "object" || value === null)
|
|
6
|
+
return false;
|
|
7
|
+
const candidate = value;
|
|
8
|
+
return (typeof candidate.userId === "string" &&
|
|
9
|
+
typeof candidate.stat === "string" &&
|
|
10
|
+
(candidate.scope === "global" || candidate.scope === "server" || candidate.scope === "profile") &&
|
|
11
|
+
typeof candidate.by === "number" &&
|
|
12
|
+
(candidate.serverId === undefined || typeof candidate.serverId === "string"));
|
|
13
|
+
}
|
|
14
|
+
export function drainPendingLeaderboardIncrements(session) {
|
|
15
|
+
const raw = session[LEADERBOARD_PENDING_KEY];
|
|
16
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
17
|
+
return { increments: [], session };
|
|
18
|
+
}
|
|
19
|
+
const rest = { ...session };
|
|
20
|
+
delete rest[LEADERBOARD_PENDING_KEY];
|
|
21
|
+
return { increments: raw.filter(isLeaderboardIncrement), session: rest };
|
|
22
|
+
}
|
|
23
|
+
export const FEED_RING_LIMIT = 20;
|
|
24
|
+
export function trimFeedEntries(entries, limit = FEED_RING_LIMIT) {
|
|
25
|
+
if (entries.length <= limit)
|
|
26
|
+
return entries;
|
|
27
|
+
return entries.slice(entries.length - limit);
|
|
28
|
+
}
|
|
29
|
+
export function shouldAutoSave(save, dirtyAt, lastSavedAt, now) {
|
|
30
|
+
if (!isSaveEnabled(save))
|
|
31
|
+
return false;
|
|
32
|
+
if (dirtyAt === undefined)
|
|
33
|
+
return false;
|
|
34
|
+
const intervalMs = parseSaveAutoMs(save.auto);
|
|
35
|
+
const anchor = lastSavedAt ?? 0;
|
|
36
|
+
return now - anchor >= intervalMs;
|
|
37
|
+
}
|
|
38
|
+
export function toServerListing(record) {
|
|
39
|
+
return {
|
|
40
|
+
serverId: record.serverId,
|
|
41
|
+
status: record.status,
|
|
42
|
+
memberCount: record.memberUserIds.length,
|
|
43
|
+
slotsPerServer: record.slotsPerServer,
|
|
44
|
+
mode: record.mode,
|
|
45
|
+
updatedAt: record.updatedAt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function buildHydratePlayers(server, profiles) {
|
|
49
|
+
const players = {};
|
|
50
|
+
for (const userId of server.memberUserIds) {
|
|
51
|
+
const profile = profiles[userId] ?? null;
|
|
52
|
+
const sessionPlayer = server.sessionPlayers[userId];
|
|
53
|
+
if (profile) {
|
|
54
|
+
players[userId] = {
|
|
55
|
+
...profile.playerState,
|
|
56
|
+
...(sessionPlayer?.session ? { session: sessionPlayer.session } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
else if (sessionPlayer) {
|
|
60
|
+
players[userId] = sessionPlayer;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
players[userId] = createEmptyPlayerRow(userId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return players;
|
|
67
|
+
}
|
|
68
|
+
export function planServerPersist(server, snapshot, save, now) {
|
|
69
|
+
const drained = drainPendingLeaderboardIncrements(snapshot.server.session);
|
|
70
|
+
const serverState = drained.increments.length > 0 ? { ...snapshot.server, session: drained.session } : snapshot.server;
|
|
71
|
+
const leaderboard = drained.increments.map((entry) => entry.scope === "server" && entry.serverId === undefined
|
|
72
|
+
? { ...entry, serverId: server.serverId }
|
|
73
|
+
: entry);
|
|
74
|
+
const sessionPlayers = {};
|
|
75
|
+
const profiles = [];
|
|
76
|
+
for (const userId of Object.keys(snapshot.players)) {
|
|
77
|
+
const player = snapshot.players[userId];
|
|
78
|
+
if (!player)
|
|
79
|
+
continue;
|
|
80
|
+
const { persistent, session } = splitProfilePlayer(player);
|
|
81
|
+
sessionPlayers[userId] = { ...persistent, session };
|
|
82
|
+
if (isSaveEnabled(save) &&
|
|
83
|
+
saveScopeIncludesPlayer(save.scope) &&
|
|
84
|
+
snapshot.dirty.players.includes(userId)) {
|
|
85
|
+
profiles.push({
|
|
86
|
+
userId,
|
|
87
|
+
gameId: server.gameId,
|
|
88
|
+
playerState: persistent,
|
|
89
|
+
revision: snapshot.revision,
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const chunks = [];
|
|
95
|
+
if (isSaveEnabled(save) && saveScopeIncludesChunks(save.scope)) {
|
|
96
|
+
for (const chunkKey of snapshot.dirty.chunks) {
|
|
97
|
+
const chunk = snapshot.chunks[chunkKey];
|
|
98
|
+
if (!chunk)
|
|
99
|
+
continue;
|
|
100
|
+
chunks.push({ serverId: server.serverId, chunkKey, snapshot: chunk, updatedAt: now });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
server: {
|
|
105
|
+
...server,
|
|
106
|
+
serverState,
|
|
107
|
+
sessionPlayers,
|
|
108
|
+
revision: snapshot.revision,
|
|
109
|
+
dirtyAt: snapshot.dirty.server || snapshot.dirty.players.length > 0 ? now : server.dirtyAt,
|
|
110
|
+
updatedAt: now,
|
|
111
|
+
lastSavedAt: now,
|
|
112
|
+
},
|
|
113
|
+
profiles,
|
|
114
|
+
chunks,
|
|
115
|
+
leaderboard,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type SaveScope = "player" | "chunks" | "player+chunks";
|
|
2
|
+
export type SaveConfig = "none" | {
|
|
3
|
+
auto: string;
|
|
4
|
+
scope: SaveScope;
|
|
5
|
+
};
|
|
6
|
+
export declare function parseSaveAutoMs(auto: string): number;
|
|
7
|
+
export declare function saveScopeIncludesPlayer(scope: SaveScope): boolean;
|
|
8
|
+
export declare function saveScopeIncludesChunks(scope: SaveScope): boolean;
|
|
9
|
+
export declare function isSaveEnabled(config: SaveConfig): config is Exclude<SaveConfig, "none">;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function parseSaveAutoMs(auto) {
|
|
2
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(auto.trim());
|
|
3
|
+
if (!match) {
|
|
4
|
+
throw new Error(`Invalid save.auto interval: ${auto}`);
|
|
5
|
+
}
|
|
6
|
+
const amount = Number(match[1]);
|
|
7
|
+
const unit = match[2];
|
|
8
|
+
switch (unit) {
|
|
9
|
+
case "ms":
|
|
10
|
+
return amount;
|
|
11
|
+
case "s":
|
|
12
|
+
return amount * 1_000;
|
|
13
|
+
case "m":
|
|
14
|
+
return amount * 60_000;
|
|
15
|
+
case "h":
|
|
16
|
+
return amount * 3_600_000;
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`Invalid save.auto unit: ${unit}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveScopeIncludesPlayer(scope) {
|
|
22
|
+
return scope === "player" || scope === "player+chunks";
|
|
23
|
+
}
|
|
24
|
+
export function saveScopeIncludesChunks(scope) {
|
|
25
|
+
return scope === "chunks" || scope === "player+chunks";
|
|
26
|
+
}
|
|
27
|
+
export function isSaveEnabled(config) {
|
|
28
|
+
return config !== "none";
|
|
29
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export declare const RUNTIME_SNAPSHOT_VERSION = 1;
|
|
2
|
+
export type RuntimeEntityRow = {
|
|
3
|
+
instanceId: string;
|
|
4
|
+
catalogId: string;
|
|
5
|
+
position?: [number, number, number];
|
|
6
|
+
rotationY?: number;
|
|
7
|
+
parentSpace?: string;
|
|
8
|
+
group?: string;
|
|
9
|
+
stats?: Record<string, {
|
|
10
|
+
current: number;
|
|
11
|
+
max: number;
|
|
12
|
+
min?: number;
|
|
13
|
+
}>;
|
|
14
|
+
targetInstanceId?: string | null;
|
|
15
|
+
userId?: string;
|
|
16
|
+
};
|
|
17
|
+
export type RuntimeObjectRow = {
|
|
18
|
+
instanceId: string;
|
|
19
|
+
catalogId: string;
|
|
20
|
+
position: [number, number, number];
|
|
21
|
+
rotationY?: number;
|
|
22
|
+
parentSpace?: string;
|
|
23
|
+
flags?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
export type RuntimeInventorySlot = {
|
|
26
|
+
item: string;
|
|
27
|
+
count: number;
|
|
28
|
+
slot?: number;
|
|
29
|
+
};
|
|
30
|
+
export type RuntimePlayerRow = {
|
|
31
|
+
userId: string;
|
|
32
|
+
inventories: Record<string, RuntimeInventorySlot[]>;
|
|
33
|
+
economy: Record<string, number>;
|
|
34
|
+
unlocks: string[];
|
|
35
|
+
quests?: unknown;
|
|
36
|
+
social?: unknown;
|
|
37
|
+
leaderboard?: Record<string, number>;
|
|
38
|
+
session?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
export type RuntimeChunkRow = {
|
|
41
|
+
chunkKey: string;
|
|
42
|
+
objects: RuntimeObjectRow[];
|
|
43
|
+
entities: RuntimeEntityRow[];
|
|
44
|
+
flags?: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
export type RuntimeServerRow = {
|
|
47
|
+
entities: RuntimeEntityRow[];
|
|
48
|
+
objects: RuntimeObjectRow[];
|
|
49
|
+
session: Record<string, unknown>;
|
|
50
|
+
feeds?: Record<string, unknown[]>;
|
|
51
|
+
};
|
|
52
|
+
export type RuntimeProfileRow = {
|
|
53
|
+
userId: string;
|
|
54
|
+
gameId: string;
|
|
55
|
+
player: RuntimePlayerRow;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
};
|
|
58
|
+
export type GameRuntimeSnapshot = {
|
|
59
|
+
version: number;
|
|
60
|
+
gameId: string;
|
|
61
|
+
serverId: string;
|
|
62
|
+
server: RuntimeServerRow;
|
|
63
|
+
players: Record<string, RuntimePlayerRow>;
|
|
64
|
+
chunks: Record<string, RuntimeChunkRow>;
|
|
65
|
+
revision: number;
|
|
66
|
+
dirty: {
|
|
67
|
+
server: boolean;
|
|
68
|
+
players: string[];
|
|
69
|
+
chunks: string[];
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export declare function createEmptyServerRow(): RuntimeServerRow;
|
|
73
|
+
export declare function createEmptyPlayerRow(userId: string): RuntimePlayerRow;
|
|
74
|
+
export declare function createRuntimeSnapshot(args: {
|
|
75
|
+
gameId: string;
|
|
76
|
+
serverId: string;
|
|
77
|
+
server?: RuntimeServerRow;
|
|
78
|
+
players?: Record<string, RuntimePlayerRow>;
|
|
79
|
+
chunks?: Record<string, RuntimeChunkRow>;
|
|
80
|
+
}): GameRuntimeSnapshot;
|
|
81
|
+
export declare function markServerDirty(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot;
|
|
82
|
+
export declare function markPlayerDirty(snapshot: GameRuntimeSnapshot, userId: string): GameRuntimeSnapshot;
|
|
83
|
+
export declare function clearDirtyFlags(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot;
|
|
84
|
+
export declare function splitProfilePlayer(player: RuntimePlayerRow): {
|
|
85
|
+
persistent: RuntimePlayerRow;
|
|
86
|
+
session: Record<string, unknown>;
|
|
87
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const RUNTIME_SNAPSHOT_VERSION = 1;
|
|
2
|
+
export function createEmptyServerRow() {
|
|
3
|
+
return {
|
|
4
|
+
entities: [],
|
|
5
|
+
objects: [],
|
|
6
|
+
session: {},
|
|
7
|
+
feeds: {},
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function createEmptyPlayerRow(userId) {
|
|
11
|
+
return {
|
|
12
|
+
userId,
|
|
13
|
+
inventories: {},
|
|
14
|
+
economy: {},
|
|
15
|
+
unlocks: [],
|
|
16
|
+
session: {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createRuntimeSnapshot(args) {
|
|
20
|
+
return {
|
|
21
|
+
version: RUNTIME_SNAPSHOT_VERSION,
|
|
22
|
+
gameId: args.gameId,
|
|
23
|
+
serverId: args.serverId,
|
|
24
|
+
server: args.server ?? createEmptyServerRow(),
|
|
25
|
+
players: args.players ?? {},
|
|
26
|
+
chunks: args.chunks ?? {},
|
|
27
|
+
revision: 0,
|
|
28
|
+
dirty: {
|
|
29
|
+
server: false,
|
|
30
|
+
players: [],
|
|
31
|
+
chunks: [],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function markServerDirty(snapshot) {
|
|
36
|
+
return {
|
|
37
|
+
...snapshot,
|
|
38
|
+
revision: snapshot.revision + 1,
|
|
39
|
+
dirty: { ...snapshot.dirty, server: true },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function markPlayerDirty(snapshot, userId) {
|
|
43
|
+
if (snapshot.dirty.players.includes(userId)) {
|
|
44
|
+
return markServerDirty(snapshot);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...snapshot,
|
|
48
|
+
revision: snapshot.revision + 1,
|
|
49
|
+
dirty: {
|
|
50
|
+
...snapshot.dirty,
|
|
51
|
+
server: true,
|
|
52
|
+
players: [...snapshot.dirty.players, userId],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function clearDirtyFlags(snapshot) {
|
|
57
|
+
return {
|
|
58
|
+
...snapshot,
|
|
59
|
+
dirty: {
|
|
60
|
+
server: false,
|
|
61
|
+
players: [],
|
|
62
|
+
chunks: [],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function splitProfilePlayer(player) {
|
|
67
|
+
const { session = {}, ...persistentFields } = player;
|
|
68
|
+
return {
|
|
69
|
+
persistent: {
|
|
70
|
+
...persistentFields,
|
|
71
|
+
session: {},
|
|
72
|
+
},
|
|
73
|
+
session,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PresenceTransport } from "../multiplayer/presenceContract.js";
|
|
2
|
+
export type JoinServerResult = {
|
|
3
|
+
serverId: string;
|
|
4
|
+
isNew: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type RunCommandArgs = {
|
|
7
|
+
serverId: string;
|
|
8
|
+
command: string;
|
|
9
|
+
input: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type TransportRunCommandResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: string;
|
|
16
|
+
};
|
|
17
|
+
export type GameRuntimeServerView = {
|
|
18
|
+
serverId: string;
|
|
19
|
+
gameId: string;
|
|
20
|
+
revision: number;
|
|
21
|
+
memberUserIds: string[];
|
|
22
|
+
serverState: unknown;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
};
|
|
25
|
+
export type GameRuntimePlayerView = {
|
|
26
|
+
userId: string;
|
|
27
|
+
gameId: string;
|
|
28
|
+
playerState: unknown;
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
};
|
|
31
|
+
export type GameRuntimeFeedView = {
|
|
32
|
+
action: string;
|
|
33
|
+
entries: unknown[];
|
|
34
|
+
};
|
|
35
|
+
export type GameRuntimeTransport = {
|
|
36
|
+
joinServer: (args: {
|
|
37
|
+
gameId: string;
|
|
38
|
+
serverId?: string;
|
|
39
|
+
}) => Promise<JoinServerResult>;
|
|
40
|
+
leaveServer: (args: {
|
|
41
|
+
serverId: string;
|
|
42
|
+
}) => Promise<void>;
|
|
43
|
+
runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>;
|
|
44
|
+
};
|
|
45
|
+
export type FeedUnsubscribe = () => void;
|
|
46
|
+
export type GameRuntimeFeeds = {
|
|
47
|
+
subscribeServer: (serverId: string, onChange: (view: GameRuntimeServerView | null) => void) => FeedUnsubscribe;
|
|
48
|
+
subscribePlayer: (args: {
|
|
49
|
+
serverId: string;
|
|
50
|
+
}, onChange: (view: GameRuntimePlayerView | null) => void) => FeedUnsubscribe;
|
|
51
|
+
subscribeFeed: (args: {
|
|
52
|
+
serverId: string;
|
|
53
|
+
action: string;
|
|
54
|
+
}, onChange: (view: GameRuntimeFeedView) => void) => FeedUnsubscribe;
|
|
55
|
+
};
|
|
56
|
+
export type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = {
|
|
57
|
+
transport: GameRuntimeTransport;
|
|
58
|
+
feeds?: GameRuntimeFeeds;
|
|
59
|
+
presence?: PresenceTransport<TPresenceRow, TPresenceLocation, TGameId>;
|
|
60
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ModelAssetRef {
|
|
2
|
+
url: string;
|
|
3
|
+
}
|
|
4
|
+
export interface AssetCatalog<TMeta extends ModelAssetRef = ModelAssetRef> {
|
|
5
|
+
register(id: string, asset: TMeta): void;
|
|
6
|
+
resolve(id: string): TMeta | null;
|
|
7
|
+
has(id: string): boolean;
|
|
8
|
+
ids(): readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function createAssetCatalog<TMeta extends ModelAssetRef = ModelAssetRef>(): AssetCatalog<TMeta>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function createAssetCatalog() {
|
|
2
|
+
const assets = new Map();
|
|
3
|
+
return {
|
|
4
|
+
register(id, asset) {
|
|
5
|
+
assets.set(id, asset);
|
|
6
|
+
},
|
|
7
|
+
resolve(id) {
|
|
8
|
+
return assets.get(id) ?? null;
|
|
9
|
+
},
|
|
10
|
+
has(id) {
|
|
11
|
+
return assets.has(id);
|
|
12
|
+
},
|
|
13
|
+
ids() {
|
|
14
|
+
return Array.from(assets.keys());
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type SceneAssetFetcher = (url: string) => void;
|
|
2
|
+
export declare function createSceneAssetPreloader(fetcher: SceneAssetFetcher): {
|
|
3
|
+
preloadUrl(url: string): void;
|
|
4
|
+
preloadUrls(urls: readonly string[]): void;
|
|
5
|
+
hasPreloaded(url: string): boolean;
|
|
6
|
+
reset(): void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createSceneAssetPreloader(fetcher) {
|
|
2
|
+
const preloadedUrls = new Set();
|
|
3
|
+
return {
|
|
4
|
+
preloadUrl(url) {
|
|
5
|
+
if (preloadedUrls.has(url))
|
|
6
|
+
return;
|
|
7
|
+
preloadedUrls.add(url);
|
|
8
|
+
fetcher(url);
|
|
9
|
+
},
|
|
10
|
+
preloadUrls(urls) {
|
|
11
|
+
for (const url of urls) {
|
|
12
|
+
this.preloadUrl(url);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
hasPreloaded(url) {
|
|
16
|
+
return preloadedUrls.has(url);
|
|
17
|
+
},
|
|
18
|
+
reset() {
|
|
19
|
+
preloadedUrls.clear();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
|
|
2
|
+
export interface WanderBehavior {
|
|
3
|
+
kind: "wander";
|
|
4
|
+
radius: number;
|
|
5
|
+
}
|
|
6
|
+
export interface PromptableBehavior {
|
|
7
|
+
kind: "promptable";
|
|
8
|
+
prompt: ProximityPrompt;
|
|
9
|
+
}
|
|
10
|
+
export interface PlayerBehavior {
|
|
11
|
+
kind: "player";
|
|
12
|
+
}
|
|
13
|
+
export type BehaviorDescriptor = WanderBehavior | PromptableBehavior | PlayerBehavior;
|
|
14
|
+
export declare function wander({ radius }: {
|
|
15
|
+
radius: number;
|
|
16
|
+
}): WanderBehavior;
|
|
17
|
+
export declare function promptable(prompt: ProximityPrompt): PromptableBehavior;
|
|
18
|
+
export declare function talkable(dialogueId: string): PromptableBehavior;
|
|
19
|
+
export declare function player(): PlayerBehavior;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { command, keybind, proximityPrompt, } from "../interaction/proximityPrompt.js";
|
|
2
|
+
export function wander({ radius }) {
|
|
3
|
+
return { kind: "wander", radius };
|
|
4
|
+
}
|
|
5
|
+
export function promptable(prompt) {
|
|
6
|
+
return { kind: "promptable", prompt };
|
|
7
|
+
}
|
|
8
|
+
const TALK_RADIUS = 2;
|
|
9
|
+
export function talkable(dialogueId) {
|
|
10
|
+
return promptable(proximityPrompt({
|
|
11
|
+
radius: TALK_RADIUS,
|
|
12
|
+
display: keybind("interact"),
|
|
13
|
+
invoke: command("dialogue.open", { id: dialogueId }),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export function player() {
|
|
17
|
+
return { kind: "player" };
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface PoolStat {
|
|
2
|
+
current: number;
|
|
3
|
+
max: number;
|
|
4
|
+
min: number;
|
|
5
|
+
}
|
|
6
|
+
export interface PoolStatPatch {
|
|
7
|
+
current?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
min?: number;
|
|
10
|
+
}
|
|
11
|
+
export type PoolStatMap = Record<string, PoolStat>;
|
|
12
|
+
export type PoolStatCatalog = Record<string, {
|
|
13
|
+
max: number;
|
|
14
|
+
min?: number;
|
|
15
|
+
current?: number;
|
|
16
|
+
}>;
|
|
17
|
+
export type PoolDeltaResult = {
|
|
18
|
+
status: "ok";
|
|
19
|
+
map: PoolStatMap;
|
|
20
|
+
stat: PoolStat;
|
|
21
|
+
hitMin: boolean;
|
|
22
|
+
hitMax: boolean;
|
|
23
|
+
} | {
|
|
24
|
+
status: "rejected";
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function getPoolStat(map: PoolStatMap, statId: string): PoolStat | null;
|
|
28
|
+
export declare function setPoolStat(map: PoolStatMap, statId: string, patch: PoolStatPatch): PoolStatMap;
|
|
29
|
+
export declare function applyPoolDelta(map: PoolStatMap, statId: string, amount: number): PoolDeltaResult;
|
|
30
|
+
export declare function seedPoolStats(catalogStats: PoolStatCatalog): PoolStatMap;
|
|
31
|
+
export interface EntityStatsApi {
|
|
32
|
+
get(instanceId: string, statId: string): PoolStat | null;
|
|
33
|
+
set(instanceId: string, statId: string, patch: PoolStatPatch): boolean;
|
|
34
|
+
delta(instanceId: string, statId: string, amount: number): null | {
|
|
35
|
+
reason: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function createEntityStatsApi(resolve: (instanceId: string) => PoolStatMap | undefined): EntityStatsApi;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function clamp(value, min, max) {
|
|
2
|
+
return Math.min(max, Math.max(min, value));
|
|
3
|
+
}
|
|
4
|
+
export function getPoolStat(map, statId) {
|
|
5
|
+
return map[statId] ?? null;
|
|
6
|
+
}
|
|
7
|
+
export function setPoolStat(map, statId, patch) {
|
|
8
|
+
const existing = map[statId];
|
|
9
|
+
const max = patch.max ?? existing?.max ?? 0;
|
|
10
|
+
const min = patch.min ?? existing?.min ?? 0;
|
|
11
|
+
const current = clamp(patch.current ?? existing?.current ?? max, min, max);
|
|
12
|
+
return { ...map, [statId]: { current, max, min } };
|
|
13
|
+
}
|
|
14
|
+
export function applyPoolDelta(map, statId, amount) {
|
|
15
|
+
const existing = map[statId];
|
|
16
|
+
if (existing === undefined) {
|
|
17
|
+
return { status: "rejected", reason: `unknown stat "${statId}"` };
|
|
18
|
+
}
|
|
19
|
+
const current = clamp(existing.current + amount, existing.min, existing.max);
|
|
20
|
+
const stat = { ...existing, current };
|
|
21
|
+
return {
|
|
22
|
+
status: "ok",
|
|
23
|
+
map: { ...map, [statId]: stat },
|
|
24
|
+
stat,
|
|
25
|
+
hitMin: current === existing.min,
|
|
26
|
+
hitMax: current === existing.max,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function seedPoolStats(catalogStats) {
|
|
30
|
+
const map = {};
|
|
31
|
+
for (const [statId, declaration] of Object.entries(catalogStats)) {
|
|
32
|
+
const min = declaration.min ?? 0;
|
|
33
|
+
const current = clamp(declaration.current ?? declaration.max, min, declaration.max);
|
|
34
|
+
map[statId] = { current, max: declaration.max, min };
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
export function createEntityStatsApi(resolve) {
|
|
39
|
+
return {
|
|
40
|
+
get(instanceId, statId) {
|
|
41
|
+
const map = resolve(instanceId);
|
|
42
|
+
if (map === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
return getPoolStat(map, statId);
|
|
45
|
+
},
|
|
46
|
+
set(instanceId, statId, patch) {
|
|
47
|
+
const map = resolve(instanceId);
|
|
48
|
+
if (map === undefined)
|
|
49
|
+
return false;
|
|
50
|
+
const next = setPoolStat(map, statId, patch);
|
|
51
|
+
map[statId] = next[statId];
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
delta(instanceId, statId, amount) {
|
|
55
|
+
const map = resolve(instanceId);
|
|
56
|
+
if (map === undefined)
|
|
57
|
+
return { reason: `unknown instance "${instanceId}"` };
|
|
58
|
+
const result = applyPoolDelta(map, statId, amount);
|
|
59
|
+
if (result.status === "rejected")
|
|
60
|
+
return { reason: result.reason };
|
|
61
|
+
map[statId] = result.stat;
|
|
62
|
+
return null;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { BehaviorDescriptor } from "./behaviors.js";
|
|
2
|
+
export type EntityRole = "player" | "npc" | "prop";
|
|
3
|
+
export interface EntityMovement {
|
|
4
|
+
walkSpeed?: number;
|
|
5
|
+
}
|
|
6
|
+
export type EntityPosition = readonly [number, number, number];
|
|
7
|
+
export type SpawnPositionInput = EntityPosition | {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
z: number;
|
|
11
|
+
};
|
|
12
|
+
export interface SceneEntity<TMeta = undefined> {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
position: EntityPosition;
|
|
16
|
+
rotationY: number;
|
|
17
|
+
rotationX: number;
|
|
18
|
+
rotationZ: number;
|
|
19
|
+
role: EntityRole;
|
|
20
|
+
movement: EntityMovement;
|
|
21
|
+
behaviors: readonly BehaviorDescriptor[];
|
|
22
|
+
meta: TMeta;
|
|
23
|
+
}
|
|
24
|
+
export interface SpawnOptions<TMeta = undefined> {
|
|
25
|
+
id?: string;
|
|
26
|
+
position?: SpawnPositionInput;
|
|
27
|
+
rotationY?: number;
|
|
28
|
+
rotationX?: number;
|
|
29
|
+
rotationZ?: number;
|
|
30
|
+
role?: EntityRole;
|
|
31
|
+
movement?: EntityMovement;
|
|
32
|
+
behaviors?: readonly BehaviorDescriptor[];
|
|
33
|
+
meta?: TMeta;
|
|
34
|
+
}
|
|
35
|
+
export interface EntityPose {
|
|
36
|
+
position: SpawnPositionInput;
|
|
37
|
+
rotationY?: number;
|
|
38
|
+
rotationX?: number;
|
|
39
|
+
rotationZ?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface EntityStore<TMeta = undefined> {
|
|
42
|
+
spawn(name: string, options?: SpawnOptions<TMeta>): string;
|
|
43
|
+
despawn(id: string): boolean;
|
|
44
|
+
update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
|
|
45
|
+
setPose(id: string, pose: EntityPose): boolean;
|
|
46
|
+
get(id: string): SceneEntity<TMeta> | null;
|
|
47
|
+
list(): readonly SceneEntity<TMeta>[];
|
|
48
|
+
clear(): void;
|
|
49
|
+
subscribe(listener: () => void): () => void;
|
|
50
|
+
snapshot(): readonly SceneEntity<TMeta>[];
|
|
51
|
+
}
|
|
52
|
+
export declare function createEntityStore<TMeta = undefined>(): EntityStore<TMeta>;
|