@jgengine/core 0.6.0 → 0.7.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 +167 -2
- 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 +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -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 +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -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 +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- 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/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/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +62 -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 +281 -4
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -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 +133 -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 +35 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -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/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -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 +83 -0
- package/dist/physics/physicsWorld.js +261 -1
- 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 +138 -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 +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +64 -2
- package/dist/runtime/gameContext.js +154 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -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 +1 -1
- 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/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/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -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/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -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/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 +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -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/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -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 +48 -0
- package/dist/world/placementController.js +97 -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/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/package.json +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A race track is an ordered ring of checkpoint trigger volumes plus a lap count. The final checkpoint
|
|
3
|
+
* is the lap/finish line: a racer completes a lap by passing all checkpoints in order and hitting the
|
|
4
|
+
* last one.
|
|
5
|
+
*/
|
|
6
|
+
export function raceTrack(config) {
|
|
7
|
+
return { checkpoints: config.checkpoints, laps: Math.max(1, Math.floor(config.laps ?? 1)) };
|
|
8
|
+
}
|
|
9
|
+
function rankIds(standings) {
|
|
10
|
+
return standings.map((s) => s.racerId);
|
|
11
|
+
}
|
|
12
|
+
/** Race ends when `count` racers have crossed the finish; ranking is the current standings order. */
|
|
13
|
+
export function firstPastPost(count = 1) {
|
|
14
|
+
return (standings) => {
|
|
15
|
+
const finished = standings.filter((s) => s.finished).length;
|
|
16
|
+
return finished >= count ? rankIds(standings) : null;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Round-cut (Fall Guys): the first `k` finishers qualify; race resolves the moment `k` cross. */
|
|
20
|
+
export function topK(k) {
|
|
21
|
+
return firstPastPost(k);
|
|
22
|
+
}
|
|
23
|
+
/** Every non-eliminated racer must finish. */
|
|
24
|
+
export function everyoneFinishes() {
|
|
25
|
+
return (standings) => {
|
|
26
|
+
const active = standings.filter((s) => !s.eliminated);
|
|
27
|
+
return active.length > 0 && active.every((s) => s.finished) ? rankIds(standings) : null;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Destruction-derby last-man-standing: ends when at most one racer is left un-eliminated. */
|
|
31
|
+
export function lastStanding() {
|
|
32
|
+
return (standings) => {
|
|
33
|
+
const alive = standings.filter((s) => !s.eliminated);
|
|
34
|
+
return alive.length <= 1 && standings.length > 1 ? rankIds(standings) : null;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function insideAabb(point, cp) {
|
|
38
|
+
return (Math.abs(point[0] - cp.center[0]) <= cp.half[0] &&
|
|
39
|
+
Math.abs(point[1] - cp.center[1]) <= cp.half[1] &&
|
|
40
|
+
Math.abs(point[2] - cp.center[2]) <= cp.half[2]);
|
|
41
|
+
}
|
|
42
|
+
function distanceToNext(point, cp) {
|
|
43
|
+
const dx = point[0] - cp.center[0];
|
|
44
|
+
const dz = point[2] - cp.center[2];
|
|
45
|
+
return Math.sqrt(dx * dx + dz * dz);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Race state machine (issue #87). Drive it each tick with `update(now, positions)` — `now` is game time
|
|
49
|
+
* (`ctx.time`), `positions` maps each racer to a world point tested against the ordered checkpoint
|
|
50
|
+
* volumes. It emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps
|
|
51
|
+
* cumulative split times for PB deltas, resolves a pluggable win condition (first-past-post, round-cut,
|
|
52
|
+
* derby last-standing), and `resetToCheckpoint` hands back a respawn pose at the racer's last checkpoint.
|
|
53
|
+
*/
|
|
54
|
+
export class RaceState {
|
|
55
|
+
track;
|
|
56
|
+
win;
|
|
57
|
+
racers = new Map();
|
|
58
|
+
order = [];
|
|
59
|
+
raceFinished = false;
|
|
60
|
+
finalRanking = [];
|
|
61
|
+
constructor(config) {
|
|
62
|
+
this.track = config.track;
|
|
63
|
+
this.win = config.win ?? firstPastPost(1);
|
|
64
|
+
}
|
|
65
|
+
addRacer(racerId, startTime = 0) {
|
|
66
|
+
if (this.racers.has(racerId))
|
|
67
|
+
return;
|
|
68
|
+
this.racers.set(racerId, {
|
|
69
|
+
racerId,
|
|
70
|
+
lap: 1,
|
|
71
|
+
nextCheckpoint: 0,
|
|
72
|
+
lastCheckpoint: -1,
|
|
73
|
+
progress: 0,
|
|
74
|
+
position: this.order.length + 1,
|
|
75
|
+
finished: false,
|
|
76
|
+
finishTime: null,
|
|
77
|
+
eliminated: false,
|
|
78
|
+
startTime,
|
|
79
|
+
splits: [],
|
|
80
|
+
});
|
|
81
|
+
this.order.push(racerId);
|
|
82
|
+
}
|
|
83
|
+
eliminate(racerId) {
|
|
84
|
+
const r = this.racers.get(racerId);
|
|
85
|
+
if (r !== undefined)
|
|
86
|
+
r.eliminated = true;
|
|
87
|
+
}
|
|
88
|
+
get finished() {
|
|
89
|
+
return this.raceFinished;
|
|
90
|
+
}
|
|
91
|
+
get ranking() {
|
|
92
|
+
return this.finalRanking;
|
|
93
|
+
}
|
|
94
|
+
progressOf(racerId) {
|
|
95
|
+
const r = this.racers.get(racerId);
|
|
96
|
+
return r === null || r === undefined ? null : this.snapshot(r);
|
|
97
|
+
}
|
|
98
|
+
snapshot(r) {
|
|
99
|
+
return {
|
|
100
|
+
racerId: r.racerId,
|
|
101
|
+
lap: r.lap,
|
|
102
|
+
nextCheckpoint: r.nextCheckpoint,
|
|
103
|
+
lastCheckpoint: r.lastCheckpoint,
|
|
104
|
+
progress: r.progress,
|
|
105
|
+
position: r.position,
|
|
106
|
+
finished: r.finished,
|
|
107
|
+
finishTime: r.finishTime,
|
|
108
|
+
eliminated: r.eliminated,
|
|
109
|
+
splits: [...r.splits],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
standings() {
|
|
113
|
+
return this.order
|
|
114
|
+
.map((id) => this.racers.get(id))
|
|
115
|
+
.slice()
|
|
116
|
+
.sort((a, b) => this.compare(a, b, null))
|
|
117
|
+
.map((r) => this.snapshot(r));
|
|
118
|
+
}
|
|
119
|
+
resetToCheckpoint(racerId) {
|
|
120
|
+
const r = this.racers.get(racerId);
|
|
121
|
+
if (r === undefined)
|
|
122
|
+
return null;
|
|
123
|
+
const cps = this.track.checkpoints;
|
|
124
|
+
const idx = r.lastCheckpoint >= 0 ? r.lastCheckpoint : 0;
|
|
125
|
+
const cp = cps[idx];
|
|
126
|
+
if (cp === undefined)
|
|
127
|
+
return null;
|
|
128
|
+
const next = cps[(idx + 1) % cps.length];
|
|
129
|
+
const heading = Math.atan2(next.center[0] - cp.center[0], next.center[2] - cp.center[2]);
|
|
130
|
+
return { position: [cp.center[0], cp.center[1], cp.center[2]], heading };
|
|
131
|
+
}
|
|
132
|
+
compare(a, b, positions) {
|
|
133
|
+
if (a.finished !== b.finished)
|
|
134
|
+
return a.finished ? -1 : 1;
|
|
135
|
+
if (a.finished && b.finished)
|
|
136
|
+
return (a.finishTime ?? 0) - (b.finishTime ?? 0);
|
|
137
|
+
if (a.eliminated !== b.eliminated)
|
|
138
|
+
return a.eliminated ? 1 : -1;
|
|
139
|
+
if (a.progress !== b.progress)
|
|
140
|
+
return b.progress - a.progress;
|
|
141
|
+
if (positions !== null) {
|
|
142
|
+
const pa = positions.get(a.racerId);
|
|
143
|
+
const pb = positions.get(b.racerId);
|
|
144
|
+
const cps = this.track.checkpoints;
|
|
145
|
+
if (pa !== undefined && pb !== undefined && cps.length > 0) {
|
|
146
|
+
const da = distanceToNext(pa, cps[a.nextCheckpoint]);
|
|
147
|
+
const db = distanceToNext(pb, cps[b.nextCheckpoint]);
|
|
148
|
+
if (da !== db)
|
|
149
|
+
return da - db;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return this.order.indexOf(a.racerId) - this.order.indexOf(b.racerId);
|
|
153
|
+
}
|
|
154
|
+
update(now, positions) {
|
|
155
|
+
const posMap = positions instanceof Map ? positions : new Map(Object.entries(positions));
|
|
156
|
+
const events = [];
|
|
157
|
+
const cps = this.track.checkpoints;
|
|
158
|
+
const count = cps.length;
|
|
159
|
+
if (count > 0) {
|
|
160
|
+
for (const id of this.order) {
|
|
161
|
+
const r = this.racers.get(id);
|
|
162
|
+
if (r.finished || r.eliminated)
|
|
163
|
+
continue;
|
|
164
|
+
const pos = posMap.get(id);
|
|
165
|
+
if (pos === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
let guard = 0;
|
|
168
|
+
while (guard < count && insideAabb(pos, cps[r.nextCheckpoint])) {
|
|
169
|
+
guard += 1;
|
|
170
|
+
const hit = r.nextCheckpoint;
|
|
171
|
+
r.lastCheckpoint = hit;
|
|
172
|
+
r.progress += 1;
|
|
173
|
+
r.splits.push(now - r.startTime);
|
|
174
|
+
events.push({ type: "checkpoint.hit", racerId: id, checkpoint: hit, lap: r.lap, time: now });
|
|
175
|
+
if (hit === count - 1) {
|
|
176
|
+
events.push({ type: "lap.completed", racerId: id, lap: r.lap, time: now });
|
|
177
|
+
if (r.lap >= this.track.laps) {
|
|
178
|
+
r.finished = true;
|
|
179
|
+
r.finishTime = now;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
r.lap += 1;
|
|
183
|
+
r.nextCheckpoint = 0;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
r.nextCheckpoint = hit + 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const sorted = this.order
|
|
192
|
+
.map((id) => this.racers.get(id))
|
|
193
|
+
.slice()
|
|
194
|
+
.sort((a, b) => this.compare(a, b, posMap));
|
|
195
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
196
|
+
const r = sorted[i];
|
|
197
|
+
const newPos = i + 1;
|
|
198
|
+
if (r.position !== newPos) {
|
|
199
|
+
events.push({ type: "position.changed", racerId: r.racerId, position: newPos, previous: r.position });
|
|
200
|
+
r.position = newPos;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!this.raceFinished) {
|
|
204
|
+
const ranking = this.win(sorted.map((r) => this.snapshot(r)), this.track);
|
|
205
|
+
if (ranking !== null) {
|
|
206
|
+
this.raceFinished = true;
|
|
207
|
+
this.finalRanking = ranking;
|
|
208
|
+
events.push({ type: "race.finished", ranking, time: now });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return events;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function createRaceState(config) {
|
|
215
|
+
return new RaceState(config);
|
|
216
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { StatModifier, StatModifierSet, Stats } from "../stats/statModifiers.js";
|
|
2
|
+
export interface RunModifierOffer<TStat extends string = string, TData = unknown> {
|
|
3
|
+
id: string;
|
|
4
|
+
weight: number;
|
|
5
|
+
maxStacks?: number;
|
|
6
|
+
label?: string;
|
|
7
|
+
stats?: StatModifierSet<TStat>;
|
|
8
|
+
data?: TData;
|
|
9
|
+
}
|
|
10
|
+
export interface RunModifierPick {
|
|
11
|
+
id: string;
|
|
12
|
+
stacks: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RunModifierStack<TStat extends string = string, TData = unknown> {
|
|
15
|
+
add(offerId: string): boolean;
|
|
16
|
+
count(offerId: string): number;
|
|
17
|
+
atMax(offerId: string): boolean;
|
|
18
|
+
picks(): RunModifierPick[];
|
|
19
|
+
offer(offerId: string): RunModifierOffer<TStat, TData> | null;
|
|
20
|
+
total(stat: TStat): StatModifier;
|
|
21
|
+
apply(stats: Stats<TStat>, sourceId?: string): void;
|
|
22
|
+
clear(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function createRunModifierStack<TStat extends string = string, TData = unknown>(offers: readonly RunModifierOffer<TStat, TData>[]): RunModifierStack<TStat, TData>;
|
|
25
|
+
export interface RunDraftConfig<TStat extends string = string, TData = unknown> {
|
|
26
|
+
offers: readonly RunModifierOffer<TStat, TData>[];
|
|
27
|
+
rng?: () => number;
|
|
28
|
+
}
|
|
29
|
+
export interface RunDraft<TStat extends string = string, TData = unknown> {
|
|
30
|
+
present(count: number, rng?: () => number): RunModifierOffer<TStat, TData>[];
|
|
31
|
+
choose(offerId: string): boolean;
|
|
32
|
+
stack(): RunModifierStack<TStat, TData>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createRunDraft<TStat extends string = string, TData = unknown>(config: RunDraftConfig<TStat, TData>): RunDraft<TStat, TData>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { pickWeighted } from "../world/scatterItems.js";
|
|
2
|
+
export function createRunModifierStack(offers) {
|
|
3
|
+
const byId = new Map();
|
|
4
|
+
for (const offer of offers) {
|
|
5
|
+
if (byId.has(offer.id))
|
|
6
|
+
throw new Error(`duplicate run modifier offer: ${offer.id}`);
|
|
7
|
+
byId.set(offer.id, offer);
|
|
8
|
+
}
|
|
9
|
+
const stacks = new Map();
|
|
10
|
+
function count(offerId) {
|
|
11
|
+
return stacks.get(offerId) ?? 0;
|
|
12
|
+
}
|
|
13
|
+
function atMax(offerId) {
|
|
14
|
+
const offer = byId.get(offerId);
|
|
15
|
+
if (offer === undefined)
|
|
16
|
+
return true;
|
|
17
|
+
return offer.maxStacks !== undefined && count(offerId) >= offer.maxStacks;
|
|
18
|
+
}
|
|
19
|
+
function total(stat) {
|
|
20
|
+
let add = 0;
|
|
21
|
+
let multiply = 1;
|
|
22
|
+
for (const [offerId, n] of stacks) {
|
|
23
|
+
const modifier = byId.get(offerId)?.stats?.[stat];
|
|
24
|
+
if (modifier === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
if (modifier.add !== undefined)
|
|
27
|
+
add += modifier.add * n;
|
|
28
|
+
if (modifier.multiply !== undefined)
|
|
29
|
+
multiply *= Math.pow(modifier.multiply, n);
|
|
30
|
+
}
|
|
31
|
+
return { add, multiply };
|
|
32
|
+
}
|
|
33
|
+
function statKeys() {
|
|
34
|
+
const keys = new Set();
|
|
35
|
+
for (const offerId of stacks.keys()) {
|
|
36
|
+
const stat = byId.get(offerId)?.stats;
|
|
37
|
+
if (stat === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
for (const key of Object.keys(stat))
|
|
40
|
+
keys.add(key);
|
|
41
|
+
}
|
|
42
|
+
return [...keys];
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
add(offerId) {
|
|
46
|
+
if (!byId.has(offerId) || atMax(offerId))
|
|
47
|
+
return false;
|
|
48
|
+
stacks.set(offerId, count(offerId) + 1);
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
count,
|
|
52
|
+
atMax,
|
|
53
|
+
picks() {
|
|
54
|
+
return [...stacks.entries()].map(([id, s]) => ({ id, stacks: s }));
|
|
55
|
+
},
|
|
56
|
+
offer(offerId) {
|
|
57
|
+
return byId.get(offerId) ?? null;
|
|
58
|
+
},
|
|
59
|
+
total,
|
|
60
|
+
apply(stats, sourceId = "run-modifiers") {
|
|
61
|
+
const set = {};
|
|
62
|
+
for (const key of statKeys())
|
|
63
|
+
set[key] = total(key);
|
|
64
|
+
stats.addSource(sourceId, set);
|
|
65
|
+
},
|
|
66
|
+
clear() {
|
|
67
|
+
stacks.clear();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function createRunDraft(config) {
|
|
72
|
+
const stack = createRunModifierStack(config.offers);
|
|
73
|
+
const defaultRng = config.rng ?? Math.random;
|
|
74
|
+
return {
|
|
75
|
+
present(count, rng = defaultRng) {
|
|
76
|
+
const pool = config.offers.filter((offer) => !stack.atMax(offer.id));
|
|
77
|
+
const remaining = pool.map((offer) => ({ value: offer, weight: offer.weight }));
|
|
78
|
+
const drawn = [];
|
|
79
|
+
while (drawn.length < count && remaining.length > 0) {
|
|
80
|
+
const picked = pickWeighted(remaining, rng());
|
|
81
|
+
if (picked === null)
|
|
82
|
+
break;
|
|
83
|
+
drawn.push(picked);
|
|
84
|
+
const index = remaining.findIndex((entry) => entry.value.id === picked.id);
|
|
85
|
+
if (index >= 0)
|
|
86
|
+
remaining.splice(index, 1);
|
|
87
|
+
}
|
|
88
|
+
return drawn;
|
|
89
|
+
},
|
|
90
|
+
choose(offerId) {
|
|
91
|
+
return stack.add(offerId);
|
|
92
|
+
},
|
|
93
|
+
stack() {
|
|
94
|
+
return stack;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/game/social.d.ts
CHANGED
|
@@ -5,10 +5,24 @@ export interface PresenceInfo {
|
|
|
5
5
|
zoneId?: string;
|
|
6
6
|
instanceId?: string;
|
|
7
7
|
}
|
|
8
|
+
export interface EmoteEntityLookup {
|
|
9
|
+
get(id: string): {
|
|
10
|
+
position: readonly [number, number, number];
|
|
11
|
+
role: string;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
export interface EmoteSpatialLookup {
|
|
15
|
+
inRadius(center: readonly [number, number, number], radius: number, filter?: (id: string) => boolean): readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export interface EmotesDeps {
|
|
18
|
+
entities: EmoteEntityLookup;
|
|
19
|
+
spatial: EmoteSpatialLookup;
|
|
20
|
+
}
|
|
8
21
|
export interface SocialDeps {
|
|
9
22
|
events: GameEvents;
|
|
10
23
|
presence?: (userId: string) => PresenceInfo;
|
|
11
24
|
now?: () => number;
|
|
25
|
+
emotes?: EmotesDeps;
|
|
12
26
|
}
|
|
13
27
|
export interface FriendEntry {
|
|
14
28
|
userId: string;
|
|
@@ -68,11 +82,24 @@ export interface Party {
|
|
|
68
82
|
list(userId: string): PartyMemberEntry[];
|
|
69
83
|
membersOf(userId: string): string[];
|
|
70
84
|
}
|
|
85
|
+
export declare const DEFAULT_EMOTE_RADIUS = 20;
|
|
86
|
+
export interface EmoteBroadcastResult {
|
|
87
|
+
from: string;
|
|
88
|
+
emoteId: string;
|
|
89
|
+
at: readonly [number, number, number];
|
|
90
|
+
recipients: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
export interface Emotes {
|
|
93
|
+
play(fromUserId: string, emoteId: string, radius?: number): EmoteBroadcastResult | {
|
|
94
|
+
reason: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
71
97
|
export interface Social {
|
|
72
98
|
friends: Friends;
|
|
73
99
|
party: Party;
|
|
74
100
|
presence: {
|
|
75
101
|
get(userId: string): PresenceInfo;
|
|
76
102
|
};
|
|
103
|
+
emotes: Emotes;
|
|
77
104
|
}
|
|
78
105
|
export declare function createSocial(deps: SocialDeps): Social;
|
package/dist/game/social.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const DEFAULT_EMOTE_RADIUS = 20;
|
|
1
2
|
const DEFAULT_INVITE_TTL_MS = 60_000;
|
|
2
3
|
export function createSocial(deps) {
|
|
3
4
|
const now = deps.now ?? Date.now;
|
|
@@ -215,6 +216,20 @@ export function createSocial(deps) {
|
|
|
215
216
|
return partyOf(userId)?.members.slice() ?? [];
|
|
216
217
|
},
|
|
217
218
|
};
|
|
219
|
+
const emotes = {
|
|
220
|
+
play(fromUserId, emoteId, radius) {
|
|
221
|
+
const emoteDeps = deps.emotes;
|
|
222
|
+
if (emoteDeps === undefined)
|
|
223
|
+
return { reason: "emotes not configured" };
|
|
224
|
+
const origin = emoteDeps.entities.get(fromUserId);
|
|
225
|
+
if (origin === null)
|
|
226
|
+
return { reason: `entity "${fromUserId}" is not spawned` };
|
|
227
|
+
const recipients = emoteDeps.spatial.inRadius(origin.position, radius ?? DEFAULT_EMOTE_RADIUS, (id) => id !== fromUserId && emoteDeps.entities.get(id)?.role === "player");
|
|
228
|
+
const result = { from: fromUserId, emoteId, at: origin.position, recipients };
|
|
229
|
+
events.emit("emote.played", result);
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
};
|
|
218
233
|
return {
|
|
219
234
|
friends,
|
|
220
235
|
party,
|
|
@@ -223,5 +238,6 @@ export function createSocial(deps) {
|
|
|
223
238
|
return deps.presence?.(userId) ?? { online: false };
|
|
224
239
|
},
|
|
225
240
|
},
|
|
241
|
+
emotes,
|
|
226
242
|
};
|
|
227
243
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
+
import { type LootFilterItem, type LootFilterRule } from "./lootFilter.js";
|
|
3
|
+
import type { Drop } from "./lootTable.js";
|
|
4
|
+
/** Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object). */
|
|
5
|
+
export declare const WORLD_ITEM_ENTITY_NAME = "world_item";
|
|
6
|
+
export declare const DEFAULT_RARITY = "common";
|
|
7
|
+
export declare const DEFAULT_PICKUP_RADIUS = 2;
|
|
8
|
+
export interface ScatterOptions {
|
|
9
|
+
radius: number;
|
|
10
|
+
minRadius?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const DEFAULT_SCATTER: ScatterOptions;
|
|
14
|
+
/** Random offset within an annulus `[minRadius, radius]` around the origin — the on-death scatter impulse. */
|
|
15
|
+
export declare function scatterOffset(rng: () => number, options?: ScatterOptions): EntityPosition;
|
|
16
|
+
export declare function scatterPosition(origin: EntityPosition, rng: () => number, options?: ScatterOptions): EntityPosition;
|
|
17
|
+
export interface WorldItemRecord {
|
|
18
|
+
instanceId: string;
|
|
19
|
+
itemId: string;
|
|
20
|
+
rarity: string;
|
|
21
|
+
baseType: string;
|
|
22
|
+
count: number;
|
|
23
|
+
affixTier?: number;
|
|
24
|
+
source?: string;
|
|
25
|
+
droppedAt: number;
|
|
26
|
+
}
|
|
27
|
+
export interface WorldItemSpawnInput {
|
|
28
|
+
itemId: string;
|
|
29
|
+
position: EntityPosition;
|
|
30
|
+
rarity?: string;
|
|
31
|
+
baseType?: string;
|
|
32
|
+
count?: number;
|
|
33
|
+
affixTier?: number;
|
|
34
|
+
source?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface WorldItemStoreDeps {
|
|
37
|
+
spawnEntity(position: EntityPosition): string;
|
|
38
|
+
despawnEntity(instanceId: string): boolean;
|
|
39
|
+
resolvePosition(instanceId: string): EntityPosition | undefined;
|
|
40
|
+
now?(): number;
|
|
41
|
+
}
|
|
42
|
+
export interface WorldItemStore {
|
|
43
|
+
spawn(input: WorldItemSpawnInput): WorldItemRecord;
|
|
44
|
+
get(instanceId: string): WorldItemRecord | null;
|
|
45
|
+
list(): readonly WorldItemRecord[];
|
|
46
|
+
nearestInRadius(from: EntityPosition, radius: number, filter?: (record: WorldItemRecord) => boolean): string | null;
|
|
47
|
+
take(instanceId: string): WorldItemRecord | null;
|
|
48
|
+
}
|
|
49
|
+
/** Pure pickup-radius + nearest-item selection, usable without a live store (click-to-grab, proximity prompts). */
|
|
50
|
+
export declare function selectNearestWorldItem(candidates: readonly {
|
|
51
|
+
instanceId: string;
|
|
52
|
+
position: EntityPosition;
|
|
53
|
+
}[], from: EntityPosition, radius: number): string | null;
|
|
54
|
+
export declare function createWorldItemStore(deps: WorldItemStoreDeps): WorldItemStore;
|
|
55
|
+
export interface ResolveDeathDropsOptions {
|
|
56
|
+
mode: "grant" | "world";
|
|
57
|
+
origin: EntityPosition;
|
|
58
|
+
resolveRarity(itemId: string): string;
|
|
59
|
+
resolveBaseType?(itemId: string): string;
|
|
60
|
+
scatter?: ScatterOptions;
|
|
61
|
+
rng?(): number;
|
|
62
|
+
source?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ResolvedDeathDrops {
|
|
65
|
+
worldSpawns: WorldItemSpawnInput[];
|
|
66
|
+
grants: Drop[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Splits rolled death drops between direct grants and scattered ground items.
|
|
70
|
+
* `mode: "grant"` is the legacy behavior (loot straight to inventory); `mode:
|
|
71
|
+
* "world"` routes item drops through a scatter impulse and leaves currency
|
|
72
|
+
* drops granting directly (coins fly to the killer, gear hits the ground).
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveDeathDrops(drops: readonly Drop[], options: ResolveDeathDropsOptions): ResolvedDeathDrops;
|
|
75
|
+
export interface RarityStyle {
|
|
76
|
+
color?: string;
|
|
77
|
+
beam?: boolean;
|
|
78
|
+
label?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface WorldItemPresentation {
|
|
81
|
+
hidden: boolean;
|
|
82
|
+
color?: string;
|
|
83
|
+
beam: boolean;
|
|
84
|
+
label?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Composes the baseline rarity→beam/color/label render binding (#32, catalog
|
|
88
|
+
* data the game supplies) with the loot-filter rule overrides (#33). A
|
|
89
|
+
* matching rule wins field-by-field; unmatched fields fall back to the
|
|
90
|
+
* rarity's baseline style.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveWorldItemPresentation(item: LootFilterItem, rarityStyle: Record<string, RarityStyle> | undefined, rules: readonly LootFilterRule[] | undefined): WorldItemPresentation;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { distanceBetween } from "../scene/spatial.js";
|
|
2
|
+
import { evaluateLootFilter } from "./lootFilter.js";
|
|
3
|
+
/** Scene-entity catalog name every dropped-item instance spawns under (see the three buckets: worldItem is an entity, never an inventory item or object). */
|
|
4
|
+
export const WORLD_ITEM_ENTITY_NAME = "world_item";
|
|
5
|
+
export const DEFAULT_RARITY = "common";
|
|
6
|
+
export const DEFAULT_PICKUP_RADIUS = 2;
|
|
7
|
+
export const DEFAULT_SCATTER = { radius: 1.5, minRadius: 0.4, height: 0 };
|
|
8
|
+
/** Random offset within an annulus `[minRadius, radius]` around the origin — the on-death scatter impulse. */
|
|
9
|
+
export function scatterOffset(rng, options = DEFAULT_SCATTER) {
|
|
10
|
+
const angle = rng() * Math.PI * 2;
|
|
11
|
+
const min = options.minRadius ?? 0;
|
|
12
|
+
const span = Math.max(options.radius - min, 0);
|
|
13
|
+
const distance = min + rng() * span;
|
|
14
|
+
return [Math.cos(angle) * distance, options.height ?? 0, Math.sin(angle) * distance];
|
|
15
|
+
}
|
|
16
|
+
export function scatterPosition(origin, rng, options = DEFAULT_SCATTER) {
|
|
17
|
+
const offset = scatterOffset(rng, options);
|
|
18
|
+
return [origin[0] + offset[0], origin[1] + offset[1], origin[2] + offset[2]];
|
|
19
|
+
}
|
|
20
|
+
/** Pure pickup-radius + nearest-item selection, usable without a live store (click-to-grab, proximity prompts). */
|
|
21
|
+
export function selectNearestWorldItem(candidates, from, radius) {
|
|
22
|
+
let bestId = null;
|
|
23
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
const distance = distanceBetween(from, candidate.position);
|
|
26
|
+
if (distance <= radius && distance < bestDistance) {
|
|
27
|
+
bestDistance = distance;
|
|
28
|
+
bestId = candidate.instanceId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return bestId;
|
|
32
|
+
}
|
|
33
|
+
export function createWorldItemStore(deps) {
|
|
34
|
+
const records = new Map();
|
|
35
|
+
const now = deps.now ?? Date.now;
|
|
36
|
+
return {
|
|
37
|
+
spawn(input) {
|
|
38
|
+
const instanceId = deps.spawnEntity(input.position);
|
|
39
|
+
const record = {
|
|
40
|
+
instanceId,
|
|
41
|
+
itemId: input.itemId,
|
|
42
|
+
rarity: input.rarity ?? DEFAULT_RARITY,
|
|
43
|
+
baseType: input.baseType ?? input.itemId,
|
|
44
|
+
count: input.count ?? 1,
|
|
45
|
+
droppedAt: now(),
|
|
46
|
+
};
|
|
47
|
+
if (input.affixTier !== undefined)
|
|
48
|
+
record.affixTier = input.affixTier;
|
|
49
|
+
if (input.source !== undefined)
|
|
50
|
+
record.source = input.source;
|
|
51
|
+
records.set(instanceId, record);
|
|
52
|
+
return record;
|
|
53
|
+
},
|
|
54
|
+
get(instanceId) {
|
|
55
|
+
return records.get(instanceId) ?? null;
|
|
56
|
+
},
|
|
57
|
+
list() {
|
|
58
|
+
return [...records.values()];
|
|
59
|
+
},
|
|
60
|
+
nearestInRadius(from, radius, filter) {
|
|
61
|
+
const candidates = [];
|
|
62
|
+
for (const record of records.values()) {
|
|
63
|
+
if (filter !== undefined && !filter(record))
|
|
64
|
+
continue;
|
|
65
|
+
const position = deps.resolvePosition(record.instanceId);
|
|
66
|
+
if (position !== undefined)
|
|
67
|
+
candidates.push({ instanceId: record.instanceId, position });
|
|
68
|
+
}
|
|
69
|
+
return selectNearestWorldItem(candidates, from, radius);
|
|
70
|
+
},
|
|
71
|
+
take(instanceId) {
|
|
72
|
+
const record = records.get(instanceId);
|
|
73
|
+
if (record === undefined)
|
|
74
|
+
return null;
|
|
75
|
+
records.delete(instanceId);
|
|
76
|
+
deps.despawnEntity(instanceId);
|
|
77
|
+
return record;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Splits rolled death drops between direct grants and scattered ground items.
|
|
83
|
+
* `mode: "grant"` is the legacy behavior (loot straight to inventory); `mode:
|
|
84
|
+
* "world"` routes item drops through a scatter impulse and leaves currency
|
|
85
|
+
* drops granting directly (coins fly to the killer, gear hits the ground).
|
|
86
|
+
*/
|
|
87
|
+
export function resolveDeathDrops(drops, options) {
|
|
88
|
+
if (options.mode !== "world")
|
|
89
|
+
return { worldSpawns: [], grants: [...drops] };
|
|
90
|
+
const rng = options.rng ?? Math.random;
|
|
91
|
+
const worldSpawns = [];
|
|
92
|
+
const grants = [];
|
|
93
|
+
for (const drop of drops) {
|
|
94
|
+
if (drop.item === undefined) {
|
|
95
|
+
grants.push(drop);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const spawn = {
|
|
99
|
+
itemId: drop.item,
|
|
100
|
+
count: drop.count,
|
|
101
|
+
rarity: options.resolveRarity(drop.item),
|
|
102
|
+
position: scatterPosition(options.origin, rng, options.scatter),
|
|
103
|
+
};
|
|
104
|
+
if (options.resolveBaseType !== undefined)
|
|
105
|
+
spawn.baseType = options.resolveBaseType(drop.item);
|
|
106
|
+
if (options.source !== undefined)
|
|
107
|
+
spawn.source = options.source;
|
|
108
|
+
worldSpawns.push(spawn);
|
|
109
|
+
}
|
|
110
|
+
return { worldSpawns, grants };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Composes the baseline rarity→beam/color/label render binding (#32, catalog
|
|
114
|
+
* data the game supplies) with the loot-filter rule overrides (#33). A
|
|
115
|
+
* matching rule wins field-by-field; unmatched fields fall back to the
|
|
116
|
+
* rarity's baseline style.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveWorldItemPresentation(item, rarityStyle, rules) {
|
|
119
|
+
const base = rarityStyle?.[item.rarity];
|
|
120
|
+
const override = rules === undefined || rules.length === 0 ? {} : evaluateLootFilter(rules, item);
|
|
121
|
+
const presentation = {
|
|
122
|
+
hidden: override.hidden ?? false,
|
|
123
|
+
beam: override.beam ?? base?.beam ?? false,
|
|
124
|
+
};
|
|
125
|
+
const color = override.color ?? base?.color;
|
|
126
|
+
if (color !== undefined)
|
|
127
|
+
presentation.color = color;
|
|
128
|
+
const label = override.label ?? base?.label;
|
|
129
|
+
if (label !== undefined)
|
|
130
|
+
presentation.label = label;
|
|
131
|
+
return presentation;
|
|
132
|
+
}
|