@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,371 @@
|
|
|
1
|
+
import { createDeathSystem, deathReasonFromEffect } from "../combat/death.js";
|
|
2
|
+
import { createEffectSystem, } from "../combat/effects.js";
|
|
3
|
+
import { createProjectileSystem } from "../combat/projectiles.js";
|
|
4
|
+
import { createCommandRegistry, } from "../commands/commandRegistry.js";
|
|
5
|
+
import { balance as walletBalance, canAfford as walletCanAfford, charge as walletCharge, chargeAll as walletChargeAll, createEmptyWallet, grant as walletGrant, } from "../economy/wallet.js";
|
|
6
|
+
import { createGameEvents } from "../game/events.js";
|
|
7
|
+
import { createGameFeed } from "../game/feed.js";
|
|
8
|
+
import { createLeaderboard } from "../game/leaderboard.js";
|
|
9
|
+
import { createLoadouts } from "../game/loadout.js";
|
|
10
|
+
import { createLootRegistry, grantDrops } from "../game/lootTable.js";
|
|
11
|
+
import { createQuestJournal } from "../game/quest.js";
|
|
12
|
+
import { createSocial } from "../game/social.js";
|
|
13
|
+
import { createTradeSystem } from "../game/trade.js";
|
|
14
|
+
import { createUnlocks } from "../game/unlocks.js";
|
|
15
|
+
import { createInventorySet, putItem, } from "../inventory/inventoryModel.js";
|
|
16
|
+
import { createItemUse, } from "../item/use.js";
|
|
17
|
+
import { createWeaponStats } from "../item/weapon.js";
|
|
18
|
+
import { createPoseState } from "../movement/poseState.js";
|
|
19
|
+
import { createEntityStatsApi, seedPoolStats, setPoolStat, } from "../scene/entityStats.js";
|
|
20
|
+
import { createObjectStore } from "../scene/objectStore.js";
|
|
21
|
+
import { createSpatialApi } from "../scene/spatial.js";
|
|
22
|
+
import { createTargeting } from "../scene/targeting.js";
|
|
23
|
+
import { createStats } from "../stats/statModifiers.js";
|
|
24
|
+
import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
|
|
25
|
+
export function createGameContext(options) {
|
|
26
|
+
const { definition, content, player } = options;
|
|
27
|
+
const now = options.now ?? Date.now;
|
|
28
|
+
const signal = createChangeSignal();
|
|
29
|
+
const entities = definition.scene;
|
|
30
|
+
const objects = createObjectStore();
|
|
31
|
+
entities.subscribe(signal.notify);
|
|
32
|
+
objects.subscribe(signal.notify);
|
|
33
|
+
const statsByInstance = new Map();
|
|
34
|
+
const entityStats = notifyAfter(createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)), ["set", "delta"], signal.notify);
|
|
35
|
+
function ensureInstanceStats(instanceId) {
|
|
36
|
+
let map = statsByInstance.get(instanceId);
|
|
37
|
+
if (map === undefined) {
|
|
38
|
+
map = {};
|
|
39
|
+
statsByInstance.set(instanceId, map);
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
function catalogEntry(instanceId) {
|
|
44
|
+
const entity = entities.get(instanceId);
|
|
45
|
+
return entity === null ? undefined : content.entityById?.(entity.name);
|
|
46
|
+
}
|
|
47
|
+
const spatial = createSpatialApi({
|
|
48
|
+
resolvePosition: (instanceId) => entities.get(instanceId)?.position,
|
|
49
|
+
candidates: () => entities.list().map((entity) => entity.id),
|
|
50
|
+
});
|
|
51
|
+
const targeting = notifyAfter(createTargeting({
|
|
52
|
+
candidates: () => entities.list().map((entity) => entity.id),
|
|
53
|
+
classify(_fromId, toId) {
|
|
54
|
+
const role = catalogEntry(toId)?.role;
|
|
55
|
+
return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
|
|
56
|
+
},
|
|
57
|
+
distance: (fromId, toId) => spatial.distance(fromId, toId),
|
|
58
|
+
}), ["setTarget", "cycleTarget"], signal.notify);
|
|
59
|
+
const combatSpatial = {
|
|
60
|
+
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
61
|
+
hasLineOfSight: (from, to) => typeof from === "string" ? spatial.hasLineOfSight(from, to) : entities.get(to) !== null,
|
|
62
|
+
positionOf: (instanceId) => entities.get(instanceId)?.position,
|
|
63
|
+
};
|
|
64
|
+
const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
|
|
65
|
+
const rawEvents = createGameEvents();
|
|
66
|
+
const events = {
|
|
67
|
+
on: rawEvents.on,
|
|
68
|
+
subscribe: rawEvents.subscribe,
|
|
69
|
+
emit(name, payload) {
|
|
70
|
+
rawEvents.emit(name, payload);
|
|
71
|
+
signal.notify();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const feed = createGameFeed();
|
|
75
|
+
const lootRegistry = createLootRegistry();
|
|
76
|
+
const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], signal.notify);
|
|
77
|
+
const rawSocial = createSocial({ events, now });
|
|
78
|
+
const social = {
|
|
79
|
+
friends: notifyAfter(rawSocial.friends, ["request", "accept", "remove", "block", "hydrate"], signal.notify),
|
|
80
|
+
party: notifyAfter(rawSocial.party, ["invite", "accept", "kick", "leave", "promote"], signal.notify),
|
|
81
|
+
presence: rawSocial.presence,
|
|
82
|
+
};
|
|
83
|
+
const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify);
|
|
84
|
+
const playerStats = createStats({});
|
|
85
|
+
const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
|
|
86
|
+
const commandRegistry = createCommandRegistry();
|
|
87
|
+
const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
|
|
88
|
+
const inventoryDeclarations = definition.inventories ?? {};
|
|
89
|
+
const inventoryIds = Object.keys(inventoryDeclarations);
|
|
90
|
+
const layouts = {};
|
|
91
|
+
for (const [inventoryId, declaration] of Object.entries(inventoryDeclarations)) {
|
|
92
|
+
layouts[inventoryId] = { slots: declaration.slots, accepts: declaration.accepts };
|
|
93
|
+
}
|
|
94
|
+
const traits = Object.values(inventoryDeclarations).find((declaration) => declaration.traits !== undefined)?.traits ?? { stackLimit: () => Number.POSITIVE_INFINITY };
|
|
95
|
+
const inventory = notifyAfter(createInventorySet(layouts, traits), ["put", "take", "move", "replaceState"], signal.notify);
|
|
96
|
+
const wallets = new Map();
|
|
97
|
+
const walletOf = (userId) => wallets.get(userId) ?? createEmptyWallet();
|
|
98
|
+
const economy = {
|
|
99
|
+
balance: (userId, currencyId) => walletBalance(walletOf(userId), currencyId),
|
|
100
|
+
grant(userId, currencyId, amount) {
|
|
101
|
+
wallets.set(userId, walletGrant(walletOf(userId), currencyId, amount));
|
|
102
|
+
signal.notify();
|
|
103
|
+
},
|
|
104
|
+
charge(userId, currencyId, amount) {
|
|
105
|
+
const result = walletCharge(walletOf(userId), currencyId, amount);
|
|
106
|
+
if (result.status === "rejected")
|
|
107
|
+
return { reason: result.reason };
|
|
108
|
+
wallets.set(userId, result.state);
|
|
109
|
+
signal.notify();
|
|
110
|
+
return null;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
function putIntoAnyInventory(itemId, count) {
|
|
114
|
+
for (const inventoryId of inventoryIds) {
|
|
115
|
+
if (inventory.put(inventoryId, itemId, count).status === "ok")
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const loot = {
|
|
120
|
+
register: lootRegistry.register,
|
|
121
|
+
has: lootRegistry.has,
|
|
122
|
+
roll: lootRegistry.roll,
|
|
123
|
+
grantToPlayer(userId, drops, source) {
|
|
124
|
+
grantDrops(drops, {
|
|
125
|
+
putItem: (itemId, count) => putIntoAnyInventory(itemId, count),
|
|
126
|
+
grantCurrency: (currencyId, amount) => economy.grant(userId, currencyId, amount),
|
|
127
|
+
});
|
|
128
|
+
const event = { userId, drops };
|
|
129
|
+
if (source !== undefined)
|
|
130
|
+
event.source = source;
|
|
131
|
+
events.emit("loot.granted", event);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const trade = createTradeSystem({
|
|
135
|
+
resolveTrade: (itemId) => content.itemById?.(itemId)?.trade,
|
|
136
|
+
wallet: {
|
|
137
|
+
canAfford: (costs) => (walletCanAfford(walletOf(player.userId), costs) ? null : "insufficient-funds"),
|
|
138
|
+
charge(costs) {
|
|
139
|
+
const result = walletChargeAll(walletOf(player.userId), costs);
|
|
140
|
+
if (result.status === "ok") {
|
|
141
|
+
wallets.set(player.userId, result.state);
|
|
142
|
+
signal.notify();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
grant(gains) {
|
|
146
|
+
for (const [currencyId, amount] of Object.entries(gains)) {
|
|
147
|
+
economy.grant(player.userId, currencyId, amount);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
inventory: {
|
|
152
|
+
put(inventoryId, itemId, count) {
|
|
153
|
+
if (layouts[inventoryId] === undefined)
|
|
154
|
+
return { reason: `unknown inventory "${inventoryId}"` };
|
|
155
|
+
const result = inventory.put(inventoryId, itemId, count);
|
|
156
|
+
return result.status === "ok" ? null : { reason: result.reason };
|
|
157
|
+
},
|
|
158
|
+
take(inventoryId, itemId, count) {
|
|
159
|
+
if (layouts[inventoryId] === undefined)
|
|
160
|
+
return { reason: `unknown inventory "${inventoryId}"` };
|
|
161
|
+
const result = inventory.take(inventoryId, itemId, count);
|
|
162
|
+
return result.status === "ok" ? null : { reason: result.reason };
|
|
163
|
+
},
|
|
164
|
+
count: (inventoryId, itemId) => inventory.count(inventoryId, itemId),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
function seedUserPool(userId, statId, pool) {
|
|
168
|
+
const map = ensureInstanceStats(userId);
|
|
169
|
+
const next = setPoolStat(map, statId, pool);
|
|
170
|
+
map[statId] = next[statId];
|
|
171
|
+
}
|
|
172
|
+
const rawQuest = createQuestJournal({
|
|
173
|
+
events,
|
|
174
|
+
rewards: {
|
|
175
|
+
grantXp(userId, amount) {
|
|
176
|
+
const existing = ensureInstanceStats(userId)["xp"];
|
|
177
|
+
const current = (existing?.current ?? 0) + amount;
|
|
178
|
+
seedUserPool(userId, "xp", { current, max: Math.max(existing?.max ?? 0, current) });
|
|
179
|
+
},
|
|
180
|
+
grantEconomy: (userId, currencyId, amount) => economy.grant(userId, currencyId, amount),
|
|
181
|
+
grantItem(userId, inventoryId, itemId, count) {
|
|
182
|
+
if (userId !== player.userId)
|
|
183
|
+
return { reason: `unknown user "${userId}"` };
|
|
184
|
+
if (layouts[inventoryId] === undefined)
|
|
185
|
+
return { reason: `unknown inventory "${inventoryId}"` };
|
|
186
|
+
const result = inventory.put(inventoryId, itemId, count);
|
|
187
|
+
return result.status === "ok" ? null : { reason: result.reason };
|
|
188
|
+
},
|
|
189
|
+
grantUnlock: (userId, unlockId) => unlocks.grant(userId, unlockId),
|
|
190
|
+
},
|
|
191
|
+
hasUnlock: (userId, id) => unlocks.has(userId, id),
|
|
192
|
+
});
|
|
193
|
+
const quest = notifyAfter(rawQuest, ["accept", "abandon", "progress", "turnIn", "grant", "revoke", "hydrate"], signal.notify);
|
|
194
|
+
const loadouts = notifyAfter(createLoadouts({
|
|
195
|
+
inventory: {
|
|
196
|
+
begin() {
|
|
197
|
+
const staged = new Map();
|
|
198
|
+
return {
|
|
199
|
+
put(inventoryId, itemId, count, slot) {
|
|
200
|
+
const layout = layouts[inventoryId];
|
|
201
|
+
if (layout === undefined)
|
|
202
|
+
return { reason: `unknown inventory "${inventoryId}"` };
|
|
203
|
+
const state = staged.get(inventoryId) ?? inventory.state(inventoryId);
|
|
204
|
+
const result = putItem(state, layout, traits, itemId, count, slot === undefined ? undefined : { slot });
|
|
205
|
+
if (result.status === "rejected")
|
|
206
|
+
return { reason: result.reason };
|
|
207
|
+
staged.set(inventoryId, result.state);
|
|
208
|
+
return null;
|
|
209
|
+
},
|
|
210
|
+
commit() {
|
|
211
|
+
for (const [inventoryId, state] of staged)
|
|
212
|
+
inventory.replaceState(inventoryId, state);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
stats: { seed: seedUserPool },
|
|
218
|
+
economy: { grant: economy.grant },
|
|
219
|
+
unlocks: { grant: unlocks.grant },
|
|
220
|
+
}), ["applyLoadout"], signal.notify);
|
|
221
|
+
function spawnEntity(name, spawnOptions) {
|
|
222
|
+
const entry = content.entityById?.(name);
|
|
223
|
+
const walkSpeed = spawnOptions?.movement?.walkSpeed ?? entry?.movement?.walkSpeed;
|
|
224
|
+
const options = walkSpeed === undefined
|
|
225
|
+
? spawnOptions
|
|
226
|
+
: { ...spawnOptions, movement: { ...spawnOptions?.movement, walkSpeed } };
|
|
227
|
+
const instanceId = entities.spawn(name, options);
|
|
228
|
+
death.revive(instanceId);
|
|
229
|
+
statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedPoolStats(entry.stats));
|
|
230
|
+
return instanceId;
|
|
231
|
+
}
|
|
232
|
+
function despawnEntity(instanceId) {
|
|
233
|
+
const existed = entities.despawn(instanceId);
|
|
234
|
+
statsByInstance.delete(instanceId);
|
|
235
|
+
targeting.clearAll(instanceId);
|
|
236
|
+
pose.clear(instanceId);
|
|
237
|
+
return existed;
|
|
238
|
+
}
|
|
239
|
+
const death = createDeathSystem({
|
|
240
|
+
resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
|
|
241
|
+
resolveIdentity(instanceId) {
|
|
242
|
+
const entity = entities.get(instanceId);
|
|
243
|
+
if (entity === null)
|
|
244
|
+
return null;
|
|
245
|
+
return {
|
|
246
|
+
catalogId: entity.name,
|
|
247
|
+
position: [entity.position[0], entity.position[1], entity.position[2]],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
loot: { roll: (tableId) => (lootRegistry.has(tableId) ? lootRegistry.roll(tableId) : []) },
|
|
251
|
+
events,
|
|
252
|
+
runCommand(name, args) {
|
|
253
|
+
commandRegistry.run(ctx, name, args);
|
|
254
|
+
},
|
|
255
|
+
despawn(instanceId) {
|
|
256
|
+
despawnEntity(instanceId);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
const effects = notifyAfter(createEffectSystem({
|
|
260
|
+
resolveReceive: (instanceId) => catalogEntry(instanceId)?.receive,
|
|
261
|
+
resolveStats: (instanceId) => statsByInstance.get(instanceId),
|
|
262
|
+
getStat: weapon.getStat,
|
|
263
|
+
spatial: combatSpatial,
|
|
264
|
+
onLethal(instanceId, lethalCtx) {
|
|
265
|
+
const catalogId = entities.get(instanceId)?.name;
|
|
266
|
+
const reason = deathReasonFromEffect({
|
|
267
|
+
...lethalCtx,
|
|
268
|
+
userIdOf: (id) => (id === player.userId ? player.userId : undefined),
|
|
269
|
+
});
|
|
270
|
+
const resolution = death.resolveDeath(instanceId, reason);
|
|
271
|
+
if (resolution.status === "resolved" &&
|
|
272
|
+
resolution.drops.length > 0 &&
|
|
273
|
+
reason.kind === "player_kill" &&
|
|
274
|
+
reason.killerUserId === player.userId) {
|
|
275
|
+
loot.grantToPlayer(player.userId, resolution.drops, catalogId);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
}), ["applyEffect"], signal.notify);
|
|
279
|
+
const projectiles = notifyAfter(createProjectileSystem({
|
|
280
|
+
effects,
|
|
281
|
+
spatial: combatSpatial,
|
|
282
|
+
getStat: weapon.getStat,
|
|
283
|
+
now,
|
|
284
|
+
}), ["fireProjectile", "settleProjectile"], signal.notify);
|
|
285
|
+
const ctx = {
|
|
286
|
+
scene: {
|
|
287
|
+
object: objects,
|
|
288
|
+
entity: {
|
|
289
|
+
spawn: spawnEntity,
|
|
290
|
+
despawn: despawnEntity,
|
|
291
|
+
setPose: entities.setPose,
|
|
292
|
+
get: entities.get,
|
|
293
|
+
list: entities.list,
|
|
294
|
+
stats: entityStats,
|
|
295
|
+
setTarget: targeting.setTarget,
|
|
296
|
+
getTarget: targeting.getTarget,
|
|
297
|
+
cycleTarget: targeting.cycleTarget,
|
|
298
|
+
canReceive: effects.canReceive,
|
|
299
|
+
preview: effects.preview,
|
|
300
|
+
effect: effects.applyEffect,
|
|
301
|
+
willHitProjectile: projectiles.willHitProjectile,
|
|
302
|
+
fireProjectile: projectiles.fireProjectile,
|
|
303
|
+
settleProjectile: projectiles.settleProjectile,
|
|
304
|
+
distance: spatial.distance,
|
|
305
|
+
inRadius: spatial.inRadius,
|
|
306
|
+
hasLineOfSight: spatial.hasLineOfSight,
|
|
307
|
+
queryArc: spatial.queryArc,
|
|
308
|
+
moveToward: spatial.moveToward,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
game: {
|
|
312
|
+
commands: {
|
|
313
|
+
define: commandRegistry.define,
|
|
314
|
+
has: commandRegistry.has,
|
|
315
|
+
names: commandRegistry.names,
|
|
316
|
+
run(name, input) {
|
|
317
|
+
const result = commandRegistry.run(ctx, name, input);
|
|
318
|
+
signal.notify();
|
|
319
|
+
return result;
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
events,
|
|
323
|
+
feed: {
|
|
324
|
+
bind: (action) => feed.bind(action, events),
|
|
325
|
+
push(action, entry) {
|
|
326
|
+
feed.push(action, entry);
|
|
327
|
+
signal.notify();
|
|
328
|
+
},
|
|
329
|
+
recent: feed.recent,
|
|
330
|
+
subscribe: feed.subscribe,
|
|
331
|
+
snapshot: feed.snapshot,
|
|
332
|
+
hydrate(data) {
|
|
333
|
+
feed.hydrate(data);
|
|
334
|
+
signal.notify();
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
loot,
|
|
338
|
+
trade,
|
|
339
|
+
quest,
|
|
340
|
+
social,
|
|
341
|
+
unlocks,
|
|
342
|
+
economy,
|
|
343
|
+
leaderboard,
|
|
344
|
+
},
|
|
345
|
+
player: {
|
|
346
|
+
userId: player.userId,
|
|
347
|
+
isNew: player.isNew,
|
|
348
|
+
inventory,
|
|
349
|
+
stats: playerStats,
|
|
350
|
+
loadout: loadouts,
|
|
351
|
+
applyLoadout: loadouts.applyLoadout,
|
|
352
|
+
movement: pose,
|
|
353
|
+
},
|
|
354
|
+
item: {
|
|
355
|
+
use: {
|
|
356
|
+
register: itemUse.register,
|
|
357
|
+
registered: itemUse.registered,
|
|
358
|
+
can: (input) => itemUse.can(ctx, input),
|
|
359
|
+
use(input) {
|
|
360
|
+
const result = itemUse.use(ctx, input);
|
|
361
|
+
signal.notify();
|
|
362
|
+
return result;
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
weapon,
|
|
366
|
+
},
|
|
367
|
+
subscribe: signal.subscribe,
|
|
368
|
+
version: signal.version,
|
|
369
|
+
};
|
|
370
|
+
return ctx;
|
|
371
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SaveConfig } from "./save.js";
|
|
2
|
+
import type { CommandDef } from "./commandRunner.js";
|
|
3
|
+
import { runCommand } from "./commandRunner.js";
|
|
4
|
+
import { type GameRuntimeSnapshot, type RuntimeChunkRow, type RuntimePlayerRow, type RuntimeProfileRow, type RuntimeServerRow } from "./snapshot.js";
|
|
5
|
+
export type ServerLoopHooks = {
|
|
6
|
+
onInit?: (ctx: RuntimeInitContext) => void;
|
|
7
|
+
onNewPlayer?: (ctx: RuntimeLoopContext) => void;
|
|
8
|
+
onTick?: (ctx: RuntimeLoopContext, dtSeconds: number) => void;
|
|
9
|
+
};
|
|
10
|
+
export type RuntimeInitContext = {
|
|
11
|
+
snapshot: GameRuntimeSnapshot;
|
|
12
|
+
setSnapshot: (snapshot: GameRuntimeSnapshot) => void;
|
|
13
|
+
};
|
|
14
|
+
export type RuntimeLoopContext = RuntimeInitContext & {
|
|
15
|
+
player: {
|
|
16
|
+
userId: string;
|
|
17
|
+
isNew: boolean;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type GameRuntimeDefinition = {
|
|
21
|
+
gameId: string;
|
|
22
|
+
save: SaveConfig;
|
|
23
|
+
commands: Record<string, CommandDef>;
|
|
24
|
+
loop?: ServerLoopHooks;
|
|
25
|
+
};
|
|
26
|
+
export type HydrateInput = {
|
|
27
|
+
gameId: string;
|
|
28
|
+
serverId: string;
|
|
29
|
+
serverRow: RuntimeServerRow;
|
|
30
|
+
playersByUserId: Record<string, RuntimePlayerRow>;
|
|
31
|
+
chunksByKey: Record<string, RuntimeChunkRow>;
|
|
32
|
+
};
|
|
33
|
+
export type GameRuntime = {
|
|
34
|
+
gameId: string;
|
|
35
|
+
save: SaveConfig;
|
|
36
|
+
hydrate: (input: HydrateInput) => GameRuntimeSnapshot;
|
|
37
|
+
runCommand: (snapshot: GameRuntimeSnapshot, actorUserId: string, commandName: string, input: unknown) => ReturnType<typeof runCommand>;
|
|
38
|
+
tick: (snapshot: GameRuntimeSnapshot, dtSeconds: number) => GameRuntimeSnapshot;
|
|
39
|
+
joinPlayer: (snapshot: GameRuntimeSnapshot, userId: string, isNew: boolean) => GameRuntimeSnapshot;
|
|
40
|
+
toProfileRow: (snapshot: GameRuntimeSnapshot, userId: string) => RuntimeProfileRow | null;
|
|
41
|
+
};
|
|
42
|
+
export declare function createGameRuntime(definition: GameRuntimeDefinition): GameRuntime;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { runCommand } from "./commandRunner.js";
|
|
2
|
+
import { createEmptyPlayerRow, createRuntimeSnapshot, } from "./snapshot.js";
|
|
3
|
+
export function createGameRuntime(definition) {
|
|
4
|
+
const loop = definition.loop;
|
|
5
|
+
let initialized = false;
|
|
6
|
+
return {
|
|
7
|
+
gameId: definition.gameId,
|
|
8
|
+
save: definition.save,
|
|
9
|
+
hydrate(input) {
|
|
10
|
+
let current = createRuntimeSnapshot({
|
|
11
|
+
gameId: input.gameId,
|
|
12
|
+
serverId: input.serverId,
|
|
13
|
+
server: input.serverRow,
|
|
14
|
+
players: input.playersByUserId,
|
|
15
|
+
chunks: input.chunksByKey,
|
|
16
|
+
});
|
|
17
|
+
if (loop?.onInit && !initialized) {
|
|
18
|
+
initialized = true;
|
|
19
|
+
loop.onInit({
|
|
20
|
+
get snapshot() {
|
|
21
|
+
return current;
|
|
22
|
+
},
|
|
23
|
+
setSnapshot(next) {
|
|
24
|
+
current = next;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return current;
|
|
29
|
+
},
|
|
30
|
+
runCommand(snapshot, actorUserId, commandName, input) {
|
|
31
|
+
return runCommand(snapshot, definition.commands, commandName, input, actorUserId);
|
|
32
|
+
},
|
|
33
|
+
tick(snapshot, dtSeconds) {
|
|
34
|
+
if (!loop?.onTick)
|
|
35
|
+
return snapshot;
|
|
36
|
+
let current = snapshot;
|
|
37
|
+
for (const userId of Object.keys(current.players)) {
|
|
38
|
+
const ctx = {
|
|
39
|
+
get snapshot() {
|
|
40
|
+
return current;
|
|
41
|
+
},
|
|
42
|
+
player: { userId, isNew: false },
|
|
43
|
+
setSnapshot(next) {
|
|
44
|
+
current = next;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
loop.onTick(ctx, dtSeconds);
|
|
48
|
+
}
|
|
49
|
+
return current;
|
|
50
|
+
},
|
|
51
|
+
joinPlayer(snapshot, userId, isNew) {
|
|
52
|
+
const players = { ...snapshot.players };
|
|
53
|
+
if (!players[userId]) {
|
|
54
|
+
players[userId] = createEmptyPlayerRow(userId);
|
|
55
|
+
}
|
|
56
|
+
let next = {
|
|
57
|
+
...snapshot,
|
|
58
|
+
players,
|
|
59
|
+
revision: snapshot.revision + 1,
|
|
60
|
+
dirty: {
|
|
61
|
+
...snapshot.dirty,
|
|
62
|
+
server: true,
|
|
63
|
+
players: snapshot.dirty.players.includes(userId)
|
|
64
|
+
? snapshot.dirty.players
|
|
65
|
+
: [...snapshot.dirty.players, userId],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
if (!loop?.onNewPlayer) {
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
const ctx = {
|
|
72
|
+
get snapshot() {
|
|
73
|
+
return next;
|
|
74
|
+
},
|
|
75
|
+
player: { userId, isNew },
|
|
76
|
+
setSnapshot(updated) {
|
|
77
|
+
next = updated;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
loop.onNewPlayer(ctx);
|
|
81
|
+
return next;
|
|
82
|
+
},
|
|
83
|
+
toProfileRow(snapshot, userId) {
|
|
84
|
+
const player = snapshot.players[userId];
|
|
85
|
+
if (!player)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
userId,
|
|
89
|
+
gameId: snapshot.gameId,
|
|
90
|
+
player,
|
|
91
|
+
updatedAt: Date.now(),
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { LeaderboardScope } from "../game/leaderboard.js";
|
|
2
|
+
import type { SaveConfig } from "./save.js";
|
|
3
|
+
import type { GameRuntimeSnapshot, RuntimeChunkRow, RuntimePlayerRow, RuntimeServerRow } from "./snapshot.js";
|
|
4
|
+
export type GameServerStatus = "open" | "running" | "closed";
|
|
5
|
+
export type GameServerRecord = {
|
|
6
|
+
serverId: string;
|
|
7
|
+
gameId: string;
|
|
8
|
+
status: GameServerStatus;
|
|
9
|
+
mode?: string;
|
|
10
|
+
modeConfig?: unknown;
|
|
11
|
+
memberUserIds: string[];
|
|
12
|
+
slotsPerServer: number;
|
|
13
|
+
save: SaveConfig;
|
|
14
|
+
serverState: RuntimeServerRow;
|
|
15
|
+
sessionPlayers: Record<string, RuntimePlayerRow>;
|
|
16
|
+
revision: number;
|
|
17
|
+
tickAnchorMs: number;
|
|
18
|
+
lastSavedAt?: number;
|
|
19
|
+
dirtyAt?: number;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
};
|
|
23
|
+
export type PlayerProfileRecord = {
|
|
24
|
+
userId: string;
|
|
25
|
+
gameId: string;
|
|
26
|
+
playerState: RuntimePlayerRow;
|
|
27
|
+
revision: number;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
};
|
|
30
|
+
export type WorldChunkRecord = {
|
|
31
|
+
serverId: string;
|
|
32
|
+
chunkKey: string;
|
|
33
|
+
snapshot: RuntimeChunkRow;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
};
|
|
36
|
+
export type LeaderboardIncrement = {
|
|
37
|
+
userId: string;
|
|
38
|
+
stat: string;
|
|
39
|
+
scope: LeaderboardScope;
|
|
40
|
+
serverId?: string;
|
|
41
|
+
by: number;
|
|
42
|
+
};
|
|
43
|
+
export type LeaderboardEntry = {
|
|
44
|
+
userId: string;
|
|
45
|
+
value: number;
|
|
46
|
+
};
|
|
47
|
+
export declare const LEADERBOARD_PENDING_KEY = "leaderboardPending";
|
|
48
|
+
export declare function drainPendingLeaderboardIncrements(session: Record<string, unknown>): {
|
|
49
|
+
increments: LeaderboardIncrement[];
|
|
50
|
+
session: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
export declare const FEED_RING_LIMIT = 20;
|
|
53
|
+
export declare function trimFeedEntries<T>(entries: T[], limit?: number): T[];
|
|
54
|
+
export declare function shouldAutoSave(save: SaveConfig, dirtyAt: number | undefined, lastSavedAt: number | undefined, now: number): boolean;
|
|
55
|
+
export type ServerListing = {
|
|
56
|
+
serverId: string;
|
|
57
|
+
status: GameServerStatus;
|
|
58
|
+
memberCount: number;
|
|
59
|
+
slotsPerServer: number;
|
|
60
|
+
mode?: string;
|
|
61
|
+
updatedAt: number;
|
|
62
|
+
};
|
|
63
|
+
export declare function toServerListing(record: GameServerRecord): ServerListing;
|
|
64
|
+
export type HostPersistence = {
|
|
65
|
+
savePlan?: (plan: ServerPersistPlan) => Promise<void>;
|
|
66
|
+
loadServer: (serverId: string) => Promise<GameServerRecord | null>;
|
|
67
|
+
saveServer: (record: GameServerRecord) => Promise<void>;
|
|
68
|
+
listServers: (gameId: string) => Promise<GameServerRecord[]>;
|
|
69
|
+
loadProfile: (args: {
|
|
70
|
+
userId: string;
|
|
71
|
+
gameId: string;
|
|
72
|
+
}) => Promise<PlayerProfileRecord | null>;
|
|
73
|
+
saveProfile: (record: PlayerProfileRecord) => Promise<void>;
|
|
74
|
+
loadChunks: (serverId: string) => Promise<WorldChunkRecord[]>;
|
|
75
|
+
saveChunks: (serverId: string, chunks: WorldChunkRecord[]) => Promise<void>;
|
|
76
|
+
loadFeed: (args: {
|
|
77
|
+
serverId: string;
|
|
78
|
+
action: string;
|
|
79
|
+
}) => Promise<unknown[]>;
|
|
80
|
+
appendFeed: (args: {
|
|
81
|
+
serverId: string;
|
|
82
|
+
action: string;
|
|
83
|
+
entry: unknown;
|
|
84
|
+
}) => Promise<unknown[]>;
|
|
85
|
+
applyLeaderboardIncrements: (gameId: string, entries: LeaderboardIncrement[]) => Promise<void>;
|
|
86
|
+
getLeaderboardTop: (args: {
|
|
87
|
+
gameId: string;
|
|
88
|
+
stat: string;
|
|
89
|
+
scope: LeaderboardScope;
|
|
90
|
+
serverId?: string;
|
|
91
|
+
limit?: number;
|
|
92
|
+
}) => Promise<LeaderboardEntry[]>;
|
|
93
|
+
getLeaderboardProfile: (args: {
|
|
94
|
+
gameId: string;
|
|
95
|
+
userId: string;
|
|
96
|
+
}) => Promise<Record<string, number>>;
|
|
97
|
+
};
|
|
98
|
+
export declare function buildHydratePlayers(server: GameServerRecord, profiles: Record<string, PlayerProfileRecord | null>): Record<string, RuntimePlayerRow>;
|
|
99
|
+
export type ServerPersistPlan = {
|
|
100
|
+
server: GameServerRecord;
|
|
101
|
+
profiles: PlayerProfileRecord[];
|
|
102
|
+
chunks: WorldChunkRecord[];
|
|
103
|
+
leaderboard: LeaderboardIncrement[];
|
|
104
|
+
};
|
|
105
|
+
export declare function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan;
|