@sagl/server 0.0.0-stage → 0.1.0-next.49

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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/dist/actions.d.ts +46 -0
  4. package/dist/actions.js +80 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/animations.d.ts +32 -0
  7. package/dist/animations.js +95 -0
  8. package/dist/animations.js.map +1 -0
  9. package/dist/audio.d.ts +180 -0
  10. package/dist/audio.js +353 -0
  11. package/dist/audio.js.map +1 -0
  12. package/dist/audio_effects.d.ts +219 -0
  13. package/dist/audio_effects.js +84 -0
  14. package/dist/audio_effects.js.map +1 -0
  15. package/dist/camera.d.ts +188 -0
  16. package/dist/camera.js +247 -0
  17. package/dist/camera.js.map +1 -0
  18. package/dist/claims.d.ts +27 -0
  19. package/dist/claims.js +48 -0
  20. package/dist/claims.js.map +1 -0
  21. package/dist/commands.d.ts +135 -0
  22. package/dist/commands.js +373 -0
  23. package/dist/commands.js.map +1 -0
  24. package/dist/console.d.ts +21 -0
  25. package/dist/console.js +100 -0
  26. package/dist/console.js.map +1 -0
  27. package/dist/constants.d.ts +654 -0
  28. package/dist/constants.js +657 -0
  29. package/dist/constants.js.map +1 -0
  30. package/dist/cutscene.d.ts +47 -0
  31. package/dist/cutscene.js +190 -0
  32. package/dist/cutscene.js.map +1 -0
  33. package/dist/draw.d.ts +106 -0
  34. package/dist/draw.js +132 -0
  35. package/dist/draw.js.map +1 -0
  36. package/dist/effects.d.ts +91 -0
  37. package/dist/effects.js +116 -0
  38. package/dist/effects.js.map +1 -0
  39. package/dist/gangzones.d.ts +47 -0
  40. package/dist/gangzones.js +122 -0
  41. package/dist/gangzones.js.map +1 -0
  42. package/dist/geo.d.ts +11 -0
  43. package/dist/geo.js +25 -0
  44. package/dist/geo.js.map +1 -0
  45. package/dist/hud.d.ts +159 -0
  46. package/dist/hud.js +247 -0
  47. package/dist/hud.js.map +1 -0
  48. package/dist/index.d.ts +40 -0
  49. package/dist/index.js +43 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/interaction.d.ts +81 -0
  52. package/dist/interaction.js +143 -0
  53. package/dist/interaction.js.map +1 -0
  54. package/dist/junctions.d.ts +1 -0
  55. package/dist/junctions.js +330 -0
  56. package/dist/junctions.js.map +1 -0
  57. package/dist/map.d.ts +55 -0
  58. package/dist/map.js +71 -0
  59. package/dist/map.js.map +1 -0
  60. package/dist/menu.d.ts +96 -0
  61. package/dist/menu.js +129 -0
  62. package/dist/menu.js.map +1 -0
  63. package/dist/mission.d.ts +482 -0
  64. package/dist/mission.js +1258 -0
  65. package/dist/mission.js.map +1 -0
  66. package/dist/models.d.ts +56 -0
  67. package/dist/models.js +71 -0
  68. package/dist/models.js.map +1 -0
  69. package/dist/modshop.d.ts +229 -0
  70. package/dist/modshop.js +904 -0
  71. package/dist/modshop.js.map +1 -0
  72. package/dist/modshop_data.d.ts +44 -0
  73. package/dist/modshop_data.js +287 -0
  74. package/dist/modshop_data.js.map +1 -0
  75. package/dist/native.d.ts +809 -0
  76. package/dist/native.js +27 -0
  77. package/dist/native.js.map +1 -0
  78. package/dist/objects.d.ts +249 -0
  79. package/dist/objects.js +455 -0
  80. package/dist/objects.js.map +1 -0
  81. package/dist/paint.d.ts +124 -0
  82. package/dist/paint.js +84 -0
  83. package/dist/paint.js.map +1 -0
  84. package/dist/paynspray.d.ts +44 -0
  85. package/dist/paynspray.js +177 -0
  86. package/dist/paynspray.js.map +1 -0
  87. package/dist/ped.d.ts +335 -0
  88. package/dist/ped.js +536 -0
  89. package/dist/ped.js.map +1 -0
  90. package/dist/player.d.ts +685 -0
  91. package/dist/player.js +1163 -0
  92. package/dist/player.js.map +1 -0
  93. package/dist/screeneffects.d.ts +99 -0
  94. package/dist/screeneffects.js +257 -0
  95. package/dist/screeneffects.js.map +1 -0
  96. package/dist/scripts.d.ts +69 -0
  97. package/dist/scripts.js +125 -0
  98. package/dist/scripts.js.map +1 -0
  99. package/dist/server.d.ts +713 -0
  100. package/dist/server.js +1080 -0
  101. package/dist/server.js.map +1 -0
  102. package/dist/shopblips.d.ts +9 -0
  103. package/dist/shopblips.js +30 -0
  104. package/dist/shopblips.js.map +1 -0
  105. package/dist/special.d.ts +33 -0
  106. package/dist/special.js +26 -0
  107. package/dist/special.js.map +1 -0
  108. package/dist/splash.d.ts +42 -0
  109. package/dist/splash.js +55 -0
  110. package/dist/splash.js.map +1 -0
  111. package/dist/text.d.ts +19 -0
  112. package/dist/text.js +25 -0
  113. package/dist/text.js.map +1 -0
  114. package/dist/ui.d.ts +141 -0
  115. package/dist/ui.js +243 -0
  116. package/dist/ui.js.map +1 -0
  117. package/dist/vehicle.d.ts +306 -0
  118. package/dist/vehicle.js +612 -0
  119. package/dist/vehicle.js.map +1 -0
  120. package/dist/vehiclecolors.d.ts +26 -0
  121. package/dist/vehiclecolors.js +49 -0
  122. package/dist/vehiclecolors.js.map +1 -0
  123. package/dist/world.d.ts +222 -0
  124. package/dist/world.js +227 -0
  125. package/dist/world.js.map +1 -0
  126. package/dist/zone.d.ts +89 -0
  127. package/dist/zone.js +140 -0
  128. package/dist/zone.js.map +1 -0
  129. package/package.json +33 -4
@@ -0,0 +1,1258 @@
1
+ /** How often waits and fail conditions are checked. */
2
+ const TICK_MS = 250;
3
+ /** Defines a mission (it only adds types). */
4
+ export function defineMission(definition) {
5
+ return definition;
6
+ }
7
+ /**
8
+ * Thrown into the script's pending waits when the mission ends (a fail
9
+ * condition, every player leaving, ...). The helper catches it: let it go.
10
+ */
11
+ export class MissionEnded extends Error {
12
+ outcome;
13
+ constructor(outcome) {
14
+ super(`mission ended: ${outcome.reason}`);
15
+ this.name = "MissionEnded";
16
+ this.outcome = outcome;
17
+ }
18
+ }
19
+ /** Starting or joining a mission was refused. `reason` says why. */
20
+ export class MissionRefused extends Error {
21
+ reason;
22
+ constructor(reason) {
23
+ super(reason);
24
+ this.name = "MissionRefused";
25
+ this.reason = reason;
26
+ }
27
+ }
28
+ /** A blip every party member has, players who join later included. */
29
+ export class MissionBlip {
30
+ #blips = new Map();
31
+ #target;
32
+ #options;
33
+ #removed = false;
34
+ /** @internal */
35
+ constructor(target, options) {
36
+ this.#target = target;
37
+ this.#options = options;
38
+ }
39
+ /** Changes its options. */
40
+ set(options) {
41
+ this.#options = { ...this.#options, ...options };
42
+ for (const b of this.#blips.values())
43
+ b.set(options);
44
+ return this;
45
+ }
46
+ remove() {
47
+ this.#removed = true;
48
+ for (const b of this.#blips.values())
49
+ b.remove();
50
+ this.#blips.clear();
51
+ }
52
+ /** @internal */
53
+ add(player) {
54
+ if (!this.#removed && !this.#blips.has(player))
55
+ this.#blips.set(player, player.hud.blip(this.#target, this.#options));
56
+ }
57
+ /** @internal */
58
+ drop(player) {
59
+ this.#blips.get(player)?.remove();
60
+ this.#blips.delete(player);
61
+ }
62
+ get removed() {
63
+ return this.#removed;
64
+ }
65
+ }
66
+ /** A HUD counter or timer every party member has. */
67
+ export class MissionCounter {
68
+ #counters = new Map();
69
+ #make;
70
+ #value;
71
+ #label;
72
+ #removed = false;
73
+ // Timers: when it reaches 0 (Date.now()), to start joiners at the right time.
74
+ #endsAt;
75
+ #pausedLeft = null; // paused: the time left
76
+ /** @internal */
77
+ constructor(label, value, endsAt, make) {
78
+ this.#label = label;
79
+ this.#value = value;
80
+ this.#endsAt = endsAt;
81
+ this.#make = (p) => {
82
+ const c = make(p, this.#label, this.value);
83
+ if (this.#pausedLeft !== null)
84
+ c.setPaused(true, this.#pausedLeft);
85
+ return c;
86
+ };
87
+ }
88
+ /** The number (a counter), the time left (a timer, ms), or how full (a bar). */
89
+ get value() {
90
+ if (this.#pausedLeft !== null)
91
+ return this.#pausedLeft;
92
+ return this.#endsAt === null ? this.#value : Math.max(0, this.#endsAt - Date.now());
93
+ }
94
+ /** A timer: whether it's held still (`pause()`). */
95
+ get paused() {
96
+ return this.#pausedLeft !== null;
97
+ }
98
+ /** Holds a timer still (it doesn't run out meanwhile); `resume()` counts on from there. */
99
+ pause() {
100
+ if (this.#endsAt === null || this.#pausedLeft !== null || this.#removed)
101
+ return;
102
+ this.#pausedLeft = Math.max(0, this.#endsAt - Date.now());
103
+ for (const c of this.#counters.values())
104
+ c.setPaused(true, this.#pausedLeft);
105
+ }
106
+ resume() {
107
+ if (this.#pausedLeft === null)
108
+ return;
109
+ const left = this.#pausedLeft;
110
+ this.#pausedLeft = null;
111
+ this.#endsAt = Date.now() + left;
112
+ for (const c of this.#counters.values())
113
+ c.setPaused(false, left);
114
+ }
115
+ /** @internal Is it a timer (counting down or up)? */
116
+ get isTimer() {
117
+ return this.#endsAt !== null;
118
+ }
119
+ set value(value) {
120
+ if (this.#endsAt !== null)
121
+ this.#endsAt = Date.now() + value;
122
+ else
123
+ this.#value = value;
124
+ for (const c of this.#counters.values())
125
+ c.value = value;
126
+ }
127
+ set label(label) {
128
+ this.#label = label;
129
+ for (const c of this.#counters.values())
130
+ c.label = label;
131
+ }
132
+ remove() {
133
+ this.#removed = true;
134
+ for (const c of this.#counters.values())
135
+ c.remove();
136
+ this.#counters.clear();
137
+ }
138
+ get removed() {
139
+ return this.#removed;
140
+ }
141
+ /** @internal */
142
+ add(player) {
143
+ if (!this.#removed && !this.#counters.has(player))
144
+ this.#counters.set(player, this.#make(player));
145
+ }
146
+ /** @internal */
147
+ drop(player) {
148
+ this.#counters.get(player)?.remove();
149
+ this.#counters.delete(player);
150
+ }
151
+ }
152
+ /**
153
+ * A running mission: its party, what it created (removed when it ends), its
154
+ * HUD, fail conditions and waits. The script gets it as `m`.
155
+ *
156
+ * Every wait (`reach`, `until`, `wait`, `step`, ...) rejects with
157
+ * `MissionEnded` when the mission ends, so the script stops where it is;
158
+ * the helper catches it. Mission calls made after the end throw it too.
159
+ */
160
+ export class Mission {
161
+ name;
162
+ definition;
163
+ /** Free-form storage for the script. */
164
+ data = {};
165
+ /** When it started (Date.now()). */
166
+ startedAt = Date.now();
167
+ /** Resolves when it ends, passed or not (never rejects). */
168
+ done;
169
+ #missions;
170
+ #server;
171
+ #players = [];
172
+ #outcome = null;
173
+ #resolveDone;
174
+ #peds = [];
175
+ #vehicles = [];
176
+ #blips = [];
177
+ #counters = [];
178
+ #objective = null;
179
+ #failWhen = [];
180
+ #listeners = [];
181
+ #intervals = new Set();
182
+ #speeches = new Set();
183
+ #restore = []; // population changes, cinematics: undone at the end
184
+ #startFrom = null;
185
+ #stage = "";
186
+ #waiters = new Set();
187
+ #timers = new Set();
188
+ #cooldowns = new Map();
189
+ #tick;
190
+ #spawn = null;
191
+ #cutscene = null;
192
+ #objects = [];
193
+ #pickups = [];
194
+ #warned = new Set(); // shown keepClose's warning instead of the objective
195
+ /** @internal */
196
+ constructor(missions, server, definition, players) {
197
+ this.#missions = missions;
198
+ this.#server = server;
199
+ this.definition = definition;
200
+ this.name = definition.name;
201
+ this.#players = [...players];
202
+ this.done = new Promise((resolve) => (this.#resolveDone = resolve));
203
+ this.#tick = setInterval(() => this.#check(), TICK_MS);
204
+ }
205
+ /** The party, leader first. */
206
+ get players() {
207
+ return this.#players;
208
+ }
209
+ /** Who started it, or the next in line once they leave. */
210
+ get leader() {
211
+ return this.#players[0];
212
+ }
213
+ /** Whether it's still running. */
214
+ get running() {
215
+ return this.#outcome === null;
216
+ }
217
+ /** How it ended, or null while it runs. */
218
+ get outcome() {
219
+ return this.#outcome;
220
+ }
221
+ // ---- The party ----------------------------------------------------------
222
+ /**
223
+ * Adds a player to the party (an invitation). They get its HUD and see its
224
+ * peds. Throws `MissionRefused` if they're in another mission, it's full,
225
+ * they don't meet `requires`, or they're cooling down.
226
+ */
227
+ addPlayer(player) {
228
+ this.#live();
229
+ if (this.#players.includes(player))
230
+ return;
231
+ if (this.#players.length >= (this.definition.maxPlayers ?? 1))
232
+ this.#missions.refuse(this.definition, player, "The mission is full.");
233
+ this.#missions.admit(this.definition, player);
234
+ this.#players.push(player);
235
+ this.#missions.bind(player, this);
236
+ this.#show(player);
237
+ this.#updateVisibility();
238
+ this.#missions.event({ type: "join", name: this.name, mission: this, player, reason: "" });
239
+ }
240
+ /** Takes a player out of the party (their HUD for it goes). The last one leaving abandons it. */
241
+ removePlayer(player) {
242
+ const i = this.#players.indexOf(player);
243
+ if (i < 0)
244
+ return;
245
+ this.#players.splice(i, 1);
246
+ this.#missions.bind(player, null);
247
+ this.#hide(player);
248
+ this.#updateVisibility();
249
+ this.#missions.event({ type: "leave", name: this.name, mission: this, player, reason: "" });
250
+ if (this.running && this.#players.length === 0)
251
+ this.#end(false, "abandoned", "");
252
+ }
253
+ // ---- Things it creates ----------------------------------------------------
254
+ /**
255
+ * A ped for the mission. When it ends, it fades out, or if it's dead its
256
+ * body is released (`ped.release()`: it lies there until the cleanup
257
+ * takes it); with `keep` it stays for good. Seen by the party (players joining later
258
+ * too) unless `visibleTo` says otherwise.
259
+ */
260
+ createPed(model, position, heading = 0, options = {}) {
261
+ this.#live();
262
+ const partyVisible = options.visibleTo === undefined;
263
+ const ped = this.#server.createPed(model, position, heading, {
264
+ ...options,
265
+ visibleTo: options.visibleTo ?? this.#players,
266
+ });
267
+ this.#peds.push({ ped, onEnd: options.onEnd ?? (options.keep ? "keep" : "release"), partyVisible });
268
+ return ped;
269
+ }
270
+ /**
271
+ * A vehicle for the mission, released when it ends (it stays until nobody
272
+ * is around, as the game's), `onEnd: "destroy"` removed at once, or
273
+ * `onEnd: "keep"` (or `keep`) left for good.
274
+ */
275
+ createVehicle(model, position, heading = 0, options = {}) {
276
+ this.#live();
277
+ const vehicle = this.#server.createVehicle(model, position, heading, options);
278
+ this.#vehicles.push({ vehicle, onEnd: options.onEnd ?? (options.keep ? "keep" : "release") });
279
+ return vehicle;
280
+ }
281
+ /** An object for the mission (the party sees it), removed when it ends. See `server.createObject`. */
282
+ createObject(model, position, rotation, options = {}) {
283
+ this.#live();
284
+ const o = this.#server.createObject(model, position, rotation, { visibleTo: this.#players, ...options });
285
+ this.#objects.push(o);
286
+ return o;
287
+ }
288
+ /** A pickup for the mission (the party sees it), removed when it ends. See `server.createPickup`. */
289
+ createPickup(options) {
290
+ this.#live();
291
+ const p = this.#server.createPickup({ visibleTo: this.#players, ...options });
292
+ this.#pickups.push(p);
293
+ return p;
294
+ }
295
+ // ---- HUD ------------------------------------------------------------------
296
+ /** Objective text at the bottom of the party's screens, for `ms` (left out: as long as reading takes). */
297
+ objective(text, ms) {
298
+ this.#live();
299
+ if (this.#held) {
300
+ // Losing the cops first: this one after.
301
+ this.#held.before = { text, ms };
302
+ return;
303
+ }
304
+ this.#objective = { text, until: ms ? Date.now() + ms : Date.now() + Math.max(3000, text.length * 80) };
305
+ // Those keepClose is warning see it once they're back.
306
+ for (const p of this.#players)
307
+ if (!this.#warned.has(p))
308
+ p.hud.objective(text, ms);
309
+ }
310
+ clearObjective() {
311
+ if (this.#held) {
312
+ this.#held.before = null;
313
+ return;
314
+ }
315
+ this.#objective = null;
316
+ for (const p of this.#players)
317
+ p.hud.clearObjective();
318
+ }
319
+ /** Big text in the middle of the party's screens. */
320
+ bigText(text, ms) {
321
+ this.#live();
322
+ for (const p of this.#players)
323
+ p.hud.bigText(text, ms);
324
+ }
325
+ /** A blip (and marker) on a spot, ped, vehicle or player, for the whole party. */
326
+ blip(target, options = {}) {
327
+ this.#live();
328
+ const blip = new MissionBlip(target, options);
329
+ this.#blips.push(blip);
330
+ for (const p of this.#players)
331
+ blip.add(p);
332
+ return blip;
333
+ }
334
+ /** A counter on the party's HUD ("Crates 2/6"). */
335
+ counter(label, options = {}) {
336
+ this.#live();
337
+ const c = new MissionCounter(label, options.value ?? 0, null, (p, l, v) => p.hud.counter(l, { ...options, value: v }));
338
+ this.#counters.push(c);
339
+ for (const p of this.#players)
340
+ c.add(p);
341
+ return c;
342
+ }
343
+ /**
344
+ * A countdown on the party's HUD (or counting up with `countUp`). With
345
+ * `fail`, the mission fails with that reason when a countdown reaches 0.
346
+ */
347
+ timer(label, ms, options = {}) {
348
+ this.#live();
349
+ const { fail, ...hud } = options;
350
+ const c = new MissionCounter(label, ms, options.countUp ? null : Date.now() + ms, (p, l, v) => p.hud.timer(l, v, hud));
351
+ this.#counters.push(c);
352
+ for (const p of this.#players)
353
+ c.add(p);
354
+ if (fail !== undefined && !options.countUp) {
355
+ const check = () => {
356
+ if (!this.running || c.removed)
357
+ return;
358
+ const left = c.value;
359
+ if (left <= 0)
360
+ this.fail(fail);
361
+ else
362
+ this.#later(check, left);
363
+ };
364
+ this.#later(check, ms);
365
+ }
366
+ return c;
367
+ }
368
+ /**
369
+ * While anyone in the party is wanted, the mission's timers stand still
370
+ * and `message` stays as the objective (default "Lose the ~r~cops~w~.");
371
+ * once they're all clear, the timers run on and the objective is back.
372
+ * For a mission that shouldn't be failed by a chase it didn't mean.
373
+ *
374
+ * ```ts
375
+ * m.holdWhileWanted();
376
+ * ```
377
+ */
378
+ holdWhileWanted(message = "Lose the ~r~cops~w~.") {
379
+ this.#live();
380
+ const check = () => {
381
+ if (!this.running)
382
+ return;
383
+ const wanted = this.#players.some((p) => p.connected && p.wantedLevel > 0);
384
+ const timers = this.#counters.filter((c) => c.isTimer && !c.removed);
385
+ if (wanted) {
386
+ for (const c of timers)
387
+ c.pause(); // and any started meanwhile
388
+ if (!this.#held) {
389
+ const o = this.#objective;
390
+ this.#held = { before: o && o.until > Date.now() ? { text: o.text, ms: o.until === Infinity ? Infinity : o.until - Date.now() } : null };
391
+ this.#objective = { text: message, until: Infinity };
392
+ for (const p of this.#players)
393
+ p.hud.objective(message, Infinity);
394
+ }
395
+ }
396
+ else if (this.#held) {
397
+ const before = this.#held.before;
398
+ this.#held = null;
399
+ for (const c of timers)
400
+ c.resume();
401
+ if (before)
402
+ this.objective(before.text, before.ms);
403
+ else
404
+ this.clearObjective();
405
+ }
406
+ this.#later(check, 500);
407
+ };
408
+ check();
409
+ }
410
+ // Held while wanted: the objective the script set meanwhile, shown after.
411
+ #held = null;
412
+ failWhen(a, b, c) {
413
+ this.#live();
414
+ let entry;
415
+ if (typeof a === "function") {
416
+ entry = { check: a, reason: b };
417
+ }
418
+ else if (b === "died") {
419
+ const ped = a;
420
+ entry = { check: () => !ped.exists || ped.dead, reason: c ?? "" };
421
+ }
422
+ else {
423
+ const vehicle = a;
424
+ entry = { check: () => !vehicle.exists || vehicle.wrecked, reason: c ?? "" };
425
+ }
426
+ this.#failWhen.push(entry);
427
+ return {
428
+ stop: () => {
429
+ const i = this.#failWhen.indexOf(entry);
430
+ if (i >= 0)
431
+ this.#failWhen.splice(i, 1);
432
+ },
433
+ };
434
+ }
435
+ /** Fails it for everyone: "MISSION FAILED!" with `reason` under it. */
436
+ fail(reason = "") {
437
+ if (this.running)
438
+ this.#end(false, "failed", reason);
439
+ }
440
+ /**
441
+ * Passes it: each player gets `money` (once) and "MISSION PASSED!" with
442
+ * `text` under it (default: the money, as "$2500"). Refused, with no
443
+ * reward, if it's sooner than `minDuration` or a player doesn't meet
444
+ * `requires` any more.
445
+ */
446
+ pass(reward = {}) {
447
+ if (!this.running)
448
+ return;
449
+ const ms = Date.now() - this.startedAt;
450
+ if (this.definition.minDuration && ms < this.definition.minDuration) {
451
+ console.warn(`[sagl] mission "${this.name}" passed in ${ms} ms (minDuration ${this.definition.minDuration}): refused`);
452
+ this.#end(false, "tooFast", "");
453
+ return;
454
+ }
455
+ for (const p of this.#players) {
456
+ const why = this.definition.requires?.(p);
457
+ if (why) {
458
+ this.#end(false, "requirements", why);
459
+ return;
460
+ }
461
+ }
462
+ const money = Math.max(0, Math.trunc(reward.money ?? 0));
463
+ if (money)
464
+ for (const p of this.#players)
465
+ p.giveMoney(money);
466
+ this.#end(true, "passed", reward.text ?? (money ? `$${money}` : ""));
467
+ }
468
+ // ---- Waiting --------------------------------------------------------------
469
+ /** Resolves after `ms`. */
470
+ wait(ms) {
471
+ this.#live();
472
+ return new Promise((resolve, reject) => {
473
+ const w = { check: () => false, resolve, reject };
474
+ this.#waiters.add(w);
475
+ this.#later(() => {
476
+ if (this.#waiters.delete(w))
477
+ resolve();
478
+ }, ms);
479
+ });
480
+ }
481
+ /**
482
+ * Resolves once `condition()` is true (checked four times a second), with
483
+ * `true`; with `timeout` (ms), with `false` if that passes first. A wait
484
+ * with a limit can't leave a scene stuck when something gets in the way.
485
+ *
486
+ * ```ts
487
+ * if (!await m.until(() => benny.vehicle === cab, { timeout: 8000 })) benny.putInVehicle(cab, 2);
488
+ * ```
489
+ */
490
+ until(condition, options = {}) {
491
+ this.#live();
492
+ return new Promise((resolve, reject) => {
493
+ const w = { check: condition, resolve: () => resolve(true), reject };
494
+ this.#waiters.add(w);
495
+ if (options.timeout !== undefined) {
496
+ this.#later(() => {
497
+ if (this.#waiters.delete(w))
498
+ resolve(false);
499
+ }, Math.max(0, options.timeout));
500
+ }
501
+ });
502
+ }
503
+ step(promise, options = {}) {
504
+ this.#live();
505
+ return new Promise((resolve, reject) => {
506
+ const w = { check: () => false, resolve: () => { }, reject };
507
+ this.#waiters.add(w);
508
+ promise.then((v) => {
509
+ if (this.#waiters.delete(w))
510
+ resolve(v);
511
+ }, (e) => {
512
+ if (this.#waiters.delete(w))
513
+ reject(e);
514
+ });
515
+ if (options.timeout !== undefined) {
516
+ this.#later(() => {
517
+ if (this.#waiters.delete(w))
518
+ resolve(undefined);
519
+ }, Math.max(0, options.timeout));
520
+ }
521
+ });
522
+ }
523
+ // ---- Lifetime helpers ---------------------------------------------------------
524
+ /**
525
+ * Listens to a server event while the mission runs: removed when it ends
526
+ * (however it ends), or earlier with the function it returns.
527
+ *
528
+ * ```ts
529
+ * m.on("playerExitVehicle", (player, vehicle) => { if (vehicle === getaway) ... });
530
+ * ```
531
+ */
532
+ on(event, listener) {
533
+ this.#live();
534
+ this.#server.on(event, listener);
535
+ let on = true;
536
+ const off = () => {
537
+ if (!on)
538
+ return;
539
+ on = false;
540
+ this.#server.off(event, listener);
541
+ const i = this.#listeners.indexOf(off);
542
+ if (i >= 0)
543
+ this.#listeners.splice(i, 1);
544
+ };
545
+ this.#listeners.push(off);
546
+ return off;
547
+ }
548
+ /**
549
+ * Calls `fn` every `ms` while the mission runs (stopped when it ends, or
550
+ * with the function it returns). An error is logged, not fatal.
551
+ */
552
+ every(ms, fn) {
553
+ this.#live();
554
+ const t = setInterval(() => {
555
+ if (!this.running)
556
+ return;
557
+ try {
558
+ fn();
559
+ }
560
+ catch (e) {
561
+ console.error(`[sagl] mission "${this.name}" every() threw:`, e);
562
+ }
563
+ }, Math.max(10, ms));
564
+ this.#intervals.add(t);
565
+ return () => {
566
+ clearInterval(t);
567
+ this.#intervals.delete(t);
568
+ };
569
+ }
570
+ /**
571
+ * Changes the population (peds, traffic, police, fire...) of the leader's
572
+ * world until the mission ends (or the function it returns is called):
573
+ * then the settings it changed are as they were.
574
+ *
575
+ * ```ts
576
+ * const fireBack = m.population({ fire: false }); // no fire engines to the car we torch
577
+ * ```
578
+ */
579
+ population(settings) {
580
+ this.#live();
581
+ const world = this.#server.world(this.leader.world);
582
+ const before = world.population;
583
+ const was = {};
584
+ for (const k of Object.keys(settings))
585
+ was[k] = before[k];
586
+ world.population = settings;
587
+ let done = false;
588
+ const restore = () => {
589
+ if (done)
590
+ return;
591
+ done = true;
592
+ world.population = was;
593
+ const i = this.#restore.indexOf(restore);
594
+ if (i >= 0)
595
+ this.#restore.splice(i, 1);
596
+ };
597
+ this.#restore.push(restore);
598
+ return restore;
599
+ }
600
+ /**
601
+ * A recorded line with its subtitles timed to it (as speech: the radio
602
+ * ducks under it), shown to the party. Resolves when it's been said;
603
+ * stopped if the mission ends. `at` a ped (or vehicle): from them; left
604
+ * out, at the party's ears (`effects: "phone"` for a caller).
605
+ *
606
+ * ```ts
607
+ * await m.say("audio/voice-call2.mp3", { effects: "phone", subtitle: VOICE.call2, prefix: "~y~" });
608
+ * await m.say("audio/benny-my-car.mp3", { at: benny, subtitle: "My car! That's my car!" });
609
+ * ```
610
+ */
611
+ async say(file, options = {}) {
612
+ this.#live();
613
+ const speech = this.#server.audio.speak(file, {
614
+ ...options,
615
+ to: options.to ?? (options.at ? undefined : this.#players),
616
+ subtitleTo: options.subtitleTo ?? this.#players,
617
+ });
618
+ this.#speeches.add(speech);
619
+ try {
620
+ await this.step(speech.done);
621
+ }
622
+ finally {
623
+ this.#speeches.delete(speech);
624
+ }
625
+ }
626
+ /**
627
+ * A cinematic shot for the party (or `players`): black bars, the camera
628
+ * on `shot`, everyone frozen and their weapons put away, while `scene`
629
+ * runs; then the camera's back behind them, they can move and their
630
+ * weapons are back, however the scene ends (the mission ending too).
631
+ * `cut(shot)` changes the shot during it; `cut(null)` back to the first.
632
+ *
633
+ * ```ts
634
+ * await m.cinematic({ position: ACROSS_STREET, lookAt: car.position }, async (cut) => {
635
+ * await m.wait(3000);
636
+ * cut({ position: car.offset({ right: 3 }), lookAt: { vehicle: car } });
637
+ * await m.wait(2000);
638
+ * });
639
+ * ```
640
+ */
641
+ async cinematic(shot, scene, options = {}) {
642
+ this.#live();
643
+ const players = (options.players ?? this.#players).filter((p) => p.connected);
644
+ const giveBack = [];
645
+ for (const p of players) {
646
+ if (options.freeze !== false)
647
+ p.frozen = true;
648
+ if (options.holster !== false)
649
+ giveBack.push(p.holster());
650
+ }
651
+ const cut = (to) => {
652
+ for (const p of players)
653
+ if (p.connected)
654
+ void p.camera.set({ ...(to ?? shot), widescreen: true });
655
+ };
656
+ cut(null);
657
+ let restored = false;
658
+ const restore = () => {
659
+ if (restored)
660
+ return;
661
+ restored = true;
662
+ for (const p of players) {
663
+ if (!p.connected)
664
+ continue;
665
+ p.camera.reset();
666
+ if (options.freeze !== false)
667
+ p.frozen = false;
668
+ }
669
+ for (const g of giveBack)
670
+ g();
671
+ const i = this.#restore.indexOf(restore);
672
+ if (i >= 0)
673
+ this.#restore.splice(i, 1);
674
+ };
675
+ this.#restore.push(restore);
676
+ try {
677
+ await scene(cut);
678
+ }
679
+ finally {
680
+ restore();
681
+ }
682
+ }
683
+ /**
684
+ * Marks where the script is (logged, and a `missionEvent` of type
685
+ * `"stage"`). Steps (`steps`) do it themselves.
686
+ */
687
+ stage(name) {
688
+ this.#stage = name;
689
+ console.log(`[mission] ${this.name} (${this.leader?.name ?? "?"}): ${name}`);
690
+ this.#missions.event({ type: "stage", name: this.name, mission: this, player: null, reason: name });
691
+ }
692
+ /** The stage (or step) it's at. */
693
+ get currentStage() {
694
+ return this.#stage;
695
+ }
696
+ /** @internal Starts the steps from this one (`missions.start(..., { from })`). */
697
+ startFrom(step) {
698
+ this.#startFrom = step;
699
+ }
700
+ /** Resolves once every one of these peds is dead or gone. */
701
+ allDead(peds) {
702
+ return this.until(() => peds.every((p) => !p.exists || p.dead)).then(() => { });
703
+ }
704
+ /**
705
+ * Waits for the party to reach a spot: the first player (the default), or
706
+ * all of them (`allPlayers`, a waiting point: each counts once they've
707
+ * been there, and the rest are waited for). Shows it with a blip and the
708
+ * game's marker meanwhile, for those who haven't reached it. Resolves
709
+ * with the player who completed it (the last, for `allPlayers`).
710
+ */
711
+ reach(spot, options = {}) {
712
+ this.#live();
713
+ const radius = options.radius ?? 3;
714
+ const reached = new Set();
715
+ const at = (p) => {
716
+ if (reached.has(p))
717
+ return true;
718
+ if (!p.alive || dist(p.position, spot) > radius)
719
+ return false;
720
+ if (options.vehicle && p.vehicle !== options.vehicle)
721
+ return false;
722
+ reached.add(p);
723
+ return true;
724
+ };
725
+ const markerOptions = options.marker === false ? null : options.marker === true || !options.marker ? {} : options.marker;
726
+ const blip = markerOptions ? this.blip(spot, markerOptions) : null;
727
+ return this.#party(at, options, (done) => {
728
+ if (blip)
729
+ for (const p of done)
730
+ blip.drop(p);
731
+ }).finally(() => blip?.remove());
732
+ }
733
+ /** Waits for the party to get in a vehicle: the first player (the default), or all of them at once. */
734
+ enter(vehicle, options = {}) {
735
+ this.#live();
736
+ return this.#party((p) => p.vehicle === vehicle && (!options.driver || vehicle.driver === p), options);
737
+ }
738
+ /**
739
+ * Waits until `condition(player)` holds for the first player (the default),
740
+ * or for every player at once (`allPlayers`). Resolves with that player.
741
+ */
742
+ untilPlayer(condition, options = {}) {
743
+ this.#live();
744
+ return this.#party(condition, options);
745
+ }
746
+ /**
747
+ * Keeps the party near someone (a ped to escort or follow, a vehicle):
748
+ * a player further than `warnAt` (m) gets `warning` instead of the
749
+ * objective until they're back in range (then the objective returns);
750
+ * with `failAt`, the mission fails (`fail`) when every player is further
751
+ * than that. Lasts until the mission ends or `stop()`.
752
+ */
753
+ keepClose(target, options = {}) {
754
+ this.#live();
755
+ const warnAt = options.warnAt ?? 25;
756
+ const warning = options.warning ?? "You're too far away. Go back.";
757
+ const warned = new Set();
758
+ let stopped = false;
759
+ const distance = (p) => {
760
+ const t = target.position, q = p.position;
761
+ return Math.hypot(t.x - q.x, t.y - q.y, t.z - q.z);
762
+ };
763
+ const check = () => {
764
+ if (stopped || !this.running)
765
+ return;
766
+ for (const p of this.#players) {
767
+ const far = distance(p) > warnAt;
768
+ if (far && !warned.has(p)) {
769
+ warned.add(p);
770
+ this.#warned.add(p);
771
+ p.hud.objective(warning, 60_000);
772
+ }
773
+ else if (!far && warned.has(p)) {
774
+ warned.delete(p);
775
+ this.#warned.delete(p);
776
+ if (this.#objective)
777
+ p.hud.objective(this.#objective.text, Math.max(1, this.#objective.until - Date.now()));
778
+ else
779
+ p.hud.clearObjective();
780
+ }
781
+ }
782
+ for (const p of [...warned]) {
783
+ if (!this.#players.includes(p)) {
784
+ warned.delete(p);
785
+ this.#warned.delete(p);
786
+ }
787
+ }
788
+ this.#later(check, 500);
789
+ };
790
+ if (options.failAt !== undefined) {
791
+ const failAt = options.failAt;
792
+ this.failWhen(() => !stopped && this.#players.length > 0 && this.#players.every((p) => distance(p) > failAt), options.fail ?? "You left them behind.");
793
+ }
794
+ check();
795
+ return {
796
+ stop: () => {
797
+ stopped = true;
798
+ for (const p of warned) {
799
+ this.#warned.delete(p);
800
+ if (this.#objective)
801
+ p.hud.objective(this.#objective.text, Math.max(1, this.#objective.until - Date.now()));
802
+ else
803
+ p.hud.clearObjective();
804
+ }
805
+ warned.clear();
806
+ },
807
+ };
808
+ }
809
+ /** True if `key` was used in the last `ms` (and starts it again if not): throttles repeated actions. */
810
+ cooldown(key, ms) {
811
+ const now = Date.now();
812
+ const until = this.#cooldowns.get(key) ?? 0;
813
+ if (now < until)
814
+ return true;
815
+ this.#cooldowns.set(key, now + ms);
816
+ return false;
817
+ }
818
+ /**
819
+ * Plays one of the game's cutscenes (e.g. `"ryder2a"`) for the party
820
+ * together; resolves when everyone's has finished. The leader's skip ends
821
+ * it for everyone (see `CutsceneOptions`). Ending the mission stops it.
822
+ */
823
+ cutscene(name, options = {}) {
824
+ this.#live();
825
+ this.#cutscene?.stop();
826
+ const play = this.#server.playCutscene(this.#players, name, options);
827
+ this.#cutscene = play;
828
+ return this.step(play.done).finally(() => {
829
+ if (this.#cutscene === play)
830
+ this.#cutscene = null;
831
+ });
832
+ }
833
+ // ---- Sections -------------------------------------------------------------
834
+ /**
835
+ * Starts a section: where the party respawns if they die, and where
836
+ * anyone far behind (more than `catchUpDistance`, or in another world)
837
+ * is brought, faded out and back in. Resolves once they're there.
838
+ */
839
+ async section(name, options) {
840
+ this.#live();
841
+ const spots = Array.isArray(options.at) ? options.at : [options.at];
842
+ if (spots.length === 0)
843
+ throw new RangeError(`section "${name}": no spots`);
844
+ this.#spawn = { at: spots, heading: options.heading ?? 0 };
845
+ const far = options.catchUpDistance ?? this.definition.catchUpDistance ?? 150;
846
+ const world = this.leader.world;
847
+ const behind = this.#players.filter((p, i) => p.alive && (p.world !== world || dist(p.position, spots[i % spots.length]) > far));
848
+ await Promise.all(behind.map((p) => this.#bring(p, 500)));
849
+ }
850
+ // ---- Internals ------------------------------------------------------------
851
+ /** @internal Runs the script. */
852
+ async run() {
853
+ for (const p of this.#players)
854
+ this.#missions.bind(p, this);
855
+ try {
856
+ const steps = this.definition.steps;
857
+ if (steps?.length) {
858
+ const state = this.definition.state ? this.definition.state() : {};
859
+ let i = this.#startFrom ? steps.findIndex((s) => s.name === this.#startFrom) : 0;
860
+ if (i < 0)
861
+ throw new RangeError(`mission "${this.name}" has no step "${this.#startFrom}"`);
862
+ if (i > 0)
863
+ await steps[i].setup?.(this, state);
864
+ for (; i < steps.length && this.running; ++i) {
865
+ this.stage(steps[i].name);
866
+ await steps[i].run(this, state);
867
+ }
868
+ }
869
+ else {
870
+ await this.definition.run?.(this);
871
+ }
872
+ if (this.running)
873
+ this.pass();
874
+ }
875
+ catch (e) {
876
+ if (e instanceof MissionEnded)
877
+ return;
878
+ console.error(`[sagl] mission "${this.name}" threw:`, e);
879
+ if (this.running)
880
+ this.#end(false, "error", "");
881
+ }
882
+ }
883
+ /** @internal A party member respawned: back to the section start. */
884
+ respawned(player) {
885
+ // After the gamemode's own playerSpawn handlers, which may place them.
886
+ if (this.running && this.#spawn)
887
+ this.#later(() => void this.#bring(player, 0), 100);
888
+ }
889
+ /** @internal A party member died. */
890
+ died(player) {
891
+ if (!this.running)
892
+ return;
893
+ if (this.definition.deathFails) {
894
+ this.#end(false, "failed", `${player.name} died.`);
895
+ }
896
+ else if (this.#players.every((p) => p === player || !p.alive)) {
897
+ this.#end(false, "failed", "Everyone died.");
898
+ }
899
+ }
900
+ // Faded out, moved to their section spot, faded in.
901
+ async #bring(player, fadeMs) {
902
+ if (!this.#spawn)
903
+ return;
904
+ const i = Math.max(0, this.#players.indexOf(player));
905
+ const spot = this.#spawn.at[i % this.#spawn.at.length];
906
+ if (fadeMs)
907
+ await player.fadeOut(fadeMs);
908
+ if (!player.connected)
909
+ return;
910
+ player.world = this.leader?.world ?? player.world;
911
+ player.setPosition(spot, this.#spawn.heading);
912
+ await new Promise((r) => setTimeout(r, 300)); // the world streams in
913
+ await player.fadeIn(fadeMs || 300);
914
+ }
915
+ #party(condition, options, progress) {
916
+ const all = options.allPlayers === true;
917
+ let last = null;
918
+ return new Promise((resolve, reject) => {
919
+ const w = {
920
+ check: () => {
921
+ const done = this.#players.filter(condition);
922
+ progress?.(done);
923
+ if (done.length === 0)
924
+ return false;
925
+ if (all && done.length < this.#players.length)
926
+ return false;
927
+ last = done[done.length - 1];
928
+ return true;
929
+ },
930
+ resolve: () => resolve(last),
931
+ reject,
932
+ };
933
+ this.#waiters.add(w);
934
+ });
935
+ }
936
+ #wait(check) {
937
+ return new Promise((resolve, reject) => {
938
+ this.#waiters.add({ check, resolve, reject });
939
+ });
940
+ }
941
+ #check() {
942
+ if (!this.running)
943
+ return;
944
+ for (const f of this.#failWhen) {
945
+ let hit = false;
946
+ try {
947
+ hit = f.check();
948
+ }
949
+ catch (e) {
950
+ console.error(`[sagl] mission "${this.name}" fail condition threw:`, e);
951
+ }
952
+ if (hit) {
953
+ this.#end(false, "failed", f.reason);
954
+ return;
955
+ }
956
+ }
957
+ for (const w of [...this.#waiters]) {
958
+ let ok = false;
959
+ try {
960
+ ok = w.check();
961
+ }
962
+ catch (e) {
963
+ this.#waiters.delete(w);
964
+ w.reject(e);
965
+ continue;
966
+ }
967
+ if (ok && this.#waiters.delete(w))
968
+ w.resolve();
969
+ }
970
+ }
971
+ #later(fn, ms) {
972
+ const t = setTimeout(() => {
973
+ this.#timers.delete(t);
974
+ fn();
975
+ }, ms);
976
+ this.#timers.add(t);
977
+ }
978
+ #live() {
979
+ if (this.#outcome)
980
+ throw new MissionEnded(this.#outcome);
981
+ }
982
+ // A joining player gets the HUD as it is.
983
+ #show(player) {
984
+ for (const b of this.#blips)
985
+ b.add(player);
986
+ for (const c of this.#counters)
987
+ c.add(player);
988
+ if (this.#objective && this.#objective.until > Date.now()) {
989
+ player.hud.objective(this.#objective.text, this.#objective.until - Date.now());
990
+ }
991
+ }
992
+ #hide(player) {
993
+ if (!player.connected)
994
+ return;
995
+ for (const b of this.#blips)
996
+ b.drop(player);
997
+ for (const c of this.#counters)
998
+ c.drop(player);
999
+ player.hud.clearObjective();
1000
+ }
1001
+ #updateVisibility() {
1002
+ for (const { ped, partyVisible } of this.#peds) {
1003
+ if (partyVisible && ped.exists)
1004
+ ped.visibleTo = this.#players;
1005
+ }
1006
+ }
1007
+ #end(passed, reason, text) {
1008
+ if (this.#outcome)
1009
+ return;
1010
+ const outcome = { passed, reason, text, players: [...this.#players], ms: Date.now() - this.startedAt };
1011
+ this.#outcome = outcome;
1012
+ clearInterval(this.#tick);
1013
+ this.#cutscene?.stop();
1014
+ for (const off of [...this.#listeners])
1015
+ off();
1016
+ for (const t of this.#intervals)
1017
+ clearInterval(t);
1018
+ this.#intervals.clear();
1019
+ for (const sp of this.#speeches)
1020
+ sp.stop();
1021
+ this.#speeches.clear();
1022
+ for (const r of [...this.#restore].reverse()) {
1023
+ try {
1024
+ r();
1025
+ }
1026
+ catch (e) {
1027
+ console.error(`[sagl] mission "${this.name}" cleanup threw:`, e);
1028
+ }
1029
+ }
1030
+ for (const t of this.#timers)
1031
+ clearTimeout(t);
1032
+ this.#timers.clear();
1033
+ const ended = new MissionEnded(outcome);
1034
+ for (const w of this.#waiters)
1035
+ w.reject(ended);
1036
+ this.#waiters.clear();
1037
+ // Cleanup: its HUD, and what it made.
1038
+ for (const b of this.#blips)
1039
+ b.remove();
1040
+ for (const c of this.#counters)
1041
+ c.remove();
1042
+ // Its characters go (fading out); bodies lie where they fell until
1043
+ // the cleanup takes them, and vehicles stay until nobody is around, as
1044
+ // in the game. `keep` ones stay for good.
1045
+ for (const { ped, onEnd } of this.#peds) {
1046
+ if (!ped.exists)
1047
+ continue;
1048
+ if (onEnd === "keep")
1049
+ ped.visibleTo = "world";
1050
+ else if (ped.dead && onEnd === "release")
1051
+ ped.release();
1052
+ else
1053
+ ped.destroy();
1054
+ }
1055
+ for (const { vehicle, onEnd } of this.#vehicles) {
1056
+ if (!vehicle.exists || onEnd === "keep")
1057
+ continue;
1058
+ if (onEnd === "destroy")
1059
+ vehicle.destroy();
1060
+ else
1061
+ vehicle.release();
1062
+ }
1063
+ for (const o of this.#objects)
1064
+ o.destroy();
1065
+ for (const p of this.#pickups)
1066
+ p.remove();
1067
+ for (const p of this.#players) {
1068
+ if (!p.connected)
1069
+ continue;
1070
+ p.hud.clearObjective();
1071
+ if (passed)
1072
+ p.hud.missionPassed(text);
1073
+ else if (reason !== "abandoned")
1074
+ p.hud.missionFailed(text);
1075
+ this.#missions.bind(p, null);
1076
+ }
1077
+ this.#missions.finished(this, outcome);
1078
+ this.#resolveDone(outcome);
1079
+ }
1080
+ }
1081
+ /** A spot that starts a mission when a player walks into it (`server.missions.marker()`). */
1082
+ export class MissionMarker {
1083
+ definition;
1084
+ position;
1085
+ #remove;
1086
+ /** @internal */
1087
+ constructor(definition, position, remove) {
1088
+ this.definition = definition;
1089
+ this.position = position;
1090
+ this.#remove = remove;
1091
+ }
1092
+ remove() {
1093
+ this.#remove();
1094
+ }
1095
+ }
1096
+ /**
1097
+ * Starts and tracks missions (`server.missions`). A player is in one
1098
+ * mission at a time.
1099
+ */
1100
+ export class Missions {
1101
+ #server;
1102
+ #emit;
1103
+ #of = new Map();
1104
+ #running = new Set();
1105
+ // Cooldowns by mission name, then player name (names are the closest thing
1106
+ // to an identity until players have accounts).
1107
+ #passedAt = new Map();
1108
+ /** @internal */
1109
+ constructor(server, emit) {
1110
+ this.#server = server;
1111
+ this.#emit = emit;
1112
+ server.on("playerQuit", (p) => this.#of.get(p)?.removePlayer(p));
1113
+ server.on("playerDeath", (p) => this.#of.get(p)?.died(p));
1114
+ server.on("playerSpawn", (p) => this.#of.get(p)?.respawned(p));
1115
+ }
1116
+ /** The missions running now. */
1117
+ get running() {
1118
+ return [...this.#running];
1119
+ }
1120
+ /** The mission a player is in, or null. */
1121
+ of(player) {
1122
+ return this.#of.get(player) ?? null;
1123
+ }
1124
+ /**
1125
+ * Starts a mission for these players (the first is the leader). Throws
1126
+ * `MissionRefused` if any of them is in a mission, cooling down or
1127
+ * doesn't meet `requires`, or there are too many.
1128
+ */
1129
+ start(definition, players, options = {}) {
1130
+ if (options.from !== undefined && !definition.steps?.some((st) => st.name === options.from)) {
1131
+ throw new RangeError(`mission "${definition.name}" has no step "${options.from}"`);
1132
+ }
1133
+ const party = Array.isArray(players) ? [...players] : [players];
1134
+ if (party.length === 0)
1135
+ throw new RangeError("a mission needs a player");
1136
+ if (party.length > (definition.maxPlayers ?? 1)) {
1137
+ this.refuse(definition, party[0], `At most ${definition.maxPlayers ?? 1} players.`);
1138
+ }
1139
+ for (const p of party)
1140
+ this.admit(definition, p);
1141
+ const m = new Mission(this, this.#server, definition, party);
1142
+ m.startFrom(options.from ?? null);
1143
+ this.#running.add(m);
1144
+ for (const p of party)
1145
+ this.#of.set(p, m);
1146
+ this.event({ type: "start", name: definition.name, mission: m, player: party[0], reason: "" });
1147
+ void m.run();
1148
+ return m;
1149
+ }
1150
+ /**
1151
+ * A start marker: a blip and the game's yellow marker for everyone not in
1152
+ * a mission. Walking into it on foot starts the mission with everyone
1153
+ * else standing in it (up to `maxPlayers`).
1154
+ */
1155
+ marker(definition, at, options = {}) {
1156
+ const radius = options.radius ?? 1.5;
1157
+ const world = options.world ?? 0;
1158
+ const blips = new Map();
1159
+ const inside = new Set();
1160
+ const refused = options.onRefused ?? ((p, why) => p.hud.objective(why, 3000));
1161
+ const tick = setInterval(() => {
1162
+ for (const p of this.#server.players) {
1163
+ const free = !this.#of.has(p) && p.world === world;
1164
+ const blip = blips.get(p);
1165
+ if (options.blip !== false && free && !blip)
1166
+ blips.set(p, p.hud.blip(at, options.blip || {}));
1167
+ if (blip && !free) {
1168
+ blip.remove();
1169
+ blips.delete(p);
1170
+ }
1171
+ // Only walking in starts it: whoever is still standing in it when a
1172
+ // mission ends (or after being refused) has to step out first.
1173
+ if (p.world !== world || dist(p.position, at) > radius) {
1174
+ inside.delete(p);
1175
+ continue;
1176
+ }
1177
+ if (inside.has(p))
1178
+ continue;
1179
+ if (!p.alive || p.vehicle)
1180
+ continue; // getting out in it counts as walking in
1181
+ inside.add(p);
1182
+ if (!free)
1183
+ continue;
1184
+ const others = this.#server.players.filter((o) => o !== p && !this.#of.has(o) && o.world === world &&
1185
+ o.alive && !o.vehicle && dist(o.position, at) <= radius);
1186
+ const party = [p, ...others].slice(0, definition.maxPlayers ?? 1);
1187
+ for (const o of party)
1188
+ inside.add(o);
1189
+ try {
1190
+ this.start(definition, party);
1191
+ }
1192
+ catch (e) {
1193
+ if (e instanceof MissionRefused)
1194
+ refused(p, e.reason);
1195
+ else
1196
+ throw e;
1197
+ }
1198
+ }
1199
+ for (const p of [...blips.keys()])
1200
+ if (!p.connected)
1201
+ blips.delete(p);
1202
+ for (const p of [...inside])
1203
+ if (!p.connected)
1204
+ inside.delete(p);
1205
+ }, TICK_MS);
1206
+ return new MissionMarker(definition, at, () => {
1207
+ clearInterval(tick);
1208
+ for (const b of blips.values())
1209
+ b.remove();
1210
+ blips.clear();
1211
+ });
1212
+ }
1213
+ /** @internal Throws MissionRefused if the player can't start or join it. */
1214
+ admit(definition, player) {
1215
+ if (this.#of.has(player))
1216
+ this.refuse(definition, player, "You're already on a mission.");
1217
+ const passed = this.#passedAt.get(definition.name)?.get(player.name);
1218
+ if (definition.cooldown && passed !== undefined && Date.now() - passed < definition.cooldown) {
1219
+ const minutes = Math.ceil((definition.cooldown - (Date.now() - passed)) / 60000);
1220
+ this.refuse(definition, player, `Try again in ${minutes} minute${minutes === 1 ? "" : "s"}.`);
1221
+ }
1222
+ const why = definition.requires?.(player);
1223
+ if (why)
1224
+ this.refuse(definition, player, why);
1225
+ }
1226
+ /** @internal */
1227
+ refuse(definition, player, reason) {
1228
+ this.event({ type: "refused", name: definition.name, mission: null, player, reason });
1229
+ throw new MissionRefused(reason);
1230
+ }
1231
+ /** @internal */
1232
+ bind(player, mission) {
1233
+ if (mission)
1234
+ this.#of.set(player, mission);
1235
+ else
1236
+ this.#of.delete(player);
1237
+ }
1238
+ /** @internal */
1239
+ finished(mission, outcome) {
1240
+ this.#running.delete(mission);
1241
+ if (outcome.passed) {
1242
+ const byName = this.#passedAt.get(mission.name) ?? new Map();
1243
+ for (const p of outcome.players)
1244
+ byName.set(p.name, Date.now());
1245
+ this.#passedAt.set(mission.name, byName);
1246
+ }
1247
+ this.event({ type: outcome.passed ? "pass" : "fail", name: mission.name, mission, player: null,
1248
+ reason: outcome.passed ? "" : outcome.reason === "failed" ? outcome.text || "failed" : outcome.reason });
1249
+ }
1250
+ /** @internal */
1251
+ event(e) {
1252
+ this.#emit(e);
1253
+ }
1254
+ }
1255
+ function dist(a, b) {
1256
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
1257
+ }
1258
+ //# sourceMappingURL=mission.js.map