@waica/behaviors 0.11.0 → 0.13.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.
@@ -1,5 +1,5 @@
1
1
  import { Component, Sprite, screenInputToLogical, } from '@waica/engine';
2
- import { INTERACTABLE_UI_PIECE, Interactable } from './interactable.js';
2
+ import { fireInteract, INTERACTABLE_UI_PIECE, Interactable } from './interactable.js';
3
3
  import { Health } from './health.js';
4
4
  import { MeleeAttack } from './melee-attack.js';
5
5
  import { buildNavigationGrid } from './navigation-grid.js';
@@ -199,7 +199,9 @@ function driveNpcOrder(clickToMove, entity, game, order) {
199
199
  }
200
200
  if (distance(pointOf(entity), pointOf(target)) <= interactable.radius) {
201
201
  game.stats.set('npcLine', interactable.line);
202
- game.ui.show(INTERACTABLE_UI_PIECE);
202
+ // Scene-scoped, same reason as interactUpdate's prompt (grill decision 8).
203
+ game.ui.show(INTERACTABLE_UI_PIECE, { scope: 'scene' });
204
+ fireInteract(target, entity);
203
205
  clickToMove.cancel(); // CA-5: one trigger per arrival, no key simulated
204
206
  return null;
205
207
  }
package/dist/health.d.ts CHANGED
@@ -45,6 +45,10 @@ export declare class Health extends Component {
45
45
  label: string;
46
46
  ref: 'stat';
47
47
  };
48
+ hurtSound: {
49
+ label: string;
50
+ ref: 'sound';
51
+ };
48
52
  };
49
53
  static transient: string[];
50
54
  max: number;
@@ -55,6 +59,8 @@ export declare class Health extends Component {
55
59
  * publishes nothing: the stat is the project's to declare.
56
60
  */
57
61
  stat: string;
62
+ /** Sound played at this entity's position on every accepted hit. Empty plays nothing (CA-12). */
63
+ hurtSound: string;
58
64
  /** Health left; 0 is dead. Filled in from max on ready. */
59
65
  current: number;
60
66
  /** Whoever dealt the last accepted hit, for the state that reacts to it. */
package/dist/health.js CHANGED
@@ -47,6 +47,7 @@ export class Health extends Component {
47
47
  max: { label: 'Max health', min: 1, max: 20, step: 1 },
48
48
  invulnerability: { label: 'Invulnerability', min: 0, max: 5, step: 0.1 },
49
49
  stat: { label: 'Stat', ref: 'stat' },
50
+ hurtSound: { label: 'Hurt sound', ref: 'sound' },
50
51
  };
51
52
  static transient = [
52
53
  'current',
@@ -64,6 +65,8 @@ export class Health extends Component {
64
65
  * publishes nothing: the stat is the project's to declare.
65
66
  */
66
67
  stat = '';
68
+ /** Sound played at this entity's position on every accepted hit. Empty plays nothing (CA-12). */
69
+ hurtSound = '';
67
70
  /** Health left; 0 is dead. Filled in from max on ready. */
68
71
  current = 0;
69
72
  /** Whoever dealt the last accepted hit, for the state that reacts to it. */
@@ -158,6 +161,8 @@ export class Health extends Component {
158
161
  current: this.current,
159
162
  source,
160
163
  });
164
+ if (this.hurtSound)
165
+ this.game.audio.play(this.hurtSound, { at: this.entity });
161
166
  this.invulnerable = this.invulnerability;
162
167
  this.blinkClock = 0;
163
168
  if (this.current === 0) {
package/dist/index.d.ts CHANGED
@@ -4,8 +4,9 @@ export { TopDownMotor, type TopDownFacing } from './topdown-motor.js';
4
4
  export { IsoMotor, type IsoFacing } from './iso-motor.js';
5
5
  export { TOPDOWN_PLAYER_ROLE, TOPDOWN_PLAYER_STATE_GRAPH, topdownPlayerUpdate, } from './topdown-player-states.js';
6
6
  export { ISO_PLAYER_ROLE, ISO_PLAYER_STATE_GRAPH, isoPlayerUpdate, } from './iso-player-states.js';
7
- export { INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
7
+ export { fireInteract, INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
8
8
  export { Collectible } from './collectible.js';
9
+ export { SceneTransition } from './scene-transition.js';
9
10
  export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH, type PatrolAxis } from './patrol.js';
10
11
  export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH, type ChaserMode } from './chaser.js';
11
12
  export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ export { TopDownMotor } from './topdown-motor.js';
4
4
  export { IsoMotor } from './iso-motor.js';
5
5
  export { TOPDOWN_PLAYER_ROLE, TOPDOWN_PLAYER_STATE_GRAPH, topdownPlayerUpdate, } from './topdown-player-states.js';
6
6
  export { ISO_PLAYER_ROLE, ISO_PLAYER_STATE_GRAPH, isoPlayerUpdate, } from './iso-player-states.js';
7
- export { INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
7
+ export { fireInteract, INTERACTABLE_UI, INTERACTABLE_UI_PIECE, Interactable, interactUpdate, } from './interactable.js';
8
8
  export { Collectible } from './collectible.js';
9
+ export { SceneTransition } from './scene-transition.js';
9
10
  export { Patrol, PATROLLER_ROLE, PATROLLER_STATE_GRAPH } from './patrol.js';
10
11
  export { Chaser, CHASER_ROLE, CHASER_STATE_GRAPH } from './chaser.js';
11
12
  export { NPC_ROLE, NPC_STATE_GRAPH } from './npc.js';
@@ -1,4 +1,4 @@
1
- import { Component, type StateContext } from '@waica/engine';
1
+ import { Component, type Entity, type StateContext } from '@waica/engine';
2
2
  /**
3
3
  * Something the player can talk to or examine: a dialogue line and the
4
4
  * radius it can be triggered from. The component is pure data — the
@@ -25,6 +25,14 @@ export declare class Interactable extends Component {
25
25
  export declare const INTERACTABLE_UI_PIECE = "npc-line";
26
26
  /** The UI fragment every archetype using Interactable must register. */
27
27
  export declare const INTERACTABLE_UI: Readonly<Record<string, string>>;
28
+ /**
29
+ * Runs onInteract on every component of `target` — the winner of the
30
+ * nearest-Interactable scan. The two paths that win it (the interact key,
31
+ * via interactUpdate, and a click-to-move NPC order arrival) both call this,
32
+ * so a sibling component (e.g. SceneTransition with trigger:'interact')
33
+ * fires the same way from either input scheme.
34
+ */
35
+ export declare function fireInteract(target: Entity, initiator: Entity): void;
28
36
  /**
29
37
  * The player role's interact lookup, run by its '*' hook in every state:
30
38
  * pressing interact near an Interactable publishes its line through the
@@ -38,6 +38,17 @@ export const INTERACTABLE_UI = {
38
38
  <div class="npc-line">{{npcLine}}</div>
39
39
  `,
40
40
  };
41
+ /**
42
+ * Runs onInteract on every component of `target` — the winner of the
43
+ * nearest-Interactable scan. The two paths that win it (the interact key,
44
+ * via interactUpdate, and a click-to-move NPC order arrival) both call this,
45
+ * so a sibling component (e.g. SceneTransition with trigger:'interact')
46
+ * fires the same way from either input scheme.
47
+ */
48
+ export function fireInteract(target, initiator) {
49
+ for (const component of [...target.components])
50
+ component.onInteract?.(initiator);
51
+ }
41
52
  /**
42
53
  * The player role's interact lookup, run by its '*' hook in every state:
43
54
  * pressing interact near an Interactable publishes its line through the
@@ -46,6 +57,7 @@ export const INTERACTABLE_UI = {
46
57
  */
47
58
  export function interactUpdate({ entity, game }) {
48
59
  let nearest = null;
60
+ let nearestEntity = null;
49
61
  let nearestDistance = Infinity;
50
62
  for (const other of game.entities) {
51
63
  if (other === entity)
@@ -56,10 +68,11 @@ export function interactUpdate({ entity, game }) {
56
68
  const distance = Math.hypot(other.position.x - entity.position.x, other.position.y - entity.position.y);
57
69
  if (distance <= interactable.radius && distance < nearestDistance) {
58
70
  nearest = interactable;
71
+ nearestEntity = other;
59
72
  nearestDistance = distance;
60
73
  }
61
74
  }
62
- if (!nearest) {
75
+ if (!nearest || !nearestEntity) {
63
76
  game.ui.hide(INTERACTABLE_UI_PIECE);
64
77
  return;
65
78
  }
@@ -67,6 +80,10 @@ export function interactUpdate({ entity, game }) {
67
80
  // The press is spent: an input:interact edge needs a NEW press.
68
81
  game.input.consume('interact');
69
82
  game.stats.set('npcLine', nearest.line);
70
- game.ui.show(INTERACTABLE_UI_PIECE);
83
+ // Scene-scoped: the scan that hides this prompt dies with the scene, so
84
+ // without a scope the prompt would survive a swap into a map where
85
+ // nothing knows to hide it — stale line and all (grill decision 8).
86
+ game.ui.show(INTERACTABLE_UI_PIECE, { scope: 'scene' });
87
+ fireInteract(nearestEntity, entity);
71
88
  }
72
89
  }
@@ -29,12 +29,18 @@ export declare class MeleeAttack extends Component {
29
29
  max: number;
30
30
  step: number;
31
31
  };
32
+ swingSound: {
33
+ label: string;
34
+ ref: 'sound';
35
+ };
32
36
  };
33
37
  damage: number;
34
38
  /** How far in front of the attacker the blow reaches, in world units. */
35
39
  range: number;
36
40
  /** How wide the blow is, across the facing. */
37
41
  width: number;
42
+ /** Sound played on every swing. Empty plays nothing (CA-12). */
43
+ swingSound: string;
38
44
  /**
39
45
  * Lands one blow along `facing`: every other living entity with a Hitbox
40
46
  * and a Health inside the strike area takes `damage` once, with this
@@ -16,12 +16,15 @@ export class MeleeAttack extends Component {
16
16
  damage: { label: 'Damage', min: 0, max: 20, step: 1 },
17
17
  range: { label: 'Range', min: 0.5, max: 5, step: 0.5 },
18
18
  width: { label: 'Width', min: 0.5, max: 5, step: 0.5 },
19
+ swingSound: { label: 'Swing sound', ref: 'sound' },
19
20
  };
20
21
  damage = 1;
21
22
  /** How far in front of the attacker the blow reaches, in world units. */
22
23
  range = 1;
23
24
  /** How wide the blow is, across the facing. */
24
25
  width = 1;
26
+ /** Sound played on every swing. Empty plays nothing (CA-12). */
27
+ swingSound = '';
25
28
  /**
26
29
  * Lands one blow along `facing`: every other living entity with a Hitbox
27
30
  * and a Health inside the strike area takes `damage` once, with this
@@ -33,6 +36,11 @@ export class MeleeAttack extends Component {
33
36
  const direction = logicalDirection(facing, this.game.projection);
34
37
  if (!direction)
35
38
  return [];
39
+ // Positional like Health's hurtSound (CA-8): an off-camera NPC's swing
40
+ // should attenuate and pan like the hit it causes, not play flat and
41
+ // dead-centre while the damage it lands sounds correctly placed.
42
+ if (this.swingSound)
43
+ this.game.audio.play(this.swingSound, { at: this.entity });
36
44
  const area = this.strikeArea(direction.x, direction.y);
37
45
  const struck = [];
38
46
  // A copy: a target with no death-handling graph is destroyed on the spot,
@@ -0,0 +1,34 @@
1
+ import { Component, type Entity } from '@waica/engine';
2
+ /**
3
+ * Replaces the live scene with `scene` (its file's stem, e.g.
4
+ * "cave.scene.json" -> "cave") when fired — a door, or any entity a player
5
+ * crosses or interacts with to leave the current map. Names only its
6
+ * destination: the incoming scene places its own Player wherever that
7
+ * scene authored it (no named entry points).
8
+ *
9
+ * With trigger:'overlap' (the default) it fires like Collectible/Hazard:
10
+ * the player's Hitbox overlapping its own — requires a sibling Hitbox.
11
+ * With trigger:'interact' it implements no radius or prompt of its own:
12
+ * it needs a sibling Interactable and fires from the shared nearest-wins
13
+ * interact scan (interactable.ts's fireInteract), so a door and an NPC in
14
+ * range arbitrate themselves with no new rule.
15
+ */
16
+ export declare class SceneTransition extends Component {
17
+ static componentName: string;
18
+ static params: {
19
+ scene: {
20
+ label: string;
21
+ };
22
+ trigger: {
23
+ label: string;
24
+ options: string[];
25
+ };
26
+ };
27
+ /** Destination scene name. */
28
+ scene: string;
29
+ trigger: 'overlap' | 'interact';
30
+ onReady(): void;
31
+ onCollide(other: Entity): void;
32
+ onInteract(_initiator: Entity): void;
33
+ private fire;
34
+ }
@@ -0,0 +1,48 @@
1
+ import { Component } from '@waica/engine';
2
+ import { Interactable } from './interactable.js';
3
+ import { isPlayer } from './player-identity.js';
4
+ /**
5
+ * Replaces the live scene with `scene` (its file's stem, e.g.
6
+ * "cave.scene.json" -> "cave") when fired — a door, or any entity a player
7
+ * crosses or interacts with to leave the current map. Names only its
8
+ * destination: the incoming scene places its own Player wherever that
9
+ * scene authored it (no named entry points).
10
+ *
11
+ * With trigger:'overlap' (the default) it fires like Collectible/Hazard:
12
+ * the player's Hitbox overlapping its own — requires a sibling Hitbox.
13
+ * With trigger:'interact' it implements no radius or prompt of its own:
14
+ * it needs a sibling Interactable and fires from the shared nearest-wins
15
+ * interact scan (interactable.ts's fireInteract), so a door and an NPC in
16
+ * range arbitrate themselves with no new rule.
17
+ */
18
+ export class SceneTransition extends Component {
19
+ static componentName = 'SceneTransition';
20
+ static params = {
21
+ scene: { label: 'Scene' },
22
+ trigger: { label: 'Trigger', options: ['overlap', 'interact'] },
23
+ };
24
+ /** Destination scene name. */
25
+ scene = '';
26
+ trigger = 'overlap';
27
+ onReady() {
28
+ if (this.trigger === 'interact' && !this.entity.has(Interactable)) {
29
+ console.warn(`[waica] SceneTransition on "${this.entity.name}" has trigger:"interact" but no ` +
30
+ 'sibling Interactable; it will never fire.');
31
+ }
32
+ }
33
+ onCollide(other) {
34
+ if (this.trigger !== 'overlap')
35
+ return;
36
+ if (!isPlayer(other))
37
+ return;
38
+ this.fire();
39
+ }
40
+ onInteract(_initiator) {
41
+ if (this.trigger !== 'interact')
42
+ return;
43
+ this.fire();
44
+ }
45
+ fire() {
46
+ this.game.loadSceneByName(this.scene);
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/behaviors",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Waica built-in behaviors — the curated game-feel library",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,6 +19,6 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@waica/engine": "^0.11.0"
22
+ "@waica/engine": "^0.13.0"
23
23
  }
24
24
  }