@jgengine/core 0.6.0 → 0.8.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 +214 -2
- package/README.md +2 -0
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +61 -0
- package/dist/ai/spawnDirector.js +162 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +64 -0
- package/dist/cards/cardPile.js +175 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +47 -0
- package/dist/combat/abilityKit.js +139 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +12 -1
- package/dist/combat/effects.js +31 -19
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +31 -0
- package/dist/combat/telegraph.js +51 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +87 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +516 -8
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +100 -0
- package/dist/game/race.js +236 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +69 -0
- package/dist/game/social.js +138 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +95 -0
- package/dist/game/worldItem.js +138 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +69 -0
- package/dist/input/pointer.js +65 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +115 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +71 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +56 -0
- package/dist/nav/navGrid.js +265 -0
- package/dist/nav/pathFollow.d.ts +31 -0
- package/dist/nav/pathFollow.js +69 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +124 -2
- package/dist/physics/physicsWorld.js +493 -41
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +143 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +99 -0
- package/dist/physics/vehicleBody.js +233 -0
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +107 -3
- package/dist/runtime/gameContext.js +253 -7
- package/dist/runtime/hostPersistence.d.ts +39 -0
- package/dist/runtime/hostPersistence.js +54 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +9 -1
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +79 -0
- package/dist/session/roundState.js +158 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +68 -0
- package/dist/tactics/tacticalGrid.js +261 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +60 -0
- package/dist/turn/turnLoop.js +223 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +69 -0
- package/dist/world/placementController.js +153 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createUnlocks, grantUnlock, hasUnlock, unlockTree } from "../game/unlocks.js";
|
|
2
|
+
export function techPrerequisitesMet(state, node) {
|
|
3
|
+
const requires = node.requires ?? [];
|
|
4
|
+
return requires.every((id) => hasUnlock(state, id));
|
|
5
|
+
}
|
|
6
|
+
export function missingPrerequisites(state, node) {
|
|
7
|
+
return (node.requires ?? []).filter((id) => !hasUnlock(state, id));
|
|
8
|
+
}
|
|
9
|
+
export function canUnlockTech(defs, state, id) {
|
|
10
|
+
const node = defs.find((n) => n.id === id);
|
|
11
|
+
if (node === undefined)
|
|
12
|
+
return { ok: false, reason: "unknown-node" };
|
|
13
|
+
if (hasUnlock(state, id))
|
|
14
|
+
return { ok: false, reason: "already-unlocked" };
|
|
15
|
+
const missing = missingPrerequisites(state, node);
|
|
16
|
+
if (missing.length > 0)
|
|
17
|
+
return { ok: false, reason: "missing-prerequisites", missing };
|
|
18
|
+
return { ok: true };
|
|
19
|
+
}
|
|
20
|
+
export function grantTech(state, node) {
|
|
21
|
+
let next = grantUnlock(state, node.id);
|
|
22
|
+
for (const extra of node.grants ?? [])
|
|
23
|
+
next = grantUnlock(next, extra);
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
export function availableTech(defs, state) {
|
|
27
|
+
return defs.filter((node) => !hasUnlock(state, node.id) && techPrerequisitesMet(state, node));
|
|
28
|
+
}
|
|
29
|
+
export function unlockedRecipes(defs, state) {
|
|
30
|
+
const recipes = [];
|
|
31
|
+
for (const node of defs) {
|
|
32
|
+
if (node.recipe !== undefined && hasUnlock(state, node.id))
|
|
33
|
+
recipes.push(node.recipe);
|
|
34
|
+
}
|
|
35
|
+
return recipes;
|
|
36
|
+
}
|
|
37
|
+
export function createTechTree(defs = []) {
|
|
38
|
+
const nodes = new Map();
|
|
39
|
+
for (const def of defs)
|
|
40
|
+
nodes.set(def.id, def);
|
|
41
|
+
const unlocks = createUnlocks(defs.map((d) => ({ id: d.id, category: d.category })));
|
|
42
|
+
function stateOf(userId) {
|
|
43
|
+
return unlocks.list(userId);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
has: (userId, id) => unlocks.has(userId, id),
|
|
47
|
+
canUnlock: (userId, id) => canUnlockTech(defs, stateOf(userId), id),
|
|
48
|
+
unlock(userId, id) {
|
|
49
|
+
const check = canUnlockTech(defs, stateOf(userId), id);
|
|
50
|
+
if (!check.ok)
|
|
51
|
+
return check;
|
|
52
|
+
const node = nodes.get(id);
|
|
53
|
+
unlocks.grant(userId, node.id);
|
|
54
|
+
for (const extra of node.grants ?? [])
|
|
55
|
+
unlocks.grant(userId, extra);
|
|
56
|
+
return { ok: true };
|
|
57
|
+
},
|
|
58
|
+
available: (userId) => availableTech(defs, stateOf(userId)),
|
|
59
|
+
recipes: (userId) => unlockedRecipes(defs, stateOf(userId)),
|
|
60
|
+
list: (userId) => unlocks.list(userId),
|
|
61
|
+
tree: (categoryId) => unlockTree(defs, categoryId),
|
|
62
|
+
node: (id) => nodes.get(id) ?? null,
|
|
63
|
+
snapshot: (userId) => unlocks.snapshot(userId),
|
|
64
|
+
hydrate: (userId, ids) => unlocks.hydrate(userId, ids),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GameEvents } from "./events.js";
|
|
2
|
+
import type { EmotesDeps } from "./social.js";
|
|
3
|
+
export type ChatChannelKind = "global" | "party" | "proximity";
|
|
4
|
+
export interface ChatRateLimit {
|
|
5
|
+
count: number;
|
|
6
|
+
perMs: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ChatChannelDef {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: ChatChannelKind;
|
|
11
|
+
radius?: number;
|
|
12
|
+
historyLimit?: number;
|
|
13
|
+
rateLimit?: ChatRateLimit;
|
|
14
|
+
}
|
|
15
|
+
export interface ChatMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
channelId: string;
|
|
18
|
+
fromUserId: string;
|
|
19
|
+
body: string;
|
|
20
|
+
at: number;
|
|
21
|
+
}
|
|
22
|
+
export type ChatRecipients = readonly string[] | "all";
|
|
23
|
+
export type ChatSendResult = {
|
|
24
|
+
message: ChatMessage;
|
|
25
|
+
recipients: ChatRecipients;
|
|
26
|
+
} | {
|
|
27
|
+
reason: string;
|
|
28
|
+
};
|
|
29
|
+
export interface ChatSnapshot {
|
|
30
|
+
messages: Record<string, ChatMessage[]>;
|
|
31
|
+
counter: number;
|
|
32
|
+
}
|
|
33
|
+
export interface Chat {
|
|
34
|
+
register(def: ChatChannelDef): void;
|
|
35
|
+
channels(): ChatChannelDef[];
|
|
36
|
+
send(fromUserId: string, channelId: string, body: string): ChatSendResult;
|
|
37
|
+
whisper(fromUserId: string, toUserId: string, body: string): ChatSendResult;
|
|
38
|
+
history(channelId: string, options?: {
|
|
39
|
+
limit?: number;
|
|
40
|
+
viewerUserId?: string;
|
|
41
|
+
}): ChatMessage[];
|
|
42
|
+
snapshot(): ChatSnapshot;
|
|
43
|
+
hydrate(data: ChatSnapshot): void;
|
|
44
|
+
}
|
|
45
|
+
export interface ChatDeps {
|
|
46
|
+
events: GameEvents;
|
|
47
|
+
now?: () => number;
|
|
48
|
+
party?: {
|
|
49
|
+
membersOf(userId: string): string[];
|
|
50
|
+
};
|
|
51
|
+
proximity?: EmotesDeps;
|
|
52
|
+
blockedBy?: (userId: string) => readonly string[];
|
|
53
|
+
maxBodyLength?: number;
|
|
54
|
+
defaultRateLimit?: ChatRateLimit;
|
|
55
|
+
}
|
|
56
|
+
export declare const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit;
|
|
57
|
+
export declare const DEFAULT_CHAT_HISTORY_LIMIT = 100;
|
|
58
|
+
export declare const DEFAULT_CHAT_BODY_LENGTH = 500;
|
|
59
|
+
export declare const DEFAULT_PROXIMITY_CHAT_RADIUS = 20;
|
|
60
|
+
export declare const WHISPER_CHANNEL_PREFIX = "whisper:";
|
|
61
|
+
export declare function whisperChannelId(a: string, b: string): string;
|
|
62
|
+
export interface ChatRateLimiter {
|
|
63
|
+
allow(key: string, atMs: number): boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter;
|
|
66
|
+
export declare function createChat(deps: ChatDeps): Chat;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export const DEFAULT_CHAT_RATE_LIMIT = { count: 10, perMs: 10_000 };
|
|
2
|
+
export const DEFAULT_CHAT_HISTORY_LIMIT = 100;
|
|
3
|
+
export const DEFAULT_CHAT_BODY_LENGTH = 500;
|
|
4
|
+
export const DEFAULT_PROXIMITY_CHAT_RADIUS = 20;
|
|
5
|
+
export const WHISPER_CHANNEL_PREFIX = "whisper:";
|
|
6
|
+
export function whisperChannelId(a, b) {
|
|
7
|
+
const [first, second] = a < b ? [a, b] : [b, a];
|
|
8
|
+
return `${WHISPER_CHANNEL_PREFIX}${first}:${second}`;
|
|
9
|
+
}
|
|
10
|
+
export function createChatRateLimiter(limit) {
|
|
11
|
+
const windows = new Map();
|
|
12
|
+
return {
|
|
13
|
+
allow(key, atMs) {
|
|
14
|
+
const cutoff = atMs - limit.perMs;
|
|
15
|
+
const stamps = (windows.get(key) ?? []).filter((stamp) => stamp > cutoff);
|
|
16
|
+
if (stamps.length >= limit.count) {
|
|
17
|
+
windows.set(key, stamps);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
stamps.push(atMs);
|
|
21
|
+
windows.set(key, stamps);
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const BUILTIN_CHANNELS = [
|
|
27
|
+
{ id: "global", kind: "global" },
|
|
28
|
+
{ id: "party", kind: "party" },
|
|
29
|
+
{ id: "proximity", kind: "proximity", radius: DEFAULT_PROXIMITY_CHAT_RADIUS },
|
|
30
|
+
];
|
|
31
|
+
export function createChat(deps) {
|
|
32
|
+
const now = deps.now ?? Date.now;
|
|
33
|
+
const events = deps.events;
|
|
34
|
+
const maxBodyLength = deps.maxBodyLength ?? DEFAULT_CHAT_BODY_LENGTH;
|
|
35
|
+
const defaultRateLimit = deps.defaultRateLimit ?? DEFAULT_CHAT_RATE_LIMIT;
|
|
36
|
+
const blockedBy = deps.blockedBy ?? (() => []);
|
|
37
|
+
const channelDefs = new Map();
|
|
38
|
+
for (const def of BUILTIN_CHANNELS)
|
|
39
|
+
channelDefs.set(def.id, { ...def });
|
|
40
|
+
const messagesByChannel = new Map();
|
|
41
|
+
const limiters = new Map();
|
|
42
|
+
let counter = 0;
|
|
43
|
+
function limiterFor(channelId, rateLimit) {
|
|
44
|
+
let limiter = limiters.get(channelId);
|
|
45
|
+
if (limiter === undefined) {
|
|
46
|
+
limiter = createChatRateLimiter(rateLimit);
|
|
47
|
+
limiters.set(channelId, limiter);
|
|
48
|
+
}
|
|
49
|
+
return limiter;
|
|
50
|
+
}
|
|
51
|
+
function hasBlocked(userId, targetUserId) {
|
|
52
|
+
return blockedBy(userId).includes(targetUserId);
|
|
53
|
+
}
|
|
54
|
+
function resolveRecipients(fromUserId, channelId, def) {
|
|
55
|
+
if (channelId.startsWith(WHISPER_CHANNEL_PREFIX)) {
|
|
56
|
+
const pair = channelId.slice(WHISPER_CHANNEL_PREFIX.length).split(":");
|
|
57
|
+
const toUserId = pair.find((userId) => userId !== fromUserId);
|
|
58
|
+
if (pair.length !== 2 || toUserId === undefined || !pair.includes(fromUserId)) {
|
|
59
|
+
return { reason: "not a participant of this whisper" };
|
|
60
|
+
}
|
|
61
|
+
if (hasBlocked(toUserId, fromUserId) || hasBlocked(fromUserId, toUserId)) {
|
|
62
|
+
return { reason: "blocked" };
|
|
63
|
+
}
|
|
64
|
+
return { recipients: [toUserId] };
|
|
65
|
+
}
|
|
66
|
+
if (def === null)
|
|
67
|
+
return { reason: `unknown channel "${channelId}"` };
|
|
68
|
+
if (def.kind === "global")
|
|
69
|
+
return { recipients: "all" };
|
|
70
|
+
if (def.kind === "party") {
|
|
71
|
+
const members = deps.party?.membersOf(fromUserId) ?? [];
|
|
72
|
+
if (!members.includes(fromUserId))
|
|
73
|
+
return { reason: "not in a party" };
|
|
74
|
+
return {
|
|
75
|
+
recipients: members.filter((userId) => userId !== fromUserId && !hasBlocked(userId, fromUserId)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const proximity = deps.proximity;
|
|
79
|
+
if (proximity === undefined)
|
|
80
|
+
return { reason: "proximity chat not configured" };
|
|
81
|
+
const origin = proximity.entities.get(fromUserId);
|
|
82
|
+
if (origin === null)
|
|
83
|
+
return { reason: `entity "${fromUserId}" is not spawned` };
|
|
84
|
+
const nearby = proximity.spatial.inRadius(origin.position, def.radius ?? DEFAULT_PROXIMITY_CHAT_RADIUS, (id) => id !== fromUserId && proximity.entities.get(id)?.role === "player");
|
|
85
|
+
return { recipients: nearby.filter((userId) => !hasBlocked(userId, fromUserId)) };
|
|
86
|
+
}
|
|
87
|
+
function append(channelId, message, historyLimit) {
|
|
88
|
+
const ring = messagesByChannel.get(channelId) ?? [];
|
|
89
|
+
ring.push(message);
|
|
90
|
+
if (ring.length > historyLimit)
|
|
91
|
+
ring.splice(0, ring.length - historyLimit);
|
|
92
|
+
messagesByChannel.set(channelId, ring);
|
|
93
|
+
}
|
|
94
|
+
function sendResolved(fromUserId, channelId, body, def) {
|
|
95
|
+
const trimmed = body.trim();
|
|
96
|
+
if (trimmed.length === 0)
|
|
97
|
+
return { reason: "empty message" };
|
|
98
|
+
if (trimmed.length > maxBodyLength)
|
|
99
|
+
return { reason: "message too long" };
|
|
100
|
+
const resolved = resolveRecipients(fromUserId, channelId, def);
|
|
101
|
+
if ("reason" in resolved)
|
|
102
|
+
return resolved;
|
|
103
|
+
const at = now();
|
|
104
|
+
const rateLimit = def?.rateLimit ?? defaultRateLimit;
|
|
105
|
+
if (!limiterFor(channelId, rateLimit).allow(`${fromUserId}|${channelId}`, at)) {
|
|
106
|
+
return { reason: "rate limited" };
|
|
107
|
+
}
|
|
108
|
+
counter += 1;
|
|
109
|
+
const message = {
|
|
110
|
+
id: `msg_${counter}`,
|
|
111
|
+
channelId,
|
|
112
|
+
fromUserId,
|
|
113
|
+
body: trimmed,
|
|
114
|
+
at,
|
|
115
|
+
};
|
|
116
|
+
append(channelId, message, def?.historyLimit ?? DEFAULT_CHAT_HISTORY_LIMIT);
|
|
117
|
+
const event = { ...message };
|
|
118
|
+
if (resolved.recipients !== "all")
|
|
119
|
+
event.recipients = resolved.recipients;
|
|
120
|
+
events.emit("chat.message", event);
|
|
121
|
+
return { message, recipients: resolved.recipients };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
register(def) {
|
|
125
|
+
channelDefs.set(def.id, { ...def });
|
|
126
|
+
limiters.delete(def.id);
|
|
127
|
+
},
|
|
128
|
+
channels() {
|
|
129
|
+
return Array.from(channelDefs.values(), (def) => ({ ...def }));
|
|
130
|
+
},
|
|
131
|
+
send(fromUserId, channelId, body) {
|
|
132
|
+
const def = channelDefs.get(channelId) ?? null;
|
|
133
|
+
return sendResolved(fromUserId, channelId, body, def);
|
|
134
|
+
},
|
|
135
|
+
whisper(fromUserId, toUserId, body) {
|
|
136
|
+
if (fromUserId === toUserId)
|
|
137
|
+
return { reason: "cannot whisper yourself" };
|
|
138
|
+
const channelId = whisperChannelId(fromUserId, toUserId);
|
|
139
|
+
return sendResolved(fromUserId, channelId, body, channelDefs.get(channelId) ?? null);
|
|
140
|
+
},
|
|
141
|
+
history(channelId, options) {
|
|
142
|
+
const ring = messagesByChannel.get(channelId) ?? [];
|
|
143
|
+
const viewerUserId = options?.viewerUserId;
|
|
144
|
+
const visible = viewerUserId === undefined
|
|
145
|
+
? ring
|
|
146
|
+
: ring.filter((message) => !hasBlocked(viewerUserId, message.fromUserId));
|
|
147
|
+
const limit = options?.limit;
|
|
148
|
+
return limit === undefined ? visible.slice() : visible.slice(-limit);
|
|
149
|
+
},
|
|
150
|
+
snapshot() {
|
|
151
|
+
const messages = {};
|
|
152
|
+
for (const [channelId, ring] of messagesByChannel) {
|
|
153
|
+
messages[channelId] = ring.map((message) => ({ ...message }));
|
|
154
|
+
}
|
|
155
|
+
return { messages, counter };
|
|
156
|
+
},
|
|
157
|
+
hydrate(data) {
|
|
158
|
+
messagesByChannel.clear();
|
|
159
|
+
for (const [channelId, ring] of Object.entries(data.messages)) {
|
|
160
|
+
messagesByChannel.set(channelId, ring.map((message) => ({ ...message })));
|
|
161
|
+
}
|
|
162
|
+
counter = data.counter;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface CosmeticLoadoutDef {
|
|
2
|
+
slots: Record<string, string>;
|
|
3
|
+
}
|
|
4
|
+
export interface CosmeticsChangedEvent {
|
|
5
|
+
userId: string;
|
|
6
|
+
slots: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface CosmeticsEvents {
|
|
9
|
+
emit(name: "cosmetics.changed", payload: CosmeticsChangedEvent): void;
|
|
10
|
+
}
|
|
11
|
+
export interface CosmeticsDeps {
|
|
12
|
+
events?: CosmeticsEvents;
|
|
13
|
+
}
|
|
14
|
+
export interface Cosmetics {
|
|
15
|
+
register(defs: Record<string, CosmeticLoadoutDef>): void;
|
|
16
|
+
has(loadoutId: string): boolean;
|
|
17
|
+
apply(userId: string, loadoutId: string): {
|
|
18
|
+
reason: string;
|
|
19
|
+
} | null;
|
|
20
|
+
equip(userId: string, slot: string, cosmeticId: string | null): void;
|
|
21
|
+
get(userId: string): Readonly<Record<string, string>>;
|
|
22
|
+
snapshot(userId: string): Record<string, string>;
|
|
23
|
+
hydrate(userId: string, state: Record<string, string>): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createCosmetics(deps?: CosmeticsDeps): Cosmetics;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const EMPTY_SLOTS = Object.freeze({});
|
|
2
|
+
export function createCosmetics(deps = {}) {
|
|
3
|
+
const definitions = new Map();
|
|
4
|
+
const equipped = new Map();
|
|
5
|
+
function slotsOf(userId) {
|
|
6
|
+
let slots = equipped.get(userId);
|
|
7
|
+
if (slots === undefined) {
|
|
8
|
+
slots = {};
|
|
9
|
+
equipped.set(userId, slots);
|
|
10
|
+
}
|
|
11
|
+
return slots;
|
|
12
|
+
}
|
|
13
|
+
function notify(userId) {
|
|
14
|
+
deps.events?.emit("cosmetics.changed", { userId, slots: { ...slotsOf(userId) } });
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
register(defs) {
|
|
18
|
+
for (const [loadoutId, def] of Object.entries(defs))
|
|
19
|
+
definitions.set(loadoutId, def);
|
|
20
|
+
},
|
|
21
|
+
has(loadoutId) {
|
|
22
|
+
return definitions.has(loadoutId);
|
|
23
|
+
},
|
|
24
|
+
apply(userId, loadoutId) {
|
|
25
|
+
const def = definitions.get(loadoutId);
|
|
26
|
+
if (def === undefined)
|
|
27
|
+
return { reason: `unknown cosmetic loadout "${loadoutId}"` };
|
|
28
|
+
const slots = slotsOf(userId);
|
|
29
|
+
Object.assign(slots, def.slots);
|
|
30
|
+
notify(userId);
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
equip(userId, slot, cosmeticId) {
|
|
34
|
+
const slots = slotsOf(userId);
|
|
35
|
+
if (cosmeticId === null)
|
|
36
|
+
delete slots[slot];
|
|
37
|
+
else
|
|
38
|
+
slots[slot] = cosmeticId;
|
|
39
|
+
notify(userId);
|
|
40
|
+
},
|
|
41
|
+
get(userId) {
|
|
42
|
+
return equipped.get(userId) ?? EMPTY_SLOTS;
|
|
43
|
+
},
|
|
44
|
+
snapshot(userId) {
|
|
45
|
+
return { ...slotsOf(userId) };
|
|
46
|
+
},
|
|
47
|
+
hydrate(userId, state) {
|
|
48
|
+
equipped.set(userId, { ...state });
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ActionCodesMap } from "../input/actionBindings.js";
|
|
2
|
+
import type { GameFeedOptions } from "./feed.js";
|
|
2
3
|
import type { ItemTraits } from "../inventory/inventoryModel.js";
|
|
4
|
+
import type { StorageTier } from "../inventory/storageTier.js";
|
|
3
5
|
import type { SaveConfig } from "../runtime/save.js";
|
|
4
|
-
import type
|
|
6
|
+
import { type AssetCatalog, type ModelAssetRef } from "../scene/assetCatalog.js";
|
|
5
7
|
import { type EntityStore } from "../scene/entityStore.js";
|
|
6
8
|
import type { TimeConfig } from "../time/simClock.js";
|
|
7
9
|
import type { WorldFeature } from "../world/features.js";
|
|
8
10
|
export interface PhysicsConfig {
|
|
9
11
|
gravity?: number;
|
|
12
|
+
jumpVelocity?: number;
|
|
10
13
|
}
|
|
11
14
|
export interface InventoryDeclaration {
|
|
12
15
|
slots: number;
|
|
@@ -14,6 +17,7 @@ export interface InventoryDeclaration {
|
|
|
14
17
|
accepts?: string | readonly string[];
|
|
15
18
|
traits?: ItemTraits;
|
|
16
19
|
applyModifiers?: boolean;
|
|
20
|
+
tier?: StorageTier;
|
|
17
21
|
}
|
|
18
22
|
export type GameServerConfig = "persistent" | {
|
|
19
23
|
mode: string;
|
|
@@ -33,6 +37,8 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
|
|
|
33
37
|
physics?: PhysicsConfig;
|
|
34
38
|
/** Simulation clock: real→game time scale, selectable speeds, calendar. Exposed as `ctx.time`; the shell feeds its scaled dt to `loop.onTick`. */
|
|
35
39
|
time?: TimeConfig;
|
|
40
|
+
/** Per-action ring-buffer capacity for `ctx.game.feed`. Default 20. */
|
|
41
|
+
feed?: GameFeedOptions;
|
|
36
42
|
inventories?: Record<string, InventoryDeclaration>;
|
|
37
43
|
input?: ActionCodesMap;
|
|
38
44
|
server?: GameServerConfig;
|
|
@@ -40,5 +46,7 @@ export interface GameDefinition<TAssetRef extends ModelAssetRef = ModelAssetRef,
|
|
|
40
46
|
ui?: unknown;
|
|
41
47
|
loop?: GameLoop<any>;
|
|
42
48
|
}
|
|
43
|
-
export type GameDefinitionConfig<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> = Omit<GameDefinition<TAssetRef, TMultiplayer>, "scene"
|
|
49
|
+
export type GameDefinitionConfig<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> = Omit<GameDefinition<TAssetRef, TMultiplayer>, "scene" | "assets"> & {
|
|
50
|
+
assets?: AssetCatalog<TAssetRef>;
|
|
51
|
+
};
|
|
44
52
|
export declare function defineGame<TAssetRef extends ModelAssetRef, TMultiplayer>(config: GameDefinitionConfig<TAssetRef, TMultiplayer>): GameDefinition<TAssetRef, TMultiplayer>;
|
package/dist/game/defineGame.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { createAssetCatalog } from "../scene/assetCatalog.js";
|
|
1
2
|
import { createEntityStore } from "../scene/entityStore.js";
|
|
2
3
|
export function defineGame(config) {
|
|
3
4
|
if (config.name.trim().length === 0) {
|
|
4
5
|
throw new Error("defineGame: name must be non-empty");
|
|
5
6
|
}
|
|
6
|
-
return { ...config, scene: createEntityStore() };
|
|
7
|
+
return { ...config, scene: createEntityStore(), assets: config.assets ?? createAssetCatalog() };
|
|
7
8
|
}
|
package/dist/game/events.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TelegraphShape } from "../combat/telegraph.js";
|
|
2
|
+
import type { CameraShake } from "../combat/hitReaction.js";
|
|
1
3
|
export type DeathReason = {
|
|
2
4
|
kind: "player_kill";
|
|
3
5
|
killerUserId: string;
|
|
@@ -61,6 +63,28 @@ export interface SocialPartyLeftEvent {
|
|
|
61
63
|
userId: string;
|
|
62
64
|
partyId: string;
|
|
63
65
|
}
|
|
66
|
+
export interface SocialWorldInvitedEvent {
|
|
67
|
+
inviteId: string;
|
|
68
|
+
fromUserId: string;
|
|
69
|
+
toUserId: string;
|
|
70
|
+
serverId: string;
|
|
71
|
+
joinCode?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface SocialWorldAcceptedEvent {
|
|
74
|
+
inviteId: string;
|
|
75
|
+
userId: string;
|
|
76
|
+
fromUserId: string;
|
|
77
|
+
serverId: string;
|
|
78
|
+
joinCode?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ChatMessageEvent {
|
|
81
|
+
id: string;
|
|
82
|
+
channelId: string;
|
|
83
|
+
fromUserId: string;
|
|
84
|
+
body: string;
|
|
85
|
+
at: number;
|
|
86
|
+
recipients?: readonly string[];
|
|
87
|
+
}
|
|
64
88
|
export interface StatLevelUpEvent {
|
|
65
89
|
userId: string;
|
|
66
90
|
stat: string;
|
|
@@ -72,6 +96,24 @@ export interface EntityFloatTextEvent {
|
|
|
72
96
|
text: string;
|
|
73
97
|
kind: string;
|
|
74
98
|
amount?: number;
|
|
99
|
+
hitType?: string;
|
|
100
|
+
element?: string;
|
|
101
|
+
crit?: boolean;
|
|
102
|
+
scale?: number;
|
|
103
|
+
}
|
|
104
|
+
export interface CombatTelegraphEvent {
|
|
105
|
+
id: number;
|
|
106
|
+
shape: TelegraphShape;
|
|
107
|
+
position: [number, number, number];
|
|
108
|
+
dir?: number;
|
|
109
|
+
windupMs: number;
|
|
110
|
+
kind: string;
|
|
111
|
+
}
|
|
112
|
+
export interface CombatHitReactionEvent {
|
|
113
|
+
instanceId?: string;
|
|
114
|
+
position: [number, number, number];
|
|
115
|
+
hitstopMs: number;
|
|
116
|
+
shake?: CameraShake;
|
|
75
117
|
}
|
|
76
118
|
export interface ProjectileSettledEvent {
|
|
77
119
|
from: string;
|
|
@@ -80,9 +122,45 @@ export interface ProjectileSettledEvent {
|
|
|
80
122
|
effect: string;
|
|
81
123
|
hit: boolean;
|
|
82
124
|
}
|
|
125
|
+
export interface WorldItemDroppedEvent {
|
|
126
|
+
instanceId: string;
|
|
127
|
+
itemId: string;
|
|
128
|
+
rarity: string;
|
|
129
|
+
count: number;
|
|
130
|
+
position: [number, number, number];
|
|
131
|
+
source?: string;
|
|
132
|
+
}
|
|
133
|
+
export interface WorldItemPickedUpEvent {
|
|
134
|
+
instanceId: string;
|
|
135
|
+
userId: string;
|
|
136
|
+
itemId: string;
|
|
137
|
+
rarity: string;
|
|
138
|
+
count: number;
|
|
139
|
+
}
|
|
140
|
+
export interface CosmeticsChangedEvent {
|
|
141
|
+
userId: string;
|
|
142
|
+
slots: Record<string, string>;
|
|
143
|
+
}
|
|
144
|
+
export interface EmotePlayedEvent {
|
|
145
|
+
from: string;
|
|
146
|
+
emoteId: string;
|
|
147
|
+
at: readonly [number, number, number];
|
|
148
|
+
recipients: readonly string[];
|
|
149
|
+
}
|
|
150
|
+
export interface PossessionSwappedEvent {
|
|
151
|
+
userId: string;
|
|
152
|
+
entityId: string;
|
|
153
|
+
previousEntityId: string;
|
|
154
|
+
}
|
|
155
|
+
export interface FormChangedEvent {
|
|
156
|
+
instanceId: string;
|
|
157
|
+
formId: string | null;
|
|
158
|
+
}
|
|
83
159
|
export interface GameEventMap {
|
|
84
160
|
"entity.died": EntityDiedEvent;
|
|
85
161
|
"entity.floatText": EntityFloatTextEvent;
|
|
162
|
+
"combat.telegraph": CombatTelegraphEvent;
|
|
163
|
+
"combat.hitReaction": CombatHitReactionEvent;
|
|
86
164
|
"loot.granted": LootGrantedEvent;
|
|
87
165
|
"inventory.added": InventoryAddedEvent;
|
|
88
166
|
"quest.accepted": QuestAcceptedEvent;
|
|
@@ -91,8 +169,17 @@ export interface GameEventMap {
|
|
|
91
169
|
"social.friend.added": SocialFriendAddedEvent;
|
|
92
170
|
"social.party.joined": SocialPartyJoinedEvent;
|
|
93
171
|
"social.party.left": SocialPartyLeftEvent;
|
|
172
|
+
"social.world.invited": SocialWorldInvitedEvent;
|
|
173
|
+
"social.world.accepted": SocialWorldAcceptedEvent;
|
|
174
|
+
"chat.message": ChatMessageEvent;
|
|
94
175
|
"stat.levelUp": StatLevelUpEvent;
|
|
95
176
|
"projectile.settled": ProjectileSettledEvent;
|
|
177
|
+
"worldItem.dropped": WorldItemDroppedEvent;
|
|
178
|
+
"worldItem.picked_up": WorldItemPickedUpEvent;
|
|
179
|
+
"cosmetics.changed": CosmeticsChangedEvent;
|
|
180
|
+
"emote.played": EmotePlayedEvent;
|
|
181
|
+
"possession.swapped": PossessionSwappedEvent;
|
|
182
|
+
"form.changed": FormChangedEvent;
|
|
96
183
|
}
|
|
97
184
|
export type GameEventHandler<TPayload> = (payload: TPayload) => void;
|
|
98
185
|
export interface GameEvents<TMap extends GameEventMap = GameEventMap> {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface LevelDescriptor<TLevelConfig> {
|
|
2
|
+
id: string;
|
|
3
|
+
config: TLevelConfig;
|
|
4
|
+
}
|
|
5
|
+
export interface LevelSequenceConfig<TLevelConfig> {
|
|
6
|
+
levels: readonly LevelDescriptor<TLevelConfig>[];
|
|
7
|
+
/** Extra attempts granted after the first per level. Defaults to 0 (a single attempt, no retries). */
|
|
8
|
+
retriesPerLevel?: number;
|
|
9
|
+
}
|
|
10
|
+
export type LevelSequenceStatus = "idle" | "playing" | "cleared" | "failed" | "complete";
|
|
11
|
+
export interface CurrentLevel<TLevelConfig> {
|
|
12
|
+
id: string;
|
|
13
|
+
index: number;
|
|
14
|
+
config: TLevelConfig;
|
|
15
|
+
attempt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface LevelSequenceProgress {
|
|
18
|
+
index: number;
|
|
19
|
+
total: number;
|
|
20
|
+
cleared: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export interface LevelSequence<TLevelConfig> {
|
|
23
|
+
current(): CurrentLevel<TLevelConfig> | null;
|
|
24
|
+
status(): LevelSequenceStatus;
|
|
25
|
+
/** Enters level 0. A no-op on an empty level list, which leaves `status()` at "idle" forever. */
|
|
26
|
+
start(): void;
|
|
27
|
+
/** Marks the current level cleared; call `advance()` to move to the next one. */
|
|
28
|
+
clear(): void;
|
|
29
|
+
/** Consumes an attempt on the current level. Returns "retry" while attempts remain, else "failed". */
|
|
30
|
+
fail(): "retry" | "failed";
|
|
31
|
+
/** Restarts the current level after a "retry"-eligible failure. Returns false if there was nothing to retry. */
|
|
32
|
+
retry(): boolean;
|
|
33
|
+
/** Moves past a cleared level, or to "complete" if it was the last one. Returns false unless status is "cleared". */
|
|
34
|
+
advance(): boolean;
|
|
35
|
+
reset(): void;
|
|
36
|
+
progress(): LevelSequenceProgress;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A pure, deterministic level campaign: an ordered list of levels, each with its own opaque config, played
|
|
40
|
+
* through a `start` → (`clear` → `advance`)* → `complete` happy path, with `fail`/`retry` handling per-level
|
|
41
|
+
* attempts. Mirrors the reducer style of `game/race.ts` and `ai/spawnDirector.ts` — no I/O, no timers, just
|
|
42
|
+
* state transitions driven by the caller.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createLevelSequence<TLevelConfig>(config: LevelSequenceConfig<TLevelConfig>): LevelSequence<TLevelConfig>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pure, deterministic level campaign: an ordered list of levels, each with its own opaque config, played
|
|
3
|
+
* through a `start` → (`clear` → `advance`)* → `complete` happy path, with `fail`/`retry` handling per-level
|
|
4
|
+
* attempts. Mirrors the reducer style of `game/race.ts` and `ai/spawnDirector.ts` — no I/O, no timers, just
|
|
5
|
+
* state transitions driven by the caller.
|
|
6
|
+
*/
|
|
7
|
+
export function createLevelSequence(config) {
|
|
8
|
+
const levels = config.levels;
|
|
9
|
+
const maxAttempts = 1 + Math.max(0, Math.floor(config.retriesPerLevel ?? 0));
|
|
10
|
+
let index = 0;
|
|
11
|
+
let attempt = 1;
|
|
12
|
+
let status = "idle";
|
|
13
|
+
const clearedIds = [];
|
|
14
|
+
function current() {
|
|
15
|
+
if (status === "idle" || status === "complete")
|
|
16
|
+
return null;
|
|
17
|
+
const level = levels[index];
|
|
18
|
+
if (level === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
return { id: level.id, index, config: level.config, attempt };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
current,
|
|
24
|
+
status: () => status,
|
|
25
|
+
start() {
|
|
26
|
+
if (levels.length === 0)
|
|
27
|
+
return;
|
|
28
|
+
index = 0;
|
|
29
|
+
attempt = 1;
|
|
30
|
+
clearedIds.length = 0;
|
|
31
|
+
status = "playing";
|
|
32
|
+
},
|
|
33
|
+
clear() {
|
|
34
|
+
if (status !== "playing" || current() === null)
|
|
35
|
+
return;
|
|
36
|
+
clearedIds.push(levels[index].id);
|
|
37
|
+
status = "cleared";
|
|
38
|
+
},
|
|
39
|
+
fail() {
|
|
40
|
+
if (status !== "playing" || current() === null)
|
|
41
|
+
return "failed";
|
|
42
|
+
status = "failed";
|
|
43
|
+
return attempt < maxAttempts ? "retry" : "failed";
|
|
44
|
+
},
|
|
45
|
+
retry() {
|
|
46
|
+
if (status !== "failed" || attempt >= maxAttempts)
|
|
47
|
+
return false;
|
|
48
|
+
attempt += 1;
|
|
49
|
+
status = "playing";
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
advance() {
|
|
53
|
+
if (status !== "cleared")
|
|
54
|
+
return false;
|
|
55
|
+
if (index >= levels.length - 1) {
|
|
56
|
+
status = "complete";
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
index += 1;
|
|
60
|
+
attempt = 1;
|
|
61
|
+
status = "playing";
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
reset() {
|
|
65
|
+
index = 0;
|
|
66
|
+
attempt = 1;
|
|
67
|
+
clearedIds.length = 0;
|
|
68
|
+
status = "idle";
|
|
69
|
+
},
|
|
70
|
+
progress() {
|
|
71
|
+
return {
|
|
72
|
+
index: status === "complete" ? levels.length : index,
|
|
73
|
+
total: levels.length,
|
|
74
|
+
cleared: [...clearedIds],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|