@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/dist/index.js ADDED
@@ -0,0 +1,1025 @@
1
+ // src/data/defaults.json
2
+ var defaults_default = {
3
+ sources: {
4
+ game: "specs/001-migracao-entregas-dos-times/artefatos/2UG1Mhe9LqJkQKKVsJkTWR/jogo.js",
5
+ attributes: "G 62004 FICHA (Personagens 3.2: every attribute 5, no traits)",
6
+ derived: "G 62005-62012 DERIV",
7
+ wounds: "G 62016 FERIDA (Personagens 3.7): share of each wound kind, and its infection chance (used only when an infection rule is on)",
8
+ infection: "G 62015 INFEC (the slice's accelerated clock: fever 30 s, delirium 70 s, death 120 s, reanimation 8 s after); G 68509 fever noise",
9
+ numbers: "G 68427-68640 player, playerUpdate, frontZombies, shove, knife; G 68678-68681 the aim spread modifiers; G 65507 walkCad"
10
+ },
11
+ attributes: { strength: 5, vigor: 5, agility: 5, perception: 5, intellect: 5, presence: 5 },
12
+ derived: {
13
+ maxHealth: { base: 70, terms: [["vigor", 6]] },
14
+ maxStamina: { base: 40, terms: [["vigor", 6], ["agility", 4]] },
15
+ runSpeed: { base: 5.2, terms: [["agility", 0.12]] },
16
+ walkSpeed: { base: 1.6, terms: [] },
17
+ footstepNoise: { base: 100, terms: [["agility", -4]], divide: 100 },
18
+ composure: { base: 20, terms: [["vigor", 3], ["presence", 4]] }
19
+ },
20
+ wounds: {
21
+ scratch: { share: 0.6, infectionChance: 0.06, bleeds: false },
22
+ laceration: { share: 0.3, infectionChance: 0.22, bleeds: true },
23
+ bite: { share: 0.1, infectionChance: 1, bleeds: true }
24
+ },
25
+ infection: {
26
+ feverSeconds: 30,
27
+ deliriumSeconds: 70,
28
+ deathSeconds: 120,
29
+ reanimateSeconds: 8,
30
+ feverNoiseEvery: 2,
31
+ feverNoiseChance: 0.5,
32
+ feverNoiseRadius: 10,
33
+ feverNoiseVolume: 2,
34
+ feverSpeed: 0.85,
35
+ feverSpread: 1.5
36
+ },
37
+ numbers: {
38
+ hurtFlash: 0.4,
39
+ hurtVolume: 0.8,
40
+ biteDamage: 25,
41
+ biteFlash: 0.8,
42
+ biteVolume: 1.2,
43
+ burnPerSecond: 3,
44
+ bleedPerSecond: 0.15,
45
+ grabSeconds: 3,
46
+ grabVolume: 1,
47
+ grabFollow: 12,
48
+ grabOffset: 0.55,
49
+ grabPushCost: 6,
50
+ grabPushMinStamina: 5,
51
+ grabPushVolume: 1.5,
52
+ grabPushes: 6,
53
+ grabPushStun: 1.2,
54
+ grabPushSpeed: 3,
55
+ grabKnifeChance: 0.7,
56
+ grabKnifeRegion: 0,
57
+ grabKnifeDamage: 22,
58
+ grabKnifeDistance: 0.5,
59
+ grabKnifeCut: 0.7,
60
+ drownDamage: 30,
61
+ drownFlash: 0.8,
62
+ noJawDamage: 8,
63
+ grabAttackCooldown: 2,
64
+ vaultSeconds: 0.7,
65
+ vaultCost: 15,
66
+ vaultHeight: 1.9,
67
+ vaultScanFrom: 0.3,
68
+ vaultScanTo: 1,
69
+ vaultScanStep: 0.1,
70
+ vaultLandFrom: 0.4,
71
+ vaultLandTo: 1.4,
72
+ vaultClearance: 0.3,
73
+ vaultCheckHeight: 0.4,
74
+ vaultNoise: 6,
75
+ vaultStartVolume: 0.8,
76
+ vaultLandVolume: 1,
77
+ crouchSpeed: 1,
78
+ walkFactor: 1.35,
79
+ aimSpeedCap: 1.4,
80
+ waterSpeed: 0.55,
81
+ runMinStamina: 1,
82
+ runDrain: 12,
83
+ regenMoving: 4,
84
+ regenStill: 8,
85
+ acceleration: 10,
86
+ radius: 0.3,
87
+ blockHeight: 0.4,
88
+ pushApartRadius: 0.55,
89
+ pushApartHeight: 1.2,
90
+ hipFollow: 10,
91
+ hipMinSpeed: 0.3,
92
+ hipLimit: 0.9,
93
+ backwardsBelow: -0.1,
94
+ cadenceBase: 0.5,
95
+ cadencePerSpeed: 0.3,
96
+ cadenceMinSpeed: 0.05,
97
+ turnFollow: 12,
98
+ stepEvery: 0.75,
99
+ stepMinSpeed: 0.3,
100
+ stepNoiseRun: 8,
101
+ stepNoiseWalk: 3,
102
+ stepNoiseCrouch: 1,
103
+ stepNoiseWater: 2,
104
+ stepVolumeRun: 0.6,
105
+ stepVolumeWalk: 0.3,
106
+ shoveCost: 10,
107
+ shoveVolume: 1.2,
108
+ shoveRange: 1.6,
109
+ shoveCone: 0.3,
110
+ shoveStun: 0.9,
111
+ shoveSpeed: 2.5,
112
+ meleeMinStamina: 5,
113
+ meleeCostLight: 5,
114
+ meleeCostHeavy: 12,
115
+ meleeHeavyKg: 1.5,
116
+ meleeVolume: 0.6,
117
+ meleeRangeBase: 0.9,
118
+ meleeCone: 0.5,
119
+ meleeHeadChance: 0.35,
120
+ meleeArmChance: 0.5,
121
+ meleeStrengthPerPoint: 0.06,
122
+ meleeStrengthBase: 5,
123
+ meleeCritToCut: 60,
124
+ panicGain: 1.2,
125
+ panicDecay: 3,
126
+ panicMax: 100,
127
+ panicSpreadAt: 0.6,
128
+ panicSpread: 1.4,
129
+ moveSpread: 0.4
130
+ },
131
+ fist: { damage: 8, crit: 0, reach: 0.5, noise: 1 }
132
+ };
133
+
134
+ // src/rng.ts
135
+ function createRng(seed) {
136
+ let state = seed >>> 0;
137
+ return () => {
138
+ state = state + 1831565813 >>> 0;
139
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
140
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
141
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
142
+ };
143
+ }
144
+
145
+ // src/player.ts
146
+ var PLAYER_DEFAULTS = defaults_default;
147
+ var TAU_APPROX = 6.2832;
148
+ var evaluate = (formula, attributes) => {
149
+ let value = formula.base;
150
+ for (const [attribute, coefficient] of formula.terms) value += coefficient * attributes[attribute];
151
+ return formula.divide === void 0 ? value : value / formula.divide;
152
+ };
153
+ var deriveVitals = (attributes, formulas = PLAYER_DEFAULTS.derived) => {
154
+ const out = {};
155
+ for (const id of Object.keys(formulas)) out[id] = evaluate(formulas[id], attributes);
156
+ return out;
157
+ };
158
+ var drawWound = (wounds, random) => {
159
+ const kinds = Object.keys(wounds);
160
+ const draw = random();
161
+ let acc = 0;
162
+ for (let k = 0; k < kinds.length - 1; k++) {
163
+ acc += wounds[kinds[k]].share;
164
+ if (draw < acc) return kinds[k];
165
+ }
166
+ return kinds[kinds.length - 1];
167
+ };
168
+ var clamp = (v, a, b) => v < a ? a : v > b ? b : v;
169
+ var lerp = (a, b, t) => a + (b - a) * t;
170
+ var freshState = (derived, position) => ({
171
+ x: position?.x ?? 0,
172
+ z: position?.z ?? 0,
173
+ y: 0,
174
+ vx: 0,
175
+ vz: 0,
176
+ yaw: position?.yaw ?? 0,
177
+ floor: 0,
178
+ alive: true,
179
+ turned: false,
180
+ health: derived.maxHealth,
181
+ stamina: derived.maxStamina,
182
+ crouch: false,
183
+ running: false,
184
+ stepTimer: 0,
185
+ phase: 0,
186
+ hip: 0,
187
+ grabbedBy: -1,
188
+ grabTimer: 0,
189
+ grabPushes: 0,
190
+ grabNoJaw: false,
191
+ grabDrown: false,
192
+ infected: false,
193
+ infectionTime: 0,
194
+ infectionSource: "",
195
+ wounds: [],
196
+ bleeding: 0,
197
+ blind: 0,
198
+ screamBoost: 0,
199
+ panic: 0,
200
+ hurtFlash: 0,
201
+ deathTime: 0,
202
+ cause: "",
203
+ downTimer: 0,
204
+ burnTimer: 0,
205
+ scentTimer: 0,
206
+ vaultTimer: 0,
207
+ vault: null
208
+ });
209
+ function createPlayer(options = {}) {
210
+ const random = options.random ?? createRng(options.seed ?? 1);
211
+ const attributes = { ...PLAYER_DEFAULTS.attributes, ...options.attributes };
212
+ const formulas = { ...PLAYER_DEFAULTS.derived, ...options.derived };
213
+ const derived = deriveVitals(attributes, formulas);
214
+ const numbers = { ...PLAYER_DEFAULTS.numbers, ...options.numbers };
215
+ const wounds = options.wounds ?? PLAYER_DEFAULTS.wounds;
216
+ const infection = options.infection === void 0 || options.infection === false ? null : options.infection === true ? PLAYER_DEFAULTS.infection : { ...PLAYER_DEFAULTS.infection, ...options.infection };
217
+ const fist = options.fist ?? PLAYER_DEFAULTS.fist;
218
+ const listeners = /* @__PURE__ */ new Map();
219
+ let P = freshState(derived, options.position);
220
+ let defaultWorld = options.world ?? {};
221
+ let world = defaultWorld;
222
+ const emit = (event, payload) => {
223
+ const set = listeners.get(event);
224
+ if (set) for (const listener of set) listener(payload);
225
+ };
226
+ const sound = (id, volume) => emit("sound", { sound: id, volume, position: [P.x, P.y, P.z] });
227
+ const noise = (x, z, radius) => emit("noise", { x, z, radius });
228
+ const message = (id, seconds) => emit("message", { id, seconds });
229
+ const inWater = (x, z) => world.inWater?.(x, z) ?? false;
230
+ const solidAt = (x, z, y) => world.solidAt?.(x, z, y) ?? false;
231
+ const cellKind = (x, z) => world.cellKind?.(x, z) ?? 0;
232
+ const blocked = (x, z) => world.blocked ? world.blocked(x, z) : solidAt(x, z, numbers.blockHeight);
233
+ const chanceOf = (kind) => infection?.chances?.[kind] ?? wounds[kind]?.infectionChance ?? 0;
234
+ const die = (cause) => {
235
+ if (!P.alive) return;
236
+ P.alive = false;
237
+ P.cause = cause;
238
+ P.deathTime = 0;
239
+ P.grabbedBy = -1;
240
+ message(cause, 6);
241
+ emit("died", { cause });
242
+ };
243
+ const infect = (source) => {
244
+ if (P.infected || !infection) return;
245
+ P.infected = true;
246
+ P.infectionTime = 0;
247
+ P.infectionSource = source;
248
+ emit("infected", { source });
249
+ };
250
+ const release = (how = "free") => {
251
+ const by = P.grabbedBy;
252
+ P.grabbedBy = -1;
253
+ if (by >= 0) emit("released", { by, how });
254
+ };
255
+ const hurt = (amount) => {
256
+ if (!P.alive) return;
257
+ P.health -= amount;
258
+ P.hurtFlash = numbers.hurtFlash;
259
+ sound("flesh", numbers.hurtVolume);
260
+ const kind = drawWound(wounds, random);
261
+ P.wounds.push(kind);
262
+ if (wounds[kind]?.bleeds) P.bleeding = 1;
263
+ emit("hurt", { amount, wound: kind });
264
+ if (infection && !P.infected && random() < chanceOf(kind)) infect(kind);
265
+ if (P.health <= 0) die(P.infected ? "infected" : "killed");
266
+ };
267
+ const bite = () => {
268
+ P.health -= numbers.biteDamage;
269
+ P.hurtFlash = numbers.biteFlash;
270
+ P.bleeding = 1;
271
+ P.wounds.push("bite");
272
+ sound("flesh", numbers.biteVolume);
273
+ emit("hurt", { amount: numbers.biteDamage, wound: "bite" });
274
+ infect("bite");
275
+ message("bitten", 5);
276
+ if (P.health <= 0) die("bitten_to_death");
277
+ };
278
+ const grab = (by, noJaw = false, drown = false) => {
279
+ P.grabbedBy = by;
280
+ P.grabTimer = numbers.grabSeconds;
281
+ P.grabPushes = 0;
282
+ P.grabNoJaw = noJaw;
283
+ P.grabDrown = drown;
284
+ message(drown ? "grabbed_drown" : "grabbed", 3);
285
+ sound("flesh", numbers.grabVolume);
286
+ emit("grabbed", { by, noJaw, drown });
287
+ };
288
+ const front = (range, cosMin, yaw, crowd = world.crowd) => {
289
+ const out = [];
290
+ if (!crowd) return out;
291
+ const fx = -Math.sin(yaw);
292
+ const fz = -Math.cos(yaw);
293
+ const n = crowd.count();
294
+ for (let i = 0; i < n; i++) {
295
+ if (!crowd.active(i) || crowd.floor(i) !== P.floor) continue;
296
+ const ex = crowd.x(i) - P.x;
297
+ const ez = crowd.z(i) - P.z;
298
+ const d = Math.hypot(ex, ez);
299
+ if (d < range && (ex * fx + ez * fz) / (d || 1) > cosMin) out.push([d, i]);
300
+ }
301
+ return out.sort((a, b) => a[0] - b[0]);
302
+ };
303
+ const shove = (yaw) => {
304
+ sound("step", numbers.shoveVolume);
305
+ const crowd = world.crowd;
306
+ const targets = [];
307
+ for (const [d, i] of front(numbers.shoveRange, numbers.shoveCone, yaw)) {
308
+ crowd.stun(i, numbers.shoveStun, (crowd.x(i) - P.x) / d * numbers.shoveSpeed, (crowd.z(i) - P.z) / d * numbers.shoveSpeed);
309
+ targets.push(i);
310
+ }
311
+ emit("shove", { targets });
312
+ };
313
+ const melee = (item, yaw) => {
314
+ const t = front(numbers.meleeRangeBase + (item.reach ?? 0), numbers.meleeCone, yaw)[0];
315
+ sound("flesh", numbers.meleeVolume);
316
+ noise(P.x, P.z, item.noise || 1);
317
+ if (!t) return;
318
+ const crowd = world.crowd;
319
+ const i = t[1];
320
+ const region = crowd.down(i) ? 0 : random() < numbers.meleeHeadChance ? 0 : random() < numbers.meleeArmChance ? 1 : 2 + Math.floor(random() * 2);
321
+ const damage = item.damage * (1 + (attributes.strength - numbers.meleeStrengthBase) * numbers.meleeStrengthPerPoint);
322
+ const cut = Math.min(1, (item.crit || 0) / numbers.meleeCritToCut);
323
+ crowd.damage(i, { region, amount: damage, distance: t[0], cut });
324
+ emit("melee", { target: i, region, damage, distance: t[0], cut });
325
+ };
326
+ const update = (dt, input = {}, frameWorld) => {
327
+ world = frameWorld ?? defaultWorld;
328
+ if (!P.alive) {
329
+ P.deathTime += dt;
330
+ if (P.infected && infection && !P.turned && P.deathTime >= infection.reanimateSeconds) {
331
+ P.turned = true;
332
+ emit("turned", { x: P.x, z: P.z, floor: P.floor });
333
+ }
334
+ return;
335
+ }
336
+ const pressed = { ...input.pressed };
337
+ const wasPressed = (action) => {
338
+ const v = pressed[action] === true;
339
+ pressed[action] = false;
340
+ return v;
341
+ };
342
+ const viewYaw = input.yaw ?? 0;
343
+ const crowd = world.crowd;
344
+ P.screamBoost = Math.max(0, P.screamBoost - dt);
345
+ P.blind = Math.max(0, P.blind - dt);
346
+ P.hurtFlash = Math.max(0, P.hurtFlash - dt);
347
+ if (P.infected && infection) {
348
+ const before = P.infectionTime;
349
+ P.infectionTime += dt;
350
+ if (before <= infection.feverSeconds && P.infectionTime > infection.feverSeconds) emit("stage", { stage: "fever" });
351
+ if (before <= infection.deliriumSeconds && P.infectionTime > infection.deliriumSeconds) emit("stage", { stage: "delirium" });
352
+ const every = 1 / infection.feverNoiseEvery;
353
+ if (P.infectionTime > infection.feverSeconds && Math.floor(P.infectionTime * every) !== Math.floor((P.infectionTime - dt) * every) && random() < infection.feverNoiseChance) {
354
+ noise(P.x, P.z, infection.feverNoiseRadius);
355
+ sound("step", infection.feverNoiseVolume);
356
+ }
357
+ if (P.infectionTime > infection.deathSeconds) {
358
+ P.health = 0;
359
+ die("infection");
360
+ return;
361
+ }
362
+ }
363
+ if (P.burnTimer > 0) {
364
+ P.burnTimer -= dt;
365
+ P.health -= numbers.burnPerSecond * dt;
366
+ if (inWater(P.x, P.z)) P.burnTimer = 0;
367
+ }
368
+ P.scentTimer = Math.max(0, P.scentTimer - dt);
369
+ if (P.bleeding) {
370
+ P.health -= dt * numbers.bleedPerSecond;
371
+ if (input.bandage && P.bleeding) {
372
+ P.bleeding = 0;
373
+ message("bandaged", 2);
374
+ }
375
+ }
376
+ if (P.health <= 0) {
377
+ die(P.infected ? "infected" : P.bleeding ? "bled_out" : "hunger_thirst");
378
+ return;
379
+ }
380
+ if (P.grabbedBy >= 0) {
381
+ const i = P.grabbedBy;
382
+ P.grabTimer -= dt;
383
+ if (crowd) {
384
+ const k = 1 - Math.exp(-dt * numbers.grabFollow);
385
+ crowd.hold(i, P.x - Math.sin(P.yaw) * numbers.grabOffset, P.z - Math.cos(P.yaw) * numbers.grabOffset, k, P.x, P.z);
386
+ }
387
+ if (wasPressed("push") && P.stamina > numbers.grabPushMinStamina) {
388
+ P.grabPushes++;
389
+ P.stamina -= numbers.grabPushCost;
390
+ sound("step", numbers.grabPushVolume);
391
+ }
392
+ if (wasPressed("knife")) {
393
+ if (random() < numbers.grabKnifeChance) {
394
+ crowd?.damage(i, { region: numbers.grabKnifeRegion, amount: numbers.grabKnifeDamage, distance: numbers.grabKnifeDistance, cut: numbers.grabKnifeCut });
395
+ message("knife_head", 1.5);
396
+ release("knife");
397
+ return;
398
+ } else message("knife_miss", 1);
399
+ }
400
+ if (P.grabPushes >= numbers.grabPushes) {
401
+ crowd?.stun(i, numbers.grabPushStun, -Math.sin(viewYaw) * numbers.grabPushSpeed, -Math.cos(viewYaw) * numbers.grabPushSpeed);
402
+ release("push");
403
+ message("pushed_off", 1.2);
404
+ return;
405
+ }
406
+ if (P.grabTimer <= 0) {
407
+ if (P.grabDrown) {
408
+ P.health -= numbers.drownDamage;
409
+ P.hurtFlash = numbers.drownFlash;
410
+ message("drowning", 2);
411
+ if (P.health <= 0) {
412
+ die("drowned");
413
+ return;
414
+ }
415
+ P.grabTimer = numbers.grabSeconds;
416
+ P.grabPushes = 0;
417
+ return;
418
+ }
419
+ if (P.grabNoJaw) {
420
+ hurt(numbers.noJawDamage);
421
+ message("no_jaw", 2);
422
+ } else bite();
423
+ crowd?.chase(i, numbers.grabAttackCooldown);
424
+ release(P.grabNoJaw ? "scratch" : "bite");
425
+ }
426
+ return;
427
+ }
428
+ if (P.downTimer > 0) {
429
+ P.downTimer -= dt;
430
+ P.vx = P.vz = 0;
431
+ if (P.downTimer <= 0) message("stood_up", 0.8);
432
+ return;
433
+ }
434
+ if (world.busy?.(dt)) return;
435
+ if (P.vaultTimer > 0 && P.vault) {
436
+ P.vaultTimer -= dt;
437
+ const k = 1 - P.vaultTimer / numbers.vaultSeconds;
438
+ P.x = lerp(P.vault[0], P.vault[2], k);
439
+ P.z = lerp(P.vault[1], P.vault[3], k);
440
+ P.y = Math.sin(k * Math.PI) * numbers.vaultHeight;
441
+ if (P.vaultTimer <= 0) {
442
+ P.y = 0;
443
+ noise(P.x, P.z, numbers.vaultNoise);
444
+ sound("step", numbers.vaultLandVolume);
445
+ }
446
+ return;
447
+ }
448
+ if (wasPressed("vault") && P.stamina > numbers.vaultCost && (world.canVault?.() ?? !P.floor)) {
449
+ const fx0 = -Math.sin(viewYaw);
450
+ const fz0 = -Math.cos(viewYaw);
451
+ for (let a = numbers.vaultScanFrom; a <= numbers.vaultScanTo; a += numbers.vaultScanStep) {
452
+ const c = cellKind(P.x + fx0 * a, P.z + fz0 * a);
453
+ if (c === 2) {
454
+ for (let e = a + numbers.vaultLandFrom; e <= a + numbers.vaultLandTo; e += numbers.vaultScanStep) {
455
+ const ex = P.x + fx0 * e;
456
+ const ez = P.z + fz0 * e;
457
+ if (!solidAt(ex, ez, numbers.vaultCheckHeight) && !solidAt(ex + fx0 * numbers.vaultClearance, ez + fz0 * numbers.vaultClearance, numbers.vaultCheckHeight)) {
458
+ P.vault = [P.x, P.z, ex, ez];
459
+ P.vaultTimer = numbers.vaultSeconds;
460
+ P.stamina -= numbers.vaultCost;
461
+ sound("step", numbers.vaultStartVolume);
462
+ emit("vault", { from: [P.x, P.z], to: [ex, ez] });
463
+ return;
464
+ }
465
+ }
466
+ break;
467
+ }
468
+ if (c > 0 && c !== 3) break;
469
+ }
470
+ }
471
+ const f = input.forward ?? 0;
472
+ const s = input.right ?? 0;
473
+ if (wasPressed("crouch")) P.crouch = !P.crouch;
474
+ const want = f || s;
475
+ P.running = !!(input.run && want && P.stamina > numbers.runMinStamina && !input.aim && !P.crouch);
476
+ const fever = P.infected && infection !== null && P.infectionTime > infection.feverSeconds;
477
+ let sp = P.running ? derived.runSpeed : P.crouch ? numbers.crouchSpeed : derived.walkSpeed * numbers.walkFactor;
478
+ if (fever) sp *= infection.feverSpeed;
479
+ if (input.aim) sp = Math.min(sp, numbers.aimSpeedCap);
480
+ const wet = inWater(P.x, P.z);
481
+ if (wet) sp *= numbers.waterSpeed;
482
+ if (P.running) P.stamina = Math.max(0, P.stamina - numbers.runDrain * dt);
483
+ else P.stamina = Math.min(derived.maxStamina, P.stamina + (want ? numbers.regenMoving : numbers.regenStill) * dt);
484
+ const fx = -Math.sin(viewYaw);
485
+ const fz = -Math.cos(viewYaw);
486
+ const mx = fx * f - fz * s;
487
+ const mz = fz * f + fx * s;
488
+ const ml = Math.hypot(mx, mz) || 1;
489
+ const tvx = want ? mx / ml * sp : 0;
490
+ const tvz = want ? mz / ml * sp : 0;
491
+ P.vx = lerp(P.vx, tvx, Math.min(1, dt * numbers.acceleration));
492
+ P.vz = lerp(P.vz, tvz, Math.min(1, dt * numbers.acceleration));
493
+ const r = numbers.radius;
494
+ const nx = P.x + P.vx * dt;
495
+ const nz = P.z + P.vz * dt;
496
+ if (!blocked(nx + Math.sign(P.vx) * r, P.z)) P.x = nx;
497
+ if (!blocked(P.x, nz + Math.sign(P.vz) * r)) P.z = nz;
498
+ world.afterMove?.(P);
499
+ if (crowd) {
500
+ const n = crowd.count();
501
+ const rr = numbers.pushApartRadius;
502
+ for (let i = 0; i < n; i++) {
503
+ if (!crowd.active(i) || Math.abs(crowd.y(i) - P.y) > numbers.pushApartHeight) continue;
504
+ const ex = P.x - crowd.x(i);
505
+ const ez = P.z - crowd.z(i);
506
+ const e2 = ex * ex + ez * ez;
507
+ if (e2 < rr * rr && e2 > 1e-5) {
508
+ const e = Math.sqrt(e2);
509
+ const k = (rr - e) / e * 0.5;
510
+ if (!solidAt(P.x + ex * k, P.z + ez * k, numbers.blockHeight)) {
511
+ P.x += ex * k;
512
+ P.z += ez * k;
513
+ }
514
+ }
515
+ }
516
+ }
517
+ const spd = Math.hypot(P.vx, P.vz);
518
+ {
519
+ const cy = Math.cos(P.yaw);
520
+ const sy = Math.sin(P.yaw);
521
+ const lx = P.vx * cy - P.vz * sy;
522
+ const lz = P.vx * sy + P.vz * cy;
523
+ const back = lz < numbers.backwardsBelow ? -1 : 1;
524
+ const hip = spd > numbers.hipMinSpeed ? clamp(Math.atan2(lx * back, lz * back), -numbers.hipLimit, numbers.hipLimit) : 0;
525
+ P.hip = P.hip + (hip - P.hip) * Math.min(1, dt * numbers.hipFollow);
526
+ P.phase += dt * (spd < numbers.cadenceMinSpeed ? 0 : numbers.cadenceBase + numbers.cadencePerSpeed * spd) * back;
527
+ }
528
+ {
529
+ const target = viewYaw + Math.PI;
530
+ let dy = target - P.yaw;
531
+ dy -= Math.round(dy / TAU_APPROX) * TAU_APPROX;
532
+ P.yaw += input.aim || input.firing ? dy : dy * Math.min(1, dt * numbers.turnFollow);
533
+ }
534
+ P.stepTimer -= dt * spd;
535
+ if (P.stepTimer <= 0 && spd > numbers.stepMinSpeed) {
536
+ P.stepTimer = numbers.stepEvery;
537
+ noise(P.x, P.z, (P.running ? numbers.stepNoiseRun : P.crouch ? numbers.stepNoiseCrouch : numbers.stepNoiseWalk) * derived.footstepNoise * (wet ? numbers.stepNoiseWater : 1));
538
+ sound("step", P.running ? numbers.stepVolumeRun : numbers.stepVolumeWalk);
539
+ emit("step", { x: P.x, z: P.z, yaw: P.yaw });
540
+ }
541
+ if (wasPressed("push") && P.stamina > numbers.shoveCost) {
542
+ P.stamina -= numbers.shoveCost;
543
+ shove(viewYaw);
544
+ }
545
+ if (wasPressed("throw")) emit("throw", {});
546
+ if (wasPressed("knife") && P.stamina > numbers.meleeMinStamina) {
547
+ const item = input.melee ?? fist;
548
+ P.stamina -= (item.kg ?? 0) > numbers.meleeHeavyKg ? numbers.meleeCostHeavy : numbers.meleeCostLight;
549
+ melee(item, viewYaw);
550
+ }
551
+ let near = 0;
552
+ if (crowd) {
553
+ const n = crowd.count();
554
+ for (let i = 0; i < n; i++) if (crowd.active(i) && crowd.chasing(i)) near += crowd.panicWeight?.(i) ?? 1;
555
+ }
556
+ P.panic = clamp(P.panic + (near * numbers.panicGain - (near ? 0 : numbers.panicDecay)) * dt, 0, numbers.panicMax);
557
+ };
558
+ return {
559
+ get state() {
560
+ return P;
561
+ },
562
+ attributes,
563
+ derived,
564
+ numbers,
565
+ infection,
566
+ update,
567
+ hurt: (amount) => hurt(amount),
568
+ bite: () => bite(),
569
+ grab,
570
+ release: () => release("free"),
571
+ knock(seconds) {
572
+ if (!P.alive) return;
573
+ P.downTimer = Math.max(P.downTimer, seconds);
574
+ P.crouch = false;
575
+ },
576
+ infect,
577
+ die,
578
+ stage() {
579
+ if (!P.infected || !infection) return "none";
580
+ return P.infectionTime > infection.deliriumSeconds ? "delirium" : P.infectionTime > infection.feverSeconds ? "fever" : "malaise";
581
+ },
582
+ timeLeft: () => P.infected && infection ? Math.max(0, infection.deathSeconds - P.infectionTime) : null,
583
+ spreadFactor() {
584
+ const panic = P.panic > derived.composure * numbers.panicSpreadAt ? numbers.panicSpread : 1;
585
+ const fever = P.infected && infection && P.infectionTime > infection.feverSeconds ? infection.feverSpread : 1;
586
+ return panic * fever * (1 + Math.hypot(P.vx, P.vz) * numbers.moveSpread);
587
+ },
588
+ front,
589
+ on(event, listener) {
590
+ let set = listeners.get(event);
591
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
592
+ set.add(listener);
593
+ return () => void set.delete(listener);
594
+ },
595
+ save: () => ({ state: structuredClone(P), attributes }),
596
+ load(save) {
597
+ P = structuredClone(save.state);
598
+ },
599
+ setWorld(next) {
600
+ defaultWorld = next;
601
+ }
602
+ };
603
+ }
604
+
605
+ // src/locales/index.ts
606
+ var PT_BR = {
607
+ grabbed: "AGARRADO! Aperte E v\xE1rias vezes para empurrar ou F para a faca",
608
+ grabbed_drown: "O AFOGADO TE PUXA PARA O FUNDO! E v\xE1rias vezes ou F para a faca",
609
+ bitten: "Voc\xEA foi mordido. N\xE3o h\xE1 cura.",
610
+ bandaged: "Curativo feito",
611
+ knife_head: "Faca na cabe\xE7a!",
612
+ knife_miss: "Errou a facada",
613
+ pushed_off: "Empurrou!",
614
+ drowning: "Afogando! Solte-se!",
615
+ no_jaw: "Sem mand\xEDbula: s\xF3 arranhou",
616
+ stood_up: "De p\xE9!",
617
+ infected: "Morreu infectado",
618
+ killed: "Morto pela horda",
619
+ bitten_to_death: "Mordido",
620
+ infection: "A infec\xE7\xE3o venceu",
621
+ bled_out: "Sangrou at\xE9 a morte",
622
+ hunger_thirst: "Fome e sede",
623
+ drowned: "Afogado",
624
+ burned: "Queimado"
625
+ };
626
+ var EN = {
627
+ grabbed: "GRABBED! Press E repeatedly to push or F for the knife",
628
+ grabbed_drown: "THE DROWNED PULLS YOU UNDER! E repeatedly or F for the knife",
629
+ bitten: "You were bitten. There is no cure.",
630
+ bandaged: "Wound dressed",
631
+ knife_head: "Knife to the head!",
632
+ knife_miss: "The stab missed",
633
+ pushed_off: "Pushed off!",
634
+ drowning: "Drowning! Break free!",
635
+ no_jaw: "No jaw: only a scratch",
636
+ stood_up: "Back on your feet!",
637
+ infected: "Died infected",
638
+ killed: "Killed by the horde",
639
+ bitten_to_death: "Bitten",
640
+ infection: "The infection won",
641
+ bled_out: "Bled to death",
642
+ hunger_thirst: "Hunger and thirst",
643
+ drowned: "Drowned",
644
+ burned: "Burned"
645
+ };
646
+ var PLAYER_TEXT = { en: EN, "pt-BR": PT_BR };
647
+ var playerTextOf = (id, locale = "pt-BR") => {
648
+ const table = locale.toLowerCase().startsWith("en") ? EN : PT_BR;
649
+ return table[id] ?? id;
650
+ };
651
+
652
+ // src/bench/locale.ts
653
+ var EN2 = {
654
+ group: "Player",
655
+ name: "Player area",
656
+ controls: {
657
+ infection: "Individual infection",
658
+ agility: "Agility",
659
+ vigor: "Vigor",
660
+ seed: "Seed",
661
+ hurt: "Take a hit (10)",
662
+ bite: "Bite",
663
+ grab: "Get grabbed",
664
+ push: "Push (E)",
665
+ knife: "Knife (F)",
666
+ bandage: "Bandage (B)",
667
+ knock: "Knocked down 1.5 s",
668
+ crouch: "Crouch (C)",
669
+ reset: "Start again"
670
+ },
671
+ hints: {
672
+ infection: "Off: a normal world, wounds never infect. On: the game rule (scratch 6%, laceration 22%, bite 100%; fever at 30 s, death at 120 s).",
673
+ agility: "The sheet: stamina, run speed and footstep noise come from it.",
674
+ vigor: "The sheet: health, stamina and composure come from it.",
675
+ seed: "The same seed gives the same wounds and the same knife rolls.",
676
+ grab: "Something grabs you from behind: 3 s to push it off (6 pushes) or to land the knife (70%).",
677
+ push: "Shoves what is in front, or pushes off what holds you."
678
+ },
679
+ metrics: {
680
+ health: "Health",
681
+ stamina: "Stamina",
682
+ bleeding: "Bleeding",
683
+ wounds: "Wounds",
684
+ stage: "Infection",
685
+ time_left: "Time left",
686
+ panic: "Panic",
687
+ spread: "Aim spread x",
688
+ grab: "Grab timer",
689
+ noise: "Last noise",
690
+ steps: "Footsteps",
691
+ message: "Message"
692
+ },
693
+ stages: { none: "None", malaise: "Malaise", fever: "Fever", delirium: "Delirium", dead: "Dead" },
694
+ scenarios: {
695
+ stamina: "Running drains stamina and resting gives it back",
696
+ bleed_bandage: "A laceration bleeds until it is dressed",
697
+ normal_world: "In a normal world a bite hurts and bleeds but never infects",
698
+ infection_kills: "With the infection on, a bite brings fever and kills at its time",
699
+ grab_break_free: "Six pushes break a grab before the bite",
700
+ determinism: "The same seed gives the same wounds"
701
+ },
702
+ yes: "yes",
703
+ no: "no"
704
+ };
705
+ var PT_BR2 = {
706
+ group: "Jogador",
707
+ name: "\xC1rea do jogador",
708
+ controls: {
709
+ infection: "Infec\xE7\xE3o individual",
710
+ agility: "Agilidade",
711
+ vigor: "Vigor",
712
+ seed: "Semente",
713
+ hurt: "Levar um golpe (10)",
714
+ bite: "Mordida",
715
+ grab: "Ser agarrado",
716
+ push: "Empurrar (E)",
717
+ knife: "Faca (F)",
718
+ bandage: "Curativo (B)",
719
+ knock: "Derrubado 1,5 s",
720
+ crouch: "Agachar (C)",
721
+ reset: "Recome\xE7ar"
722
+ },
723
+ hints: {
724
+ infection: "Desligada: mundo normal, ferida nunca infecta. Ligada: a regra do jogo (arranh\xE3o 6%, lacera\xE7\xE3o 22%, mordida 100%; febre aos 30 s, morte aos 120 s).",
725
+ agility: "A ficha: f\xF4lego, velocidade de corrida e ru\xEDdo dos passos saem dela.",
726
+ vigor: "A ficha: vida, f\xF4lego e compostura saem dela.",
727
+ seed: "A mesma semente d\xE1 as mesmas feridas e os mesmos golpes de faca.",
728
+ grab: "Algo te agarra por tr\xE1s: 3 s para empurrar (6 empurr\xF5es) ou acertar a faca (70%).",
729
+ push: "Empurra o que est\xE1 na frente, ou se solta do que te segura."
730
+ },
731
+ metrics: {
732
+ health: "Vida",
733
+ stamina: "F\xF4lego",
734
+ bleeding: "Sangrando",
735
+ wounds: "Feridas",
736
+ stage: "Infec\xE7\xE3o",
737
+ time_left: "Tempo restante",
738
+ panic: "P\xE2nico",
739
+ spread: "Dispers\xE3o x",
740
+ grab: "Agarr\xE3o",
741
+ noise: "\xDAltimo ru\xEDdo",
742
+ steps: "Passos",
743
+ message: "Mensagem"
744
+ },
745
+ stages: { none: "Nenhuma", malaise: "Mal-estar", fever: "Febre", delirium: "Del\xEDrio", dead: "Morto" },
746
+ scenarios: {
747
+ stamina: "Correr gasta f\xF4lego e descansar devolve",
748
+ bleed_bandage: "Uma lacera\xE7\xE3o sangra at\xE9 o curativo",
749
+ normal_world: "No mundo normal a mordida machuca e sangra, mas n\xE3o infecta",
750
+ infection_kills: "Com a infec\xE7\xE3o ligada, a mordida d\xE1 febre e mata no tempo dela",
751
+ grab_break_free: "Seis empurr\xF5es soltam o agarr\xE3o antes da mordida",
752
+ determinism: "A mesma semente d\xE1 as mesmas feridas"
753
+ },
754
+ yes: "sim",
755
+ no: "n\xE3o"
756
+ };
757
+ var playerBenchTextOf = (locale) => locale.toLowerCase().startsWith("en") ? EN2 : PT_BR2;
758
+
759
+ // src/bench/module.ts
760
+ var MOVING_SPEED = 0.2;
761
+ var SPRINT_SPEED = 3.4;
762
+ var FRAME = 1 / 30;
763
+ var createGrabber = () => {
764
+ const agent = { x: 0, z: 0, on: false, stunned: 0, hits: 0 };
765
+ const crowd = {
766
+ count: () => agent.on ? 1 : 0,
767
+ active: () => agent.on,
768
+ x: () => agent.x,
769
+ z: () => agent.z,
770
+ y: () => 0,
771
+ floor: () => 0,
772
+ chasing: () => agent.on,
773
+ down: () => agent.stunned > 0,
774
+ hold(_i, x, z, k) {
775
+ agent.x += (x - agent.x) * k;
776
+ agent.z += (z - agent.z) * k;
777
+ },
778
+ stun(_i, seconds) {
779
+ agent.stunned = seconds;
780
+ agent.on = false;
781
+ },
782
+ chase() {
783
+ agent.on = false;
784
+ },
785
+ damage() {
786
+ agent.hits += 1;
787
+ agent.on = false;
788
+ }
789
+ };
790
+ return { agent, crowd };
791
+ };
792
+ function createPlayerTestModule(options = {}) {
793
+ const locale = options.locale ?? "pt-BR";
794
+ const text = playerBenchTextOf(locale);
795
+ const { locale: _locale, ...playerOptions } = options;
796
+ void _locale;
797
+ const settings = { infection: options.infection === void 0 ? false : options.infection !== false, agility: options.attributes?.agility ?? 5, vigor: options.attributes?.vigor ?? 5, seed: options.seed ?? 1 };
798
+ let host = null;
799
+ const grabber = createGrabber();
800
+ let lastNoise = 0;
801
+ let steps = 0;
802
+ let lastMessage = "";
803
+ let pending = {};
804
+ let bandage = false;
805
+ let last = null;
806
+ const offs = [];
807
+ const build = () => {
808
+ for (const off of offs.splice(0)) off();
809
+ const made = createPlayer({ ...playerOptions, seed: settings.seed, infection: settings.infection, attributes: { ...playerOptions.attributes, agility: settings.agility, vigor: settings.vigor } });
810
+ offs.push(made.on("noise", (e) => void (lastNoise = e.radius)));
811
+ offs.push(made.on("step", () => void (steps += 1)));
812
+ offs.push(made.on("message", (e) => void (lastMessage = playerTextOf(e.id, locale))));
813
+ offs.push(made.on("sound", (e) => void host?.audio?.play({ sound: e.sound, volume: e.volume, position: e.position })));
814
+ offs.push(made.on("died", (e) => host?.log(`player died: ${e.cause}`, { cause: e.cause })));
815
+ lastNoise = 0;
816
+ steps = 0;
817
+ lastMessage = "";
818
+ return made;
819
+ };
820
+ let player = build();
821
+ const view = () => {
822
+ const camera = host?.camera;
823
+ if (!camera?.position) return null;
824
+ const Vector3 = host?.THREE?.Vector3;
825
+ if (camera.getWorldDirection && Vector3) {
826
+ const look = camera.getWorldDirection(new Vector3());
827
+ return { x: camera.position.x, z: camera.position.z, yaw: Math.atan2(-look.x, -look.z) };
828
+ }
829
+ return { x: camera.position.x, z: camera.position.z, yaw: camera.rotation?.y ?? 0 };
830
+ };
831
+ const frame = (dt) => {
832
+ const here = view();
833
+ const speed = here && last && dt > 0 ? Math.hypot(here.x - last.x, here.z - last.z) / dt : 0;
834
+ if (here) {
835
+ player.state.x = here.x;
836
+ player.state.z = here.z;
837
+ }
838
+ const input = { forward: speed > MOVING_SPEED ? 1 : 0, run: speed > SPRINT_SPEED, yaw: here?.yaw ?? 0, bandage, pressed: pending };
839
+ pending = {};
840
+ bandage = false;
841
+ player.update(dt, input, { crowd: grabber.crowd });
842
+ if (here) {
843
+ player.state.x = here.x;
844
+ player.state.z = here.z;
845
+ last = { x: here.x, z: here.z };
846
+ }
847
+ if (grabber.agent.stunned > 0) grabber.agent.stunned = Math.max(0, grabber.agent.stunned - dt);
848
+ };
849
+ const grab = () => {
850
+ const P = player.state;
851
+ grabber.agent.on = true;
852
+ grabber.agent.x = P.x - Math.sin(P.yaw) * player.numbers.grabOffset;
853
+ grabber.agent.z = P.z - Math.cos(P.yaw) * player.numbers.grabOffset;
854
+ player.grab(0);
855
+ };
856
+ const onChange = (change) => {
857
+ if (change.kind === "action") {
858
+ const id2 = change.id;
859
+ if (id2 === "hurt") player.hurt(10);
860
+ else if (id2 === "bite") player.bite();
861
+ else if (id2 === "grab") grab();
862
+ else if (id2 === "knock") player.knock(1.5);
863
+ else if (id2 === "bandage") bandage = true;
864
+ else if (id2 === "push" || id2 === "knife" || id2 === "crouch") pending = { ...pending, [id2]: true };
865
+ else if (id2 === "reset") player = build();
866
+ return;
867
+ }
868
+ if (change.kind !== "control") return;
869
+ const { id, value } = change;
870
+ if (id === "infection" && typeof value === "boolean" && value !== settings.infection) settings.infection = value;
871
+ else if ((id === "agility" || id === "vigor" || id === "seed") && typeof value === "number" && value !== settings[id]) settings[id] = value;
872
+ else return;
873
+ player = build();
874
+ };
875
+ const controls = () => [
876
+ { kind: "boolean", id: "infection", name: text.controls.infection, hint: text.hints.infection, default: settings.infection },
877
+ { kind: "number", id: "agility", name: text.controls.agility, hint: text.hints.agility, min: 1, max: 8, step: 1, default: settings.agility },
878
+ { kind: "number", id: "vigor", name: text.controls.vigor, hint: text.hints.vigor, min: 1, max: 8, step: 1, default: settings.vigor },
879
+ { kind: "number", id: "seed", name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: settings.seed },
880
+ { kind: "action", id: "hurt", name: text.controls.hurt },
881
+ { kind: "action", id: "bite", name: text.controls.bite },
882
+ { kind: "action", id: "grab", name: text.controls.grab, hint: text.hints.grab },
883
+ { kind: "action", id: "push", name: text.controls.push, hint: text.hints.push },
884
+ { kind: "action", id: "knife", name: text.controls.knife },
885
+ { kind: "action", id: "bandage", name: text.controls.bandage },
886
+ { kind: "action", id: "knock", name: text.controls.knock },
887
+ { kind: "action", id: "crouch", name: text.controls.crouch },
888
+ { kind: "action", id: "reset", name: text.controls.reset }
889
+ ];
890
+ const stageName = () => player.state.alive ? text.stages[player.stage()] : text.stages.dead;
891
+ const metrics = () => [
892
+ { id: "health", name: text.metrics.health, digits: 1, max: player.derived.maxHealth, read: () => Math.max(0, player.state.health) },
893
+ { id: "stamina", name: text.metrics.stamina, digits: 1, max: player.derived.maxStamina, read: () => player.state.stamina },
894
+ { id: "bleeding", name: text.metrics.bleeding, read: () => player.state.bleeding ? text.yes : text.no },
895
+ { id: "wounds", name: text.metrics.wounds, digits: 0, read: () => player.state.wounds.length },
896
+ { id: "stage", name: text.metrics.stage, read: stageName },
897
+ { id: "time_left", name: text.metrics.time_left, unit: "s", digits: 0, read: () => player.timeLeft() ?? 0 },
898
+ { id: "panic", name: text.metrics.panic, digits: 1, read: () => player.state.panic },
899
+ { id: "spread", name: text.metrics.spread, digits: 2, read: () => player.spreadFactor() },
900
+ { id: "grab_timer", name: text.metrics.grab, unit: "s", digits: 1, read: () => player.state.grabbedBy >= 0 ? player.state.grabTimer : 0 },
901
+ { id: "noise", name: text.metrics.noise, unit: "m", digits: 1, read: () => lastNoise },
902
+ { id: "steps", name: text.metrics.steps, digits: 0, read: () => steps },
903
+ { id: "message", name: text.metrics.message, read: () => lastMessage }
904
+ ];
905
+ const fresh = (extra = {}) => createPlayer({ ...playerOptions, ...extra });
906
+ const run = (made, seconds, input = {}, crowd) => {
907
+ for (let t = 0; t < seconds; t += FRAME) made.update(FRAME, input, crowd ? { crowd } : void 0);
908
+ };
909
+ const scenarios = () => [
910
+ {
911
+ id: "player_stamina",
912
+ name: text.scenarios.stamina,
913
+ run: ({ expect }) => {
914
+ const made = fresh({ seed: 1 });
915
+ const full = made.state.stamina;
916
+ run(made, 4, { forward: 1, run: true });
917
+ const tired = made.state.stamina;
918
+ expect(tired < full - 40, "four seconds of running spend 48 of stamina", { full, tired });
919
+ run(made, 3);
920
+ expect(made.state.stamina > tired, "standing still gives it back", { tired, rested: made.state.stamina });
921
+ }
922
+ },
923
+ {
924
+ id: "player_bleed_bandage",
925
+ name: text.scenarios.bleed_bandage,
926
+ run: ({ expect }) => {
927
+ const made = fresh({ seed: 1 });
928
+ made.bite();
929
+ const after = made.state.health;
930
+ run(made, 2);
931
+ expect(made.state.bleeding === 1 && made.state.health < after, "the wound bleeds", { health: made.state.health });
932
+ run(made, FRAME, { bandage: true });
933
+ const dressed = made.state.health;
934
+ run(made, 2);
935
+ expect(made.state.bleeding === 0 && made.state.health === dressed, "dressed, it stops", { health: made.state.health });
936
+ }
937
+ },
938
+ {
939
+ id: "player_normal_world",
940
+ name: text.scenarios.normal_world,
941
+ run: ({ expect }) => {
942
+ const made = fresh({ seed: 1, infection: false });
943
+ made.bite();
944
+ run(made, 10);
945
+ expect(!made.state.infected && made.state.health < made.derived.maxHealth, "bitten, hurt, not infected", { health: made.state.health });
946
+ }
947
+ },
948
+ {
949
+ id: "player_infection_kills",
950
+ name: text.scenarios.infection_kills,
951
+ run: ({ expect }) => {
952
+ const made = fresh({ seed: 1, infection: true });
953
+ made.bite();
954
+ run(made, FRAME, { bandage: true });
955
+ run(made, 40);
956
+ expect(made.stage() === "fever", "fever after 30 s", { stage: made.stage() });
957
+ run(made, 90);
958
+ expect(!made.state.alive && made.state.cause === "infection", "dead at 120 s", { cause: made.state.cause });
959
+ }
960
+ },
961
+ {
962
+ id: "player_grab_break_free",
963
+ name: text.scenarios.grab_break_free,
964
+ run: ({ expect }) => {
965
+ const made = fresh({ seed: 1 });
966
+ const { agent, crowd } = createGrabber();
967
+ agent.on = true;
968
+ made.grab(0);
969
+ for (let push = 0; push < 6; push++) {
970
+ made.update(FRAME, { pressed: { push: true } }, { crowd });
971
+ made.update(FRAME, {}, { crowd });
972
+ }
973
+ expect(made.state.grabbedBy === -1 && made.state.wounds.length === 0, "free, and no bite", { grabbedBy: made.state.grabbedBy });
974
+ }
975
+ },
976
+ {
977
+ id: "player_determinism",
978
+ name: text.scenarios.determinism,
979
+ run: ({ expect }) => {
980
+ const wounds = (seed) => {
981
+ const made = fresh({ seed });
982
+ for (let k = 0; k < 12; k++) made.hurt(1);
983
+ return made.state.wounds.join(",");
984
+ };
985
+ expect(wounds(4) === wounds(4), "the same seed gives the same wounds");
986
+ expect(wounds(4) !== wounds(5), "another seed gives others");
987
+ }
988
+ }
989
+ ];
990
+ const module = {
991
+ id: "player_area",
992
+ name: text.name,
993
+ group: text.group,
994
+ get player() {
995
+ return player;
996
+ },
997
+ items: () => [],
998
+ controls: () => controls(),
999
+ metrics: () => metrics(),
1000
+ scenarios: () => scenarios(),
1001
+ attach(lent) {
1002
+ host = lent;
1003
+ last = null;
1004
+ const off = lent.bench?.onChange(onChange);
1005
+ const offFrame = lent.onFrame(frame);
1006
+ return () => {
1007
+ off?.();
1008
+ offFrame();
1009
+ host = null;
1010
+ };
1011
+ }
1012
+ };
1013
+ return module;
1014
+ }
1015
+ export {
1016
+ PLAYER_DEFAULTS,
1017
+ PLAYER_TEXT,
1018
+ createPlayer,
1019
+ createPlayerTestModule,
1020
+ createRng,
1021
+ deriveVitals,
1022
+ drawWound,
1023
+ playerTextOf
1024
+ };
1025
+ //# sourceMappingURL=index.js.map