@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
package/dist/player.js ADDED
@@ -0,0 +1,1163 @@
1
+ import { encodeEffects } from "./audio_effects.js";
2
+ import { GameMap, mapColor } from "./map.js";
3
+ import { PlayerHud } from "./hud.js";
4
+ import { PlayerSplash } from "./splash.js";
5
+ import { PlayerCamera } from "./camera.js";
6
+ import { PlayerMenus } from "./menu.js";
7
+ import { ScreenEffects } from "./screeneffects.js";
8
+ import { ScreenDraw } from "./draw.js";
9
+ import { PlayerUi } from "./ui.js";
10
+ import { speechOf } from "./ped.js";
11
+ import { Key } from "./constants.js";
12
+ import { specialPresets, kSpecialSlot } from "./special.js";
13
+ import { native } from "./native.js";
14
+ export class Player {
15
+ /** Slot number, unique among connected players (reused after they leave). */
16
+ id;
17
+ /** Their name: as given when joining, or `setName`'s. */
18
+ get name() {
19
+ return this.#state()?.name ?? this.#name;
20
+ }
21
+ #name;
22
+ /**
23
+ * Gives them another name (SA-MP's SetPlayerName): 3-24 letters, digits
24
+ * and `_[]().$@=`, and not another player's (any case). Everyone's game
25
+ * shows it. **Returns** false if it can't be theirs.
26
+ */
27
+ setName(name) {
28
+ if (!this.#connected || !this.#native.renamePlayer(this.id, name))
29
+ return false;
30
+ this.#name = name;
31
+ return true;
32
+ }
33
+ /**
34
+ * Watches another player or a vehicle (SA-MP's TogglePlayerSpectating
35
+ * with PlayerSpectatePlayer / PlayerSpectateVehicle): no one sees them
36
+ * meanwhile, and their camera follows the target (a player's vehicle
37
+ * while they're in one). Another target switches to it; null ends it, and
38
+ * they're back where they were.
39
+ *
40
+ * ```ts
41
+ * admin.spectate(suspect);
42
+ * admin.spectate(null);
43
+ * ```
44
+ */
45
+ spectate(target) {
46
+ if (!this.#connected || target === this)
47
+ return;
48
+ this.#spectating = target;
49
+ const kind = target === null ? 0 : target instanceof Player ? 1 : 2;
50
+ this.#native.spectate(this.id, kind, target?.id ?? 0);
51
+ }
52
+ /**
53
+ * Whether `viewer` sees them at all (their character, name, blip):
54
+ * hidden, they aren't in the viewer's game. For scenes each player plays
55
+ * alone in the same spot (each sees only themselves answer a phone).
56
+ * Default visible.
57
+ */
58
+ setVisibleFor(viewer, visible) {
59
+ if (this.#connected && viewer !== this && viewer.connected)
60
+ this.#native.setVisibleFor(this.id, viewer.id, visible);
61
+ }
62
+ /** Who or what they watch (`spectate`), or null. */
63
+ get spectating() {
64
+ return this.#spectating;
65
+ }
66
+ #spectating = null;
67
+ /**
68
+ * Their own weather (`Weather`, 0-22; -1 the game's own cycle), over
69
+ * their world's (SA-MP's SetPlayerWeather); null: their world's again.
70
+ */
71
+ get weather() {
72
+ return this.#weather;
73
+ }
74
+ set weather(weather) {
75
+ if (weather !== null && (!Number.isInteger(weather) || weather < -1 || weather > 22)) {
76
+ throw new RangeError(`not a weather: ${weather} (0-22, -1)`);
77
+ }
78
+ this.#weather = weather;
79
+ if (this.#connected)
80
+ this.#native.setPlayerWeather(this.id, weather);
81
+ }
82
+ #weather = null;
83
+ /**
84
+ * A box on the map they can't leave (SA-MP's SetPlayerWorldBounds, e.g. a
85
+ * prison yard): their game pushes them back in, on foot or driving.
86
+ * Null: free again.
87
+ *
88
+ * ```ts
89
+ * player.worldBounds = { minX: 101.6, maxX: 337.6, minY: 1798.7, maxY: 1941 };
90
+ * ```
91
+ */
92
+ get worldBounds() {
93
+ return this.#bounds ? { ...this.#bounds } : null;
94
+ }
95
+ set worldBounds(b) {
96
+ if (b && (!(b.minX < b.maxX) || !(b.minY < b.maxY)))
97
+ throw new RangeError("worldBounds: min must be below max");
98
+ this.#bounds = b ? { ...b } : null;
99
+ if (this.#connected)
100
+ this.#native.worldBounds(this.id, !!b, b?.minX ?? 0, b?.maxX ?? 0, b?.minY ?? 0, b?.maxY ?? 0);
101
+ }
102
+ #bounds = null;
103
+ /**
104
+ * Plays them the police scanner reporting a crime (SA-MP's
105
+ * PlayCrimeReportForPlayer): `crime` is the game's (`Crime`), `suspect`
106
+ * a player or a spot (the scanner names its area).
107
+ *
108
+ * ```ts
109
+ * for (const cop of cops) cop.playCrimeReport(robber, Crime.FireWeapon);
110
+ * ```
111
+ */
112
+ playCrimeReport(suspect, crime) {
113
+ if (!Number.isInteger(crime) || crime < 1 || crime > 22)
114
+ throw new RangeError(`not a crime: ${crime} (1-22)`);
115
+ const at = suspect instanceof Player ? suspect.position : suspect;
116
+ if (this.#connected)
117
+ this.#native.crimeReport(this.id, crime, at.x, at.y, at.z);
118
+ }
119
+ /**
120
+ * The game's stunt bonuses for them: wheelies, stoppies, two wheels and
121
+ * insane stunt jumps (as SA-MP's EnableStuntBonusForPlayer). Off, their
122
+ * game gives neither the message nor the money. Default on, or as
123
+ * `server.stuntBonus`.
124
+ */
125
+ get stuntBonus() {
126
+ return this.#stuntBonus;
127
+ }
128
+ set stuntBonus(on) {
129
+ this.#stuntBonus = on;
130
+ if (this.#connected)
131
+ this.#native.setStuntBonus(this.id, on);
132
+ }
133
+ #stuntBonus = true;
134
+ /** A number by their name in the player list (SetPlayerScore); shown once anyone's isn't 0. */
135
+ get score() {
136
+ return this.#score;
137
+ }
138
+ set score(score) {
139
+ this.#score = Math.trunc(score) | 0;
140
+ if (this.#connected)
141
+ this.#native.setScore(this.id, this.#score);
142
+ }
143
+ #score = 0;
144
+ /** Free-form per-player storage for gamemode data. */
145
+ data = {};
146
+ #native;
147
+ #vehicle;
148
+ #connected = true;
149
+ /** @internal Created by Server; gamemodes receive Player objects from events. */
150
+ constructor(nativeServer, id, vehicle) {
151
+ this.#native = nativeServer;
152
+ this.#vehicle = vehicle;
153
+ this.id = id;
154
+ this.#name = this.#state()?.name ?? `player${id}`;
155
+ }
156
+ /** False once the player has left. */
157
+ get connected() {
158
+ return this.#connected;
159
+ }
160
+ /** @internal */
161
+ markDisconnected() {
162
+ this.#connected = false;
163
+ this.#camera?.disconnected();
164
+ this.#ui?.disconnected();
165
+ }
166
+ #state() {
167
+ return this.#connected ? this.#native.player(this.id) : null;
168
+ }
169
+ /**
170
+ * The player's IP address. Through a trusted proxy (proxy mode), the one
171
+ * the proxy forwarded rather than the proxy's own.
172
+ */
173
+ get ip() {
174
+ return this.#state()?.ip ?? "";
175
+ }
176
+ /** True when the player came through a trusted proxy. */
177
+ get viaProxy() {
178
+ return this.#state()?.viaProxy ?? false;
179
+ }
180
+ /**
181
+ * The address the player connected to, as they typed it (e.g.
182
+ * "race.example.com" and 7777), for telling apart servers that share an IP.
183
+ */
184
+ get connectHost() {
185
+ return this.#state()?.connectHost ?? "";
186
+ }
187
+ get connectPort() {
188
+ return this.#state()?.connectPort ?? 0;
189
+ }
190
+ /** Round-trip time in milliseconds. */
191
+ get ping() {
192
+ return this.#state()?.ping ?? 0;
193
+ }
194
+ /** True while the player is alive in the world (as their client reports). */
195
+ /**
196
+ * Whether they're within `radius` m of `position` (and, `maxDz`, no more
197
+ * than that above or below it): the check before trusting a client
198
+ * script that says its player is at something.
199
+ */
200
+ isNear(position, radius, maxDz = 3) {
201
+ const p = this.position;
202
+ return Math.hypot(p.x - position.x, p.y - position.y) <= radius && Math.abs(p.z - position.z) <= maxDz;
203
+ }
204
+ get alive() {
205
+ return this.#state()?.alive ?? false;
206
+ }
207
+ /** True while the player has the pause menu open. The game keeps running. */
208
+ get paused() {
209
+ return this.#state()?.paused ?? false;
210
+ }
211
+ /** False while the player's game window is in the background (alt-tabbed). */
212
+ get focused() {
213
+ return this.#state()?.focused ?? true;
214
+ }
215
+ /** The vehicle they are in (driving or as a passenger), or null. */
216
+ get vehicle() {
217
+ const id = this.#state()?.vehicleId;
218
+ return id === null || id === undefined ? null : this.#vehicle(id) ?? null;
219
+ }
220
+ /**
221
+ * The vehicle they last drove, while it exists. Ambient cleanup leaves it
222
+ * alone while they're connected (unless it's a wreck); destroy it yourself
223
+ * to get rid of it.
224
+ */
225
+ get lastVehicle() {
226
+ const id = this.#state()?.lastVehicleId;
227
+ return id === null || id === undefined ? null : this.#vehicle(id) ?? null;
228
+ }
229
+ /** The vehicle they remote-control (`remoteControl()`), or null. */
230
+ get remoteVehicle() {
231
+ const id = this.#state()?.remoteVehicleId;
232
+ return id === null || id === undefined ? null : this.#vehicle(id) ?? null;
233
+ }
234
+ /**
235
+ * Remote control, as with an RC vehicle: they drive `vehicle` (nobody in
236
+ * it, in their world) with their own controls from where they stand, the
237
+ * camera following it, until they press the enter/exit key, it blows up,
238
+ * or this is called with null. `detonate`: fire blows it up (and an RC
239
+ * Bandit blows up touching a car), as in the game's RC missions. Works
240
+ * with any vehicle, not just RC ones. False if it can't be given (they're
241
+ * in a vehicle, someone is in it, another world, a wreck).
242
+ */
243
+ remoteControl(vehicle, options = {}) {
244
+ if (!this.#connected)
245
+ return false;
246
+ return this.#native.setRemoteControl(this.id, vehicle?.id ?? 0, options.detonate ?? false);
247
+ }
248
+ /** Their seat in `vehicle`: 0 is the driver's, 1-8 passengers'. null on foot. */
249
+ get seat() {
250
+ return this.#state()?.seat ?? null;
251
+ }
252
+ /** Ped model id (0 is CJ). Setting it changes the model for everyone. */
253
+ get skin() {
254
+ return this.#state()?.skin ?? 0;
255
+ }
256
+ set skin(model) {
257
+ if (!native.isValidSkin(model)) {
258
+ throw new RangeError(`not a valid ped model: ${model}`);
259
+ }
260
+ if (this.#connected)
261
+ this.#native.setSkin(this.id, model);
262
+ }
263
+ /**
264
+ * Money, as the server keeps it. The player's game shows exactly this;
265
+ * anything their game changes by itself is undone.
266
+ */
267
+ get money() {
268
+ return this.#state()?.money ?? 0;
269
+ }
270
+ set money(value) {
271
+ if (!Number.isFinite(value))
272
+ throw new RangeError(`not a number: ${value}`);
273
+ if (this.#connected)
274
+ this.#native.setMoney(this.id, Math.trunc(value));
275
+ }
276
+ /** This player's own additions to the pause menu's map (see `GameMap`). */
277
+ get map() {
278
+ return (this.#map ??= new GameMap(this.#native, this.id));
279
+ }
280
+ #map;
281
+ /** Full-screen effects (your shaders) on their screen. See `ScreenEffects`. */
282
+ get screenEffects() {
283
+ return (this.#screenEffects ??= new ScreenEffects(this.#native, this, () => []));
284
+ }
285
+ #screenEffects;
286
+ /** Their camera: fixed shots, glides between them, following something, or back behind them. See `PlayerCamera`. */
287
+ get camera() {
288
+ return (this.#camera ??= new PlayerCamera(this.#native, this.id));
289
+ }
290
+ #camera;
291
+ /**
292
+ * Their splash screen: the loading screen kept up (or shown again) while
293
+ * you set the scene, their game's sound muted. See `PlayerSplash`.
294
+ */
295
+ get splash() {
296
+ return (this.#splash ??= new PlayerSplash(this.#native, this.id, false));
297
+ }
298
+ #splash;
299
+ /** @internal Joined with the splash up (the server option `splash`). */
300
+ holdSplash() {
301
+ return (this.#splash = new PlayerSplash(this.#native, this.id, true));
302
+ }
303
+ /** Their mission HUD: radar blips, objective text, timers and counters, result screens. */
304
+ get hud() {
305
+ return (this.#hud ??= new PlayerHud(this.#native, this.id));
306
+ }
307
+ #hud;
308
+ /** Drawing on their screen, SA-MP textdraw style: see `ScreenDraw`. */
309
+ get draw() {
310
+ let next = 1;
311
+ return (this.#draw ??= new ScreenDraw((spec) => { if (this.#connected)
312
+ this.#native.draw(this.id, spec); }, () => next++));
313
+ }
314
+ #draw;
315
+ /** UI documents (RML client files) the server opens on their screen: see `PlayerUi`. */
316
+ get ui() {
317
+ return (this.#ui ??= new PlayerUi(this.#native, this));
318
+ }
319
+ #ui;
320
+ /**
321
+ * Sends data (anything JSON can hold) on a channel to this player's client
322
+ * scripts, where `sagl.on(channel, fn)` gets it. See `server.channel()`.
323
+ */
324
+ emit(channel, data) {
325
+ if (!channel || channel.length > 64)
326
+ throw new RangeError("channel names are 1-64 characters");
327
+ const json = JSON.stringify(data ?? null);
328
+ if (json === undefined)
329
+ throw new TypeError("channel data must be JSON-serialisable");
330
+ if (this.#connected)
331
+ this.#native.sendScriptMessage(this.id, channel, json);
332
+ }
333
+ /**
334
+ * Wanted level, 0-6 stars, as the server keeps it; their game shows it
335
+ * and cops respond. Setting it sets it (capped at `maxWantedLevel`).
336
+ * Their crimes only change it where the population's `wanted` is on.
337
+ */
338
+ get wantedLevel() {
339
+ return this.#state()?.wantedLevel ?? 0;
340
+ }
341
+ set wantedLevel(level) {
342
+ if (!Number.isInteger(level) || level < 0)
343
+ throw new RangeError(`not a wanted level: ${level}`);
344
+ if (this.#connected)
345
+ this.#native.setWantedLevel(this.id, level);
346
+ }
347
+ /**
348
+ * Clears their wanted level. With `parole` (as the game's Pay 'n' Spray):
349
+ * their old stars flash for 20 seconds, and a crime meanwhile brings
350
+ * them back (where the population's `wanted` is on).
351
+ *
352
+ * ```ts
353
+ * player.clearWantedLevel({ parole: true });
354
+ * ```
355
+ */
356
+ clearWantedLevel(options = {}) {
357
+ if (!this.#connected)
358
+ return;
359
+ if (options.parole)
360
+ this.#native.paroleWantedLevel(this.id);
361
+ else
362
+ this.#native.setWantedLevel(this.id, 0);
363
+ }
364
+ /** The most their wanted level can be, 0-6 (default 6). */
365
+ get maxWantedLevel() {
366
+ return this.#state()?.maxWantedLevel ?? 6;
367
+ }
368
+ set maxWantedLevel(level) {
369
+ if (!Number.isInteger(level) || level < 0)
370
+ throw new RangeError(`not a wanted level: ${level}`);
371
+ if (this.#connected)
372
+ this.#native.setMaxWantedLevel(this.id, level);
373
+ }
374
+ /**
375
+ * Their virtual world (default 0). They see and meet players and vehicles
376
+ * in it, and see into worlds it views (`server.setWorldView`). Changing it
377
+ * takes them out of a vehicle that stays behind; move both with
378
+ * `vehicle.world`.
379
+ */
380
+ get world() {
381
+ return this.#state()?.world ?? 0;
382
+ }
383
+ set world(world) {
384
+ if (this.#connected)
385
+ this.#native.setPlayerWorld(this.id, world);
386
+ }
387
+ /**
388
+ * The interior they're in: the game's areas, 0 outside, 1-18 its
389
+ * interiors (as SA-MP's SetPlayerInterior). Their game shows that
390
+ * interior; they see, and are seen by, players and vehicles in the same.
391
+ * Teleports keep it: set both to go in or out.
392
+ *
393
+ * ```ts
394
+ * player.interior = 3;
395
+ * player.setPosition({ x: 384.8, y: 173.8, z: 1008.4 }); // the bike school
396
+ * ```
397
+ */
398
+ get interior() {
399
+ return this.#state()?.interior ?? 0;
400
+ }
401
+ set interior(interior) {
402
+ if (!Number.isInteger(interior) || interior < 0 || interior > 255)
403
+ throw new RangeError(`not an interior: ${interior}`);
404
+ if (this.#connected)
405
+ this.#native.setPlayerInterior(this.id, interior);
406
+ }
407
+ // ---- Stats and how they are played (their game and everyone's copy). ----
408
+ /** Never tires when sprinting (or swimming, cycling). */
409
+ get infiniteSprint() {
410
+ return this.#state()?.infiniteSprint ?? false;
411
+ }
412
+ set infiniteSprint(infinite) {
413
+ if (this.#connected)
414
+ this.#native.setInfiniteSprint(this.id, infinite);
415
+ }
416
+ /**
417
+ * Walks, runs and idles like their skin (true) or like CJ (false, the
418
+ * default). Holding a gun they move like CJ either way: the player's
419
+ * armed moves are made for his animations.
420
+ */
421
+ get skinAnimations() {
422
+ return this.#state()?.skinAnimations ?? false;
423
+ }
424
+ set skinAnimations(skin) {
425
+ if (this.#connected)
426
+ this.#native.setSkinAnimations(this.id, skin);
427
+ }
428
+ /**
429
+ * Sets a game stat (`Stat`): stamina, muscle, max health, weapon and
430
+ * driving skills... Only the server changes stats: playing doesn't, and
431
+ * ones never set stay at a new game's values. 0-1000 for most.
432
+ */
433
+ setStat(stat, value) {
434
+ if (this.#connected && !this.#native.setStat(this.id, stat, value)) {
435
+ throw new RangeError(`not a stat (or too many set): ${stat}`);
436
+ }
437
+ }
438
+ /** A stat as the server set it, or undefined (a new game's value). */
439
+ getStat(stat) {
440
+ return this.#state()?.stats[stat];
441
+ }
442
+ /** Back to a new game's value. */
443
+ resetStat(stat) {
444
+ if (this.#connected)
445
+ this.#native.clearStat(this.id, stat);
446
+ }
447
+ // ---- Weapons: the server's inventory; their game only has what's given. ----
448
+ /** The weapon in their hand (`Weapon`), 0 unarmed. */
449
+ get weapon() {
450
+ return this.#state()?.weapon ?? 0;
451
+ }
452
+ /** Puts a weapon they have in their hand (0: unarmed). */
453
+ set weapon(weapon) {
454
+ if (this.#connected)
455
+ this.#native.setCurrentWeapon(this.id, weapon);
456
+ }
457
+ /**
458
+ * Spawns them again, as SA-MP's SpawnPlayer: out of any vehicle,
459
+ * animations and special actions stopped, unfrozen, the camera behind
460
+ * them, health 100 and armour 0, then `playerSpawn` fires so your spawn
461
+ * code puts them where they belong (a short fade covers the move).
462
+ * Weapons, money and wanted level stay. A dead player respawns by
463
+ * themselves (`playerSpawn` follows): nothing happens then.
464
+ *
465
+ * ```ts
466
+ * kickFromFaction(player);
467
+ * await player.respawn(); // at the civilians' spawn, by your playerSpawn handler
468
+ * ```
469
+ */
470
+ async respawn() {
471
+ if (!this.#connected || !this.alive)
472
+ return;
473
+ await this.fadeOut(300);
474
+ if (!this.#connected)
475
+ return;
476
+ this.removeFromVehicle();
477
+ this.specialAction = null;
478
+ this.stopAnim();
479
+ this.frozen = false;
480
+ this.camera.reset();
481
+ this.health = 100;
482
+ this.armour = 0;
483
+ this.#respawned?.(this);
484
+ await new Promise((r) => setTimeout(r, 300)); // the world streams in where they were put
485
+ if (this.#connected)
486
+ await this.fadeIn(300);
487
+ }
488
+ #respawned = null;
489
+ /** @internal */
490
+ setRespawner(fn) {
491
+ this.#respawned = fn;
492
+ }
493
+ /**
494
+ * Puts the weapon in their hand away (unarmed). **Returns** a function
495
+ * that gives it back (if they still have it).
496
+ *
497
+ * ```ts
498
+ * const giveBack = player.holster();
499
+ * await scene();
500
+ * giveBack();
501
+ * ```
502
+ */
503
+ holster() {
504
+ const held = this.weapon;
505
+ if (this.#connected && held)
506
+ this.weapon = 0;
507
+ let done = false;
508
+ return () => {
509
+ if (done || !this.#connected || !held)
510
+ return;
511
+ done = true;
512
+ if (this.weapons.some((w) => w.weapon === held))
513
+ this.weapon = held;
514
+ };
515
+ }
516
+ /** What they carry, slot by slot, with ammo (the server's count). */
517
+ get weapons() {
518
+ return this.#state()?.weapons ?? [];
519
+ }
520
+ /**
521
+ * Gives a weapon with `ammo` (into its slot, replacing whatever was there),
522
+ * or adds `ammo` if they have it already.
523
+ */
524
+ giveWeapon(weapon, ammo) {
525
+ if (this.#connected && !this.#native.giveWeapon(this.id, weapon, Math.max(0, Math.trunc(ammo)))) {
526
+ throw new RangeError(`not a weapon: ${weapon}`);
527
+ }
528
+ }
529
+ /** Sets the ammo of a weapon they have. */
530
+ setAmmo(weapon, ammo) {
531
+ if (this.#connected)
532
+ this.#native.setWeaponAmmo(this.id, weapon, Math.max(0, Math.trunc(ammo)));
533
+ }
534
+ removeWeapon(weapon) {
535
+ if (this.#connected)
536
+ this.#native.removeWeapon(this.id, weapon);
537
+ }
538
+ /** Takes all their weapons. */
539
+ resetWeapons() {
540
+ if (this.#connected)
541
+ this.#native.resetWeapons(this.id);
542
+ }
543
+ /**
544
+ * Puts them out of their vehicle, beside it; with `jump`, they get out as
545
+ * the game does (diving out of one that's moving: a bail).
546
+ */
547
+ removeFromVehicle(options = {}) {
548
+ if (this.#connected)
549
+ this.#native.removeFromVehicle(this.id, options.jump ?? false);
550
+ }
551
+ /** Radio station names aren't shown on their screen (in any vehicle). */
552
+ get radioNameHidden() {
553
+ return this.#state()?.radioNameHidden ?? false;
554
+ }
555
+ set radioNameHidden(hidden) {
556
+ if (this.#connected)
557
+ this.#native.setRadioNameHidden(this.id, hidden);
558
+ }
559
+ /** Adds (or, if negative, takes) money. */
560
+ giveMoney(amount) {
561
+ this.money = this.money + amount;
562
+ }
563
+ // What was just set (until their game has had time to report it): the
564
+ // value read back, and what the other setter sends with its own.
565
+ #justSet = { at: 0 };
566
+ #recent(key) {
567
+ return Date.now() - this.#justSet.at < 1000 ? this.#justSet[key] : undefined;
568
+ }
569
+ get health() {
570
+ return this.#recent("health") ?? this.#state()?.health ?? 0;
571
+ }
572
+ set health(value) {
573
+ if (!this.#connected)
574
+ return;
575
+ const armour = this.armour;
576
+ this.#justSet = { ...(Date.now() - this.#justSet.at < 1000 ? this.#justSet : {}), health: value, at: Date.now() };
577
+ this.#native.setHealth(this.id, value, armour);
578
+ }
579
+ get armour() {
580
+ return this.#recent("armour") ?? this.#state()?.armour ?? 0;
581
+ }
582
+ set armour(value) {
583
+ if (!this.#connected)
584
+ return;
585
+ const health = this.health;
586
+ this.#justSet = { ...(Date.now() - this.#justSet.at < 1000 ? this.#justSet : {}), armour: value, at: Date.now() };
587
+ this.#native.setHealth(this.id, health, value);
588
+ }
589
+ /** Position in metres (world coordinates); their vehicle's while driving. */
590
+ get position() {
591
+ const s = this.#state();
592
+ return { x: s?.x ?? 0, y: s?.y ?? 0, z: s?.z ?? 0 };
593
+ }
594
+ /** Facing direction in degrees, 0 = north, increasing anticlockwise. */
595
+ get heading() {
596
+ return ((this.#state()?.heading ?? 0) * 180) / Math.PI;
597
+ }
598
+ /** Velocity in m/s, as their last sync (a passenger: their vehicle's). */
599
+ get velocity() {
600
+ const v = this.#state()?.velocity;
601
+ return { x: v?.x ?? 0, y: v?.y ?? 0, z: v?.z ?? 0 };
602
+ }
603
+ /**
604
+ * Whether they're aiming a gun at `target` (a ped, player, vehicle or
605
+ * spot): holding aim with a firearm, within `range` m (default 10), their
606
+ * camera pointing within `angle` degrees of it (default 12). For a ped
607
+ * that gives up at gunpoint.
608
+ *
609
+ * ```ts
610
+ * if (player.isAimingAt(benny, { range: 6 })) void benny.handsUp(3000);
611
+ * ```
612
+ */
613
+ isAimingAt(target, options = {}) {
614
+ const s = this.#state();
615
+ if (!s || !s.cameraFront || !s.cameraPosition || s.vehicleId !== null)
616
+ return false;
617
+ if ((s.keys & Key.Aim) === 0 || s.weapon < 22 || s.weapon > 38)
618
+ return false; // aiming a firearm
619
+ const at = "position" in target ? target.position : target;
620
+ const range = options.range ?? 10;
621
+ if (Math.hypot(at.x - s.x, at.y - s.y, at.z - s.z) > range)
622
+ return false;
623
+ // From their camera to it (its middle, a little up), against where the camera looks.
624
+ const d = { x: at.x - s.cameraPosition.x, y: at.y - s.cameraPosition.y, z: at.z + 0.3 - s.cameraPosition.z };
625
+ const len = Math.hypot(d.x, d.y, d.z) || 1;
626
+ const f = s.cameraFront;
627
+ const cos = (d.x * f.x + d.y * f.y + d.z * f.z) / len;
628
+ return cos >= Math.cos(((options.angle ?? 12) * Math.PI) / 180);
629
+ }
630
+ /** The buttons they hold (`Key` bits), as their last sync. See `playerKeys`. */
631
+ get keys() {
632
+ return this.#state()?.keys ?? 0;
633
+ }
634
+ /** Teleports the player. `heading` in degrees. */
635
+ setPosition(position, heading = 0) {
636
+ if (this.#connected) {
637
+ this.#native.setPosition(this.id, position.x, position.y, position.z, (heading * Math.PI) / 180);
638
+ }
639
+ }
640
+ /**
641
+ * Fades their screen out to `color` (default black) over `ms`, and keeps
642
+ * it there until `fadeIn()`. Resolves when it's dark.
643
+ */
644
+ fadeOut(ms = 500, color = "#000000") {
645
+ if (this.#connected)
646
+ this.#native.screenFade(this.id, false, Math.max(0, ms), mapColor(color, 0xff) >>> 8);
647
+ return new Promise((resolve) => setTimeout(resolve, ms));
648
+ }
649
+ /** Fades their screen back in over `ms`. Resolves when it's clear. */
650
+ fadeIn(ms = 500) {
651
+ if (this.#connected)
652
+ this.#native.screenFade(this.id, true, Math.max(0, ms), 0);
653
+ return new Promise((resolve) => setTimeout(resolve, ms));
654
+ }
655
+ /**
656
+ * Plays one of the game's animations on them (everyone sees it), e.g.
657
+ * `playAnim("PED", "phone_talk", { loop: true })`. `ms` stops it after
658
+ * that long; without, it plays to its end (a looping one: until
659
+ * `stopAnim()`, or they move off). Blocks other than "PED" stream in
660
+ * first. Resolves after `ms` (or at once). `hold` keeps it on its last
661
+ * frame until the next replaces it (SA-MP's ApplyAnimation freeze): for
662
+ * animations played one after another, which otherwise blend back
663
+ * towards standing in the moment between them. `upperBody` plays it on
664
+ * the upper body only (the game's secondary task): they can walk
665
+ * meanwhile, e.g. `playAnim("GANGS", "smkcig_prtl", { upperBody: true })`.
666
+ */
667
+ playAnim(block, name, options = {}) {
668
+ if (!/^[A-Za-z0-9_]{1,23}$/.test(block) || !/^[A-Za-z0-9_]{1,31}$/.test(name)) {
669
+ throw new RangeError(`not an animation: ${block}/${name}`);
670
+ }
671
+ const ms = Math.max(0, Math.trunc(options.ms ?? 0));
672
+ if (this.#connected)
673
+ this.#native.playerAnim(this.id, block, name, options.loop ?? false, ms, false, options.hold ?? false, options.upperBody ?? false);
674
+ return new Promise((resolve) => setTimeout(resolve, ms));
675
+ }
676
+ /** Stops whatever animation they're playing. */
677
+ stopAnim() {
678
+ if (this.#connected)
679
+ this.#native.playerAnim(this.id, "", "", false, 0, true);
680
+ }
681
+ /**
682
+ * Whether they're frozen: they can't move (their controls are off, as in
683
+ * a cutscene) until unfrozen. Animations still play.
684
+ */
685
+ get frozen() {
686
+ return this.#frozen;
687
+ }
688
+ set frozen(frozen) {
689
+ this.#frozen = frozen;
690
+ if (this.#connected)
691
+ this.#native.freezePlayer(this.id, frozen);
692
+ }
693
+ #frozen = false;
694
+ /**
695
+ * Moves them on foot to a spot nearby and turns them to `heading`
696
+ * (degrees): the game's own slide, as its machine scripts put a player in
697
+ * front of a vending machine. Unlike `setPosition`, no teleport (no drop
698
+ * from the game standing them on `z`). Resolves when they're there, or
699
+ * after `ms` (default 2000). Works while `frozen`.
700
+ *
701
+ * ```ts
702
+ * player.frozen = true;
703
+ * await player.slideTo({ x: 2326.2, y: -1646.1, z: 14.2 }, 0);
704
+ * await player.playAnim("VENDING", "VEND_Use", { ms: 2600 });
705
+ * ```
706
+ */
707
+ slideTo(position, heading = this.heading, ms = 2000) {
708
+ if (!this.#connected)
709
+ return Promise.resolve();
710
+ this.#native.slidePlayer(this.id, position.x, position.y, position.z, heading);
711
+ return new Promise((resolve) => {
712
+ const started = Date.now();
713
+ const check = setInterval(() => {
714
+ const p = this.position;
715
+ const turn = Math.abs(((this.heading - heading) % 360 + 540) % 360 - 180);
716
+ if (!this.#connected || Date.now() - started >= ms ||
717
+ (Math.hypot(p.x - position.x, p.y - position.y) < 0.2 && turn < 10)) {
718
+ clearInterval(check);
719
+ resolve();
720
+ }
721
+ }, 100);
722
+ });
723
+ }
724
+ /**
725
+ * Says a line of the game's mission dialogue as CJ (`dialogue`, its GXT
726
+ * key such as "RYD2_CA", from mission table `table`): its recording from
727
+ * this player, with its text as the subtitle, heard by `to` or everyone
728
+ * within 40 m. Resolves when it's done. `effects` puts the recording
729
+ * through audio effects (`"phone"`...; then the lips don't move).
730
+ */
731
+ say(line) {
732
+ const spoken = speechOf(this.#native, line);
733
+ if (this.#connected) {
734
+ this.#native.playerSay(this.id, spoken.dialogue, spoken.subtitle, line.ms ?? 0, line.to ? line.to.filter((p) => p.connected).map((p) => p.id) : null, encodeEffects(line.effects));
735
+ }
736
+ return new Promise((resolve) => setTimeout(resolve, spoken.ms));
737
+ }
738
+ /**
739
+ * A subtitle at the bottom of their screen, in the game's style, for `ms`
740
+ * (left out: as long as reading it takes). Shown only if they have
741
+ * subtitles on in their pause menu.
742
+ */
743
+ subtitle(text, ms = 0) {
744
+ if (this.#connected)
745
+ this.#native.subtitle(this.id, text.slice(0, 255), Math.max(0, ms));
746
+ }
747
+ /**
748
+ * Their colour: their name in chat and their name tag (as SA-MP's
749
+ * SetPlayerColor), unless `nameTag.color` says otherwise. 0xRRGGBB,
750
+ * default white.
751
+ */
752
+ get color() {
753
+ return this.#color;
754
+ }
755
+ set color(color) {
756
+ this.#color = color & 0xffffff;
757
+ this.#sendTag?.(this);
758
+ }
759
+ #color = 0xffffff;
760
+ /**
761
+ * Their name tag, as everyone sees it (unless `setNameTagFor` says
762
+ * otherwise for someone): `hidden` (a mask), `text` instead of their name,
763
+ * `color` (0xRRGGBB) instead of `player.color`. Setting it replaces it
764
+ * (`{}`: as normal).
765
+ *
766
+ * ```ts
767
+ * player.nameTag = { text: `Stranger ${player.id}` }; // a mask
768
+ * player.nameTag = { hidden: true };
769
+ * ```
770
+ */
771
+ get nameTag() {
772
+ return { ...this.#tag };
773
+ }
774
+ set nameTag(tag) {
775
+ this.#tag = { ...tag };
776
+ this.#sendTag?.(this);
777
+ }
778
+ /**
779
+ * Their name tag as `viewer` sees it, over `nameTag` (as SA-MP's
780
+ * ShowPlayerNameTagForPlayer, with text and colour too); null: as
781
+ * everyone sees it.
782
+ *
783
+ * ```ts
784
+ * target.setNameTagFor(admin, { text: `${target.name} (masked)` });
785
+ * ```
786
+ */
787
+ setNameTagFor(viewer, tag) {
788
+ if (tag)
789
+ this.#tagFor.set(viewer.id, { ...tag });
790
+ else
791
+ this.#tagFor.delete(viewer.id);
792
+ this.#sendTag?.(this, viewer);
793
+ }
794
+ /** @internal Their tag as `viewer` sees it: [hidden, text, colour]. */
795
+ tagFor(viewer) {
796
+ const t = { ...this.#tag, ...this.#tagFor.get(viewer.id) };
797
+ const color = t.color ?? this.#color;
798
+ return [t.hidden ?? false, t.text ?? "", color === 0xffffff ? null : color & 0xffffff];
799
+ }
800
+ /** @internal A viewer left: their overrides go. */
801
+ forgetViewer(viewer) {
802
+ this.#tagFor.delete(viewer.id);
803
+ }
804
+ #tag = {};
805
+ #tagFor = new Map();
806
+ #sendTag = null;
807
+ /** @internal */
808
+ setTagSender(fn) {
809
+ this.#sendTag = fn;
810
+ }
811
+ /**
812
+ * Shows a message to this player. `color` is 0xRRGGBB; `{RRGGBB}` in the
813
+ * text colours what follows. `markdown`: the text is markdown (bold,
814
+ * italic, code, links) instead. `{key:ID}` shows an action's key either way.
815
+ */
816
+ send(text, color, options = {}) {
817
+ if (this.#connected)
818
+ this.#native.sendMessage(this.id, text, color ?? 0, options.markdown ?? false);
819
+ }
820
+ /**
821
+ * Their checkpoint, as SA-MP's SetPlayerCheckpoint: a red cylinder on the
822
+ * ground at `position`, `size` m across (default 2), with a radar blip.
823
+ * One at a time: setting another replaces it. Entering and leaving it
824
+ * fire `playerEnterCheckpoint` / `playerLeaveCheckpoint`.
825
+ *
826
+ * ```ts
827
+ * player.setCheckpoint({ x: 2495, y: -1690, z: 13.5 }, 3);
828
+ * server.on("playerEnterCheckpoint", (player) => { player.disableCheckpoint(); ... });
829
+ * ```
830
+ */
831
+ setCheckpoint(position, size = 2) {
832
+ this.checkpoint = { position: { ...position }, size };
833
+ this.inCheckpoint = false;
834
+ if (this.#connected)
835
+ this.#native.checkpoint(this.id, { race: false, enabled: true, ...position, size });
836
+ }
837
+ /** Takes their checkpoint away (DisablePlayerCheckpoint). */
838
+ disableCheckpoint() {
839
+ this.checkpoint = null;
840
+ this.inCheckpoint = false;
841
+ if (this.#connected)
842
+ this.#native.checkpoint(this.id, { race: false, enabled: false });
843
+ }
844
+ /**
845
+ * Their race checkpoint, as SA-MP's SetPlayerRaceCheckpoint: the game's
846
+ * own, its arrow pointing at `next`. `type`: 0 normal, 1 finish, 2
847
+ * nothing (no arrow), 3 in the air (a ring), 4 the air finish. Entering
848
+ * and leaving fire `playerEnterRaceCheckpoint` / `playerLeaveRaceCheckpoint`.
849
+ */
850
+ setRaceCheckpoint(position, options = {}) {
851
+ const size = options.size ?? 4, type = options.type ?? 0, next = options.next ?? position;
852
+ this.raceCheckpoint = { position: { ...position }, size, type, next: { ...next } };
853
+ this.inRaceCheckpoint = false;
854
+ if (this.#connected) {
855
+ this.#native.checkpoint(this.id, {
856
+ race: true, enabled: true, ...position, size, type, nextX: next.x, nextY: next.y, nextZ: next.z,
857
+ });
858
+ }
859
+ }
860
+ /** Takes their race checkpoint away. */
861
+ disableRaceCheckpoint() {
862
+ this.raceCheckpoint = null;
863
+ this.inRaceCheckpoint = false;
864
+ if (this.#connected)
865
+ this.#native.checkpoint(this.id, { race: true, enabled: false });
866
+ }
867
+ /** Their checkpoint, or null. */
868
+ checkpoint = null;
869
+ /** In it now. */
870
+ inCheckpoint = false;
871
+ /** Their race checkpoint, or null. */
872
+ raceCheckpoint = null;
873
+ /** In it now. */
874
+ inRaceCheckpoint = false;
875
+ /**
876
+ * Shows them a dialog, as SA-MP's ShowPlayerDialog, and resolves with
877
+ * their answer: `response` true for the first button (Enter), false for
878
+ * the second (Escape); `listItem` the row they chose (list styles, else
879
+ * -1); `input` what they typed. One at a time: a new one replaces (and
880
+ * resolves, `response` false) the one showing.
881
+ *
882
+ * ```ts
883
+ * const { response, input } = await player.dialog({ style: "password", title: "Login", body: "Your password:", button1: "Login", button2: "Quit" });
884
+ * const { listItem } = await player.dialog({ style: "tablist", title: "Buy", body: "Pistol\t$500\nShotgun\t$1200", button1: "Buy", button2: "Close" });
885
+ * ```
886
+ */
887
+ dialog(d) {
888
+ const styles = { msgbox: 0, input: 1, list: 2, password: 3, tablist: 4, tablistHeaders: 5 };
889
+ const style = styles[d.style ?? "msgbox"];
890
+ if (style === undefined)
891
+ throw new RangeError(`not a dialog style: ${d.style}`);
892
+ this.#dialogAnswer?.({ response: false, listItem: -1, input: "" });
893
+ const id = (this.#dialogId = (this.#dialogId % 0x7fffffff) + 1);
894
+ if (!this.#connected)
895
+ return Promise.resolve({ response: false, listItem: -1, input: "" });
896
+ this.#native.dialog(this.id, {
897
+ id, style, title: (d.title ?? "").slice(0, 128), body: (d.body ?? "").slice(0, 4095),
898
+ button1: (d.button1 ?? "OK").slice(0, 32), button2: (d.button2 ?? "").slice(0, 32), markdown: d.markdown ?? false,
899
+ });
900
+ return new Promise((resolve) => {
901
+ this.#dialogAnswer = (a) => {
902
+ this.#dialogAnswer = null;
903
+ resolve(a);
904
+ };
905
+ });
906
+ }
907
+ /**
908
+ * Shows a menu as the game's shops have (SA-MP's CreateMenu /
909
+ * ShowMenuForPlayer): a box at the top left, a title, rows of up to two
910
+ * columns, one chosen. Up/Down move (`onChoose`), Enter selects
911
+ * (`onSelect`), Backspace or Escape go back (`onBack`; the menu stays up
912
+ * until you close it or show another). One at a time: a new one
913
+ * replaces it. Keys go to the menu while it's up, so freeze them or
914
+ * leave them be as you need.
915
+ *
916
+ * ```ts
917
+ * const menu = player.menu({
918
+ * title: "Ammu-Nation",
919
+ * rows: [["Pistol", "$500"], ["Shotgun", "$1200"]],
920
+ * footer: `Cash: $${player.money}`,
921
+ * onSelect: (row) => { ... },
922
+ * onBack: (m) => m.close(),
923
+ * });
924
+ * ```
925
+ */
926
+ menu(options) {
927
+ return this.#menus.open(options);
928
+ }
929
+ /** The menu showing, or null. */
930
+ get currentMenu() {
931
+ const m = this.#menus.current;
932
+ return m?.isOpen ? m : null;
933
+ }
934
+ /** @internal */
935
+ menuHeard(id, kind, row) {
936
+ this.#menus.heard(id, kind, row);
937
+ }
938
+ /** @internal Gone: no more menu events. */
939
+ forgetMenus() {
940
+ this.#menus.forget();
941
+ }
942
+ /** Takes away the dialog showing (it resolves, `response` false). */
943
+ hideDialog() {
944
+ if (!this.#dialogAnswer)
945
+ return;
946
+ if (this.#connected)
947
+ this.#native.dialog(this.id, { id: this.#dialogId, hide: true });
948
+ this.#dialogAnswer({ response: false, listItem: -1, input: "" });
949
+ }
950
+ /** @internal Their answer to dialog `id`. */
951
+ dialogAnswered(id, a) {
952
+ if (id === this.#dialogId)
953
+ this.#dialogAnswer?.(a);
954
+ }
955
+ #menusOf;
956
+ get #menus() {
957
+ return (this.#menusOf ??= new PlayerMenus(this.#native, this.id));
958
+ }
959
+ #dialogId = 0;
960
+ #dialogAnswer = null;
961
+ /**
962
+ * An object they hold, as SA-MP's SetPlayerAttachedObject: in `slot`
963
+ * (0-9; setting a slot again replaces it), on `bone` (`Bone`: SA-MP's
964
+ * 1-18, head, hands, spine...), offset, turned (degrees) and scaled from
965
+ * it. Everyone sees it, following every animation.
966
+ *
967
+ * ```ts
968
+ * player.attachObject(0, 19006, Bone.Head, { offset: { x: 0.1, y: 0.04, z: 0 }, rotation: { x: 90, y: 90, z: 0 } }); // glasses
969
+ * ```
970
+ */
971
+ attachObject(slot, model, bone, options = {}) {
972
+ if (!Number.isInteger(slot) || slot < 0 || slot > 9)
973
+ throw new RangeError(`not an attachment slot: ${slot} (0-9)`);
974
+ if (!Number.isInteger(bone) || bone < 1 || bone > 18)
975
+ throw new RangeError(`not a bone: ${bone} (1-18)`);
976
+ const o = options.offset ?? { x: 0, y: 0, z: 0 }, r = options.rotation ?? { x: 0, y: 0, z: 0 }, s = options.scale ?? { x: 1, y: 1, z: 1 };
977
+ const spec = { playerId: this.id, slot, model, bone, ox: o.x, oy: o.y, oz: o.z, rx: r.x, ry: r.y, rz: r.z, sx: s.x, sy: s.y, sz: s.z };
978
+ this.attachments.set(slot, spec);
979
+ this.#broadcastAttachment?.(spec);
980
+ }
981
+ /**
982
+ * What they're doing, as SA-MP's SetPlayerSpecialAction (or null):
983
+ * drinking (`"drinkBeer"`, `"drinkWine"`, `"drinkSprunk"`: a bottle or
984
+ * can in hand, fire to drink, which makes them drunker for beer and wine),
985
+ * `"smoke"` (fire for a drag), `"phone"` (on the phone; they can walk),
986
+ * `"carry"` (arms out as if carrying something; they can walk),
987
+ * `"handsUp"`, `"dance1"`-`"dance4"`, `"piss"` (these keep them in place),
988
+ * `"jetpack"`. Enter/exit drops a drink or cigarette (back to null).
989
+ * Everyone sees it, players arriving later too. It ends when they die,
990
+ * or get in a vehicle (except the phone).
991
+ *
992
+ * ```ts
993
+ * player.specialAction = "handsUp";
994
+ * player.specialAction = null;
995
+ * ```
996
+ */
997
+ get specialAction() {
998
+ return this.#special;
999
+ }
1000
+ set specialAction(action) {
1001
+ if (action !== null && !(action in specialPresets))
1002
+ throw new RangeError(`not a special action: ${action}`);
1003
+ if (!this.#connected)
1004
+ return;
1005
+ const was = this.#special ? specialPresets[this.#special] : null;
1006
+ const now = action ? specialPresets[action] : null;
1007
+ this.#special = action;
1008
+ if (was?.prop && !now?.prop)
1009
+ this.removeAttachedObject(kSpecialSlot);
1010
+ if (now?.prop) {
1011
+ const spec = { playerId: this.id, slot: kSpecialSlot, model: now.prop.model, bone: now.prop.bone, ox: now.prop.offset.x, oy: now.prop.offset.y, oz: now.prop.offset.z,
1012
+ rx: now.prop.rotation.x, ry: now.prop.rotation.y, rz: now.prop.rotation.z, sx: 1, sy: 1, sz: 1 };
1013
+ this.attachments.set(kSpecialSlot, spec);
1014
+ this.#broadcastAttachment?.(spec);
1015
+ }
1016
+ const l = now?.loop;
1017
+ this.#native.playerLoop(this.id, l?.block ?? "", l?.name ?? "", l?.upperBody ?? false, now?.jetpack ?? false, l?.hold ?? false, !!now?.use);
1018
+ }
1019
+ #special = null;
1020
+ /** @internal Their buttons changed: what their special action does with them. */
1021
+ specialKeys(keys, old) {
1022
+ const p = this.#special ? specialPresets[this.#special] : null;
1023
+ if (!p)
1024
+ return;
1025
+ const pressed = (k) => (keys & k) !== 0 && (old & k) === 0;
1026
+ if (p.drop && pressed(Key.EnterExit)) {
1027
+ this.specialAction = null;
1028
+ }
1029
+ else if (p.use && pressed(Key.Fire)) {
1030
+ void this.playAnim(p.use.block, p.use.name, { upperBody: true });
1031
+ if (p.use.drunk)
1032
+ this.drunkLevel = Math.min(50000, this.drunkLevel + p.use.drunk);
1033
+ }
1034
+ }
1035
+ /** @internal They died or got in a vehicle: it ends (the phone stays on in a vehicle). */
1036
+ specialEnds(inVehicle) {
1037
+ if (this.#special && !(inVehicle && this.#special === "phone"))
1038
+ this.specialAction = null;
1039
+ }
1040
+ /** Takes the object in `slot` away (RemovePlayerAttachedObject). */
1041
+ removeAttachedObject(slot) {
1042
+ if (!this.attachments.delete(slot))
1043
+ return;
1044
+ this.#broadcastAttachment?.({ playerId: this.id, slot, remove: true });
1045
+ }
1046
+ /** @internal What their client script attached (or took away), for everyone. */
1047
+ attachedByScript(spec) {
1048
+ if (spec.remove) {
1049
+ this.attachments.delete(spec.slot);
1050
+ }
1051
+ else {
1052
+ if (!Number.isInteger(spec.bone) || spec.bone < 1 || spec.bone > 18)
1053
+ return;
1054
+ this.attachments.set(spec.slot, spec);
1055
+ }
1056
+ this.#broadcastAttachment?.(spec);
1057
+ }
1058
+ /** Whether `slot` holds an object (IsPlayerAttachedObjectSlotUsed). */
1059
+ isAttachmentSlotUsed(slot) {
1060
+ return this.attachments.has(slot);
1061
+ }
1062
+ /** @internal What they hold, by slot. */
1063
+ attachments = new Map();
1064
+ #broadcastAttachment = null;
1065
+ /** @internal */
1066
+ setAttachmentSender(fn) {
1067
+ this.#broadcastAttachment = fn;
1068
+ }
1069
+ /**
1070
+ * Text over their head for a while, as SA-MP's SetPlayerChatBubble: seen
1071
+ * by those within `distance` m (default 20), for `ms` (default 5000).
1072
+ * Another replaces it. A label on them that goes by itself.
1073
+ *
1074
+ * ```ts
1075
+ * server.on("playerChat", (p, text) => { p.chatBubble(text, { color: 0xffffffff }); });
1076
+ * ```
1077
+ */
1078
+ chatBubble(text, options = {}) {
1079
+ this.#bubble?.remove();
1080
+ if (this.#bubbleTimer)
1081
+ clearTimeout(this.#bubbleTimer);
1082
+ if (!this.#connected || !this.#makeBubble || !text)
1083
+ return;
1084
+ // SA-MP's {RRGGBB}: a leading one is its colour; others go (labels
1085
+ // take the game's ~ codes).
1086
+ let color = options.color ?? 0xffffffff;
1087
+ const lead = /^\{([0-9a-fA-F]{6})\}/.exec(text);
1088
+ if (lead)
1089
+ color = ((parseInt(lead[1], 16) << 8) | 0xff) >>> 0;
1090
+ text = text.replace(/\{[0-9a-fA-F]{6}\}/g, "");
1091
+ const label = this.#makeBubble(text, color, options.distance ?? 20);
1092
+ this.#bubble = label;
1093
+ this.#bubbleTimer = setTimeout(() => {
1094
+ label.remove();
1095
+ if (this.#bubble === label)
1096
+ this.#bubble = null;
1097
+ }, Math.max(1, options.ms ?? 5000));
1098
+ }
1099
+ #bubble = null;
1100
+ #bubbleTimer = null;
1101
+ #makeBubble = null;
1102
+ /** @internal */
1103
+ setBubbleMaker(fn) {
1104
+ this.#makeBubble = fn;
1105
+ }
1106
+ /**
1107
+ * Puts them straight into a vehicle's seat (0 the driver's, the default;
1108
+ * 1-8 passengers), as SA-MP's PutPlayerInVehicle. The vehicle must be in
1109
+ * their world and near enough for their game to have it (move them to it
1110
+ * first if it's far).
1111
+ */
1112
+ putInVehicle(vehicle, seat = 0) {
1113
+ if (!Number.isInteger(seat) || seat < 0 || seat > 8)
1114
+ throw new RangeError(`not a seat: ${seat}`);
1115
+ if (this.#connected && vehicle.exists)
1116
+ this.#native.putInVehicle(this.id, vehicle.id, seat);
1117
+ }
1118
+ /**
1119
+ * Their fighting style (`FightingStyle`: Normal, Boxing, KungFu,
1120
+ * KneeHead, GrabKick, Elbow), as SA-MP's SetPlayerFightingStyle (the
1121
+ * same numbers). Everyone sees them fight so.
1122
+ */
1123
+ get fightingStyle() {
1124
+ return this.#state()?.fightingStyle || 4;
1125
+ }
1126
+ set fightingStyle(style) {
1127
+ if (![4, 5, 6, 7, 15, 16].includes(style))
1128
+ throw new RangeError(`not a fighting style: ${style}`);
1129
+ if (this.#connected)
1130
+ this.#native.setFightingStyle(this.id, style);
1131
+ }
1132
+ /**
1133
+ * How drunk they are, as SA-MP's SetPlayerDrunkLevel: above 2000 their
1134
+ * camera sways (the more, the worse), and it falls by 60 a second
1135
+ * (reading it gives what it's down to now). 0: sober.
1136
+ */
1137
+ get drunkLevel() {
1138
+ return Math.max(0, Math.round(this.#drunk - (Date.now() - this.#drunkAt) * 0.06));
1139
+ }
1140
+ set drunkLevel(level) {
1141
+ const l = Math.max(0, Math.min(50000, Math.trunc(level)));
1142
+ this.#drunk = l;
1143
+ this.#drunkAt = Date.now();
1144
+ if (this.#connected)
1145
+ this.#native.setDrunkLevel(this.id, l);
1146
+ }
1147
+ #drunk = 0;
1148
+ #drunkAt = 0;
1149
+ /** Empties their chat's log. */
1150
+ clearChat() {
1151
+ if (this.#connected)
1152
+ this.#native.clearChat(this.id);
1153
+ }
1154
+ /** Disconnects the player, telling them why. */
1155
+ kick(reason = "") {
1156
+ if (this.#connected)
1157
+ this.#native.kick(this.id, reason);
1158
+ }
1159
+ toString() {
1160
+ return `${this.name} (${this.id})`;
1161
+ }
1162
+ }
1163
+ //# sourceMappingURL=player.js.map