@jgengine/core 0.27.0 → 0.28.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 +130 -0
- package/dist/ai/policeResponse.d.ts +253 -0
- package/dist/ai/policeResponse.js +421 -0
- package/dist/anim/actionLayer.d.ts +47 -0
- package/dist/anim/actionLayer.js +63 -0
- package/dist/anim/ikSolver.d.ts +14 -0
- package/dist/anim/ikSolver.js +37 -0
- package/dist/anim/locomotionGraph.d.ts +58 -3
- package/dist/anim/locomotionGraph.js +142 -15
- package/dist/anim/ragdollRig.d.ts +53 -0
- package/dist/anim/ragdollRig.js +132 -0
- package/dist/anim/skeletonStandard.d.ts +140 -0
- package/dist/anim/skeletonStandard.js +326 -0
- package/dist/combat/damageRuntime.d.ts +14 -1
- package/dist/combat/damageRuntime.js +62 -4
- package/dist/combat/death.js +7 -3
- package/dist/combat/deathReason.d.ts +7 -1
- package/dist/combat/effects.d.ts +25 -0
- package/dist/combat/effects.js +16 -0
- package/dist/combat/statusRuntime.d.ts +2 -0
- package/dist/combat/statusRuntime.js +1 -0
- package/dist/economy/loanBook.d.ts +182 -0
- package/dist/economy/loanBook.js +303 -0
- package/dist/economy/market.d.ts +161 -0
- package/dist/economy/market.js +248 -0
- package/dist/economy/transactionLedger.d.ts +162 -0
- package/dist/economy/transactionLedger.js +301 -0
- package/dist/editor/creatorStorage.js +3 -1
- package/dist/editor/simulation.d.ts +5 -2
- package/dist/editor/simulation.js +29 -19
- package/dist/game/cameraConfig.d.ts +14 -0
- package/dist/game/clipRoles.d.ts +5 -4
- package/dist/game/clipRoles.js +34 -4
- package/dist/game/events.d.ts +27 -0
- package/dist/game/gameSave.d.ts +142 -0
- package/dist/game/gameSave.js +174 -0
- package/dist/game/playableGame.d.ts +55 -4
- package/dist/game/saveSlotIndex.d.ts +54 -0
- package/dist/game/saveSlotIndex.js +71 -0
- package/dist/game/saveSlots.d.ts +2 -0
- package/dist/game/saveSlots.js +1 -1
- package/dist/game/saveStore.d.ts +8 -0
- package/dist/game/saveStore.js +16 -0
- package/dist/gameplay.d.ts +12 -2
- package/dist/gameplay.js +10 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +120 -1
- package/dist/mission/compositions.d.ts +85 -0
- package/dist/mission/compositions.js +130 -0
- package/dist/mission/mission.d.ts +435 -0
- package/dist/mission/mission.js +770 -0
- package/dist/mission/triggers.d.ts +61 -0
- package/dist/mission/triggers.js +108 -0
- package/dist/nav/traffic.d.ts +0 -1
- package/dist/nav/traffic.js +1 -2
- package/dist/runtime/context/combat.js +11 -0
- package/dist/settings/accessibilitySettings.d.ts +81 -0
- package/dist/settings/accessibilitySettings.js +152 -0
- package/dist/settings/graphicsProfile.d.ts +3 -0
- package/dist/settings/graphicsProfile.js +4 -0
- package/dist/settings/settingsModel.d.ts +1 -1
- package/dist/settings/settingsModel.js +1 -0
- package/dist/sim/nearAgents.d.ts +8 -1
- package/dist/sim/nearAgents.js +34 -17
- package/dist/sim/offScreen.d.ts +2 -1
- package/dist/sim/offScreen.js +14 -3
- package/dist/sim/population.d.ts +147 -0
- package/dist/sim/population.js +182 -0
- package/dist/sim/queue.d.ts +90 -0
- package/dist/sim/queue.js +171 -0
- package/dist/sim/rides.d.ts +287 -0
- package/dist/sim/rides.js +471 -0
- package/dist/sim/routine.js +19 -11
- package/dist/ui/loadingTips.d.ts +41 -0
- package/dist/ui/loadingTips.js +64 -0
- package/dist/ui/menuNav.d.ts +79 -0
- package/dist/ui/menuNav.js +147 -0
- package/dist/ui/menuTransition.d.ts +50 -0
- package/dist/ui/menuTransition.js +67 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.js +2 -0
- package/dist/vfx/effectLibrary.d.ts +98 -0
- package/dist/vfx/effectLibrary.js +181 -0
- package/dist/vfx/particleBudget.d.ts +40 -0
- package/dist/vfx/particleBudget.js +39 -0
- package/dist/vfx/particleDirector.d.ts +16 -0
- package/dist/vfx/particleDirector.js +18 -0
- package/dist/world/authoredSimulation.js +7 -1
- package/dist/world/captureCamera.d.ts +30 -0
- package/dist/world/captureCamera.js +19 -0
- package/dist/world/features.d.ts +15 -4
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world.d.ts +2 -1
- package/dist/world.js +1 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-combat/SKILL.md +6 -0
- package/skills/jgengine-combat/api.md +5 -4
- package/skills/jgengine-combat/reference.md +4 -0
- package/skills/jgengine-gameplay/SKILL.md +55 -0
- package/skills/jgengine-gameplay/api.md +335 -10
- package/skills/jgengine-gameplay/capabilities.md +83 -1
- package/skills/jgengine-world/SKILL.md +4 -0
- package/skills/jgengine-world/api.md +128 -6
- package/skills/jgengine-world/capabilities.md +41 -1
- package/skills/jgengine-world/recipes/character-animation.md +25 -2
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Vec3 } from "../world/geometry.js";
|
|
2
|
+
/** One visitor on site: the id its body, its needs and its money are keyed by. */
|
|
3
|
+
export interface Visitor {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
/** Game-second it arrived; a restored visitor keeps the original. */
|
|
6
|
+
readonly admittedAt: number;
|
|
7
|
+
}
|
|
8
|
+
/** What a flow policy reads to decide how many arrive and when one has had enough. */
|
|
9
|
+
export interface VisitorFlow {
|
|
10
|
+
/** Visitors on site this tick — the member count, never a tally kept beside it. */
|
|
11
|
+
readonly onSite: number;
|
|
12
|
+
/** Most visitors the gates hold this tick, after resolving {@link VisitorPolicy.capacity}. */
|
|
13
|
+
readonly capacity: number;
|
|
14
|
+
/** Arrivals since {@link VisitorPopulation.rollDay}. */
|
|
15
|
+
readonly arrivalsToday: number;
|
|
16
|
+
readonly departuresToday: number;
|
|
17
|
+
readonly now: number;
|
|
18
|
+
}
|
|
19
|
+
/** The game's world behind every counted visitor: it creates and removes the body. */
|
|
20
|
+
export interface VisitorWorld {
|
|
21
|
+
/**
|
|
22
|
+
* Create one visitor's body and its game-side record, and return where the body now stands.
|
|
23
|
+
* `null` means the world refused it — no room, no walkable spot — and a refused body is never
|
|
24
|
+
* counted as on site or as an arrival.
|
|
25
|
+
*/
|
|
26
|
+
readonly admit: (visitor: Visitor) => Vec3 | null;
|
|
27
|
+
/** Remove one visitor's body and record. Called for every departure, refusal and reset. */
|
|
28
|
+
readonly release: (visitor: Visitor) => void;
|
|
29
|
+
/**
|
|
30
|
+
* True when the world currently holds a body for this id. `adopt` and `restore` refuse an id the
|
|
31
|
+
* world does not hold, so the count can never name a body that is not on the ground.
|
|
32
|
+
*/
|
|
33
|
+
readonly has: (id: string) => boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The game's flow policy: what draws visitors in, how many fit, and when one has had enough. */
|
|
36
|
+
export interface VisitorPolicy {
|
|
37
|
+
/** Most visitors on site at once; a function lets the cap follow a rating, a level or a build. */
|
|
38
|
+
readonly capacity: number | ((now: number) => number);
|
|
39
|
+
/** Visitors arriving per second at this instant; `0` with the gates shut. */
|
|
40
|
+
readonly arrivalRate: (flow: VisitorFlow) => number;
|
|
41
|
+
/** True when this visitor should leave now. Omit for a population that only leaves when told. */
|
|
42
|
+
readonly shouldLeave?: (visitor: Visitor, flow: VisitorFlow) => boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Flow policy fields a scenario can change in play. */
|
|
45
|
+
export interface VisitorPolicyPatch {
|
|
46
|
+
readonly capacity?: number | ((now: number) => number);
|
|
47
|
+
readonly arrivalRate?: (flow: VisitorFlow) => number;
|
|
48
|
+
/** `null` removes the leaving rule, so visitors stay until {@link VisitorPopulation.leave}. */
|
|
49
|
+
readonly shouldLeave?: ((visitor: Visitor, flow: VisitorFlow) => boolean) | null;
|
|
50
|
+
readonly arrivalBudget?: number;
|
|
51
|
+
/** `null` counts every admitted body as on site. */
|
|
52
|
+
readonly inside?: ((position: Vec3) => boolean) | null;
|
|
53
|
+
}
|
|
54
|
+
/** Counters a HUD, a probe or a test reads. The on-site figure is the visitor list's length. */
|
|
55
|
+
export interface VisitorPopulationStats {
|
|
56
|
+
onSite: number;
|
|
57
|
+
capacity: number;
|
|
58
|
+
arrivalsToday: number;
|
|
59
|
+
departuresToday: number;
|
|
60
|
+
/** Highest on-site count since the last {@link VisitorPopulation.rollDay}. */
|
|
61
|
+
peakToday: number;
|
|
62
|
+
/** Bodies the world or the bounds check refused. Never counted as arrivals. */
|
|
63
|
+
refused: number;
|
|
64
|
+
/** Arrival credit not yet spent. */
|
|
65
|
+
pending: number;
|
|
66
|
+
}
|
|
67
|
+
/** How a body the game made itself joins the population. */
|
|
68
|
+
export interface VisitorAdoption {
|
|
69
|
+
/** Game-second it arrived; defaults to the current tick's clock. */
|
|
70
|
+
readonly admittedAt?: number;
|
|
71
|
+
/** True for a body that is a new arrival today; false for one a save or a seed restored. */
|
|
72
|
+
readonly arrival?: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Serializable state. No on-site count is stored: it is the restored visitor list. */
|
|
75
|
+
export interface VisitorPopulationSnapshot {
|
|
76
|
+
visitors: {
|
|
77
|
+
id: string;
|
|
78
|
+
admittedAt: number;
|
|
79
|
+
}[];
|
|
80
|
+
arrivalsToday: number;
|
|
81
|
+
departuresToday: number;
|
|
82
|
+
peakToday: number;
|
|
83
|
+
pending: number;
|
|
84
|
+
/** Next id suffix, so a restored save does not reuse an id. */
|
|
85
|
+
sequence: number;
|
|
86
|
+
}
|
|
87
|
+
/** Options for {@link createVisitorPopulation}. */
|
|
88
|
+
export interface VisitorPopulationOptions {
|
|
89
|
+
readonly world: VisitorWorld;
|
|
90
|
+
readonly policy: VisitorPolicy;
|
|
91
|
+
/** First half of every generated id. Default `"visitor#"`. */
|
|
92
|
+
readonly idPrefix?: string;
|
|
93
|
+
/** First suffix used after `idPrefix`. Default 1. */
|
|
94
|
+
readonly sequenceStart?: number;
|
|
95
|
+
/** Most bodies one tick may admit, so a long frame cannot flood the park. Default 4. */
|
|
96
|
+
readonly arrivalBudget?: number;
|
|
97
|
+
/** A body must stand here to count as on site; omit to count every admitted body. */
|
|
98
|
+
readonly inside?: (position: Vec3) => boolean;
|
|
99
|
+
/** Register a devtools probe under this name so the overlay shows the crowd and the day. */
|
|
100
|
+
readonly devtoolsName?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The crowd a scene reports. Every visitor is a member with a body the game's world made, so the
|
|
104
|
+
* on-site figure is the member count and the daily figure is the sum of admissions the world
|
|
105
|
+
* accepted — there is no second tally to drift from what is on the ground. Arrivals, capacity and
|
|
106
|
+
* leaving are the game's policy; ids, day rollover and counting are this seam's.
|
|
107
|
+
*/
|
|
108
|
+
export interface VisitorPopulation {
|
|
109
|
+
/** Run the flow for `dt` seconds: leaving first, then arrivals within the budget and the cap. */
|
|
110
|
+
tick(dt: number, now: number): void;
|
|
111
|
+
/** Visitors on site. Reads the member count — no scan of the world, safe for a per-frame HUD. */
|
|
112
|
+
onSite(): number;
|
|
113
|
+
arrivalsToday(): number;
|
|
114
|
+
departuresToday(): number;
|
|
115
|
+
peakToday(): number;
|
|
116
|
+
/** Ids on site, arrival order. Allocates — HUD lists and tests. */
|
|
117
|
+
ids(): readonly string[];
|
|
118
|
+
has(id: string): boolean;
|
|
119
|
+
visitor(id: string): Visitor | null;
|
|
120
|
+
/** Send one visitor home now (it reached the exit, or the game evicted it). */
|
|
121
|
+
leave(id: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Take over a body the game created itself — one restored from a save, seeded at park open, or
|
|
124
|
+
* scripted. It is on site from now on; pass `arrival: true` when it is also a new arrival today.
|
|
125
|
+
*/
|
|
126
|
+
adopt(id: string, adoption?: VisitorAdoption): boolean;
|
|
127
|
+
/** Close the day: arrivals, departures and peak reset. The crowd stays where it stands. */
|
|
128
|
+
rollDay(): void;
|
|
129
|
+
retune(patch: VisitorPolicyPatch): void;
|
|
130
|
+
stats(): VisitorPopulationStats;
|
|
131
|
+
snapshot(): VisitorPopulationSnapshot;
|
|
132
|
+
/**
|
|
133
|
+
* Replace the members with the saved ones. Only visitors the world already holds come back, so
|
|
134
|
+
* restore the bodies first (or `adopt` each one as it spawns); the rest count as refused.
|
|
135
|
+
*/
|
|
136
|
+
restore(next: VisitorPopulationSnapshot): void;
|
|
137
|
+
/** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
|
|
138
|
+
dispose(): void;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create the population a scene reports, with the game owning arrival and leaving policy. A
|
|
142
|
+
* visitor only exists once the world has made its body, so the HUD's count and the bodies on
|
|
143
|
+
* screen are the same list: `admit` returning `null` is a refused arrival, not a number.
|
|
144
|
+
*
|
|
145
|
+
* @capability visitor-population a crowd whose on-site and daily counts are the bodies the game made
|
|
146
|
+
*/
|
|
147
|
+
export declare function createVisitorPopulation(options: VisitorPopulationOptions): VisitorPopulation;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { devtools } from "../devtools/devtools.js";
|
|
2
|
+
const DEFAULT_ARRIVAL_BUDGET = 4;
|
|
3
|
+
function countOf(value) {
|
|
4
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Create the population a scene reports, with the game owning arrival and leaving policy. A
|
|
8
|
+
* visitor only exists once the world has made its body, so the HUD's count and the bodies on
|
|
9
|
+
* screen are the same list: `admit` returning `null` is a refused arrival, not a number.
|
|
10
|
+
*
|
|
11
|
+
* @capability visitor-population a crowd whose on-site and daily counts are the bodies the game made
|
|
12
|
+
*/
|
|
13
|
+
export function createVisitorPopulation(options) {
|
|
14
|
+
const world = options.world;
|
|
15
|
+
let policy = { ...options.policy };
|
|
16
|
+
const idPrefix = options.idPrefix ?? "visitor#";
|
|
17
|
+
let arrivalBudget = Math.max(1, Math.floor(options.arrivalBudget ?? DEFAULT_ARRIVAL_BUDGET));
|
|
18
|
+
let inside = options.inside;
|
|
19
|
+
let members = new Map();
|
|
20
|
+
let sequence = Math.max(1, Math.floor(options.sequenceStart ?? 1));
|
|
21
|
+
let arrivalsToday = 0;
|
|
22
|
+
let departuresToday = 0;
|
|
23
|
+
let peakToday = 0;
|
|
24
|
+
let refused = 0;
|
|
25
|
+
let pending = 0;
|
|
26
|
+
let now = 0;
|
|
27
|
+
let capacityNow = 0;
|
|
28
|
+
function resolveCapacity(at) {
|
|
29
|
+
const value = typeof policy.capacity === "function" ? policy.capacity(at) : policy.capacity;
|
|
30
|
+
return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
|
|
31
|
+
}
|
|
32
|
+
capacityNow = resolveCapacity(0);
|
|
33
|
+
function flow() {
|
|
34
|
+
return { onSite: members.size, capacity: capacityNow, arrivalsToday, departuresToday, now };
|
|
35
|
+
}
|
|
36
|
+
function admitOne(at) {
|
|
37
|
+
const visitor = { id: `${idPrefix}${sequence}`, admittedAt: at };
|
|
38
|
+
sequence += 1;
|
|
39
|
+
const position = world.admit(visitor);
|
|
40
|
+
if (position === null || (inside !== undefined && !inside(position))) {
|
|
41
|
+
if (position !== null)
|
|
42
|
+
world.release(visitor);
|
|
43
|
+
refused += 1;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
members.set(visitor.id, visitor);
|
|
47
|
+
arrivalsToday += 1;
|
|
48
|
+
if (members.size > peakToday)
|
|
49
|
+
peakToday = members.size;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function removeVisitor(visitor) {
|
|
53
|
+
members.delete(visitor.id);
|
|
54
|
+
world.release(visitor);
|
|
55
|
+
departuresToday += 1;
|
|
56
|
+
}
|
|
57
|
+
function probeLine() {
|
|
58
|
+
return `${members.size}/${capacityNow} on site ${arrivalsToday} arrived ${departuresToday} left ${refused} refused`;
|
|
59
|
+
}
|
|
60
|
+
const unregister = options.devtoolsName === undefined ? null : devtools.probes.register(options.devtoolsName, () => probeLine());
|
|
61
|
+
return {
|
|
62
|
+
tick(dt, at) {
|
|
63
|
+
if (!(dt > 0) || !Number.isFinite(at))
|
|
64
|
+
return;
|
|
65
|
+
now = at;
|
|
66
|
+
capacityNow = resolveCapacity(at);
|
|
67
|
+
const read = flow();
|
|
68
|
+
const leave = policy.shouldLeave;
|
|
69
|
+
if (leave !== undefined) {
|
|
70
|
+
for (const visitor of members.values()) {
|
|
71
|
+
if (leave(visitor, read))
|
|
72
|
+
removeVisitor(visitor);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (members.size < capacityNow)
|
|
76
|
+
pending += Math.max(0, policy.arrivalRate(read)) * dt;
|
|
77
|
+
let budget = arrivalBudget;
|
|
78
|
+
while (pending >= 1 && budget > 0 && members.size < capacityNow) {
|
|
79
|
+
pending -= 1;
|
|
80
|
+
budget -= 1;
|
|
81
|
+
admitOne(at);
|
|
82
|
+
}
|
|
83
|
+
if (pending < 0)
|
|
84
|
+
pending = 0;
|
|
85
|
+
},
|
|
86
|
+
onSite: () => members.size,
|
|
87
|
+
arrivalsToday: () => arrivalsToday,
|
|
88
|
+
departuresToday: () => departuresToday,
|
|
89
|
+
peakToday: () => peakToday,
|
|
90
|
+
ids() {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const id of members.keys())
|
|
93
|
+
out.push(id);
|
|
94
|
+
return out;
|
|
95
|
+
},
|
|
96
|
+
has: (id) => members.has(id),
|
|
97
|
+
visitor: (id) => members.get(id) ?? null,
|
|
98
|
+
leave(id) {
|
|
99
|
+
const visitor = members.get(id);
|
|
100
|
+
if (visitor === undefined)
|
|
101
|
+
return false;
|
|
102
|
+
removeVisitor(visitor);
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
adopt(id, adoption) {
|
|
106
|
+
if (id === "" || members.has(id) || !world.has(id))
|
|
107
|
+
return false;
|
|
108
|
+
const saved = adoption?.admittedAt;
|
|
109
|
+
const at = saved !== undefined && Number.isFinite(saved) ? saved : now;
|
|
110
|
+
members.set(id, { id, admittedAt: at });
|
|
111
|
+
if (members.size > peakToday)
|
|
112
|
+
peakToday = members.size;
|
|
113
|
+
if (adoption?.arrival === true)
|
|
114
|
+
arrivalsToday += 1;
|
|
115
|
+
return true;
|
|
116
|
+
},
|
|
117
|
+
rollDay() {
|
|
118
|
+
arrivalsToday = 0;
|
|
119
|
+
departuresToday = 0;
|
|
120
|
+
peakToday = members.size;
|
|
121
|
+
},
|
|
122
|
+
retune(patch) {
|
|
123
|
+
if (patch.capacity !== undefined) {
|
|
124
|
+
policy = { ...policy, capacity: patch.capacity };
|
|
125
|
+
capacityNow = resolveCapacity(now);
|
|
126
|
+
}
|
|
127
|
+
if (patch.arrivalRate !== undefined)
|
|
128
|
+
policy = { ...policy, arrivalRate: patch.arrivalRate };
|
|
129
|
+
if (patch.shouldLeave === null)
|
|
130
|
+
policy = { capacity: policy.capacity, arrivalRate: policy.arrivalRate };
|
|
131
|
+
else if (patch.shouldLeave !== undefined)
|
|
132
|
+
policy = { ...policy, shouldLeave: patch.shouldLeave };
|
|
133
|
+
if (patch.arrivalBudget !== undefined) {
|
|
134
|
+
arrivalBudget = Math.max(1, Math.floor(Number.isFinite(patch.arrivalBudget) ? patch.arrivalBudget : arrivalBudget));
|
|
135
|
+
}
|
|
136
|
+
if (patch.inside === null)
|
|
137
|
+
inside = undefined;
|
|
138
|
+
else if (patch.inside !== undefined)
|
|
139
|
+
inside = patch.inside;
|
|
140
|
+
},
|
|
141
|
+
stats: () => ({
|
|
142
|
+
onSite: members.size,
|
|
143
|
+
capacity: capacityNow,
|
|
144
|
+
arrivalsToday,
|
|
145
|
+
departuresToday,
|
|
146
|
+
peakToday,
|
|
147
|
+
refused,
|
|
148
|
+
pending,
|
|
149
|
+
}),
|
|
150
|
+
snapshot() {
|
|
151
|
+
const visitors = [];
|
|
152
|
+
for (const visitor of members.values())
|
|
153
|
+
visitors.push({ id: visitor.id, admittedAt: visitor.admittedAt });
|
|
154
|
+
return { visitors, arrivalsToday, departuresToday, peakToday, pending, sequence };
|
|
155
|
+
},
|
|
156
|
+
restore(next) {
|
|
157
|
+
for (const visitor of members.values())
|
|
158
|
+
world.release(visitor);
|
|
159
|
+
members = new Map();
|
|
160
|
+
arrivalsToday = countOf(next.arrivalsToday);
|
|
161
|
+
departuresToday = countOf(next.departuresToday);
|
|
162
|
+
peakToday = countOf(next.peakToday);
|
|
163
|
+
pending = Number.isFinite(next.pending) && next.pending > 0 ? next.pending : 0;
|
|
164
|
+
sequence = Math.max(1, Math.floor(Number.isFinite(next.sequence) ? next.sequence : 1));
|
|
165
|
+
for (const saved of next.visitors) {
|
|
166
|
+
if (typeof saved?.id !== "string" || saved.id === "" || members.has(saved.id))
|
|
167
|
+
continue;
|
|
168
|
+
if (!world.has(saved.id)) {
|
|
169
|
+
refused += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const at = Number.isFinite(saved.admittedAt) ? saved.admittedAt : 0;
|
|
173
|
+
members.set(saved.id, { id: saved.id, admittedAt: at });
|
|
174
|
+
}
|
|
175
|
+
if (members.size > peakToday)
|
|
176
|
+
peakToday = members.size;
|
|
177
|
+
},
|
|
178
|
+
dispose() {
|
|
179
|
+
unregister?.();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seconds one guest occupies the head of the line before any boarding interval has been measured.
|
|
3
|
+
* A bootstrap only: the estimate switches to the queue's own measured throughput as soon as two
|
|
4
|
+
* boardings have been observed.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_QUEUE_SEED_SERVICE_SECONDS = 8;
|
|
7
|
+
/** Boarding events the throughput window keeps. */
|
|
8
|
+
export declare const DEFAULT_QUEUE_WINDOW = 8;
|
|
9
|
+
/** One guest waiting their turn, and when they joined the line. */
|
|
10
|
+
export interface QueueEntry {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly joinedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/** A wait estimate read off the queue's own measured throughput. */
|
|
15
|
+
export interface QueueWait {
|
|
16
|
+
/** Guests in front of the reader (the whole line for a new arrival). */
|
|
17
|
+
readonly ahead: number;
|
|
18
|
+
/** Seconds that wait. */
|
|
19
|
+
readonly seconds: number;
|
|
20
|
+
/** Seconds one guest spends at the head; the seed until an interval has been measured. */
|
|
21
|
+
readonly perGuestSeconds: number;
|
|
22
|
+
/** Guests boarded per second over the measured window; `0` before any interval is observed. */
|
|
23
|
+
readonly throughput: number;
|
|
24
|
+
/** True while the estimate still rests on the seed rather than a measured interval. */
|
|
25
|
+
readonly seeded: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Tunables for {@link createWaitQueue}, every one retunable through a restore. */
|
|
28
|
+
export interface WaitQueueOptions {
|
|
29
|
+
/** Seconds per guest used before the queue has measured an interval. Default 8. */
|
|
30
|
+
readonly seedServiceSeconds?: number;
|
|
31
|
+
/** Boarding events the throughput window keeps. Default 8. */
|
|
32
|
+
readonly window?: number;
|
|
33
|
+
/** Register a devtools probe under this name so the perf overlay shows the live line. */
|
|
34
|
+
readonly devtoolsName?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Serializable line state: who is waiting, and the boardings the estimate is measured from. */
|
|
37
|
+
export interface WaitQueueSnapshot {
|
|
38
|
+
entries: QueueEntry[];
|
|
39
|
+
boards: {
|
|
40
|
+
at: number;
|
|
41
|
+
count: number;
|
|
42
|
+
}[];
|
|
43
|
+
seedServiceSeconds: number;
|
|
44
|
+
window: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A real line: guests join the back, leave the head, and the wait a new arrival faces comes from how
|
|
48
|
+
* fast this queue has actually been boarding — not from a configured constant. A broken ride stops
|
|
49
|
+
* boarding, the line grows, and the estimate grows with it.
|
|
50
|
+
*/
|
|
51
|
+
export interface WaitQueue {
|
|
52
|
+
/** Join the back of the line; false when the id is already waiting. */
|
|
53
|
+
enqueue(id: string, now: number): boolean;
|
|
54
|
+
/** Leave from anywhere in the line. */
|
|
55
|
+
remove(id: string): boolean;
|
|
56
|
+
has(id: string): boolean;
|
|
57
|
+
/** 0-based place in line, or `-1` when not waiting. */
|
|
58
|
+
positionOf(id: string): number;
|
|
59
|
+
/** When the guest joined, or `null` when not waiting. */
|
|
60
|
+
joinedAt(id: string): number | null;
|
|
61
|
+
size(): number;
|
|
62
|
+
headId(): string | null;
|
|
63
|
+
/** Id at a 0-based index from the head, or `null` past the end. */
|
|
64
|
+
at(index: number): string | null;
|
|
65
|
+
/** Everyone in line, head first. Allocates — debug and tests. */
|
|
66
|
+
ids(): readonly string[];
|
|
67
|
+
/** Take the guest at the head, or `null` when the line is empty. */
|
|
68
|
+
dequeue(): string | null;
|
|
69
|
+
/** Record that `count` guests left the head at `now`; the throughput window reads these. */
|
|
70
|
+
recordBoard(now: number, count: number): void;
|
|
71
|
+
/**
|
|
72
|
+
* Wait facing `id`, or the whole line when `id` is omitted or not waiting — what a guest joining
|
|
73
|
+
* now would face.
|
|
74
|
+
*/
|
|
75
|
+
wait(id?: string): QueueWait;
|
|
76
|
+
snapshot(): WaitQueueSnapshot;
|
|
77
|
+
restore(next: WaitQueueSnapshot): void;
|
|
78
|
+
/** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
|
|
79
|
+
dispose(): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a line whose wait estimate is derived from its own throughput. The queue owns the order and
|
|
83
|
+
* the measurement; the ride owns how fast guests are taken off the head, so a slower cycle raises the
|
|
84
|
+
* estimate without the queue being told a cycle time.
|
|
85
|
+
*
|
|
86
|
+
* `enqueue` is O(1) and `dequeue` amortized O(1) — a long line never re-indexes.
|
|
87
|
+
*
|
|
88
|
+
* @capability wait-queue a real line whose wait estimate comes from its own measured throughput
|
|
89
|
+
*/
|
|
90
|
+
export declare function createWaitQueue(options?: WaitQueueOptions): WaitQueue;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { devtools } from "../devtools/devtools.js";
|
|
2
|
+
/**
|
|
3
|
+
* Seconds one guest occupies the head of the line before any boarding interval has been measured.
|
|
4
|
+
* A bootstrap only: the estimate switches to the queue's own measured throughput as soon as two
|
|
5
|
+
* boardings have been observed.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_QUEUE_SEED_SERVICE_SECONDS = 8;
|
|
8
|
+
/** Boarding events the throughput window keeps. */
|
|
9
|
+
export const DEFAULT_QUEUE_WINDOW = 8;
|
|
10
|
+
/**
|
|
11
|
+
* Create a line whose wait estimate is derived from its own throughput. The queue owns the order and
|
|
12
|
+
* the measurement; the ride owns how fast guests are taken off the head, so a slower cycle raises the
|
|
13
|
+
* estimate without the queue being told a cycle time.
|
|
14
|
+
*
|
|
15
|
+
* `enqueue` is O(1) and `dequeue` amortized O(1) — a long line never re-indexes.
|
|
16
|
+
*
|
|
17
|
+
* @capability wait-queue a real line whose wait estimate comes from its own measured throughput
|
|
18
|
+
*/
|
|
19
|
+
export function createWaitQueue(options = {}) {
|
|
20
|
+
let seedServiceSeconds = options.seedServiceSeconds ?? DEFAULT_QUEUE_SEED_SERVICE_SECONDS;
|
|
21
|
+
let window = Math.max(2, Math.floor(options.window ?? DEFAULT_QUEUE_WINDOW));
|
|
22
|
+
let entries = [];
|
|
23
|
+
let boards = [];
|
|
24
|
+
let head = 0;
|
|
25
|
+
const waiting = new Set();
|
|
26
|
+
const live = () => entries.length - head;
|
|
27
|
+
function compact() {
|
|
28
|
+
if (head > 32 && head * 2 >= entries.length) {
|
|
29
|
+
entries = entries.slice(head);
|
|
30
|
+
head = 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Guests boarded per second, from consecutive boarding intervals; `0` until one is observed. */
|
|
34
|
+
function throughput() {
|
|
35
|
+
if (boards.length < 2)
|
|
36
|
+
return 0;
|
|
37
|
+
const first = boards[0];
|
|
38
|
+
const last = boards[boards.length - 1];
|
|
39
|
+
const span = last.at - first.at;
|
|
40
|
+
if (!(span > 0))
|
|
41
|
+
return 0;
|
|
42
|
+
let guests = 0;
|
|
43
|
+
for (let index = 1; index < boards.length; index += 1)
|
|
44
|
+
guests += boards[index].count;
|
|
45
|
+
return guests / span;
|
|
46
|
+
}
|
|
47
|
+
function readWait(ahead) {
|
|
48
|
+
const rate = throughput();
|
|
49
|
+
const perGuestSeconds = rate > 0 ? 1 / rate : Math.max(0, seedServiceSeconds);
|
|
50
|
+
return {
|
|
51
|
+
ahead,
|
|
52
|
+
seconds: ahead * perGuestSeconds,
|
|
53
|
+
perGuestSeconds,
|
|
54
|
+
throughput: rate,
|
|
55
|
+
seeded: rate <= 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const unregister = options.devtoolsName === undefined ? null : devtools.probes.register(options.devtoolsName, () => probeLine());
|
|
59
|
+
function probeLine() {
|
|
60
|
+
const wait = readWait(live());
|
|
61
|
+
return `${live()} in line · ${wait.seconds.toFixed(1)}s wait · ${wait.perGuestSeconds.toFixed(2)}s per guest${wait.seeded ? " (seed)" : ""}`;
|
|
62
|
+
}
|
|
63
|
+
function indexOf(id) {
|
|
64
|
+
for (let index = head; index < entries.length; index += 1) {
|
|
65
|
+
if (entries[index].id === id)
|
|
66
|
+
return index;
|
|
67
|
+
}
|
|
68
|
+
return -1;
|
|
69
|
+
}
|
|
70
|
+
function positionOfId(id) {
|
|
71
|
+
const at = indexOf(id);
|
|
72
|
+
return at < 0 ? -1 : at - head;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
enqueue(id, now) {
|
|
76
|
+
if (waiting.has(id))
|
|
77
|
+
return false;
|
|
78
|
+
entries.push({ id, joinedAt: Number.isFinite(now) ? now : 0 });
|
|
79
|
+
waiting.add(id);
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
82
|
+
remove(id) {
|
|
83
|
+
if (!waiting.has(id))
|
|
84
|
+
return false;
|
|
85
|
+
waiting.delete(id);
|
|
86
|
+
const at = indexOf(id);
|
|
87
|
+
if (at === head) {
|
|
88
|
+
head += 1;
|
|
89
|
+
compact();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
entries.splice(at, 1);
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
has: (id) => waiting.has(id),
|
|
96
|
+
positionOf: positionOfId,
|
|
97
|
+
joinedAt(id) {
|
|
98
|
+
if (!waiting.has(id))
|
|
99
|
+
return null;
|
|
100
|
+
const at = indexOf(id);
|
|
101
|
+
return at < 0 ? null : entries[at].joinedAt;
|
|
102
|
+
},
|
|
103
|
+
size: live,
|
|
104
|
+
headId: () => entries[head]?.id ?? null,
|
|
105
|
+
at(index) {
|
|
106
|
+
const absolute = head + Math.floor(index);
|
|
107
|
+
if (!(index >= 0) || absolute >= entries.length)
|
|
108
|
+
return null;
|
|
109
|
+
return entries[absolute]?.id ?? null;
|
|
110
|
+
},
|
|
111
|
+
ids() {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let index = head; index < entries.length; index += 1)
|
|
114
|
+
out.push(entries[index].id);
|
|
115
|
+
return out;
|
|
116
|
+
},
|
|
117
|
+
dequeue() {
|
|
118
|
+
const entry = entries[head];
|
|
119
|
+
if (entry === undefined)
|
|
120
|
+
return null;
|
|
121
|
+
head += 1;
|
|
122
|
+
waiting.delete(entry.id);
|
|
123
|
+
compact();
|
|
124
|
+
return entry.id;
|
|
125
|
+
},
|
|
126
|
+
recordBoard(now, count) {
|
|
127
|
+
const boarded = Math.floor(count);
|
|
128
|
+
if (!(boarded > 0) || !Number.isFinite(now))
|
|
129
|
+
return;
|
|
130
|
+
boards.push({ at: now, count: boarded });
|
|
131
|
+
if (boards.length > window)
|
|
132
|
+
boards = boards.slice(boards.length - window);
|
|
133
|
+
},
|
|
134
|
+
wait(id) {
|
|
135
|
+
const position = id === undefined ? -1 : positionOfId(id);
|
|
136
|
+
return readWait(position >= 0 ? position : live());
|
|
137
|
+
},
|
|
138
|
+
snapshot: () => ({
|
|
139
|
+
entries: entries.slice(head).map((entry) => ({ id: entry.id, joinedAt: entry.joinedAt })),
|
|
140
|
+
boards: boards.map((board) => ({ at: board.at, count: board.count })),
|
|
141
|
+
seedServiceSeconds,
|
|
142
|
+
window,
|
|
143
|
+
}),
|
|
144
|
+
restore(next) {
|
|
145
|
+
entries = [];
|
|
146
|
+
head = 0;
|
|
147
|
+
waiting.clear();
|
|
148
|
+
for (const entry of next.entries) {
|
|
149
|
+
if (typeof entry?.id !== "string" || entry.id === "")
|
|
150
|
+
continue;
|
|
151
|
+
entries.push({ id: entry.id, joinedAt: Number.isFinite(entry.joinedAt) ? entry.joinedAt : 0 });
|
|
152
|
+
waiting.add(entry.id);
|
|
153
|
+
}
|
|
154
|
+
boards = [];
|
|
155
|
+
for (const board of next.boards) {
|
|
156
|
+
if (!Number.isFinite(board?.at) || !(board?.count > 0))
|
|
157
|
+
continue;
|
|
158
|
+
boards.push({ at: board.at, count: Math.floor(board.count) });
|
|
159
|
+
}
|
|
160
|
+
if (boards.length > window)
|
|
161
|
+
boards = boards.slice(boards.length - window);
|
|
162
|
+
if (Number.isFinite(next.seedServiceSeconds))
|
|
163
|
+
seedServiceSeconds = Math.max(0, next.seedServiceSeconds);
|
|
164
|
+
if (Number.isFinite(next.window))
|
|
165
|
+
window = Math.max(2, Math.floor(next.window));
|
|
166
|
+
},
|
|
167
|
+
dispose() {
|
|
168
|
+
unregister?.();
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|