@adatechnology/game-player 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/dist/index.d.ts +419 -0
- package/dist/index.global.js +1050 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1025 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Fernandes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @adatechnology/game-player
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The player's body for games, pure and seeded: vitals from an attribute sheet, stamina, wounds and bleeding, the bandage, being grabbed and breaking
|
|
4
|
+
free (six pushes or the knife), knock-downs, vaulting a low wall, footstep noise, panic, the shove and the melee blow against any crowd, and an
|
|
5
|
+
optional individual infection. No three.js, no audio, no game content: it emits events (`noise`, `sound`, `message`...) and the game plays and shows them.
|
|
6
|
+
|
|
7
|
+
It is a literal port of the team game's player (`jogo.js` G 68427-68640: `player`, `playerUpdate`, `frontZombies`, `shove`, `knife`, with `FICHA`,
|
|
8
|
+
`DERIV`, `INFEC` and `FERIDA`). Every number is in `src/data/defaults.json` with its source line, and the golden `game-player.json` (18 scripted runs of
|
|
9
|
+
the game code in `node:vm`, recorded by `tools/characters-catalog`) is replayed frame by frame by the tests.
|
|
10
|
+
|
|
11
|
+
- **The normal world comes first.** The infection is off by default: wounds bleed, a bite hurts, nothing turns. `infection: true` turns on the game
|
|
12
|
+
rule (scratch 6%, laceration 22%, bite 100%; fever at 30 s, delirium at 70 s, death at 120 s, the body turns 8 s later).
|
|
13
|
+
- **The sheet** (`attributes`): strength, vigor, agility, perception, intellect, presence (5 each by default). The derived values (`maxHealth`,
|
|
14
|
+
`maxStamina`, `runSpeed`, `walkSpeed`, `footstepNoise`, `composure`) are linear formulas given as data (`derived`).
|
|
15
|
+
- **The world** is a set of callbacks (`cellKind`, `solidAt`, `blocked`, `inWater`, `busy`, `canVault`, `afterMove`); the default is an open dry field.
|
|
16
|
+
- **The crowd** is the `PlayerCrowd` interface (read by index, written by `hold`, `stun`, `chase`, `damage`); `game-crowd` implements it, and any game
|
|
17
|
+
with its own agents writes a small adapter.
|
|
18
|
+
- **Every draw is in the game's order** on the stream you pass (`random`), so a game that shares one stream keeps its sequence.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pnpm add @adatechnology/game-player
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Browser without a bundler (the global is `GamePlayer`):
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<script src="https://unpkg.com/@adatechnology/game-player/dist/index.global.js"></script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createPlayer, playerTextOf } from '@adatechnology/game-player'
|
|
36
|
+
|
|
37
|
+
const player = createPlayer({ seed: 7, attributes: { agility: 7 }, world: { solidAt: grid.solidAt, cellKind: grid.cellKind, inWater, crowd } })
|
|
38
|
+
player.on('noise', ({ x, z, radius }) => crowd.emitNoise(x, z, radius))
|
|
39
|
+
player.on('sound', (request) => audio.play(request)) // { sound: 'step' | 'flesh', volume, position }
|
|
40
|
+
player.on('message', ({ id, seconds }) => hud.show(playerTextOf(id, locale), seconds))
|
|
41
|
+
player.on('died', ({ cause }) => endScreen(cause))
|
|
42
|
+
|
|
43
|
+
// every frame: held keys as booleans, the keys pressed this frame under `pressed`
|
|
44
|
+
player.update(dt, { forward: 1, right: 0, run: shift, aim, yaw: camera.yaw, bandage: keyB, pressed: { crouch, push, knife, vault, throw: t } })
|
|
45
|
+
player.state.health // also stamina, bleeding, wounds, grabbedBy, panic...
|
|
46
|
+
player.spreadFactor() // panic x fever x moving, for the weapon's spread
|
|
47
|
+
|
|
48
|
+
player.hurt(12) // a hit: draws the wound kind
|
|
49
|
+
player.grab(agentIndex) // 3 s to break free
|
|
50
|
+
player.knock(1.5)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Test area
|
|
54
|
+
|
|
55
|
+
`createPlayerTestModule()` (`player_area`, only the generic contracts) makes the room's camera the player body and puts the vitals on the bench;
|
|
56
|
+
`examples/player-test-area` loads it into the shared test room (`PlayerArea.loadInto(room)`, `sala-jogador.html`), and `examples/interactables-test-area`
|
|
57
|
+
carries it too.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { TestableModule } from '@adatechnology/game-contracts';
|
|
2
|
+
|
|
3
|
+
var numbers = {
|
|
4
|
+
hurtFlash: 0.4,
|
|
5
|
+
hurtVolume: 0.8,
|
|
6
|
+
biteDamage: 25,
|
|
7
|
+
biteFlash: 0.8,
|
|
8
|
+
biteVolume: 1.2,
|
|
9
|
+
burnPerSecond: 3,
|
|
10
|
+
bleedPerSecond: 0.15,
|
|
11
|
+
grabSeconds: 3,
|
|
12
|
+
grabVolume: 1,
|
|
13
|
+
grabFollow: 12,
|
|
14
|
+
grabOffset: 0.55,
|
|
15
|
+
grabPushCost: 6,
|
|
16
|
+
grabPushMinStamina: 5,
|
|
17
|
+
grabPushVolume: 1.5,
|
|
18
|
+
grabPushes: 6,
|
|
19
|
+
grabPushStun: 1.2,
|
|
20
|
+
grabPushSpeed: 3,
|
|
21
|
+
grabKnifeChance: 0.7,
|
|
22
|
+
grabKnifeRegion: 0,
|
|
23
|
+
grabKnifeDamage: 22,
|
|
24
|
+
grabKnifeDistance: 0.5,
|
|
25
|
+
grabKnifeCut: 0.7,
|
|
26
|
+
drownDamage: 30,
|
|
27
|
+
drownFlash: 0.8,
|
|
28
|
+
noJawDamage: 8,
|
|
29
|
+
grabAttackCooldown: 2,
|
|
30
|
+
vaultSeconds: 0.7,
|
|
31
|
+
vaultCost: 15,
|
|
32
|
+
vaultHeight: 1.9,
|
|
33
|
+
vaultScanFrom: 0.3,
|
|
34
|
+
vaultScanTo: 1,
|
|
35
|
+
vaultScanStep: 0.1,
|
|
36
|
+
vaultLandFrom: 0.4,
|
|
37
|
+
vaultLandTo: 1.4,
|
|
38
|
+
vaultClearance: 0.3,
|
|
39
|
+
vaultCheckHeight: 0.4,
|
|
40
|
+
vaultNoise: 6,
|
|
41
|
+
vaultStartVolume: 0.8,
|
|
42
|
+
vaultLandVolume: 1,
|
|
43
|
+
crouchSpeed: 1,
|
|
44
|
+
walkFactor: 1.35,
|
|
45
|
+
aimSpeedCap: 1.4,
|
|
46
|
+
waterSpeed: 0.55,
|
|
47
|
+
runMinStamina: 1,
|
|
48
|
+
runDrain: 12,
|
|
49
|
+
regenMoving: 4,
|
|
50
|
+
regenStill: 8,
|
|
51
|
+
acceleration: 10,
|
|
52
|
+
radius: 0.3,
|
|
53
|
+
blockHeight: 0.4,
|
|
54
|
+
pushApartRadius: 0.55,
|
|
55
|
+
pushApartHeight: 1.2,
|
|
56
|
+
hipFollow: 10,
|
|
57
|
+
hipMinSpeed: 0.3,
|
|
58
|
+
hipLimit: 0.9,
|
|
59
|
+
backwardsBelow: -0.1,
|
|
60
|
+
cadenceBase: 0.5,
|
|
61
|
+
cadencePerSpeed: 0.3,
|
|
62
|
+
cadenceMinSpeed: 0.05,
|
|
63
|
+
turnFollow: 12,
|
|
64
|
+
stepEvery: 0.75,
|
|
65
|
+
stepMinSpeed: 0.3,
|
|
66
|
+
stepNoiseRun: 8,
|
|
67
|
+
stepNoiseWalk: 3,
|
|
68
|
+
stepNoiseCrouch: 1,
|
|
69
|
+
stepNoiseWater: 2,
|
|
70
|
+
stepVolumeRun: 0.6,
|
|
71
|
+
stepVolumeWalk: 0.3,
|
|
72
|
+
shoveCost: 10,
|
|
73
|
+
shoveVolume: 1.2,
|
|
74
|
+
shoveRange: 1.6,
|
|
75
|
+
shoveCone: 0.3,
|
|
76
|
+
shoveStun: 0.9,
|
|
77
|
+
shoveSpeed: 2.5,
|
|
78
|
+
meleeMinStamina: 5,
|
|
79
|
+
meleeCostLight: 5,
|
|
80
|
+
meleeCostHeavy: 12,
|
|
81
|
+
meleeHeavyKg: 1.5,
|
|
82
|
+
meleeVolume: 0.6,
|
|
83
|
+
meleeRangeBase: 0.9,
|
|
84
|
+
meleeCone: 0.5,
|
|
85
|
+
meleeHeadChance: 0.35,
|
|
86
|
+
meleeArmChance: 0.5,
|
|
87
|
+
meleeStrengthPerPoint: 0.06,
|
|
88
|
+
meleeStrengthBase: 5,
|
|
89
|
+
meleeCritToCut: 60,
|
|
90
|
+
panicGain: 1.2,
|
|
91
|
+
panicDecay: 3,
|
|
92
|
+
panicMax: 100,
|
|
93
|
+
panicSpreadAt: 0.6,
|
|
94
|
+
panicSpread: 1.4,
|
|
95
|
+
moveSpread: 0.4
|
|
96
|
+
};
|
|
97
|
+
var defaults = {
|
|
98
|
+
numbers: numbers};
|
|
99
|
+
|
|
100
|
+
type AttributeId = 'strength' | 'vigor' | 'agility' | 'perception' | 'intellect' | 'presence';
|
|
101
|
+
type Attributes = Readonly<Record<AttributeId, number>>;
|
|
102
|
+
/** `(base + Σ coefficient × attribute) / divide`, summed left to right in the order given (the game's own order, so the floats match). */
|
|
103
|
+
type LinearFormula = {
|
|
104
|
+
readonly base: number;
|
|
105
|
+
readonly terms: readonly (readonly [AttributeId, number])[];
|
|
106
|
+
readonly divide?: number;
|
|
107
|
+
};
|
|
108
|
+
type DerivedId = 'maxHealth' | 'maxStamina' | 'runSpeed' | 'walkSpeed' | 'footstepNoise' | 'composure';
|
|
109
|
+
type DerivedFormulas = Readonly<Record<DerivedId, LinearFormula>>;
|
|
110
|
+
type Derived = Readonly<Record<DerivedId, number>>;
|
|
111
|
+
/** A wound kind: its share of the hits (drawn in the listed order, the last one takes the rest), its infection chance and whether it bleeds. */
|
|
112
|
+
type WoundRule = {
|
|
113
|
+
readonly share: number;
|
|
114
|
+
readonly infectionChance: number;
|
|
115
|
+
readonly bleeds: boolean;
|
|
116
|
+
};
|
|
117
|
+
type WoundTable = Readonly<Record<string, WoundRule>>;
|
|
118
|
+
/** An individual infection that runs on its own clock and kills at `deathSeconds`. Off by default: a normal world has none. */
|
|
119
|
+
type InfectionRule = {
|
|
120
|
+
readonly feverSeconds: number;
|
|
121
|
+
readonly deliriumSeconds: number;
|
|
122
|
+
readonly deathSeconds: number;
|
|
123
|
+
/** Seconds after death the body turns (`turned` event). */
|
|
124
|
+
readonly reanimateSeconds: number;
|
|
125
|
+
/** With fever, every this many seconds there is a `feverNoiseChance` of a cough: noise of `feverNoiseRadius` m. */
|
|
126
|
+
readonly feverNoiseEvery: number;
|
|
127
|
+
readonly feverNoiseChance: number;
|
|
128
|
+
readonly feverNoiseRadius: number;
|
|
129
|
+
readonly feverNoiseVolume: number;
|
|
130
|
+
readonly feverSpeed: number;
|
|
131
|
+
readonly feverSpread: number;
|
|
132
|
+
/** Infection chance per wound kind; a kind left out uses the wound table's `infectionChance`. */
|
|
133
|
+
readonly chances?: Readonly<Record<string, number>>;
|
|
134
|
+
};
|
|
135
|
+
type PlayerNumbers = typeof defaults.numbers;
|
|
136
|
+
/** What is in the hand for melee (the fist when absent): damage, critical (0..60 turns into a cut), reach in metres, noise radius, mass. */
|
|
137
|
+
type MeleeItem = {
|
|
138
|
+
readonly damage: number;
|
|
139
|
+
readonly crit?: number;
|
|
140
|
+
readonly reach?: number;
|
|
141
|
+
readonly noise?: number;
|
|
142
|
+
readonly kg?: number;
|
|
143
|
+
};
|
|
144
|
+
/** One frame of input. Held keys are booleans; the `pressed` ones are true only on the frame the key went down. */
|
|
145
|
+
type PlayerInput = {
|
|
146
|
+
/** -1 back, 0, 1 forward. */
|
|
147
|
+
readonly forward?: number;
|
|
148
|
+
/** -1 left, 0, 1 right. */
|
|
149
|
+
readonly right?: number;
|
|
150
|
+
readonly run?: boolean;
|
|
151
|
+
/** Aiming down the sights: speed is capped and the body turns with the view at once. */
|
|
152
|
+
readonly aim?: boolean;
|
|
153
|
+
/** The trigger is held or the weapon is cooling: the body turns with the view at once. */
|
|
154
|
+
readonly firing?: boolean;
|
|
155
|
+
/** Where the view looks (radians, the camera's yaw: forward is `(-sin, -cos)`). */
|
|
156
|
+
readonly yaw?: number;
|
|
157
|
+
/** Held: dress the bleeding wound. */
|
|
158
|
+
readonly bandage?: boolean;
|
|
159
|
+
readonly pressed?: Readonly<Partial<Record<PlayerAction, boolean>>>;
|
|
160
|
+
/** The melee item in hand. */
|
|
161
|
+
readonly melee?: MeleeItem;
|
|
162
|
+
};
|
|
163
|
+
type PlayerAction = 'crouch' | 'push' | 'knife' | 'vault' | 'throw';
|
|
164
|
+
/**
|
|
165
|
+
* The crowd the player touches (a horde, any crowd): read by index, written through a few verbs. `game-crowd` implements it; a game with its own
|
|
166
|
+
* agents writes a small adapter. Only agents that are `active` (close and simulated every frame) are pushed, counted for panic or hit.
|
|
167
|
+
*/
|
|
168
|
+
interface PlayerCrowd {
|
|
169
|
+
count(): number;
|
|
170
|
+
active(i: number): boolean;
|
|
171
|
+
x(i: number): number;
|
|
172
|
+
z(i: number): number;
|
|
173
|
+
y(i: number): number;
|
|
174
|
+
floor(i: number): number;
|
|
175
|
+
chasing(i: number): boolean;
|
|
176
|
+
/** On the ground: crawling or stunned. A melee blow on it always goes to the head. */
|
|
177
|
+
down(i: number): boolean;
|
|
178
|
+
/** How much it adds to panic (default 1). */
|
|
179
|
+
panicWeight?(i: number): number;
|
|
180
|
+
/** The grabbing agent is glued to the player's body: move by `k` of the way to (x, z), stop, and face (lookX, lookZ). */
|
|
181
|
+
hold(i: number, x: number, z: number, k: number, lookX: number, lookZ: number): void;
|
|
182
|
+
stun(i: number, seconds: number, vx: number, vz: number): void;
|
|
183
|
+
/** Back to chasing, with the attack cooling for `cooldown` seconds. */
|
|
184
|
+
chase(i: number, cooldown: number): void;
|
|
185
|
+
damage(i: number, hit: {
|
|
186
|
+
region: number;
|
|
187
|
+
amount: number;
|
|
188
|
+
distance: number;
|
|
189
|
+
cut: number;
|
|
190
|
+
}): void;
|
|
191
|
+
}
|
|
192
|
+
/** What the player asks of the world. Everything is optional: the default is an open, dry field. */
|
|
193
|
+
interface PlayerWorld {
|
|
194
|
+
/** Occupancy code of the cell at (x, z): -1 outside, 0 free, 1 solid, 2 low wall that can be vaulted, 3 open door, others solid. */
|
|
195
|
+
cellKind?(x: number, z: number): number;
|
|
196
|
+
solidAt?(x: number, z: number, y: number): boolean;
|
|
197
|
+
/** Blocks the player's body at (x, z). Default `solidAt(x, z, blockHeight)` (a roof or a floor plan passes its own). */
|
|
198
|
+
blocked?(x: number, z: number): boolean;
|
|
199
|
+
inWater?(x: number, z: number): boolean;
|
|
200
|
+
/** Something else drives the player this frame (a car seat, a chair, a ladder): skip moving. */
|
|
201
|
+
busy?(dt: number): boolean;
|
|
202
|
+
/** Low walls can be vaulted here (the game says no on a roof or an upper floor). Default: only on the ground floor. */
|
|
203
|
+
canVault?(): boolean;
|
|
204
|
+
/** After the move (stairs change the floor and the height). */
|
|
205
|
+
afterMove?(player: PlayerState): void;
|
|
206
|
+
crowd?: PlayerCrowd;
|
|
207
|
+
}
|
|
208
|
+
type PlayerState = {
|
|
209
|
+
x: number;
|
|
210
|
+
z: number;
|
|
211
|
+
y: number;
|
|
212
|
+
vx: number;
|
|
213
|
+
vz: number;
|
|
214
|
+
/** The body's yaw (the view's yaw + π, smoothed). */
|
|
215
|
+
yaw: number;
|
|
216
|
+
floor: number;
|
|
217
|
+
alive: boolean;
|
|
218
|
+
turned: boolean;
|
|
219
|
+
health: number;
|
|
220
|
+
stamina: number;
|
|
221
|
+
crouch: boolean;
|
|
222
|
+
running: boolean;
|
|
223
|
+
stepTimer: number;
|
|
224
|
+
phase: number;
|
|
225
|
+
hip: number;
|
|
226
|
+
grabbedBy: number;
|
|
227
|
+
grabTimer: number;
|
|
228
|
+
grabPushes: number;
|
|
229
|
+
grabNoJaw: boolean;
|
|
230
|
+
grabDrown: boolean;
|
|
231
|
+
infected: boolean;
|
|
232
|
+
infectionTime: number;
|
|
233
|
+
infectionSource: string;
|
|
234
|
+
wounds: string[];
|
|
235
|
+
bleeding: number;
|
|
236
|
+
blind: number;
|
|
237
|
+
screamBoost: number;
|
|
238
|
+
panic: number;
|
|
239
|
+
hurtFlash: number;
|
|
240
|
+
deathTime: number;
|
|
241
|
+
cause: string;
|
|
242
|
+
downTimer: number;
|
|
243
|
+
burnTimer: number;
|
|
244
|
+
scentTimer: number;
|
|
245
|
+
vaultTimer: number;
|
|
246
|
+
vault: [number, number, number, number] | null;
|
|
247
|
+
};
|
|
248
|
+
type PlayerSoundId = 'step' | 'flesh';
|
|
249
|
+
type PlayerMessageId = 'grabbed' | 'grabbed_drown' | 'bitten' | 'bandaged' | 'knife_head' | 'knife_miss' | 'pushed_off' | 'drowning' | 'no_jaw' | 'stood_up';
|
|
250
|
+
type KnownDeathCause = 'infected' | 'killed' | 'bitten_to_death' | 'infection' | 'bled_out' | 'hunger_thirst' | 'drowned' | 'burned';
|
|
251
|
+
/** Why the player died: the rule's own causes, or any id the host passes to `die`. */
|
|
252
|
+
type DeathCause = KnownDeathCause | (string & {});
|
|
253
|
+
type PlayerEventMap = {
|
|
254
|
+
noise: {
|
|
255
|
+
x: number;
|
|
256
|
+
z: number;
|
|
257
|
+
radius: number;
|
|
258
|
+
};
|
|
259
|
+
sound: {
|
|
260
|
+
sound: PlayerSoundId;
|
|
261
|
+
volume: number;
|
|
262
|
+
position: [number, number, number];
|
|
263
|
+
};
|
|
264
|
+
step: {
|
|
265
|
+
x: number;
|
|
266
|
+
z: number;
|
|
267
|
+
yaw: number;
|
|
268
|
+
};
|
|
269
|
+
message: {
|
|
270
|
+
id: PlayerMessageId | DeathCause;
|
|
271
|
+
seconds: number;
|
|
272
|
+
};
|
|
273
|
+
hurt: {
|
|
274
|
+
amount: number;
|
|
275
|
+
wound: string;
|
|
276
|
+
};
|
|
277
|
+
infected: {
|
|
278
|
+
source: string;
|
|
279
|
+
};
|
|
280
|
+
stage: {
|
|
281
|
+
stage: 'fever' | 'delirium';
|
|
282
|
+
};
|
|
283
|
+
died: {
|
|
284
|
+
cause: DeathCause;
|
|
285
|
+
};
|
|
286
|
+
turned: {
|
|
287
|
+
x: number;
|
|
288
|
+
z: number;
|
|
289
|
+
floor: number;
|
|
290
|
+
};
|
|
291
|
+
grabbed: {
|
|
292
|
+
by: number;
|
|
293
|
+
noJaw: boolean;
|
|
294
|
+
drown: boolean;
|
|
295
|
+
};
|
|
296
|
+
released: {
|
|
297
|
+
by: number;
|
|
298
|
+
how: 'push' | 'knife' | 'bite' | 'scratch' | 'free';
|
|
299
|
+
};
|
|
300
|
+
vault: {
|
|
301
|
+
from: [number, number];
|
|
302
|
+
to: [number, number];
|
|
303
|
+
};
|
|
304
|
+
shove: {
|
|
305
|
+
targets: number[];
|
|
306
|
+
};
|
|
307
|
+
melee: {
|
|
308
|
+
target: number;
|
|
309
|
+
region: number;
|
|
310
|
+
damage: number;
|
|
311
|
+
distance: number;
|
|
312
|
+
cut: number;
|
|
313
|
+
};
|
|
314
|
+
throw: Record<string, never>;
|
|
315
|
+
};
|
|
316
|
+
type PlayerEventName = keyof PlayerEventMap;
|
|
317
|
+
type PlayerEvent<K extends PlayerEventName = PlayerEventName> = PlayerEventMap[K];
|
|
318
|
+
type PlayerSave = {
|
|
319
|
+
readonly state: PlayerState;
|
|
320
|
+
readonly attributes: Attributes;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
declare const PLAYER_DEFAULTS: {
|
|
324
|
+
readonly sources: Readonly<Record<string, string>>;
|
|
325
|
+
readonly attributes: Attributes;
|
|
326
|
+
readonly derived: DerivedFormulas;
|
|
327
|
+
readonly wounds: WoundTable;
|
|
328
|
+
readonly infection: InfectionRule;
|
|
329
|
+
readonly numbers: PlayerNumbers;
|
|
330
|
+
readonly fist: MeleeItem;
|
|
331
|
+
};
|
|
332
|
+
type PlayerOptions = {
|
|
333
|
+
/** The random stream (the game passes its single stream). Default `createRng(seed)`. */
|
|
334
|
+
readonly random?: () => number;
|
|
335
|
+
readonly seed?: number;
|
|
336
|
+
/** The sheet: every attribute 5 by default. */
|
|
337
|
+
readonly attributes?: Partial<Attributes>;
|
|
338
|
+
readonly derived?: Partial<DerivedFormulas>;
|
|
339
|
+
readonly wounds?: WoundTable;
|
|
340
|
+
/** An individual infection (`true` uses the defaults; an object patches them). Off by default: the normal world comes first. */
|
|
341
|
+
readonly infection?: boolean | Partial<InfectionRule>;
|
|
342
|
+
readonly numbers?: Partial<PlayerNumbers>;
|
|
343
|
+
readonly fist?: MeleeItem;
|
|
344
|
+
readonly position?: {
|
|
345
|
+
readonly x?: number;
|
|
346
|
+
readonly z?: number;
|
|
347
|
+
readonly yaw?: number;
|
|
348
|
+
};
|
|
349
|
+
readonly world?: PlayerWorld;
|
|
350
|
+
};
|
|
351
|
+
type Player = {
|
|
352
|
+
readonly state: PlayerState;
|
|
353
|
+
readonly attributes: Attributes;
|
|
354
|
+
readonly derived: Derived;
|
|
355
|
+
readonly numbers: PlayerNumbers;
|
|
356
|
+
readonly infection: InfectionRule | null;
|
|
357
|
+
update(dt: number, input?: PlayerInput, world?: PlayerWorld): void;
|
|
358
|
+
/** A hit that opens a wound (drawn from the wound table) and may infect. `by` is the agent that hit, if any. */
|
|
359
|
+
hurt(amount: number, by?: number): void;
|
|
360
|
+
/** A bite: damage, bleeding, a bite wound and (with an infection rule) the infection. */
|
|
361
|
+
bite(by?: number): void;
|
|
362
|
+
grab(by: number, noJaw?: boolean, drown?: boolean): void;
|
|
363
|
+
release(): void;
|
|
364
|
+
knock(seconds: number): void;
|
|
365
|
+
infect(source: string): void;
|
|
366
|
+
/** Kills the player with a cause (the host's deaths: a fall, a fire). */
|
|
367
|
+
die(cause: DeathCause): void;
|
|
368
|
+
/** Infection stage: `none`, `malaise`, `fever`, `delirium`. */
|
|
369
|
+
stage(): 'none' | 'malaise' | 'fever' | 'delirium';
|
|
370
|
+
/** Seconds the infection has left, or null. */
|
|
371
|
+
timeLeft(): number | null;
|
|
372
|
+
/** What the player's state does to aim spread (panic, fever, moving); the weapon multiplies its own spread by it. */
|
|
373
|
+
spreadFactor(): number;
|
|
374
|
+
/** The agents in front (nearest first) within `range` m and a cone of `cosMin`, as `[distance, index]`. */
|
|
375
|
+
front(range: number, cosMin: number, yaw: number, crowd?: PlayerCrowd): [number, number][];
|
|
376
|
+
on<K extends PlayerEventName>(event: K, listener: (event: PlayerEventMap[K]) => void): () => void;
|
|
377
|
+
save(): PlayerSave;
|
|
378
|
+
load(save: PlayerSave): void;
|
|
379
|
+
/** The world `update` uses when it is given none. */
|
|
380
|
+
setWorld(world: PlayerWorld): void;
|
|
381
|
+
};
|
|
382
|
+
/** The derived values of a sheet (the game's `DERIV`). */
|
|
383
|
+
declare const deriveVitals: (attributes: Attributes, formulas?: DerivedFormulas) => Derived;
|
|
384
|
+
/** Which wound a hit opens: one draw, the kinds in the listed order, the last one takes the rest (the game's `FERIDA` draw). */
|
|
385
|
+
declare const drawWound: (wounds: WoundTable, random: () => number) => string;
|
|
386
|
+
/**
|
|
387
|
+
* The player body: a literal port of the team game's `player` and `playerUpdate` (G 68427-68640) with the world, the crowd and the input as
|
|
388
|
+
* arguments instead of globals. The draws on `random` happen in the game's order (wound kind, infection, the fever cough, the knife when grabbed,
|
|
389
|
+
* the melee region), so a game that passes its own stream keeps its sequence.
|
|
390
|
+
*/
|
|
391
|
+
declare function createPlayer(options?: PlayerOptions): Player;
|
|
392
|
+
|
|
393
|
+
/** Deterministic mulberry32 generator; returns floats in [0, 1). */
|
|
394
|
+
declare function createRng(seed: number): () => number;
|
|
395
|
+
|
|
396
|
+
type PlayerText = Readonly<Record<PlayerMessageId | KnownDeathCause, string>>;
|
|
397
|
+
declare const PLAYER_TEXT: Readonly<Record<'en' | 'pt-BR', PlayerText>>;
|
|
398
|
+
/** The text of a message or death cause id in a locale (`pt-BR` or `en`, the language part is enough); unknown ids come back as they are. */
|
|
399
|
+
declare const playerTextOf: (id: string, locale?: string) => string;
|
|
400
|
+
|
|
401
|
+
type PlayerTestModuleOptions = PlayerOptions & {
|
|
402
|
+
readonly locale?: string;
|
|
403
|
+
};
|
|
404
|
+
/** What the scenarios use of the context the test room passes. */
|
|
405
|
+
interface PlayerScenarioLike {
|
|
406
|
+
expect(condition: boolean, label: string, data?: Record<string, unknown>): void;
|
|
407
|
+
}
|
|
408
|
+
type PlayerTestModule = TestableModule<PlayerScenarioLike> & {
|
|
409
|
+
/** The player in use (replaced when the bench changes the sheet, the seed or the infection). */
|
|
410
|
+
readonly player: Player;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* The player area of a test room, against the generic contracts only. The room's camera is the player: where it walks, the player walks; its speed
|
|
414
|
+
* decides walking or running, so stamina, footsteps and their noise follow the viewer. The bench hurts, bites, grabs and knocks the player down,
|
|
415
|
+
* switches the individual infection (off by default: a normal world) and sets the sheet. Footsteps and flesh sounds go to `host.audio`.
|
|
416
|
+
*/
|
|
417
|
+
declare function createPlayerTestModule(options?: PlayerTestModuleOptions): PlayerTestModule;
|
|
418
|
+
|
|
419
|
+
export { type AttributeId, type Attributes, type DeathCause, type Derived, type DerivedFormulas, type DerivedId, type InfectionRule, type KnownDeathCause, type LinearFormula, type MeleeItem, PLAYER_DEFAULTS, PLAYER_TEXT, type Player, type PlayerAction, type PlayerCrowd, type PlayerEvent, type PlayerEventMap, type PlayerEventName, type PlayerInput, type PlayerMessageId, type PlayerNumbers, type PlayerOptions, type PlayerSave, type PlayerSoundId, type PlayerState, type PlayerTestModule, type PlayerTestModuleOptions, type PlayerText, type PlayerWorld, type WoundRule, type WoundTable, createPlayer, createPlayerTestModule, createRng, deriveVitals, drawWound, playerTextOf };
|