@sagl/server 0.0.0-stage → 0.1.0-next.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/actions.d.ts +46 -0
- package/dist/actions.js +80 -0
- package/dist/actions.js.map +1 -0
- package/dist/animations.d.ts +32 -0
- package/dist/animations.js +95 -0
- package/dist/animations.js.map +1 -0
- package/dist/audio.d.ts +180 -0
- package/dist/audio.js +353 -0
- package/dist/audio.js.map +1 -0
- package/dist/audio_effects.d.ts +219 -0
- package/dist/audio_effects.js +84 -0
- package/dist/audio_effects.js.map +1 -0
- package/dist/camera.d.ts +188 -0
- package/dist/camera.js +247 -0
- package/dist/camera.js.map +1 -0
- package/dist/claims.d.ts +27 -0
- package/dist/claims.js +48 -0
- package/dist/claims.js.map +1 -0
- package/dist/commands.d.ts +135 -0
- package/dist/commands.js +373 -0
- package/dist/commands.js.map +1 -0
- package/dist/console.d.ts +21 -0
- package/dist/console.js +100 -0
- package/dist/console.js.map +1 -0
- package/dist/constants.d.ts +654 -0
- package/dist/constants.js +657 -0
- package/dist/constants.js.map +1 -0
- package/dist/cutscene.d.ts +47 -0
- package/dist/cutscene.js +190 -0
- package/dist/cutscene.js.map +1 -0
- package/dist/draw.d.ts +106 -0
- package/dist/draw.js +132 -0
- package/dist/draw.js.map +1 -0
- package/dist/effects.d.ts +91 -0
- package/dist/effects.js +116 -0
- package/dist/effects.js.map +1 -0
- package/dist/gangzones.d.ts +47 -0
- package/dist/gangzones.js +122 -0
- package/dist/gangzones.js.map +1 -0
- package/dist/geo.d.ts +11 -0
- package/dist/geo.js +25 -0
- package/dist/geo.js.map +1 -0
- package/dist/hud.d.ts +159 -0
- package/dist/hud.js +247 -0
- package/dist/hud.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction.d.ts +81 -0
- package/dist/interaction.js +143 -0
- package/dist/interaction.js.map +1 -0
- package/dist/junctions.d.ts +1 -0
- package/dist/junctions.js +330 -0
- package/dist/junctions.js.map +1 -0
- package/dist/map.d.ts +55 -0
- package/dist/map.js +71 -0
- package/dist/map.js.map +1 -0
- package/dist/menu.d.ts +96 -0
- package/dist/menu.js +129 -0
- package/dist/menu.js.map +1 -0
- package/dist/mission.d.ts +482 -0
- package/dist/mission.js +1258 -0
- package/dist/mission.js.map +1 -0
- package/dist/models.d.ts +56 -0
- package/dist/models.js +71 -0
- package/dist/models.js.map +1 -0
- package/dist/modshop.d.ts +229 -0
- package/dist/modshop.js +904 -0
- package/dist/modshop.js.map +1 -0
- package/dist/modshop_data.d.ts +44 -0
- package/dist/modshop_data.js +287 -0
- package/dist/modshop_data.js.map +1 -0
- package/dist/native.d.ts +809 -0
- package/dist/native.js +27 -0
- package/dist/native.js.map +1 -0
- package/dist/objects.d.ts +249 -0
- package/dist/objects.js +455 -0
- package/dist/objects.js.map +1 -0
- package/dist/paint.d.ts +124 -0
- package/dist/paint.js +84 -0
- package/dist/paint.js.map +1 -0
- package/dist/paynspray.d.ts +44 -0
- package/dist/paynspray.js +177 -0
- package/dist/paynspray.js.map +1 -0
- package/dist/ped.d.ts +335 -0
- package/dist/ped.js +536 -0
- package/dist/ped.js.map +1 -0
- package/dist/player.d.ts +685 -0
- package/dist/player.js +1163 -0
- package/dist/player.js.map +1 -0
- package/dist/screeneffects.d.ts +99 -0
- package/dist/screeneffects.js +257 -0
- package/dist/screeneffects.js.map +1 -0
- package/dist/scripts.d.ts +69 -0
- package/dist/scripts.js +125 -0
- package/dist/scripts.js.map +1 -0
- package/dist/server.d.ts +713 -0
- package/dist/server.js +1080 -0
- package/dist/server.js.map +1 -0
- package/dist/shopblips.d.ts +9 -0
- package/dist/shopblips.js +30 -0
- package/dist/shopblips.js.map +1 -0
- package/dist/special.d.ts +33 -0
- package/dist/special.js +26 -0
- package/dist/special.js.map +1 -0
- package/dist/splash.d.ts +42 -0
- package/dist/splash.js +55 -0
- package/dist/splash.js.map +1 -0
- package/dist/text.d.ts +19 -0
- package/dist/text.js +25 -0
- package/dist/text.js.map +1 -0
- package/dist/ui.d.ts +141 -0
- package/dist/ui.js +243 -0
- package/dist/ui.js.map +1 -0
- package/dist/vehicle.d.ts +306 -0
- package/dist/vehicle.js +612 -0
- package/dist/vehicle.js.map +1 -0
- package/dist/vehiclecolors.d.ts +26 -0
- package/dist/vehiclecolors.js +49 -0
- package/dist/vehiclecolors.js.map +1 -0
- package/dist/world.d.ts +222 -0
- package/dist/world.js +227 -0
- package/dist/world.js.map +1 -0
- package/dist/zone.d.ts +89 -0
- package/dist/zone.js +140 -0
- package/dist/zone.js.map +1 -0
- 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
|