incanto 0.4.3 → 0.6.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.
Files changed (44) hide show
  1. package/dist/2d.d.ts +80 -6
  2. package/dist/2d.js +4 -4
  3. package/dist/3d.d.ts +200 -15
  4. package/dist/3d.js +4 -4
  5. package/dist/{audio-player-DqUR3XFs.d.ts → audio-player-DkBqRTs4.d.ts} +1 -1
  6. package/dist/{behavior-BAQq7HGM.d.ts → behavior-CWhW3oa6.d.ts} +54 -0
  7. package/dist/{create-game-CZHROKcT.js → create-game-B_sW_eiE.js} +59 -27
  8. package/dist/{create-game-DiTq3-fQ.js → create-game-DkbZCNaV.js} +292 -14
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DP2WPYom.js → duplicate-BEvGBtb_.js} +1 -1
  11. package/dist/{gameplay-Ccruc3Wd.js → gameplay-CDFgSG6z.js} +293 -24
  12. package/dist/gameplay.d.ts +113 -2
  13. package/dist/gameplay.js +2 -2
  14. package/dist/index.d.ts +139 -5
  15. package/dist/index.js +59 -6
  16. package/dist/{loader-CGs_G-r0.js → loader-B4OEXDZ8.js} +60 -0
  17. package/dist/{loader-Mo0KghCv.d.ts → loader-D2u0fVW2.d.ts} +1 -1
  18. package/dist/net.d.ts +1 -1
  19. package/dist/net.js +1 -1
  20. package/dist/{particle-sim-DYuSUxvK.js → particle-sim-CFkILGwh.js} +84 -3
  21. package/dist/{particle-sim-CbN4YUuH.d.ts → particle-sim-CwJ5rI_P.d.ts} +3 -0
  22. package/dist/{physics-2d-KuMWPTf6.js → physics-2d-Cgli1aju.js} +95 -10
  23. package/dist/{physics-3d-DmNCeh58.js → physics-3d-CBAQ12LY.js} +121 -43
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-DPEV9_9t.js → register-BJCfuuZ2.js} +79 -5
  27. package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
  28. package/dist/{register-BRy8FNox.js → register-Dd7Juujf.js} +389 -49
  29. package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-DryTXVd4.js → agent8-MciFwn0v.js} +1 -1
  33. package/editor/assets/index-mofVSmuA.js +7417 -0
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +980 -126
  37. package/skills/incanto-3d-models.md +24 -1
  38. package/skills/incanto-building-2d-games.md +6 -0
  39. package/skills/incanto-building-3d-games.md +46 -1
  40. package/skills/incanto-gameplay-behaviors.md +60 -0
  41. package/skills/incanto-hud.md +58 -0
  42. package/skills/incanto-node-reference.md +148 -0
  43. package/skills/incanto-physics-and-input.md +47 -0
  44. package/editor/assets/index-N8se-PhP.js +0 -7365
@@ -1,6 +1,7 @@
1
- import { _ as registerBehavior, m as Behavior } from "./loader-CGs_G-r0.js";
1
+ import { _ as registerBehavior, m as Behavior, n as loadScene } from "./loader-B4OEXDZ8.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { t as duplicateNode } from "./duplicate-DP2WPYom.js";
3
+ import { t as jsonClone } from "./json-BLk7H2Qa.js";
4
+ import { t as duplicateNode } from "./duplicate-BEvGBtb_.js";
4
5
  //#region src/gameplay/spatial.ts
5
6
  /** Duck-type test: every spatial node (Node2D/Node3D) exposes `position: number[]`. */
6
7
  function hasPosition$1(node) {
@@ -36,13 +37,6 @@ function add(a, b) {
36
37
  for (let i = 0; i < n; i++) out[i] = (a[i] ?? 0) + (b[i] ?? 0);
37
38
  return out;
38
39
  }
39
- /** Component-wise `a - b`. */
40
- function sub(a, b) {
41
- const n = Math.max(a.length, b.length);
42
- const out = new Array(n);
43
- for (let i = 0; i < n; i++) out[i] = (a[i] ?? 0) - (b[i] ?? 0);
44
- return out;
45
- }
46
40
  /** Vector length. */
47
41
  function length(v) {
48
42
  let sum = 0;
@@ -60,15 +54,28 @@ function normalize(v) {
60
54
  * whether the destination was reached this step (clamped — no overshoot).
61
55
  */
62
56
  function moveToward(from, to, maxStep) {
63
- const delta = sub(to, from);
64
- const dist = length(delta);
65
- if (dist <= maxStep || dist === 0) return {
66
- position: [...to],
67
- reached: true
68
- };
57
+ const n = Math.max(from.length, to.length);
58
+ let sum = 0;
59
+ for (let i = 0; i < n; i++) {
60
+ const d = (to[i] ?? 0) - (from[i] ?? 0);
61
+ sum += d * d;
62
+ }
63
+ const dist = Math.sqrt(sum);
64
+ const position = new Array(n);
65
+ if (dist <= maxStep || dist === 0) {
66
+ for (let i = 0; i < n; i++) position[i] = to[i] ?? 0;
67
+ return {
68
+ position,
69
+ reached: true
70
+ };
71
+ }
69
72
  const f = maxStep / dist;
73
+ for (let i = 0; i < n; i++) {
74
+ const c = from[i] ?? 0;
75
+ position[i] = c + ((to[i] ?? 0) - c) * f;
76
+ }
70
77
  return {
71
- position: from.map((c, i) => c + (delta[i] ?? 0) * f),
78
+ position,
72
79
  reached: false
73
80
  };
74
81
  }
@@ -427,19 +434,128 @@ var FollowCamera = class extends Behavior {
427
434
  if (this.deadzone < 0) throw new IncantoError("PROP_TYPE_MISMATCH", `FollowCamera on '${this.node.getPath()}': "deadzone" must be >= 0, got ${this.deadzone}.`, { prop: "deadzone" });
428
435
  requirePosition(this);
429
436
  }
437
+ shakeMag = 0;
438
+ shakeT = 0;
439
+ shakeFalloff = .28;
440
+ /** Kick the camera (impacts, explosions). Composes with the follow. */
441
+ shake(magnitude, seconds = .28) {
442
+ this.shakeMag = Math.max(this.shakeMag, magnitude);
443
+ this.shakeT = seconds;
444
+ this.shakeFalloff = seconds;
445
+ }
430
446
  update(dt) {
431
447
  const cam = this.node;
432
448
  if (!hasPosition$1(cam)) return;
433
449
  const target = cam.getNodeOrNull(this.target);
434
450
  if (!target || !hasPosition$1(target)) return;
435
451
  const desired = add(target.position, this.offset);
436
- if (this.deadzone > 0 && distance$1(cam.position, desired) <= this.deadzone) return;
437
- const f = smoothingFactor(this.smoothing, dt);
438
- if (f >= 1) {
439
- cam.position = [...desired];
440
- return;
452
+ if (!(this.deadzone > 0 && distance$1(cam.position, desired) <= this.deadzone)) {
453
+ const f = smoothingFactor(this.smoothing, dt);
454
+ if (f >= 1) cam.position = [...desired];
455
+ else cam.position = cam.position.map((c, i) => c + ((desired[i] ?? c) - c) * f);
456
+ }
457
+ if (this.shakeT > 0) {
458
+ this.shakeT -= dt;
459
+ const k = this.shakeMag * Math.max(0, this.shakeT) / this.shakeFalloff;
460
+ if (this.shakeT <= 0) this.shakeMag = 0;
461
+ const jolted = cam.position.slice();
462
+ for (let i = 0; i < jolted.length; i++) {
463
+ const scale = jolted.length >= 3 && i === 1 ? .5 : 1;
464
+ jolted[i] = (jolted[i] ?? 0) + (this.engine.rng.next() * 2 - 1) * k * scale;
465
+ }
466
+ cam.position = jolted;
441
467
  }
442
- cam.position = cam.position.map((c, i) => c + ((desired[i] ?? c) - c) * f);
468
+ }
469
+ };
470
+ //#endregion
471
+ //#region src/gameplay/game-flow.ts
472
+ /**
473
+ * Reload the CURRENT scene from its source JSON — fresh nodes, reset physics,
474
+ * rewired input. The restart primitive every game-over screen wants.
475
+ */
476
+ function restartScene(engine) {
477
+ const source = engine.scene?.source;
478
+ if (!source) return;
479
+ engine.timeScale = 1;
480
+ engine.setScene(loadScene(jsonClone(source)));
481
+ }
482
+ /**
483
+ * The win/lose/restart state machine 6+ examples hand-rolled as `over`/`win`
484
+ * booleans. Attach to any node (the scene root is natural):
485
+ *
486
+ * const flow = root.behavior as GameFlow; // script: { "name": "GameFlow" }
487
+ * flow.gameOver('YOU DIED'); // freezes time, sticky banner
488
+ * flow.win('AREA CLEAR');
489
+ * flow.pause(); flow.resume();
490
+ *
491
+ * While in `gameover`/`won`, pressing `restartAction` (default action name
492
+ * 'restart' — declare it in the scene input map, or leave undeclared and call
493
+ * `flow.restart()` yourself) reloads the scene from source. Banners render
494
+ * through a `%Banner` UiBanner when one exists; otherwise states are silent
495
+ * (drive your own UI off the `flowChanged` signal).
496
+ */
497
+ var GameFlow = class extends Behavior {
498
+ static signals = ["flowChanged"];
499
+ static props = {
500
+ restartAction: { default: "restart" },
501
+ freezeOnEnd: { default: true },
502
+ bannerPath: { default: "%Banner" }
503
+ };
504
+ /** Input action that restarts from gameover/won (declare it in `input{}`). */
505
+ restartAction = "restart";
506
+ /** Freeze `engine.timeScale` on gameover/won (banner UI keeps rendering). */
507
+ freezeOnEnd = true;
508
+ /** Where the flow looks for a UiBanner ('' = never). */
509
+ bannerPath = "%Banner";
510
+ state = "playing";
511
+ frozeScale = false;
512
+ gameOver(text = "GAME OVER", color = "#ef4444") {
513
+ this.transition("gameover", text, color);
514
+ }
515
+ win(text = "YOU WIN", color = "#4ade80") {
516
+ this.transition("won", text, color);
517
+ }
518
+ pause() {
519
+ if (this.state !== "playing") return;
520
+ this.state = "paused";
521
+ this.freeze();
522
+ this.node.emit("flowChanged", this.state);
523
+ }
524
+ resume() {
525
+ if (this.state !== "paused") return;
526
+ this.state = "playing";
527
+ this.thaw();
528
+ this.node.emit("flowChanged", this.state);
529
+ }
530
+ restart() {
531
+ this.thaw();
532
+ restartScene(this.engine);
533
+ }
534
+ transition(state, text, color) {
535
+ if (this.state === "gameover" || this.state === "won") return;
536
+ this.state = state;
537
+ if (this.freezeOnEnd) this.freeze();
538
+ if (this.bannerPath !== "") this.node.getNodeOrNull(this.bannerPath)?.show(text, {
539
+ color,
540
+ seconds: 0
541
+ });
542
+ this.node.emit("flowChanged", state);
543
+ }
544
+ freeze() {
545
+ if (this.frozeScale) return;
546
+ this.frozeScale = true;
547
+ this.engine.timeScale = 0;
548
+ }
549
+ thaw() {
550
+ if (!this.frozeScale) return;
551
+ this.frozeScale = false;
552
+ this.engine.timeScale = 1;
553
+ }
554
+ update() {
555
+ if (this.state !== "gameover" && this.state !== "won") return;
556
+ try {
557
+ if (this.engine.input.justPressed(this.restartAction)) this.restart();
558
+ } catch {}
443
559
  }
444
560
  };
445
561
  //#endregion
@@ -508,6 +624,153 @@ function collectGroup(node, group) {
508
624
  return out;
509
625
  }
510
626
  //#endregion
627
+ //#region src/gameplay/juice.ts
628
+ /**
629
+ * Game-feel primitives ("juice"): the weapon cooldown, camera shake, screen
630
+ * flash and hit-stop that action examples kept hand-rolling.
631
+ */
632
+ /**
633
+ * A fire-rate / ability cooldown — replaces the copy-pasted
634
+ * `this.clock += dt * 1000; if (now > nextFire)` pattern:
635
+ *
636
+ * private gun = new Cooldown(0.2);
637
+ * override update(dt: number): void {
638
+ * this.gun.tick(dt);
639
+ * if (this.engine.input.isPressed('fire') && this.gun.tryUse()) this.shoot();
640
+ * }
641
+ */
642
+ var Cooldown = class {
643
+ seconds;
644
+ remaining = 0;
645
+ constructor(seconds) {
646
+ this.seconds = seconds;
647
+ }
648
+ /** Advance time. Call once per update with the frame dt. */
649
+ tick(dt) {
650
+ if (this.remaining > 0) this.remaining -= dt;
651
+ }
652
+ get ready() {
653
+ return this.remaining <= 0;
654
+ }
655
+ /** 0..1 — how far through the cooldown we are (1 = ready). UiBar-friendly. */
656
+ get progress() {
657
+ return this.seconds <= 0 ? 1 : Math.min(1, Math.max(0, 1 - this.remaining / this.seconds));
658
+ }
659
+ /** Consume if ready. Returns whether the action should fire. */
660
+ tryUse() {
661
+ if (this.remaining > 0) return false;
662
+ this.remaining = this.seconds;
663
+ return true;
664
+ }
665
+ /** Force-ready (pickups that reset your reload). */
666
+ reset() {
667
+ this.remaining = 0;
668
+ }
669
+ };
670
+ const SHAKE_FALLOFF_SECONDS = .28;
671
+ /**
672
+ * Camera shake as a standalone behavior for cameras WITHOUT another script.
673
+ * (FollowCamera has this built in — one script per node.) Composes with any
674
+ * other position writer by applying only the DELTA of its own offset, so the
675
+ * camera returns exactly to where the other writer left it.
676
+ *
677
+ * (camera.behavior as CameraShake).shake(8); // pixels (2D) / meters·100 feel (3D: use ~0.2)
678
+ */
679
+ var CameraShake = class extends Behavior {
680
+ static props = { falloff: { default: SHAKE_FALLOFF_SECONDS } };
681
+ /** Seconds a shake takes to decay to zero. */
682
+ falloff = SHAKE_FALLOFF_SECONDS;
683
+ magnitude = 0;
684
+ t = 0;
685
+ prev = [];
686
+ shake(magnitude, seconds) {
687
+ this.magnitude = Math.max(this.magnitude, magnitude);
688
+ this.t = seconds ?? this.falloff;
689
+ }
690
+ update(dt) {
691
+ const node = requirePosition(this);
692
+ const pos = node.position;
693
+ for (let i = 0; i < this.prev.length; i++) pos[i] = (pos[i] ?? 0) - (this.prev[i] ?? 0);
694
+ if (this.t <= 0) {
695
+ if (this.prev.length > 0) {
696
+ node.position = pos.slice();
697
+ this.prev.length = 0;
698
+ }
699
+ return;
700
+ }
701
+ this.t -= dt;
702
+ const k = this.magnitude * Math.max(0, this.t) / this.falloff;
703
+ if (this.t <= 0) this.magnitude = 0;
704
+ const next = pos.slice();
705
+ if (this.prev.length !== next.length) this.prev = new Array(next.length).fill(0);
706
+ for (let i = 0; i < next.length; i++) {
707
+ const scale = next.length >= 3 && i === 1 ? .5 : 1;
708
+ this.prev[i] = (this.engine.rng.next() * 2 - 1) * k * scale;
709
+ next[i] = (next[i] ?? 0) + this.prev[i];
710
+ }
711
+ node.position = next;
712
+ }
713
+ };
714
+ /**
715
+ * Full-screen color flash (damage red, pickup white). DOM overlay above
716
+ * everything; headless no-op. Repeated calls restart the fade.
717
+ */
718
+ function screenFlash(color = "#ffffff", opacity = .35, seconds = .25) {
719
+ if (typeof document === "undefined") return;
720
+ const id = "incanto-screen-flash";
721
+ let el = document.getElementById(id);
722
+ if (!el) {
723
+ el = document.createElement("div");
724
+ el.id = id;
725
+ el.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:9999;opacity:0;transition:opacity .05s;";
726
+ document.body.appendChild(el);
727
+ }
728
+ el.style.background = color;
729
+ el.style.transition = "opacity .05s";
730
+ el.style.opacity = String(opacity);
731
+ const fade = () => {
732
+ el.style.transition = `opacity ${seconds}s`;
733
+ el.style.opacity = "0";
734
+ };
735
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(fade);
736
+ else fade();
737
+ }
738
+ /**
739
+ * Hit-stop: freeze game time for `seconds` of REAL time, then restore the
740
+ * previous timeScale. Stacking calls extend the freeze instead of fighting.
741
+ */
742
+ function hitStop(engine, seconds = .08) {
743
+ const state = hitStops.get(engine);
744
+ if (state) {
745
+ state.until = Math.max(state.until, realNow() + seconds * 1e3);
746
+ return;
747
+ }
748
+ const restore = engine.timeScale;
749
+ const entry = {
750
+ until: realNow() + seconds * 1e3,
751
+ restore
752
+ };
753
+ hitStops.set(engine, entry);
754
+ engine.timeScale = 0;
755
+ const pump = () => {
756
+ if (realNow() >= entry.until) {
757
+ engine.timeScale = entry.restore;
758
+ hitStops.delete(engine);
759
+ return;
760
+ }
761
+ schedule(pump);
762
+ };
763
+ schedule(pump);
764
+ }
765
+ const hitStops = /* @__PURE__ */ new Map();
766
+ function realNow() {
767
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
768
+ }
769
+ function schedule(fn) {
770
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(fn);
771
+ else setTimeout(fn, 16);
772
+ }
773
+ //#endregion
511
774
  //#region src/gameplay/lifetime.ts
512
775
  /**
513
776
  * Self-destruct after a fixed time — bullets, particles, temporary spawns,
@@ -876,7 +1139,11 @@ var Projectile = class extends Behavior {
876
1139
  update(dt) {
877
1140
  const node = this.node;
878
1141
  if (this.gravity !== 0) this.velocity[1] = (this.velocity[1] ?? 0) + this.gravity * dt;
879
- node.position = add(node.position, this.velocity.map((v) => v * dt));
1142
+ const pos = node.position;
1143
+ const n = Math.max(pos.length, this.velocity.length);
1144
+ const next = new Array(n);
1145
+ for (let i = 0; i < n; i++) next[i] = (pos[i] ?? 0) + (this.velocity[i] ?? 0) * dt;
1146
+ node.position = next;
880
1147
  }
881
1148
  /** Resolve `direction` to a unit vector, deriving 'forward' from rotation. */
882
1149
  resolveDirection() {
@@ -1470,6 +1737,8 @@ var ZombieAI = class extends Behavior {
1470
1737
  */
1471
1738
  /** Every built-in gameplay behavior, keyed by its registration name. */
1472
1739
  const GAMEPLAY_BEHAVIORS = {
1740
+ CameraShake,
1741
+ GameFlow,
1473
1742
  Health,
1474
1743
  Lifetime,
1475
1744
  ScoreKeeper,
@@ -1498,4 +1767,4 @@ function registerGameplayBehaviors(opts) {
1498
1767
  for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
1499
1768
  }
1500
1769
  //#endregion
1501
- export { Health as _, Wander as a, Projectile as c, Oscillate as d, MoveTo as f, DamageOnContact as g, FollowCamera as h, WaveSpawner as i, Pickup as l, Interactable as m, registerGameplayBehaviors as n, Spawner as o, Lifetime as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Patrol as u, Collector as v, Chase as y };
1770
+ export { Health as C, DamageOnContact as S, Chase as T, screenFlash as _, Wander as a, restartScene as b, Projectile as c, Oscillate as d, MoveTo as f, hitStop as g, Cooldown as h, WaveSpawner as i, Pickup as l, CameraShake as m, registerGameplayBehaviors as n, Spawner as o, Lifetime as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Patrol as u, Interactable as v, Collector as w, FollowCamera as x, GameFlow as y };
@@ -1,5 +1,5 @@
1
1
  import { c as JsonValue } from "./schema-CcoWb32N.js";
2
- import { T as Node, l as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-BAQq7HGM.js";
2
+ import { T as Node, l as PropSchema, n as BehaviorCtor, t as Behavior, v as Engine } from "./behavior-CWhW3oa6.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
5
5
  /**
@@ -154,6 +154,11 @@ declare class FollowCamera extends Behavior {
154
154
  smoothing: number;
155
155
  deadzone: number;
156
156
  override onReady(): void;
157
+ private shakeMag;
158
+ private shakeT;
159
+ private shakeFalloff;
160
+ /** Kick the camera (impacts, explosions). Composes with the follow. */
161
+ shake(magnitude: number, seconds?: number): void;
157
162
  override update(dt: number): void;
158
163
  }
159
164
  //#endregion
@@ -528,6 +533,112 @@ declare class ZombieAI extends Behavior {
528
533
  private pickGoal;
529
534
  }
530
535
  //#endregion
536
+ //#region src/gameplay/game-flow.d.ts
537
+ /**
538
+ * Reload the CURRENT scene from its source JSON — fresh nodes, reset physics,
539
+ * rewired input. The restart primitive every game-over screen wants.
540
+ */
541
+ declare function restartScene(engine: Engine): void;
542
+ type GameFlowState = "playing" | "paused" | "gameover" | "won";
543
+ /**
544
+ * The win/lose/restart state machine 6+ examples hand-rolled as `over`/`win`
545
+ * booleans. Attach to any node (the scene root is natural):
546
+ *
547
+ * const flow = root.behavior as GameFlow; // script: { "name": "GameFlow" }
548
+ * flow.gameOver('YOU DIED'); // freezes time, sticky banner
549
+ * flow.win('AREA CLEAR');
550
+ * flow.pause(); flow.resume();
551
+ *
552
+ * While in `gameover`/`won`, pressing `restartAction` (default action name
553
+ * 'restart' — declare it in the scene input map, or leave undeclared and call
554
+ * `flow.restart()` yourself) reloads the scene from source. Banners render
555
+ * through a `%Banner` UiBanner when one exists; otherwise states are silent
556
+ * (drive your own UI off the `flowChanged` signal).
557
+ */
558
+ declare class GameFlow extends Behavior {
559
+ static readonly signals: readonly string[];
560
+ static readonly props: Record<string, {
561
+ default: string | boolean;
562
+ }>;
563
+ /** Input action that restarts from gameover/won (declare it in `input{}`). */
564
+ restartAction: string;
565
+ /** Freeze `engine.timeScale` on gameover/won (banner UI keeps rendering). */
566
+ freezeOnEnd: boolean;
567
+ /** Where the flow looks for a UiBanner ('' = never). */
568
+ bannerPath: string;
569
+ state: GameFlowState;
570
+ private frozeScale;
571
+ gameOver(text?: string, color?: string): void;
572
+ win(text?: string, color?: string): void;
573
+ pause(): void;
574
+ resume(): void;
575
+ restart(): void;
576
+ private transition;
577
+ private freeze;
578
+ private thaw;
579
+ override update(): void;
580
+ }
581
+ //#endregion
582
+ //#region src/gameplay/juice.d.ts
583
+ /**
584
+ * Game-feel primitives ("juice"): the weapon cooldown, camera shake, screen
585
+ * flash and hit-stop that action examples kept hand-rolling.
586
+ */
587
+ /**
588
+ * A fire-rate / ability cooldown — replaces the copy-pasted
589
+ * `this.clock += dt * 1000; if (now > nextFire)` pattern:
590
+ *
591
+ * private gun = new Cooldown(0.2);
592
+ * override update(dt: number): void {
593
+ * this.gun.tick(dt);
594
+ * if (this.engine.input.isPressed('fire') && this.gun.tryUse()) this.shoot();
595
+ * }
596
+ */
597
+ declare class Cooldown {
598
+ readonly seconds: number;
599
+ private remaining;
600
+ constructor(seconds: number);
601
+ /** Advance time. Call once per update with the frame dt. */
602
+ tick(dt: number): void;
603
+ get ready(): boolean;
604
+ /** 0..1 — how far through the cooldown we are (1 = ready). UiBar-friendly. */
605
+ get progress(): number;
606
+ /** Consume if ready. Returns whether the action should fire. */
607
+ tryUse(): boolean;
608
+ /** Force-ready (pickups that reset your reload). */
609
+ reset(): void;
610
+ }
611
+ /**
612
+ * Camera shake as a standalone behavior for cameras WITHOUT another script.
613
+ * (FollowCamera has this built in — one script per node.) Composes with any
614
+ * other position writer by applying only the DELTA of its own offset, so the
615
+ * camera returns exactly to where the other writer left it.
616
+ *
617
+ * (camera.behavior as CameraShake).shake(8); // pixels (2D) / meters·100 feel (3D: use ~0.2)
618
+ */
619
+ declare class CameraShake extends Behavior {
620
+ static readonly props: Record<string, {
621
+ default: number;
622
+ }>;
623
+ /** Seconds a shake takes to decay to zero. */
624
+ falloff: number;
625
+ private magnitude;
626
+ private t;
627
+ private prev;
628
+ shake(magnitude: number, seconds?: number): void;
629
+ override update(dt: number): void;
630
+ }
631
+ /**
632
+ * Full-screen color flash (damage red, pickup white). DOM overlay above
633
+ * everything; headless no-op. Repeated calls restart the fade.
634
+ */
635
+ declare function screenFlash(color?: string, opacity?: number, seconds?: number): void;
636
+ /**
637
+ * Hit-stop: freeze game time for `seconds` of REAL time, then restore the
638
+ * previous timeScale. Stacking calls extend the freeze instead of fighting.
639
+ */
640
+ declare function hitStop(engine: Engine, seconds?: number): void;
641
+ //#endregion
531
642
  //#region src/gameplay/index.d.ts
532
643
  /** Every built-in gameplay behavior, keyed by its registration name. */
533
644
  declare const GAMEPLAY_BEHAVIORS: Readonly<Record<string, BehaviorCtor>>;
@@ -540,4 +651,4 @@ declare function registerGameplayBehaviors(opts?: {
540
651
  replace?: boolean;
541
652
  }): void;
542
653
  //#endregion
543
- export { Chase, Collector, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, registerGameplayBehaviors };
654
+ export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, hitStop, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as Health, a as Wander, c as Projectile, d as Oscillate, f as MoveTo, g as DamageOnContact, h as FollowCamera, i as WaveSpawner, l as Pickup, m as Interactable, n as registerGameplayBehaviors, o as Spawner, p as Lifetime, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as Collector, y as Chase } from "./gameplay-Ccruc3Wd.js";
2
- export { Chase, Collector, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, registerGameplayBehaviors };
1
+ import { C as Health, S as DamageOnContact, T as Chase, _ as screenFlash, a as Wander, b as restartScene, c as Projectile, d as Oscillate, f as MoveTo, g as hitStop, h as Cooldown, i as WaveSpawner, l as Pickup, m as CameraShake, n as registerGameplayBehaviors, o as Spawner, p as Lifetime, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as Interactable, w as Collector, x as FollowCamera, y as GameFlow } from "./gameplay-CDFgSG6z.js";
2
+ export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, hitStop, registerGameplayBehaviors, restartScene, screenFlash };