@adatechnology/game-vfx-three 0.2.1 → 0.3.1

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as THREE_NS from 'three';
2
- import { EffectSink, TestableModule } from '@adatechnology/game-contracts';
2
+ import { EffectSink, WarmUpProbes, TestableModule } from '@adatechnology/game-contracts';
3
3
  import { VfxWorld, ScreenState, VfxOptions, VfxProfileName, EffectDef, ExplosionDef, GroundField, SpriteKindSystem, MarkEvent } from '@adatechnology/game-vfx';
4
4
 
5
5
  /** The only three.js members the renderer needs at runtime. Pass your own `THREE` instance (the whole namespace works). */
@@ -56,6 +56,8 @@ interface VfxRenderer extends EffectSink {
56
56
  readonly world: VfxWorld;
57
57
  /** Steps the world, then redraws everything from it. Call once per frame (a shared world is stepped here, nowhere else). */
58
58
  update(dt: number): void;
59
+ /** Meshes that use the materials of the solid particles, to render once so that the first effect does not build their shader programs in the middle of a frame. */
60
+ warmUpProbes(): WarmUpProbes;
59
61
  /** Shakes the camera for the render that follows (the Lab's L1092) and answers the function that puts it back. */
60
62
  applyCamera(): () => void;
61
63
  stats(): VfxRendererStats;
@@ -44,6 +44,15 @@ var GameVfxThree = (() => {
44
44
  createVfxTestModule: () => createVfxTestModule
45
45
  });
46
46
 
47
+ // ../game-contracts/dist/index.js
48
+ var KIT_ORIGIN_KEY = "kitOrigin";
49
+ function markKitOrigin(object, origin) {
50
+ const node = object;
51
+ node.userData = node.userData ?? {};
52
+ node.userData[KIT_ORIGIN_KEY] = { ...origin };
53
+ return object;
54
+ }
55
+
47
56
  // ../game-vfx/dist/index.js
48
57
  var __defProp2 = Object.defineProperty;
49
58
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -3098,6 +3107,17 @@ var GameVfxThree = (() => {
3098
3107
  ])
3099
3108
  ]
3100
3109
  };
3110
+ var FUEL_LEAK_ONLY = {
3111
+ id: "fuel_leak_only",
3112
+ pageId: "vazamento",
3113
+ team: "vehicles",
3114
+ budget: { particles: 200, lights: 0 },
3115
+ loop: 11,
3116
+ view: FUEL_LEAK.view,
3117
+ setup: FUEL_LEAK.setup,
3118
+ timeline: [decal(POOL[0], 0, POOL[2], 2.2, 2040368, { grow: 3, life: 60, ar: 0.6, fluid: "oil" })],
3119
+ tick: [emit("drip", "normal", 12, { p: TANK, box: [0.05, 0, 0.05], sp: 0, life: 1, size: 0.03, c0: 5921402, a: 0.9, g: 9.8, fi: 0, gnd: 1 })]
3120
+ };
3101
3121
  var CAR_EXPLOSION = {
3102
3122
  id: "car_explosion",
3103
3123
  pageId: "explosao",
@@ -3451,6 +3471,8 @@ var GameVfxThree = (() => {
3451
3471
  ];
3452
3472
  var PI = Math.PI;
3453
3473
  var SMOKE = { color: 9079430, width: 10, height: 8, gravity: -0.08, drag: 2.2, growth: 2.1 };
3474
+ var DEFAULT_SMOKE = { puffs: 2, radius: 0.01, opacity: 0.14, life: 0.7, drift: 0.2, rise: 0.06, growth: 2.1, flash: 1 };
3475
+ var DEFAULT_SPREAD = { drift: 0.1, rise: 0.04, side: 0.06 };
3454
3476
  var FLASH_SECONDS = 0.055;
3455
3477
  var FLASH_RAMP = [0.6, 0.9];
3456
3478
  var FLASH_OPACITY = 0.95;
@@ -3458,8 +3480,10 @@ var GameVfxThree = (() => {
3458
3480
  var FLAME_ADDITIVE = 3;
3459
3481
  var ORIGIN2 = [0, 0, 0];
3460
3482
  var SIZE = variable("size");
3483
+ var FLASH_SCALE = variable("flash");
3484
+ var fallback = (name, defaultValue) => ite(ne(variable(name), void 0), variable(name), defaultValue);
3461
3485
  var wide = (width) => mul(sub(rand(), 0.5), width);
3462
- var puff2 = (point, velocity, world, radius, life, opacity) => mesh({
3486
+ var puff2 = (point, velocity, world, radius, life, opacity, growth = SMOKE.growth) => mesh({
3463
3487
  shape: "sphere",
3464
3488
  p: point,
3465
3489
  v: velocity,
@@ -3474,7 +3498,7 @@ var GameVfxThree = (() => {
3474
3498
  lit: true,
3475
3499
  g: SMOKE.gravity,
3476
3500
  drag: SMOKE.drag,
3477
- grow: SMOKE.growth
3501
+ grow: growth
3478
3502
  });
3479
3503
  var flameBit = (point, velocity, world, radius, life) => [
3480
3504
  set("bitPoint", point),
@@ -3507,37 +3531,55 @@ var GameVfxThree = (() => {
3507
3531
  id: "viewmodel_muzzle_flash",
3508
3532
  pageId: "event:viewmodel_muzzle_flash",
3509
3533
  team: "weapons",
3510
- budget: { particles: 12, lights: 1 },
3534
+ budget: { particles: 14, lights: 1 },
3511
3535
  loop: 0.1,
3536
+ setup: [
3537
+ set("size", fallback("size", 1)),
3538
+ set("puffs", fallback("puffs", DEFAULT_SMOKE.puffs)),
3539
+ set("radius", fallback("radius", DEFAULT_SMOKE.radius)),
3540
+ set("opacity", fallback("opacity", DEFAULT_SMOKE.opacity)),
3541
+ set("life", fallback("life", DEFAULT_SMOKE.life)),
3542
+ set("drift", fallback("drift", DEFAULT_SMOKE.drift)),
3543
+ set("rise", fallback("rise", DEFAULT_SMOKE.rise)),
3544
+ set("growth", fallback("growth", DEFAULT_SMOKE.growth)),
3545
+ set("flash", fallback("flash", DEFAULT_SMOKE.flash)),
3546
+ set("driftSpread", fallback("driftSpread", DEFAULT_SPREAD.drift)),
3547
+ set("riseSpread", fallback("riseSpread", DEFAULT_SPREAD.rise)),
3548
+ set("sideSpread", fallback("sideSpread", DEFAULT_SPREAD.side)),
3549
+ set("lifeSpread", ite(ne(variable("lifeSpread"), void 0), variable("lifeSpread"), mul(variable("life"), 0.3)))
3550
+ ],
3512
3551
  timeline: [
3513
3552
  set("angles", []),
3514
3553
  repeat(4, append("angles", add(div(mul(INDEX, PI), 2), mul(rand(), 0.6)))),
3515
3554
  set("roll", mul(rand(), PI)),
3516
- flash(ORIGIN2, 16756832, mul(4, SIZE), FLASH_SECONDS, mul(2, SIZE), true, true),
3517
- flashPiece("cone_open", mul(0.013, SIZE), mul(0.085, SIZE), 10, [mul(0.042, SIZE), 0, 0], [0, 0, -PI / 2], 16757575),
3518
- repeat(
3519
- 4,
3520
- set("angle", pick(variable("angles"), INDEX)),
3521
- flashPiece(
3522
- "cone",
3523
- mul(5e-3, SIZE),
3524
- mul(0.04, SIZE),
3525
- 6,
3526
- [mul(0.012, SIZE), mul(cos(variable("angle")), 0.016, SIZE), mul(sin(variable("angle")), 0.016, SIZE)],
3527
- [variable("angle"), 0, -PI / 2],
3528
- 16757575
3529
- )
3530
- ),
3531
- flashPiece("sphere", mul(0.016, SIZE), void 0, 12, [mul(8e-3, SIZE), 0, 0], [0, 0, 0], 16773568),
3555
+ when(gt(FLASH_SCALE, 0), [
3556
+ flash(ORIGIN2, 16756832, mul(4, SIZE, FLASH_SCALE), FLASH_SECONDS, mul(2, SIZE, FLASH_SCALE), true, true),
3557
+ flashPiece("cone_open", mul(0.013, SIZE, FLASH_SCALE), mul(0.085, SIZE, FLASH_SCALE), 10, [mul(0.042, SIZE, FLASH_SCALE), 0, 0], [0, 0, -PI / 2], 16757575),
3558
+ repeat(
3559
+ 4,
3560
+ set("angle", pick(variable("angles"), INDEX)),
3561
+ flashPiece(
3562
+ "cone",
3563
+ mul(5e-3, SIZE, FLASH_SCALE),
3564
+ mul(0.04, SIZE, FLASH_SCALE),
3565
+ 6,
3566
+ [mul(0.012, SIZE, FLASH_SCALE), mul(cos(variable("angle")), 0.016, SIZE, FLASH_SCALE), mul(sin(variable("angle")), 0.016, SIZE, FLASH_SCALE)],
3567
+ [variable("angle"), 0, -PI / 2],
3568
+ 16757575
3569
+ )
3570
+ ),
3571
+ flashPiece("sphere", mul(0.016, SIZE, FLASH_SCALE), void 0, 12, [mul(8e-3, SIZE, FLASH_SCALE), 0, 0], [0, 0, 0], 16773568)
3572
+ ]),
3532
3573
  repeat(
3533
- 6,
3574
+ variable("puffs"),
3534
3575
  puff2(
3535
3576
  ORIGIN2,
3536
- [add(0.15, mul(rand(), 0.35)), add(0.03, mul(rand(), 0.12)), mul(sub(rand(), 0.5), 0.14)],
3577
+ [add(variable("drift"), mul(rand(), variable("driftSpread"))), add(variable("rise"), mul(rand(), variable("riseSpread"))), mul(sub(rand(), 0.5), variable("sideSpread"))],
3537
3578
  [0, 0, 0],
3538
- mul(0.013, SIZE),
3539
- add(1.3, mul(rand(), 0.6)),
3540
- 0.45
3579
+ mul(variable("radius"), SIZE),
3580
+ add(variable("life"), mul(rand(), variable("lifeSpread"))),
3581
+ variable("opacity"),
3582
+ variable("growth")
3541
3583
  )
3542
3584
  )
3543
3585
  ]
@@ -3591,8 +3633,10 @@ var GameVfxThree = (() => {
3591
3633
  aliasOf(ENGINE_SMOKE, "vehicle_fire", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 3 }, loop: 12 }),
3592
3634
  /** The exhaust of a car under way: `engine_smoke` in the best condition, for half a second so a moving car asks again. */
3593
3635
  aliasOf(ENGINE_SMOKE, "vehicle_exhaust", { params: { id: "condition", options: ["100", "60", "30", "10"], default: 0 }, loop: 0.5 }),
3594
- /** A hole in the tank: the page's own petrol leak (drips, pool, sparks, fire running to the tank). */
3595
- aliasOf(FUEL_LEAK, "vehicle_leak", {})
3636
+ /** A hole in the tank: the page's petrol leak without the fire and the blast (the pool and the drips). A normal bullet does only this. */
3637
+ aliasOf(FUEL_LEAK_ONLY, "vehicle_leak", {}),
3638
+ /** The leak caught fire (an incendiary or tracer round, a spark, a fire near): the page's whole sequence, the fire running along the pool to the tank and the blast. */
3639
+ aliasOf(FUEL_LEAK, "vehicle_leak_ignited", {})
3596
3640
  ];
3597
3641
  var ALL_EFFECTS = [BLOOD_HIT, HEADSHOT, DISMEMBER, BLOATER_CLOUD, SCREAMER_SHRIEK, CORPSE_DECAY, REANIMATION, EYES_IN_FOG, MUZZLE_FLASH, IMPACT_BY_MATERIAL, PENETRATION, CASINGS_AND_MAGAZINE, MOLOTOV, GRENADE, EXPLOSIVE_BARREL, SHOPFRONT_GLASS, DOOR_STAGES, BUILDING_COLLAPSE, BLOCK_WALL, METAL_DOOR_EFFECT, IRON_RAILING, WOODEN_FENCE, HIT_POINT_BREAKAGE, PRESSURE_CYLINDERS, BUILDING_FIRE, RAIN_AND_PUDDLES, FOG_AND_ASH, SCRAPING_SPARKS, ENGINE_SMOKE, FUEL_LEAK, CAR_EXPLOSION, WINDSHIELD, HEADLIGHTS_IN_RAIN, DAMAGE_AND_BITE, INFECTION_PHASES, PANIC, BLOATER_BLINDNESS, CAMPFIRE, COUGH_AND_BREATH];
3598
3642
  var WORKSHOP_EFFECTS = Object.fromEntries(ALL_EFFECTS.map((effect) => [effect.id, effect]));
@@ -7238,7 +7282,7 @@ var GameVfxThree = (() => {
7238
7282
  blood_hit: {
7239
7283
  name: "Blood by infection age",
7240
7284
  short: "A mist on the way in, a jet on the way out, a pool that grows. Age changes colour and amount.",
7241
- cost: "\u2264 70 part.",
7285
+ cost: "\u2264 70 particles",
7242
7286
  costNote: "1 pool \xB7 6 splashes",
7243
7287
  paramLabel: "Age",
7244
7288
  when: "Every bullet or blow that hits a zombie. The blood value comes from the bestiary age: 1.0 \xB7 0.7 \xB7 0.35 \xB7 0.1.",
@@ -7249,7 +7293,7 @@ var GameVfxThree = (() => {
7249
7293
  headshot: {
7250
7294
  name: "Headshot",
7251
7295
  short: "The head bursts, the eyes go out on the same frame and the body drops.",
7252
- cost: "\u2264 70 part.",
7296
+ cost: "\u2264 70 particles",
7253
7297
  costNote: "8 debris \xB7 1 pool",
7254
7298
  when: "A head hit that kills (bestiary rule: a head kills at once).",
7255
7299
  look: "A red mist around the head, a jet of drops backwards, 6 to 8 pieces of bone and skin, a swap to the burst-neck mesh. The eye glow goes out on the same frame, which is the most readable signal from afar.",
@@ -7259,7 +7303,7 @@ var GameVfxThree = (() => {
7259
7303
  dismember: {
7260
7304
  name: "Dismemberment",
7261
7305
  short: "The arm comes off whole with physics and the shoulder gushes for half a second.",
7262
- cost: "\u2264 60 part.",
7306
+ cost: "\u2264 60 particles",
7263
7307
  costNote: "1 limb \xB7 4 splashes",
7264
7308
  when: "Damage to a limb above the threshold (multipliers by region from the bestiary) or an axe blow.",
7265
7309
  look: "The limb becomes a loose piece with physics and bounces. A short pulsing jet from the shoulder, drops that form a trail to the pool. The zombie staggers but keeps walking.",
@@ -7269,7 +7313,7 @@ var GameVfxThree = (() => {
7269
7313
  bloater_cloud: {
7270
7314
  name: "Bloater cloud",
7271
7315
  short: "The belly pulses for 0.5 s, bursts and leaves 4 m of gas for 8 s.",
7272
- cost: "\u2264 180 part.",
7316
+ cost: "\u2264 180 particles",
7273
7317
  costNote: "server entity",
7274
7318
  when: "Death of the Bloater, by any cause.",
7275
7319
  look: "A 0.5 s warning with the belly swelling, a burst with pieces and a greenish jet, and a low moss-green cloud that undulates on the ground for 8 s. Loose green glows inside the cloud mark the area.",
@@ -7279,7 +7323,7 @@ var GameVfxThree = (() => {
7279
7323
  screamer_shriek: {
7280
7324
  name: "Screamer shriek",
7281
7325
  short: "A 1.5 s wind-up with steam, a wave on the ground and the horde's eyes light up.",
7282
- cost: "\u2264 150 part.",
7326
+ cost: "\u2264 150 particles",
7283
7327
  costNote: "3 rings \xB7 aberration",
7284
7328
  when: "The Screamer spots the player and starts the shriek (1.5 s wind-up, calls everyone within 150 m).",
7285
7329
  look: "Head back and steam from the mouth during the wind-up. On the shriek, three pressure rings on the ground, dust raised in the radius, a short chromatic aberration on the screen of whoever is near, and the eyes of nearby zombies flare when they hear.",
@@ -7299,7 +7343,7 @@ var GameVfxThree = (() => {
7299
7343
  corpse_decay: {
7300
7344
  name: "Corpse: darken and dissolve",
7301
7345
  short: "15 minutes in 8 seconds: fall, darken, flies and turn to dust.",
7302
- cost: "\u2264 40 part.",
7346
+ cost: "\u2264 40 particles",
7303
7347
  costNote: "pool of 400 bodies",
7304
7348
  when: "Zombie death. Bestiary timeline: ragdoll 3 s, frozen mesh, darken, dissolve between 14 and 15 min.",
7305
7349
  look: "The skin loses colour until almost black, the pool dries to brown, flies appear from minute 5 and in the last minute the body sinks and gives off dark dust.",
@@ -7309,7 +7353,7 @@ var GameVfxThree = (() => {
7309
7353
  reanimation: {
7310
7354
  name: "Reanimation",
7311
7355
  short: "Veins light up, spasms, the eyes switch on and the body gets up.",
7312
- cost: "\u2264 30 part.",
7356
+ cost: "\u2264 30 particles",
7313
7357
  costNote: "emissive in the shader",
7314
7358
  when: "End of infection of a player or NPC: reanimates in 2 to 6 min as Fresh (70% Slow, 30% Runner).",
7315
7359
  look: "Green pulses through the skin (emissive in the shader, not particles), spasms in the arms and head, the eyes light up from the inside out and the body stands. Breath as steam on the first shriek.",
@@ -7319,7 +7363,7 @@ var GameVfxThree = (() => {
7319
7363
  muzzle_flash: {
7320
7364
  name: "Muzzle flash",
7321
7365
  short: "Core, petals and smoke scaled by weapon. The Barrett raises dust.",
7322
- cost: "\u2264 60 part.",
7366
+ cost: "\u2264 60 particles",
7323
7367
  costNote: "1 light \xB7 50 ms",
7324
7368
  paramLabel: "Weapon",
7325
7369
  when: "Every shot. In first person it uses the weapon model; from afar it is only the core sprite.",
@@ -7330,7 +7374,7 @@ var GameVfxThree = (() => {
7330
7374
  impact: {
7331
7375
  name: "Impact by material",
7332
7376
  short: "Concrete, metal, wood, glass, flesh and water: each with its own response.",
7333
- cost: "\u2264 40 part.",
7377
+ cost: "\u2264 40 particles",
7334
7378
  costNote: "1 hole",
7335
7379
  paramLabel: "Material",
7336
7380
  when: "Every bullet that hits something. The material comes from the mesh surface tag.",
@@ -7341,7 +7385,7 @@ var GameVfxThree = (() => {
7341
7385
  penetration: {
7342
7386
  name: "Bullet through the horde",
7343
7387
  short: "Barrett tracer passing through 5 zombies, in 10x slow motion.",
7344
- cost: "\u2264 250 part.",
7388
+ cost: "\u2264 250 particles",
7345
7389
  costNote: "aggregate event",
7346
7390
  when: "Weapon with penetration: the AK goes through 2, the Remington 700 through 3, the Barrett through 6 and cars.",
7347
7391
  look: "A tracer with a hot-air trail, blood exiting each body in the order the bullet passes (each in the colour of its age), bodies falling one after another and a final impact on the concrete.",
@@ -7361,7 +7405,7 @@ var GameVfxThree = (() => {
7361
7405
  molotov: {
7362
7406
  name: "Molotov cocktail",
7363
7407
  short: "A lit wick in the arc, glass breaking and a pool of fire that spreads.",
7364
- cost: "\u2264 300 part.",
7408
+ cost: "\u2264 300 particles",
7365
7409
  costNote: "2 lights \xB7 entity",
7366
7410
  when: "A molotov hitting the ground or a zombie.",
7367
7411
  look: "A bottle in an arc with the wick giving off flame and smoke. On impact, green shards and a splash of fire that opens from 0.5 m to 2.2 m in 0.8 s. Low flames, embers, dark smoke and a flickering light for 6 s, then embers and a burn mark.",
@@ -7371,7 +7415,7 @@ var GameVfxThree = (() => {
7371
7415
  grenade: {
7372
7416
  name: "Fragmentation grenade",
7373
7417
  short: "A bounce on the asphalt, flash, shock wave, shrapnel and a ring of dust.",
7374
- cost: "\u2264 400 part.",
7418
+ cost: "\u2264 400 particles",
7375
7419
  costNote: "aggregate event",
7376
7420
  when: "A grenade exploding (1.8 s fuse after the throw).",
7377
7421
  look: "The grenade bounces twice. A layered explosion: a white flash that paints the screen, a short fireball, a shock ring on the ground and another facing the camera, bright shrapnel that bounces, a ring of low dust, grey smoke that rises and a burn mark. Zombies in the radius fall outwards.",
@@ -7381,7 +7425,7 @@ var GameVfxThree = (() => {
7381
7425
  explosive_barrel: {
7382
7426
  name: "Explosive barrel",
7383
7427
  short: "A mushroom of fire, the lid flying, petrol burning on the ground and a column of smoke.",
7384
- cost: "\u2264 450 part.",
7428
+ cost: "\u2264 450 particles",
7385
7429
  costNote: "2 lights \xB7 fire entity",
7386
7430
  when: "A red barrel with damage above the threshold, a shot or fire touching it.",
7387
7431
  look: "Everything the grenade does, plus: a fireball that rises into a mushroom, the lid and red pieces flying high, three pools of petrol burning for 5 s and a black smoke column that serves as a landmark from afar.",
@@ -7401,7 +7445,7 @@ var GameVfxThree = (() => {
7401
7445
  door_stages: {
7402
7446
  name: "Door giving way in 3 stages",
7403
7447
  short: "Blows shake dust from the frame, a plank cracks and the door falls.",
7404
- cost: "\u2264 120 part.",
7448
+ cost: "\u2264 120 particles",
7405
7449
  costNote: "3 stages",
7406
7450
  when: "The horde hitting a door or barricade (the Brute does 3x damage). Stages: 66%, 33%, 0% health.",
7407
7451
  look: "Each blow shakes the door and loosens dust from the frame and splinters. At stage 2 the middle plank cracks and falls. At 3 the door topples and raises a cloud of low dust, and the zombie comes out of the dark.",
@@ -7411,7 +7455,7 @@ var GameVfxThree = (() => {
7411
7455
  building_collapse: {
7412
7456
  name: "Collapse",
7413
7457
  short: "Dust trickling from the awning, blocks falling and a cloud of dust rolling down the street.",
7414
- cost: "\u2264 350 part.",
7458
+ cost: "\u2264 350 particles",
7415
7459
  costNote: "26 debris",
7416
7460
  when: "A structure that loses its health: explosion, vehicle impact, a big horde pushing (destruction of the map buildings).",
7417
7461
  look: "Before: fine dust trickling and pebbles falling for 1 s, as a warning. Then: large and small blocks fall, the camera shakes and a cloud of grey-beige dust rolls along the ground and hangs in the air for 5 s, hiding zombies.",
@@ -7421,7 +7465,7 @@ var GameVfxThree = (() => {
7421
7465
  block_wall: {
7422
7466
  name: "Wall brought down with holes",
7423
7467
  short: "A burst leaves holes and chips; the wall falls in blocks and the holes stay on them.",
7424
- cost: "\u2264 300 part.",
7468
+ cost: "\u2264 300 particles",
7425
7469
  costNote: "30 blocks \xB7 attached holes",
7426
7470
  when: "A concrete wall, low wall or barricade that loses health from shots, explosion, the Brute or a car (map and arsenal destruction).",
7427
7471
  look: "Each bullet opens a dark hole with a chipped rim, giving off dust and pebbles. When the wall gives way it splits into blocks that topple with physics and a cloud of dust rolls along the ground. The bullet holes are attached to the blocks: they stay on the fallen pieces, turned with them, and do not vanish when the wall breaks.",
@@ -7431,7 +7475,7 @@ var GameVfxThree = (() => {
7431
7475
  metal_door: {
7432
7476
  name: "Metal door denting",
7433
7477
  short: "Each blow sinks the plate, flakes the paint and throws sparks; the hinge bursts.",
7434
- cost: "\u2264 160 part.",
7478
+ cost: "\u2264 160 particles",
7435
7479
  costNote: "deformable mesh",
7436
7480
  when: "A door, gate or steel plate taking a zombie blow (Brute 3x), a 12-gauge shot or a car hit.",
7437
7481
  look: "The plate sinks where it is hit (a deformed mesh, not a model swap), with 3 visible levels. Each blow gives off sparks, flakes of green paint and rust dust. When the health runs out the top hinge bursts in sparks, the door topples and scrapes on the ground giving off more sparks.",
@@ -7441,7 +7485,7 @@ var GameVfxThree = (() => {
7441
7485
  iron_railing: {
7442
7486
  name: "Iron railing bending",
7443
7487
  short: "The horde pushes, the bars bow, the weld pops in a spark and the railing falls.",
7444
- cost: "\u2264 200 part.",
7488
+ cost: "\u2264 200 particles",
7445
7489
  costNote: "3 deformable bars",
7446
7490
  when: "A railing, iron gate, handrail or chain-link fence with zombies pushing (horde pressure) or a car hitting it.",
7447
7491
  look: "The bars bow gradually at the point of pressure and the vertical ones tilt with them. Rust falls in a thread during the strain. At two moments the weld pops with a crackle of sparks. At the end the middle post gives and the railing topples, leaving the bent iron on the ground.",
@@ -7451,7 +7495,7 @@ var GameVfxThree = (() => {
7451
7495
  wooden_fence: {
7452
7496
  name: "Wooden fence breaking",
7453
7497
  short: "Planks snap one by one, chips and dust, and the section topples as the horde passes.",
7454
- cost: "\u2264 150 part.",
7498
+ cost: "\u2264 150 particles",
7455
7499
  costNote: "12 planks",
7456
7500
  when: "A fence, yard gate or plank barricade taking a shot, blow or horde pressure.",
7457
7501
  look: "Each plank snaps at a random point: the stump stays on the rail and the top piece flies off with chips and wood dust. The zombie appears through the gap. At the end the rail cracks and the whole section topples forward.",
@@ -7461,7 +7505,7 @@ var GameVfxThree = (() => {
7461
7505
  hit_point_breakage: {
7462
7506
  name: "Breakage by the point hit",
7463
7507
  short: "A shot to the leg, a machete on the corner, a bat on the top and a 12-gauge up close: each weapon breaks the part it hit.",
7464
- cost: "\u2264 120 part.",
7508
+ cost: "\u2264 120 particles",
7465
7509
  costNote: "parts with their own health",
7466
7510
  when: "Any destructible item. The damage goes to the part that was hit, not to the whole item.",
7467
7511
  look: "A 9 mm shot to the front leg: the lower part of the leg breaks off and the table tips toward that corner. A machete on the corner of the top: the corner comes off whole in a straight cut, with chips in a line. A bat on the other corner: the top cracks and falls apart in pieces with dust. A 12-gauge up close on what is left: 9 grouped holes that stay in the wood.",
@@ -7471,7 +7515,7 @@ var GameVfxThree = (() => {
7471
7515
  pressure_cylinders: {
7472
7516
  name: "Extinguisher and oxygen cylinder",
7473
7517
  short: "A shot to the extinguisher makes a white cloud and a spin; on oxygen the valve breaks and the cylinder becomes a rocket.",
7474
- cost: "\u2264 250 part.",
7518
+ cost: "\u2264 250 particles",
7475
7519
  costNote: "cloud entity",
7476
7520
  paramLabel: "Cylinder",
7477
7521
  when: "A shot, a strong blow or fire on an extinguisher, oxygen cylinder or welding cylinder (extinguisher and oxygen_cylinder keys of the map catalogue).",
@@ -7482,7 +7526,7 @@ var GameVfxThree = (() => {
7482
7526
  building_fire: {
7483
7527
  name: "Building on fire",
7484
7528
  short: "Fire coming out of the windows, licking the facade, embers and smoke with the wind.",
7485
- cost: "\u2264 600 part.",
7529
+ cost: "\u2264 600 particles",
7486
7530
  costNote: "2 lights \xB7 reference",
7487
7531
  when: "A building fire (molotov, explosion, vehicle). It lasts minutes and spreads to the neighbour.",
7488
7532
  look: "Windows lit orange that flicker, tongues of fire coming out of the top of each window and climbing the facade, embers carried by the wind and a thick slanted column of smoke. The light paints the street.",
@@ -7523,7 +7567,7 @@ var GameVfxThree = (() => {
7523
7567
  engine_smoke: {
7524
7568
  name: "Engine smoke",
7525
7569
  short: "White steam, grey smoke, then black with fire, depending on condition.",
7526
- cost: "\u2264 120 part.",
7570
+ cost: "\u2264 120 particles",
7527
7571
  costNote: "continuous",
7528
7572
  paramLabel: "Condition",
7529
7573
  when: "Whenever the engine is on or hot, from the part condition (0 to 100) in the fleet brief.",
@@ -7534,7 +7578,7 @@ var GameVfxThree = (() => {
7534
7578
  fuel_leak: {
7535
7579
  name: "Tank leak and fire",
7536
7580
  short: "Dripping petrol, a pool, a spark and the fire running to the tank.",
7537
- cost: "\u2264 400 part.",
7581
+ cost: "\u2264 400 particles",
7538
7582
  costNote: "fire entity",
7539
7583
  when: "A tank punctured by a shot or a crash. A spark, a shot or nearby fire lights the pool.",
7540
7584
  look: "Drops of petrol with an oily sheen forming a growing pool. A shot on the metal throws a spark, the fire lights on the pool, runs to the car and, if nobody puts it out, the car explodes.",
@@ -7544,7 +7588,7 @@ var GameVfxThree = (() => {
7544
7588
  car_explosion: {
7545
7589
  name: "Car explosion",
7546
7590
  short: "A fireball, the car jumping, the bonnet and doors flying, the shell burning.",
7547
- cost: "\u2264 600 part.",
7591
+ cost: "\u2264 600 particles",
7548
7592
  costNote: "rare event",
7549
7593
  when: "A tank on fire for more than 4 s, or damage above the threshold on a car with fuel.",
7550
7594
  look: "A large explosion (grenade scale 1.8), the car jumps and lands crooked, the bonnet, doors and a wheel fly off, the glass bursts into shards. The bodywork goes black and the shell burns with black smoke for minutes.",
@@ -7605,7 +7649,7 @@ var GameVfxThree = (() => {
7605
7649
  bloater_blindness: {
7606
7650
  name: "Bloater blindness",
7607
7651
  short: "Green gas in the face: a blurred screen for 5 s and mucus on the lens.",
7608
- cost: "post + 30 part.",
7652
+ cost: "post + 30 particles",
7609
7653
  costNote: "server entity",
7610
7654
  when: "A player inside the Bloater cloud (radius 4 m) when it spawns.",
7611
7655
  look: "A green cloud bursts between the zombie and the camera, the screen goes green and blurred and clears in 5 s, with drops of green mucus on the lens that run down. A cough shakes the camera twice.",
@@ -7615,7 +7659,7 @@ var GameVfxThree = (() => {
7615
7659
  campfire: {
7616
7660
  name: "Campsite fire",
7617
7661
  short: "A low flame, embers spiralling up and a flickering warm light.",
7618
- cost: "\u2264 120 part.",
7662
+ cost: "\u2264 120 particles",
7619
7663
  costNote: "1 fire light",
7620
7664
  when: "A campfire lit by the group or by NPCs. It warms, lights and draws zombies from afar (light).",
7621
7665
  look: "Small low flames, embers that rise turning and fade, thin smoke and a warm light that flickers and paints the survivor.",
@@ -7625,7 +7669,7 @@ var GameVfxThree = (() => {
7625
7669
  cough_and_breath: {
7626
7670
  name: "Cough, breath and flashlight",
7627
7671
  short: "Breath vapour in the cold, the fever cough visible to others and the flashlight beam.",
7628
- cost: "\u2264 40 part.",
7672
+ cost: "\u2264 40 particles",
7629
7673
  costNote: "1 spot with shadow",
7630
7674
  when: "A cold night: breath always. Infection fever: a cough every few seconds, audible at 10 m.",
7631
7675
  look: "A short puff of vapour every 2.4 s. On a cough, three quick bigger puffs and the body jolts. The flashlight shows the beam in the fog and is the only light with a shadow.",
@@ -8575,6 +8619,81 @@ void main(){
8575
8619
  };
8576
8620
  }
8577
8621
 
8622
+ // src/mesh-layer.ts
8623
+ var SPHERE_DEFAULT_WIDTH = 8;
8624
+ var SPHERE_DEFAULT_HEIGHT = 6;
8625
+ function geometryOf(THREE, particle) {
8626
+ switch (particle.shape) {
8627
+ case "cone":
8628
+ return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
8629
+ case "cone_open":
8630
+ return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
8631
+ case "sphere":
8632
+ return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8633
+ case "icosahedron":
8634
+ return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8635
+ }
8636
+ }
8637
+ function materialOf(THREE, particle) {
8638
+ if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
8639
+ return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
8640
+ }
8641
+ function createMeshLayerProbes(THREE) {
8642
+ const geometry = new THREE.BoxGeometry(1e-3, 1e-3, 1e-3);
8643
+ return [{ lit: true, additive: false }, { lit: false, additive: false }, { lit: false, additive: true }].map(
8644
+ (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
8645
+ );
8646
+ }
8647
+ function createMeshLayer(THREE, pool) {
8648
+ const group = new THREE.Group();
8649
+ group.name = "workshop-meshes";
8650
+ const drawn = /* @__PURE__ */ new Map();
8651
+ const frame = new THREE.Quaternion();
8652
+ const euler = new THREE.Euler();
8653
+ const turn = new THREE.Quaternion();
8654
+ const offset = new THREE.Vector3();
8655
+ function release(mesh2) {
8656
+ mesh2.removeFromParent();
8657
+ mesh2.geometry.dispose();
8658
+ mesh2.material.dispose();
8659
+ }
8660
+ return {
8661
+ group,
8662
+ sync() {
8663
+ const live = /* @__PURE__ */ new Set();
8664
+ for (const particle of pool.particles) {
8665
+ live.add(particle.id);
8666
+ let mesh2 = drawn.get(particle.id);
8667
+ if (!mesh2) {
8668
+ mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
8669
+ mesh2.frustumCulled = false;
8670
+ mesh2.renderOrder = particle.additive ? 5 : 3;
8671
+ drawn.set(particle.id, mesh2);
8672
+ group.add(mesh2);
8673
+ }
8674
+ frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
8675
+ offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
8676
+ mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
8677
+ turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
8678
+ mesh2.quaternion.copy(frame).multiply(turn);
8679
+ mesh2.scale.setScalar(particle.scale);
8680
+ mesh2.material.opacity = particle.opacity;
8681
+ }
8682
+ for (const [id, mesh2] of drawn) {
8683
+ if (live.has(id)) continue;
8684
+ release(mesh2);
8685
+ drawn.delete(id);
8686
+ }
8687
+ return pool.count;
8688
+ },
8689
+ dispose() {
8690
+ for (const mesh2 of drawn.values()) release(mesh2);
8691
+ drawn.clear();
8692
+ group.removeFromParent();
8693
+ }
8694
+ };
8695
+ }
8696
+
8578
8697
  // src/screen-overlay.ts
8579
8698
  var clamp5 = (value, min2, max2) => value < min2 ? min2 : value > max2 ? max2 : value;
8580
8699
  function layer(container, name, blend) {
@@ -8754,75 +8873,6 @@ void main() {
8754
8873
  };
8755
8874
  }
8756
8875
 
8757
- // src/mesh-layer.ts
8758
- var SPHERE_DEFAULT_WIDTH = 8;
8759
- var SPHERE_DEFAULT_HEIGHT = 6;
8760
- function geometryOf(THREE, particle) {
8761
- switch (particle.shape) {
8762
- case "cone":
8763
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
8764
- case "cone_open":
8765
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
8766
- case "sphere":
8767
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
8768
- case "icosahedron":
8769
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
8770
- }
8771
- }
8772
- function materialOf(THREE, particle) {
8773
- if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
8774
- return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
8775
- }
8776
- function createMeshLayer(THREE, pool) {
8777
- const group = new THREE.Group();
8778
- group.name = "workshop-meshes";
8779
- const drawn = /* @__PURE__ */ new Map();
8780
- const frame = new THREE.Quaternion();
8781
- const euler = new THREE.Euler();
8782
- const turn = new THREE.Quaternion();
8783
- const offset = new THREE.Vector3();
8784
- function release(mesh2) {
8785
- mesh2.removeFromParent();
8786
- mesh2.geometry.dispose();
8787
- mesh2.material.dispose();
8788
- }
8789
- return {
8790
- group,
8791
- sync() {
8792
- const live = /* @__PURE__ */ new Set();
8793
- for (const particle of pool.particles) {
8794
- live.add(particle.id);
8795
- let mesh2 = drawn.get(particle.id);
8796
- if (!mesh2) {
8797
- mesh2 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
8798
- mesh2.frustumCulled = false;
8799
- mesh2.renderOrder = particle.additive ? 5 : 3;
8800
- drawn.set(particle.id, mesh2);
8801
- group.add(mesh2);
8802
- }
8803
- frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
8804
- offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
8805
- mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
8806
- turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
8807
- mesh2.quaternion.copy(frame).multiply(turn);
8808
- mesh2.scale.setScalar(particle.scale);
8809
- mesh2.material.opacity = particle.opacity;
8810
- }
8811
- for (const [id, mesh2] of drawn) {
8812
- if (live.has(id)) continue;
8813
- release(mesh2);
8814
- drawn.delete(id);
8815
- }
8816
- return pool.count;
8817
- },
8818
- dispose() {
8819
- for (const mesh2 of drawn.values()) release(mesh2);
8820
- drawn.clear();
8821
- group.removeFromParent();
8822
- }
8823
- };
8824
- }
8825
-
8826
8876
  // src/shaders/workshop-sprite.ts
8827
8877
  var WORKSHOP_SPRITE_VERTEX = `attribute float size;attribute float alpha;attribute vec3 pcol;varying vec3 vC;varying float vA;uniform float uScale;
8828
8878
  void main(){vC=pcol;vA=alpha;vec4 mv=modelViewMatrix*vec4(position,1.);gl_PointSize=clamp(size*uScale/-mv.z,1.,512.);gl_Position=projectionMatrix*mv;}`;
@@ -9193,7 +9243,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9193
9243
  const requested = options.vfx;
9194
9244
  const world = isWorld(requested) ? requested : createVfx({ profile: options.quality ?? "medium", reducedMotion, ...requested });
9195
9245
  const pools = world.pools();
9196
- const root = new THREE.Group();
9246
+ let anchors;
9247
+ const releaseAnchors = () => {
9248
+ if (!anchors) return;
9249
+ anchors.meshes[0]?.geometry.dispose();
9250
+ for (const mesh2 of anchors.meshes) mesh2.material.dispose();
9251
+ anchors = void 0;
9252
+ };
9253
+ const root = markKitOrigin(new THREE.Group(), { owner: "game-vfx-three", kind: "effect" });
9197
9254
  options.scene.add(root);
9198
9255
  const direction = new THREE.Vector3(...options.lightDirection ?? LAB_LIGHT_DIRECTION).normalize();
9199
9256
  const sky = hexToLinearRgb(SKY_COLOR);
@@ -9309,6 +9366,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9309
9366
  syncAll();
9310
9367
  overlay?.update(screenNow());
9311
9368
  },
9369
+ warmUpProbes() {
9370
+ releaseAnchors();
9371
+ const meshes = createMeshLayerProbes(THREE);
9372
+ for (const mesh2 of meshes) mesh2.frustumCulled = false;
9373
+ const group = new THREE.Group();
9374
+ group.add(...meshes);
9375
+ anchors = { meshes, group };
9376
+ return { group, dispose: () => {
9377
+ } };
9378
+ },
9312
9379
  applyCamera() {
9313
9380
  const shake = screenNow().shake;
9314
9381
  if (reducedMotion || !(shake > SHAKE_FLOOR)) return () => {
@@ -9327,6 +9394,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9327
9394
  },
9328
9395
  screen: screenNow,
9329
9396
  dispose() {
9397
+ releaseAnchors();
9330
9398
  stopMarks();
9331
9399
  overlay?.dispose();
9332
9400
  workshopLayers?.dispose();
@@ -9591,6 +9659,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9591
9659
  controls,
9592
9660
  layers,
9593
9661
  metrics,
9662
+ // opening the effects area puts the camera at the bench view (the initial distance does not move it: that would take the player's view on load)
9663
+ area: () => ({
9664
+ placements: [],
9665
+ view: { eye: eyeFor(DEFAULT_DISTANCE), target: [0, 1.2, 0] }
9666
+ }),
9594
9667
  attach(host) {
9595
9668
  const bench = host.bench;
9596
9669
  let own;
@@ -9610,6 +9683,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9610
9683
  let phase = 0;
9611
9684
  let handle = null;
9612
9685
  let sinceRepeat = 0;
9686
+ let live = false;
9613
9687
  read = () => {
9614
9688
  const def = explosions[effect];
9615
9689
  const recipe = recipes[effect];
@@ -9648,7 +9722,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9648
9722
  }
9649
9723
  );
9650
9724
  bench?.sheet?.(sheetFor(id));
9651
- replay();
9725
+ if (live) replay();
9652
9726
  }
9653
9727
  function look() {
9654
9728
  const eye = eyeFor(distance);
@@ -9665,7 +9739,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9665
9739
  choose(change.value);
9666
9740
  else if (change.id === "variant" && typeof change.value === "string") {
9667
9741
  variant = change.value;
9668
- if (recipes[effect]?.params) replay();
9742
+ if (live && recipes[effect]?.params) replay();
9669
9743
  } else if (change.id === "auto_repeat") auto = change.value === true;
9670
9744
  else if (change.id === "distance" && typeof change.value === "number") {
9671
9745
  distance = change.value;
@@ -9675,6 +9749,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9675
9749
  };
9676
9750
  const offChange = bench?.onChange(apply);
9677
9751
  const offFrame = host.onFrame((dt) => {
9752
+ live = true;
9678
9753
  phase += dt;
9679
9754
  sinceRepeat += dt;
9680
9755
  own?.update(dt);