@jgengine/core 0.14.0 → 0.15.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 +500 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/audio/musicState.d.ts +47 -0
- package/dist/audio/musicState.js +38 -0
- package/dist/audio/stepCadence.d.ts +47 -0
- package/dist/audio/stepCadence.js +29 -0
- package/dist/audio/synth.d.ts +12 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +145 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/gameContext.js +6 -1
- package/dist/runtime/gameContextTypes.d.ts +18 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +18 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +1 -44
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- package/dist/world/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +17 -2
- package/dist/world.js +17 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +397 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One selectable response on a conversation node — the text a player clicks and the
|
|
3
|
+
* node it advances to. `kind` is a free style tag the presenter interprets; the model
|
|
4
|
+
* never reads it (no genre baked in).
|
|
5
|
+
*/
|
|
6
|
+
export interface DialogueGraphChoice {
|
|
7
|
+
/** Player-facing choice text. */
|
|
8
|
+
text: string;
|
|
9
|
+
/** Id of the node this choice advances to. Omitted / `null` ends the conversation. */
|
|
10
|
+
to?: string | null;
|
|
11
|
+
/** Free-string style/category tag the game styles on (never interpreted by the model). */
|
|
12
|
+
kind?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One conversation node: who is speaking, the line they say, and the branches out of it.
|
|
16
|
+
* `speaker`/`speakerKind`/`portrait` are opaque display data — the model never interprets them.
|
|
17
|
+
*/
|
|
18
|
+
export interface DialogueGraphNode {
|
|
19
|
+
/** Stable node id — choices point at it via {@link DialogueGraphChoice.to}. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Free-string speaker name shown in the view. Omitted → an unattributed line. */
|
|
22
|
+
speaker?: string;
|
|
23
|
+
/** Free-string speaker style tag (the game maps it to a portrait frame / name color). */
|
|
24
|
+
speakerKind?: string;
|
|
25
|
+
/** Opaque portrait key or URL the view renders in its portrait slot. */
|
|
26
|
+
portrait?: string;
|
|
27
|
+
/** The line this node speaks. */
|
|
28
|
+
text: string;
|
|
29
|
+
/** Branching responses. Empty / omitted → a terminal node the player can close. */
|
|
30
|
+
choices?: readonly DialogueGraphChoice[];
|
|
31
|
+
}
|
|
32
|
+
/** A serializable branching conversation: a start node id and the nodes it can reach. */
|
|
33
|
+
export interface DialogueGraph {
|
|
34
|
+
/** Id of the node the conversation opens on. */
|
|
35
|
+
start: string;
|
|
36
|
+
/** Every node in the conversation. */
|
|
37
|
+
nodes: readonly DialogueGraphNode[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The render-ready snapshot of a conversation at one node — everything a view needs to
|
|
41
|
+
* draw speaker, line, and choice buttons, with no traversal logic in the component.
|
|
42
|
+
*/
|
|
43
|
+
export interface DialogueGraphView {
|
|
44
|
+
/** Current node id. */
|
|
45
|
+
nodeId: string;
|
|
46
|
+
/** Speaker name to display, or `undefined` for an unattributed line. */
|
|
47
|
+
speaker?: string;
|
|
48
|
+
/** Speaker style tag the game skins on. */
|
|
49
|
+
speakerKind?: string;
|
|
50
|
+
/** Portrait key/URL for the portrait slot, or `undefined` when none. */
|
|
51
|
+
portrait?: string;
|
|
52
|
+
/** The current line text. */
|
|
53
|
+
text: string;
|
|
54
|
+
/** Branching responses at this node (empty at a terminal node). */
|
|
55
|
+
choices: readonly DialogueGraphChoice[];
|
|
56
|
+
/** True when this node has no onward choices — the conversation can be closed. */
|
|
57
|
+
done: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Serializable run state — the current node id and the ids already visited. */
|
|
60
|
+
export interface DialogueGraphSnapshot {
|
|
61
|
+
/** Node the run is currently sitting on. */
|
|
62
|
+
nodeId: string;
|
|
63
|
+
/** Node ids visited so far, in order (a start node is the first). */
|
|
64
|
+
visited: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
/** Options for {@link createDialogueRun}. */
|
|
67
|
+
export interface DialogueRunOptions {
|
|
68
|
+
/** Node id to open on. Default: the graph's `start`. */
|
|
69
|
+
startAt?: string;
|
|
70
|
+
/** Visited node ids to seed (e.g. restored from a save). */
|
|
71
|
+
visited?: Iterable<string>;
|
|
72
|
+
}
|
|
73
|
+
/** An observable walk through a {@link DialogueGraph}: current view, choose to advance, serialize. */
|
|
74
|
+
export interface DialogueRun {
|
|
75
|
+
/** The graph being walked. */
|
|
76
|
+
readonly graph: DialogueGraph;
|
|
77
|
+
/** Render-ready view at the current node, or `null` if the current id is unknown. */
|
|
78
|
+
current(): DialogueGraphView | null;
|
|
79
|
+
/** The current node's id. */
|
|
80
|
+
currentId(): string;
|
|
81
|
+
/** Advance via the choice at `index` on the current node; returns the new view (or `null`). */
|
|
82
|
+
choose(index: number): DialogueGraphView | null;
|
|
83
|
+
/** Jump straight to a node id (bypassing choices); returns its view (or `null`). */
|
|
84
|
+
goTo(nodeId: string): DialogueGraphView | null;
|
|
85
|
+
/** Re-open on the start node, clearing visited history. */
|
|
86
|
+
reset(): DialogueGraphView | null;
|
|
87
|
+
/** Whether the current node is terminal (no onward choices). */
|
|
88
|
+
isDone(): boolean;
|
|
89
|
+
/** Whether a node id has been visited this run. */
|
|
90
|
+
hasVisited(nodeId: string): boolean;
|
|
91
|
+
/** Observe advance / jump / reset. Returns an unsubscribe fn. */
|
|
92
|
+
subscribe(listener: () => void): () => void;
|
|
93
|
+
/** Serializable run state for a save. */
|
|
94
|
+
snapshot(): DialogueGraphSnapshot;
|
|
95
|
+
/** Restore from a {@link DialogueGraphSnapshot}. */
|
|
96
|
+
restore(snapshot: DialogueGraphSnapshot): void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Project a {@link DialogueGraph} node id to its render-ready {@link DialogueGraphView} —
|
|
100
|
+
* the pure "current view" selector a stateless renderer reads (speaker, line, choices,
|
|
101
|
+
* done). Returns `null` when the id is not in the graph. No traversal or mutation.
|
|
102
|
+
*
|
|
103
|
+
* @capability dialogue-graph-view select a conversation node's render-ready view (speaker, line, branching choices) from a serializable dialogue graph
|
|
104
|
+
*/
|
|
105
|
+
export declare function selectDialogueView(graph: DialogueGraph, nodeId: string): DialogueGraphView | null;
|
|
106
|
+
/**
|
|
107
|
+
* Walk a branching {@link DialogueGraph}: hold the current node, expose its render-ready
|
|
108
|
+
* view, and advance by choosing one of the current node's responses (each choice names the
|
|
109
|
+
* node it leads to; a choice with no `to` ends the conversation). Purely a serializable
|
|
110
|
+
* model — a React host renders `current()` and calls `choose(index)` — so no game
|
|
111
|
+
* re-implements node lookup, choice-to-node traversal, or "am I at the end" bookkeeping.
|
|
112
|
+
* `snapshot`/`restore` round-trip the run through a save.
|
|
113
|
+
*
|
|
114
|
+
* @capability dialogue-run walk a serializable branching dialogue graph — current node view, choose-to-advance traversal, visited history, snapshot/restore
|
|
115
|
+
*/
|
|
116
|
+
export declare function createDialogueRun(graph: DialogueGraph, options?: DialogueRunOptions): DialogueRun;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project a {@link DialogueGraph} node id to its render-ready {@link DialogueGraphView} —
|
|
3
|
+
* the pure "current view" selector a stateless renderer reads (speaker, line, choices,
|
|
4
|
+
* done). Returns `null` when the id is not in the graph. No traversal or mutation.
|
|
5
|
+
*
|
|
6
|
+
* @capability dialogue-graph-view select a conversation node's render-ready view (speaker, line, branching choices) from a serializable dialogue graph
|
|
7
|
+
*/
|
|
8
|
+
export function selectDialogueView(graph, nodeId) {
|
|
9
|
+
const node = graph.nodes.find((candidate) => candidate.id === nodeId);
|
|
10
|
+
if (node === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
const choices = node.choices ?? [];
|
|
13
|
+
return {
|
|
14
|
+
nodeId: node.id,
|
|
15
|
+
speaker: node.speaker,
|
|
16
|
+
speakerKind: node.speakerKind,
|
|
17
|
+
portrait: node.portrait,
|
|
18
|
+
text: node.text,
|
|
19
|
+
choices,
|
|
20
|
+
done: choices.length === 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Walk a branching {@link DialogueGraph}: hold the current node, expose its render-ready
|
|
25
|
+
* view, and advance by choosing one of the current node's responses (each choice names the
|
|
26
|
+
* node it leads to; a choice with no `to` ends the conversation). Purely a serializable
|
|
27
|
+
* model — a React host renders `current()` and calls `choose(index)` — so no game
|
|
28
|
+
* re-implements node lookup, choice-to-node traversal, or "am I at the end" bookkeeping.
|
|
29
|
+
* `snapshot`/`restore` round-trip the run through a save.
|
|
30
|
+
*
|
|
31
|
+
* @capability dialogue-run walk a serializable branching dialogue graph — current node view, choose-to-advance traversal, visited history, snapshot/restore
|
|
32
|
+
*/
|
|
33
|
+
export function createDialogueRun(graph, options = {}) {
|
|
34
|
+
const start = options.startAt ?? graph.start;
|
|
35
|
+
const listeners = new Set();
|
|
36
|
+
let currentId = start;
|
|
37
|
+
const visited = [];
|
|
38
|
+
function markVisited(id) {
|
|
39
|
+
if (visited[visited.length - 1] !== id)
|
|
40
|
+
visited.push(id);
|
|
41
|
+
}
|
|
42
|
+
for (const id of options.visited ?? [])
|
|
43
|
+
markVisited(id);
|
|
44
|
+
markVisited(currentId);
|
|
45
|
+
function notify() {
|
|
46
|
+
for (const listener of listeners)
|
|
47
|
+
listener();
|
|
48
|
+
}
|
|
49
|
+
function moveTo(nodeId) {
|
|
50
|
+
currentId = nodeId;
|
|
51
|
+
markVisited(nodeId);
|
|
52
|
+
notify();
|
|
53
|
+
return selectDialogueView(graph, currentId);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
graph,
|
|
57
|
+
current() {
|
|
58
|
+
return selectDialogueView(graph, currentId);
|
|
59
|
+
},
|
|
60
|
+
currentId() {
|
|
61
|
+
return currentId;
|
|
62
|
+
},
|
|
63
|
+
choose(index) {
|
|
64
|
+
const view = selectDialogueView(graph, currentId);
|
|
65
|
+
const choice = view?.choices[index];
|
|
66
|
+
if (choice === undefined)
|
|
67
|
+
return view;
|
|
68
|
+
const to = choice.to;
|
|
69
|
+
if (to === undefined || to === null)
|
|
70
|
+
return view; // terminal choice — caller closes
|
|
71
|
+
return moveTo(to);
|
|
72
|
+
},
|
|
73
|
+
goTo(nodeId) {
|
|
74
|
+
return moveTo(nodeId);
|
|
75
|
+
},
|
|
76
|
+
reset() {
|
|
77
|
+
visited.length = 0;
|
|
78
|
+
return moveTo(start);
|
|
79
|
+
},
|
|
80
|
+
isDone() {
|
|
81
|
+
return (selectDialogueView(graph, currentId)?.done ?? true) === true;
|
|
82
|
+
},
|
|
83
|
+
hasVisited(nodeId) {
|
|
84
|
+
return visited.includes(nodeId);
|
|
85
|
+
},
|
|
86
|
+
subscribe(listener) {
|
|
87
|
+
listeners.add(listener);
|
|
88
|
+
return () => listeners.delete(listener);
|
|
89
|
+
},
|
|
90
|
+
snapshot() {
|
|
91
|
+
return { nodeId: currentId, visited: [...visited] };
|
|
92
|
+
},
|
|
93
|
+
restore(snapshot) {
|
|
94
|
+
currentId = snapshot.nodeId;
|
|
95
|
+
visited.length = 0;
|
|
96
|
+
for (const id of snapshot.visited)
|
|
97
|
+
markVisited(id);
|
|
98
|
+
if (!visited.includes(currentId))
|
|
99
|
+
markVisited(currentId);
|
|
100
|
+
notify();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type TimedFeedEntry } from "./feed.js";
|
|
2
|
+
/**
|
|
3
|
+
* One event pushed onto the ticker. `kind` and `icon` are free strings the game owns
|
|
4
|
+
* and the model never interprets — they only ride through to the renderer so a game can
|
|
5
|
+
* color, icon, and group each entry however it likes ("kill", "assist", "info", …).
|
|
6
|
+
*/
|
|
7
|
+
export interface EventTickerInput {
|
|
8
|
+
/** Free-string category the game styles; the model never branches on it. */
|
|
9
|
+
kind: string;
|
|
10
|
+
/** The line to show (e.g. `"Ranger eliminated Marauder"`). */
|
|
11
|
+
text: string;
|
|
12
|
+
/** Optional free-string icon name the renderer resolves. */
|
|
13
|
+
icon?: string;
|
|
14
|
+
}
|
|
15
|
+
/** A stored ticker entry — an {@link EventTickerInput} stamped with an id and clock time. */
|
|
16
|
+
export interface EventTickerEntry extends TimedFeedEntry {
|
|
17
|
+
/** Stable id assigned at push time. */
|
|
18
|
+
id: number;
|
|
19
|
+
/** Clock time (ms) the entry was pushed, for age-based fade/prune. */
|
|
20
|
+
at: number;
|
|
21
|
+
/** The game-owned free-string category. */
|
|
22
|
+
kind: string;
|
|
23
|
+
/** The line to show. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** Optional free-string icon name. */
|
|
26
|
+
icon: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A live ticker entry as handed to the renderer: the stored entry plus a `fade` value
|
|
30
|
+
* `0..1` (age / `ttlMs`) — `0` for a fresh entry, approaching `1` as it nears expiry —
|
|
31
|
+
* so the UI can drop opacity as an entry ages out. `fade` is always `0` when no `ttlMs`
|
|
32
|
+
* is configured.
|
|
33
|
+
*/
|
|
34
|
+
export interface EventTickerView extends EventTickerEntry {
|
|
35
|
+
/** How faded this entry should be, `0` (fresh) → `1` (about to expire). */
|
|
36
|
+
fade: number;
|
|
37
|
+
}
|
|
38
|
+
/** Serializable state of the ticker, for save/restore. */
|
|
39
|
+
export interface EventTickerSnapshot {
|
|
40
|
+
/** Wall-clock (ms) the snapshot was taken, so restore re-anchors entry ages. */
|
|
41
|
+
now: number;
|
|
42
|
+
/** Next id to hand out. */
|
|
43
|
+
nextId: number;
|
|
44
|
+
/** The kept entries, oldest first. */
|
|
45
|
+
entries: readonly EventTickerEntry[];
|
|
46
|
+
}
|
|
47
|
+
/** Options for {@link createEventTicker}. */
|
|
48
|
+
export interface EventTickerOptions {
|
|
49
|
+
/** Injected clock (ms). Default `Date.now`. */
|
|
50
|
+
now?: () => number;
|
|
51
|
+
/** Max entries kept — older ones drop off the tail. Omit for no count cap. */
|
|
52
|
+
limit?: number;
|
|
53
|
+
/**
|
|
54
|
+
* How long (ms) an entry lives before it should be fully faded and pruned. Omit to keep
|
|
55
|
+
* entries indefinitely with `fade` pinned at `0`.
|
|
56
|
+
*/
|
|
57
|
+
ttlMs?: number;
|
|
58
|
+
}
|
|
59
|
+
/** A live, observable event/kill-feed ticker. */
|
|
60
|
+
export interface EventTicker {
|
|
61
|
+
/**
|
|
62
|
+
* Append an event. Stamps `at` from the injected clock, assigns an id, applies the count
|
|
63
|
+
* cap, and notifies subscribers. Returns the new entry's id.
|
|
64
|
+
*/
|
|
65
|
+
push(input: EventTickerInput): number;
|
|
66
|
+
/**
|
|
67
|
+
* Prune entries older than `ttlMs` (relative to `nowOverride ?? now()`), then return the
|
|
68
|
+
* live entries newest-first, each carrying its `fade` `0..1`. Safe to call every frame:
|
|
69
|
+
* it drops expired entries but does not notify (the caller re-renders on its own tick).
|
|
70
|
+
*/
|
|
71
|
+
recent(nowOverride?: number): EventTickerView[];
|
|
72
|
+
/** The kept entries oldest-first, without `fade` — a plain snapshot of the buffer. */
|
|
73
|
+
entries(): EventTickerEntry[];
|
|
74
|
+
/** Drop every entry. Notifies subscribers. */
|
|
75
|
+
clear(): void;
|
|
76
|
+
/** Observe changes (push, clear, restore). Returns an unsubscribe fn. */
|
|
77
|
+
subscribe(listener: () => void): () => void;
|
|
78
|
+
/** Serializable state for a save. */
|
|
79
|
+
snapshot(): EventTickerSnapshot;
|
|
80
|
+
/** Restore from an {@link EventTickerSnapshot}, re-anchoring entry ages to the current clock. */
|
|
81
|
+
restore(snapshot: EventTickerSnapshot): void;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A thin, serializable, observable event/kill-feed ticker over the flat {@link appendFeed} /
|
|
85
|
+
* {@link pruneFeed} feed helpers: a single rolling, count-capped, time-fading list of
|
|
86
|
+
* free-string entries. A game calls `push({ kind, text, icon? })` and the ticker stamps and
|
|
87
|
+
* caps it; `recent()` prunes expired entries and returns the live list newest-first, each with
|
|
88
|
+
* a `fade` `0..1` (age / `ttlMs`) for the renderer to drop opacity as entries age out. Nothing
|
|
89
|
+
* here is genre-specific: `kind`, `text`, and `icon` are free strings the game owns and styles,
|
|
90
|
+
* and the model never interprets them. `snapshot`/`restore` round-trips the buffer through a
|
|
91
|
+
* save, re-anchoring ages so fades resume correctly.
|
|
92
|
+
*
|
|
93
|
+
* @capability event-ticker rolling capped, timed-fade event/kill-feed ticker of free-string entries over the feed helpers — push/recent-with-fade, snapshot/restore
|
|
94
|
+
*/
|
|
95
|
+
export declare function createEventTicker(config?: EventTickerOptions): EventTicker;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { appendFeed, pruneFeed } from "./feed.js";
|
|
2
|
+
function clamp01(v) {
|
|
3
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A thin, serializable, observable event/kill-feed ticker over the flat {@link appendFeed} /
|
|
7
|
+
* {@link pruneFeed} feed helpers: a single rolling, count-capped, time-fading list of
|
|
8
|
+
* free-string entries. A game calls `push({ kind, text, icon? })` and the ticker stamps and
|
|
9
|
+
* caps it; `recent()` prunes expired entries and returns the live list newest-first, each with
|
|
10
|
+
* a `fade` `0..1` (age / `ttlMs`) for the renderer to drop opacity as entries age out. Nothing
|
|
11
|
+
* here is genre-specific: `kind`, `text`, and `icon` are free strings the game owns and styles,
|
|
12
|
+
* and the model never interprets them. `snapshot`/`restore` round-trips the buffer through a
|
|
13
|
+
* save, re-anchoring ages so fades resume correctly.
|
|
14
|
+
*
|
|
15
|
+
* @capability event-ticker rolling capped, timed-fade event/kill-feed ticker of free-string entries over the feed helpers — push/recent-with-fade, snapshot/restore
|
|
16
|
+
*/
|
|
17
|
+
export function createEventTicker(config = {}) {
|
|
18
|
+
const now = config.now ?? Date.now;
|
|
19
|
+
const limit = config.limit;
|
|
20
|
+
const ttlMs = config.ttlMs;
|
|
21
|
+
let entries = [];
|
|
22
|
+
let nextId = 1;
|
|
23
|
+
const listeners = new Set();
|
|
24
|
+
function notify() {
|
|
25
|
+
for (const listener of listeners)
|
|
26
|
+
listener();
|
|
27
|
+
}
|
|
28
|
+
/** Drop expired entries in place; returns `true` when the list changed. */
|
|
29
|
+
function prune(t) {
|
|
30
|
+
if (ttlMs === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
const kept = pruneFeed(entries, t, ttlMs);
|
|
33
|
+
if (kept === entries)
|
|
34
|
+
return false;
|
|
35
|
+
entries = kept;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
push(input) {
|
|
40
|
+
const id = nextId++;
|
|
41
|
+
const entry = {
|
|
42
|
+
id,
|
|
43
|
+
at: now(),
|
|
44
|
+
kind: input.kind,
|
|
45
|
+
text: input.text,
|
|
46
|
+
icon: input.icon,
|
|
47
|
+
};
|
|
48
|
+
entries = appendFeed(entries, entry, limit === undefined ? undefined : { limit });
|
|
49
|
+
notify();
|
|
50
|
+
return id;
|
|
51
|
+
},
|
|
52
|
+
recent(nowOverride) {
|
|
53
|
+
const t = nowOverride ?? now();
|
|
54
|
+
prune(t);
|
|
55
|
+
const out = [];
|
|
56
|
+
// Newest first: walk the tail-to-head of the oldest-first buffer.
|
|
57
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
58
|
+
const entry = entries[i];
|
|
59
|
+
const fade = ttlMs === undefined || ttlMs <= 0 ? 0 : clamp01((t - entry.at) / ttlMs);
|
|
60
|
+
out.push({ ...entry, fade });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
},
|
|
64
|
+
entries() {
|
|
65
|
+
return entries.slice();
|
|
66
|
+
},
|
|
67
|
+
clear() {
|
|
68
|
+
if (entries.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
entries = [];
|
|
71
|
+
notify();
|
|
72
|
+
},
|
|
73
|
+
subscribe(listener) {
|
|
74
|
+
listeners.add(listener);
|
|
75
|
+
return () => listeners.delete(listener);
|
|
76
|
+
},
|
|
77
|
+
snapshot() {
|
|
78
|
+
return {
|
|
79
|
+
now: now(),
|
|
80
|
+
nextId,
|
|
81
|
+
entries: entries.map((entry) => ({ ...entry })),
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
restore(snapshot) {
|
|
85
|
+
nextId = snapshot.nextId;
|
|
86
|
+
const drift = now() - snapshot.now;
|
|
87
|
+
entries = snapshot.entries.map((entry) => ({ ...entry, at: entry.at + drift }));
|
|
88
|
+
notify();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
package/dist/game/events.d.ts
CHANGED
|
@@ -17,15 +17,29 @@ export interface EntityAnimationEvent {
|
|
|
17
17
|
instanceId: string;
|
|
18
18
|
event: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* One granted reward: an inventory `item` id or a `currency` id, plus how many. Named because every
|
|
22
|
+
* consumer of `loot.granted` needs it — a toast renderer, a loot log, a session summary — and each
|
|
23
|
+
* one that re-declares the shape inline is a cast the compiler cannot check.
|
|
24
|
+
*/
|
|
25
|
+
export interface LootDrop {
|
|
26
|
+
item?: string;
|
|
27
|
+
currency?: string;
|
|
28
|
+
count: number;
|
|
29
|
+
}
|
|
20
30
|
export interface LootGrantedEvent {
|
|
21
31
|
userId: string;
|
|
22
|
-
drops:
|
|
23
|
-
item?: string;
|
|
24
|
-
currency?: string;
|
|
25
|
-
count: number;
|
|
26
|
-
}[];
|
|
32
|
+
drops: LootDrop[];
|
|
27
33
|
source?: string;
|
|
28
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the drops out of a `loot.granted` payload that arrived as `unknown` — a `FeedEntry.data`, a
|
|
37
|
+
* replicated event, a persisted log row. Returns `[]` for anything that is not shaped like a loot
|
|
38
|
+
* grant, so a renderer never has to guard the payload itself: `entry.data as { drops?: ... }` is the
|
|
39
|
+
* cast four separate call sites were writing, and the one that omitted the `| undefined` would have
|
|
40
|
+
* thrown on a payload-less entry.
|
|
41
|
+
*/
|
|
42
|
+
export declare function lootDropsOf(data: unknown): readonly LootDrop[];
|
|
29
43
|
export interface InventoryAddedEvent {
|
|
30
44
|
userId: string;
|
|
31
45
|
item: string;
|
|
@@ -145,6 +159,11 @@ export interface ProjectileSettledEvent {
|
|
|
145
159
|
at: [number, number, number];
|
|
146
160
|
effect: string;
|
|
147
161
|
hit: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* True for arced/exploding shots (grenades, launchers), false for direct-fire (bullets, bolts).
|
|
164
|
+
* Straight muzzle→impact tracers only make sense for direct-fire shots, so renderers skip the arced ones.
|
|
165
|
+
*/
|
|
166
|
+
ballistic: boolean;
|
|
148
167
|
}
|
|
149
168
|
export interface WorldItemDroppedEvent {
|
|
150
169
|
instanceId: string;
|
package/dist/game/events.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the drops out of a `loot.granted` payload that arrived as `unknown` — a `FeedEntry.data`, a
|
|
3
|
+
* replicated event, a persisted log row. Returns `[]` for anything that is not shaped like a loot
|
|
4
|
+
* grant, so a renderer never has to guard the payload itself: `entry.data as { drops?: ... }` is the
|
|
5
|
+
* cast four separate call sites were writing, and the one that omitted the `| undefined` would have
|
|
6
|
+
* thrown on a payload-less entry.
|
|
7
|
+
*/
|
|
8
|
+
export function lootDropsOf(data) {
|
|
9
|
+
if (typeof data !== "object" || data === null)
|
|
10
|
+
return [];
|
|
11
|
+
const drops = data.drops;
|
|
12
|
+
if (!Array.isArray(drops))
|
|
13
|
+
return [];
|
|
14
|
+
return drops.filter((drop) => typeof drop === "object" &&
|
|
15
|
+
drop !== null &&
|
|
16
|
+
typeof drop.count === "number" &&
|
|
17
|
+
Number.isFinite(drop.count));
|
|
18
|
+
}
|
|
1
19
|
/**
|
|
2
20
|
* A typed publish/subscribe bus for gameplay events that systems and HUDs subscribe to.
|
|
3
21
|
*
|
package/dist/game/gamePhase.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare const GAME_PHASE_STORE_KEY = "jg.phase";
|
|
|
16
16
|
* transition and the dock follows.
|
|
17
17
|
*/
|
|
18
18
|
export declare function setGamePhase(ctx: GameContext, phase: GamePhase): void;
|
|
19
|
-
/** Current phase; defaults to `playing` when unset
|
|
19
|
+
/** Current phase; defaults to `playing` when unset — a game that runs live from boot declares that intent explicitly with `defineGame({ lifecycle: "always-live" })` rather than relying on the silent default. */
|
|
20
20
|
export declare function gamePhase(ctx: GameContext): GamePhase;
|
|
21
21
|
export declare function isPlaying(ctx: GameContext): boolean;
|
|
22
22
|
/**
|
package/dist/game/gamePhase.js
CHANGED
|
@@ -11,7 +11,7 @@ export function setGamePhase(ctx, phase) {
|
|
|
11
11
|
ctx.game.store.set(GAME_PHASE_STORE_KEY, phase);
|
|
12
12
|
setPlayControlsActive(ctx, phase === "playing");
|
|
13
13
|
}
|
|
14
|
-
/** Current phase; defaults to `playing` when unset
|
|
14
|
+
/** Current phase; defaults to `playing` when unset — a game that runs live from boot declares that intent explicitly with `defineGame({ lifecycle: "always-live" })` rather than relying on the silent default. */
|
|
15
15
|
export function gamePhase(ctx) {
|
|
16
16
|
const value = ctx.game.store.get(GAME_PHASE_STORE_KEY);
|
|
17
17
|
return value === "menu" || value === "paused" || value === "ended" ? value : "playing";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single input score to rank. Accepts the raw shape produced by
|
|
3
|
+
* {@link LeaderboardRow} (a `LeaderboardRow` is assignable to this) as well as a
|
|
4
|
+
* minimal `{ userId, value }` pair with an optional display `label` the game owns.
|
|
5
|
+
*/
|
|
6
|
+
export interface RankableRow {
|
|
7
|
+
/** Stable identity of the scoring player/profile. */
|
|
8
|
+
userId: string;
|
|
9
|
+
/** The numeric score to rank on. */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Optional display name the UI shows instead of `userId`. Free string the game owns. */
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* How equal scores share ranks:
|
|
16
|
+
* - `"standard"` (competition ranking): ties share a rank and the next distinct
|
|
17
|
+
* score skips ahead — `1, 2, 2, 4`.
|
|
18
|
+
* - `"dense"`: ties share a rank and the next distinct score is the very next
|
|
19
|
+
* integer — `1, 2, 2, 3`.
|
|
20
|
+
*/
|
|
21
|
+
export type TieMode = "standard" | "dense";
|
|
22
|
+
/** Options for {@link rankLeaderboard}. All optional. */
|
|
23
|
+
export interface RankLeaderboardOptions {
|
|
24
|
+
/**
|
|
25
|
+
* `userId` of the local player. The matching entry gets `isLocal: true` so the
|
|
26
|
+
* renderer can highlight "you" in the table. No effect on ordering.
|
|
27
|
+
*/
|
|
28
|
+
highlightUserId?: string;
|
|
29
|
+
/** Tie handling for equal scores. Default `"standard"`. */
|
|
30
|
+
tieMode?: TieMode;
|
|
31
|
+
/** Keep only the first N ranked entries (applied after ranking). Default: keep all. */
|
|
32
|
+
limit?: number;
|
|
33
|
+
/** Sort direction on `value`. `"desc"` (default) ranks the highest score #1; `"asc"` ranks the lowest #1. */
|
|
34
|
+
order?: "desc" | "asc";
|
|
35
|
+
}
|
|
36
|
+
/** One render-ready row of the ranked table produced by {@link rankLeaderboard}. */
|
|
37
|
+
export interface RankedEntry {
|
|
38
|
+
/** 1-based rank after tie handling. */
|
|
39
|
+
rank: number;
|
|
40
|
+
/** Carried through from the input row. */
|
|
41
|
+
userId: string;
|
|
42
|
+
/** Carried through from the input row. */
|
|
43
|
+
value: number;
|
|
44
|
+
/** The display label if the input row carried one, else `undefined`. */
|
|
45
|
+
label: string | undefined;
|
|
46
|
+
/** `true` when this row shares its `value` with at least one other ranked row. */
|
|
47
|
+
isTie: boolean;
|
|
48
|
+
/** `true` when `userId === options.highlightUserId` — the local "you" row. */
|
|
49
|
+
isLocal: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Turn raw leaderboard rows into a render-ready ranked table. Pure and
|
|
53
|
+
* allocation-bounded: it sorts a copy by `value` (descending by default), assigns
|
|
54
|
+
* 1-based ranks with correct tie handling (`"standard"` → `1, 2, 2, 4`; `"dense"`
|
|
55
|
+
* → `1, 2, 2, 3`), flags rows that share a score (`isTie`), marks the local player
|
|
56
|
+
* (`isLocal`, via `highlightUserId`), and finally applies `limit`. Sorting is
|
|
57
|
+
* stable, so rows with equal scores keep their input order. Nothing here styles or
|
|
58
|
+
* branches on game meaning — pair it with {@link medalFor} and a game-owned theme
|
|
59
|
+
* to render a reskinnable scoreboard. Accepts a {@link LeaderboardRow}`[]` straight
|
|
60
|
+
* from `leaderboard.snapshot()`/`getTop()` since those satisfy {@link RankableRow}.
|
|
61
|
+
*
|
|
62
|
+
* @capability scoreboard pure tie-aware ranking selector over the leaderboard model — standard/dense ranks, local-entry highlight, top-N, medal tokens for a reskinnable table
|
|
63
|
+
*/
|
|
64
|
+
export declare function rankLeaderboard(rows: readonly RankableRow[], options?: RankLeaderboardOptions): RankedEntry[];
|
|
65
|
+
/**
|
|
66
|
+
* A free-string medal token for the top-three ranks — `"gold"` (1), `"silver"`
|
|
67
|
+
* (2), `"bronze"` (3) — or `null` for any other rank. It is a semantic token, not
|
|
68
|
+
* a color: the UI maps it to an icon/color/theme, and the model never styles. Feed
|
|
69
|
+
* it a {@link RankedEntry.rank}.
|
|
70
|
+
*/
|
|
71
|
+
export declare function medalFor(rank: number): "gold" | "silver" | "bronze" | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn raw leaderboard rows into a render-ready ranked table. Pure and
|
|
3
|
+
* allocation-bounded: it sorts a copy by `value` (descending by default), assigns
|
|
4
|
+
* 1-based ranks with correct tie handling (`"standard"` → `1, 2, 2, 4`; `"dense"`
|
|
5
|
+
* → `1, 2, 2, 3`), flags rows that share a score (`isTie`), marks the local player
|
|
6
|
+
* (`isLocal`, via `highlightUserId`), and finally applies `limit`. Sorting is
|
|
7
|
+
* stable, so rows with equal scores keep their input order. Nothing here styles or
|
|
8
|
+
* branches on game meaning — pair it with {@link medalFor} and a game-owned theme
|
|
9
|
+
* to render a reskinnable scoreboard. Accepts a {@link LeaderboardRow}`[]` straight
|
|
10
|
+
* from `leaderboard.snapshot()`/`getTop()` since those satisfy {@link RankableRow}.
|
|
11
|
+
*
|
|
12
|
+
* @capability scoreboard pure tie-aware ranking selector over the leaderboard model — standard/dense ranks, local-entry highlight, top-N, medal tokens for a reskinnable table
|
|
13
|
+
*/
|
|
14
|
+
export function rankLeaderboard(rows, options = {}) {
|
|
15
|
+
const { highlightUserId, tieMode = "standard", limit, order = "desc" } = options;
|
|
16
|
+
const dir = order === "asc" ? 1 : -1;
|
|
17
|
+
// Stable sort on value in the requested direction; equal values keep input order.
|
|
18
|
+
const sorted = rows
|
|
19
|
+
.map((row, index) => ({ row, index }))
|
|
20
|
+
.sort((a, b) => {
|
|
21
|
+
const byValue = (a.row.value - b.row.value) * dir;
|
|
22
|
+
return byValue !== 0 ? byValue : a.index - b.index;
|
|
23
|
+
})
|
|
24
|
+
.map((entry) => entry.row);
|
|
25
|
+
const ranked = [];
|
|
26
|
+
let lastValue;
|
|
27
|
+
let lastRank = 0;
|
|
28
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
29
|
+
const row = sorted[i];
|
|
30
|
+
let rank;
|
|
31
|
+
if (lastValue !== undefined && row.value === lastValue) {
|
|
32
|
+
// Tie: share the previous rank regardless of mode.
|
|
33
|
+
rank = lastRank;
|
|
34
|
+
}
|
|
35
|
+
else if (tieMode === "dense") {
|
|
36
|
+
rank = lastRank + 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Standard/competition: skip ahead by the count of rows already placed.
|
|
40
|
+
rank = i + 1;
|
|
41
|
+
}
|
|
42
|
+
// isTie: shares value with a neighbor in the sorted list.
|
|
43
|
+
const prev = sorted[i - 1];
|
|
44
|
+
const next = sorted[i + 1];
|
|
45
|
+
const isTie = (prev !== undefined && prev.value === row.value) || (next !== undefined && next.value === row.value);
|
|
46
|
+
ranked.push({
|
|
47
|
+
rank,
|
|
48
|
+
userId: row.userId,
|
|
49
|
+
value: row.value,
|
|
50
|
+
label: row.label,
|
|
51
|
+
isTie,
|
|
52
|
+
isLocal: highlightUserId !== undefined && row.userId === highlightUserId,
|
|
53
|
+
});
|
|
54
|
+
lastValue = row.value;
|
|
55
|
+
lastRank = rank;
|
|
56
|
+
}
|
|
57
|
+
return limit !== undefined && limit >= 0 ? ranked.slice(0, limit) : ranked;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A free-string medal token for the top-three ranks — `"gold"` (1), `"silver"`
|
|
61
|
+
* (2), `"bronze"` (3) — or `null` for any other rank. It is a semantic token, not
|
|
62
|
+
* a color: the UI maps it to an icon/color/theme, and the model never styles. Feed
|
|
63
|
+
* it a {@link RankedEntry.rank}.
|
|
64
|
+
*/
|
|
65
|
+
export function medalFor(rank) {
|
|
66
|
+
switch (rank) {
|
|
67
|
+
case 1:
|
|
68
|
+
return "gold";
|
|
69
|
+
case 2:
|
|
70
|
+
return "silver";
|
|
71
|
+
case 3:
|
|
72
|
+
return "bronze";
|
|
73
|
+
default:
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/game/lootTable.d.ts
CHANGED
|
@@ -39,12 +39,21 @@ export interface LootRegistry {
|
|
|
39
39
|
has(id: string): boolean;
|
|
40
40
|
roll(id: string, rng?: () => number): Drop[];
|
|
41
41
|
}
|
|
42
|
+
/** Options for {@link createLootRegistry} — inject a default RNG so bare `roll(id)` uses the world stream. */
|
|
43
|
+
export interface LootRegistryOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Default `[0,1)` stream when `roll` is called without an explicit rng.
|
|
46
|
+
* Prefer the world stream from `createGameContext` (`ctx.rng`); bare registries
|
|
47
|
+
* still fall back to `Math.random` (warning once) for portable call sites outside a game context.
|
|
48
|
+
*/
|
|
49
|
+
rng?: () => number;
|
|
50
|
+
}
|
|
42
51
|
/**
|
|
43
52
|
* Register named loot tables and roll weighted randomized drops from them.
|
|
44
53
|
*
|
|
45
54
|
* @capability loot-table register loot tables and roll weighted randomized drops
|
|
46
55
|
*/
|
|
47
|
-
export declare function createLootRegistry(): LootRegistry;
|
|
56
|
+
export declare function createLootRegistry(options?: LootRegistryOptions): LootRegistry;
|
|
48
57
|
/**
|
|
49
58
|
* Validates a loot table definition and returns it unchanged, for use with {@link createLootRegistry}.
|
|
50
59
|
*
|