@waica/behaviors 0.20.0 → 0.22.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.
@@ -0,0 +1,27 @@
1
+ import { Component } from '@waica/engine';
2
+ /**
3
+ * A puff from this entity's ParticleEmitter wherever
4
+ * damage lands. Health announces every accepted hit on `game.events` as
5
+ * 'damage'; this moves its own (sprite-less) entity to the entity that took
6
+ * it and bursts there. The emitter should be in `space: 'world'`, so the
7
+ * puffs already in flight stay where they started when the next hit moves
8
+ * the source elsewhere. One of these serves every Health in the scene.
9
+ */
10
+ export declare class DamagePuff extends Component {
11
+ static componentName: string;
12
+ static displayName: string;
13
+ static params: {
14
+ count: {
15
+ label: string;
16
+ min: number;
17
+ max: number;
18
+ step: number;
19
+ };
20
+ };
21
+ /** Particles per hit. */
22
+ count: number;
23
+ private unsubscribe?;
24
+ onReady(): void;
25
+ onDestroy(): void;
26
+ private puffAt;
27
+ }
@@ -0,0 +1,40 @@
1
+ import { Component, Entity, ParticleEmitter } from '@waica/engine';
2
+ /**
3
+ * A puff from this entity's ParticleEmitter wherever
4
+ * damage lands. Health announces every accepted hit on `game.events` as
5
+ * 'damage'; this moves its own (sprite-less) entity to the entity that took
6
+ * it and bursts there. The emitter should be in `space: 'world'`, so the
7
+ * puffs already in flight stay where they started when the next hit moves
8
+ * the source elsewhere. One of these serves every Health in the scene.
9
+ */
10
+ export class DamagePuff extends Component {
11
+ static componentName = 'DamagePuff';
12
+ static displayName = 'Damage puff';
13
+ static params = {
14
+ count: { label: 'Particles', min: 1, max: 64, step: 1 },
15
+ };
16
+ /** Particles per hit. */
17
+ count = 10;
18
+ unsubscribe;
19
+ onReady() {
20
+ this.unsubscribe = this.game.events.on('damage', (payload) => this.puffAt(payload));
21
+ }
22
+ onDestroy() {
23
+ this.unsubscribe?.();
24
+ this.unsubscribe = undefined;
25
+ }
26
+ puffAt(payload) {
27
+ const target = damagedEntity(payload);
28
+ const puff = this.entity.get(ParticleEmitter);
29
+ if (!target || !puff)
30
+ return;
31
+ this.entity.position.set(target.position.x, target.position.y, this.entity.position.z);
32
+ puff.emit(this.count);
33
+ }
34
+ }
35
+ /** The entity a 'damage' event names, if the payload really carries one. */
36
+ function damagedEntity(payload) {
37
+ if (typeof payload !== 'object' || payload === null || !('entity' in payload))
38
+ return undefined;
39
+ return payload.entity instanceof Entity ? payload.entity : undefined;
40
+ }
@@ -0,0 +1,51 @@
1
+ import { Component } from '@waica/engine';
2
+ /**
3
+ * Dust from this entity's own ParticleEmitter at the
4
+ * two moments a platformer body meets the ground hard — a small puff when a
5
+ * jump takes off from the ground, a bigger burst when the body lands. Both
6
+ * come from the same emitter (the dust look is authored on it in the
7
+ * prefab); only the count differs. It hooks the StateMachine's own edges
8
+ * instead of polling:
9
+ *
10
+ * - takeoff: entering 'jump' right after leaving 'idle' or 'run'. Leaving
11
+ * any other state first counts as airborne: the graph enters 'jump' from
12
+ * 'fall' in mid-air (a coyote jump, a stomp bounce), and a project state
13
+ * such as a dash can do the same. Those raise no dust.
14
+ * - landing: leaving 'jump' or 'fall' while the motor is grounded. A jump
15
+ * whose apex already lands on a ledge goes jump -> idle without a fall.
16
+ *
17
+ * To know which state was left, it adds an instance onExit hook to every
18
+ * state the machine declares (all but '*'). A role's own hooks for those
19
+ * states still run. A role 'default' onExit, however, only runs for states
20
+ * without an onExit of their own, so this hook would shadow it. No stock
21
+ * role declares one.
22
+ *
23
+ * Its siblings must already be mounted when it is, so it goes after the
24
+ * StateMachine and the ParticleEmitter in the prefab.
25
+ */
26
+ export declare class DustPuffs extends Component {
27
+ static componentName: string;
28
+ static displayName: string;
29
+ static params: {
30
+ jumpCount: {
31
+ label: string;
32
+ min: number;
33
+ max: number;
34
+ step: number;
35
+ };
36
+ landCount: {
37
+ label: string;
38
+ min: number;
39
+ max: number;
40
+ step: number;
41
+ };
42
+ };
43
+ static transient: string[];
44
+ /** Particles when a jump takes off from the ground. */
45
+ jumpCount: number;
46
+ /** Particles when the body lands. */
47
+ landCount: number;
48
+ /** Whether the last state left was a grounded one: set on every declared state's exit, read on entering 'jump'. */
49
+ private leftGround;
50
+ onReady(): void;
51
+ }
@@ -0,0 +1,73 @@
1
+ import { Component, ParticleEmitter, StateMachine } from '@waica/engine';
2
+ import { PlatformerMotor } from './platformer-motor.js';
3
+ /** The stock player graph's grounded states: a jump entered from one of these is a takeoff. */
4
+ const GROUND_STATES = ['idle', 'run'];
5
+ /** The airborne states a landing can end: a grounded exit from either is a touchdown. */
6
+ const AIR_STATES = ['jump', 'fall'];
7
+ /**
8
+ * Dust from this entity's own ParticleEmitter at the
9
+ * two moments a platformer body meets the ground hard — a small puff when a
10
+ * jump takes off from the ground, a bigger burst when the body lands. Both
11
+ * come from the same emitter (the dust look is authored on it in the
12
+ * prefab); only the count differs. It hooks the StateMachine's own edges
13
+ * instead of polling:
14
+ *
15
+ * - takeoff: entering 'jump' right after leaving 'idle' or 'run'. Leaving
16
+ * any other state first counts as airborne: the graph enters 'jump' from
17
+ * 'fall' in mid-air (a coyote jump, a stomp bounce), and a project state
18
+ * such as a dash can do the same. Those raise no dust.
19
+ * - landing: leaving 'jump' or 'fall' while the motor is grounded. A jump
20
+ * whose apex already lands on a ledge goes jump -> idle without a fall.
21
+ *
22
+ * To know which state was left, it adds an instance onExit hook to every
23
+ * state the machine declares (all but '*'). A role's own hooks for those
24
+ * states still run. A role 'default' onExit, however, only runs for states
25
+ * without an onExit of their own, so this hook would shadow it. No stock
26
+ * role declares one.
27
+ *
28
+ * Its siblings must already be mounted when it is, so it goes after the
29
+ * StateMachine and the ParticleEmitter in the prefab.
30
+ */
31
+ export class DustPuffs extends Component {
32
+ static componentName = 'DustPuffs';
33
+ static displayName = 'Dust puffs';
34
+ static params = {
35
+ jumpCount: { label: 'Jump puff', min: 0, max: 64, step: 1 },
36
+ landCount: { label: 'Landing burst', min: 0, max: 64, step: 1 },
37
+ };
38
+ static transient = ['leftGround'];
39
+ /** Particles when a jump takes off from the ground. */
40
+ jumpCount = 5;
41
+ /** Particles when the body lands. */
42
+ landCount = 12;
43
+ /** Whether the last state left was a grounded one: set on every declared state's exit, read on entering 'jump'. */
44
+ leftGround = false;
45
+ onReady() {
46
+ const machine = this.entity.get(StateMachine);
47
+ const dust = this.entity.get(ParticleEmitter);
48
+ if (!machine || !dust) {
49
+ console.warn(`[waica] "${this.entity.name}": DustPuffs needs a StateMachine and a ParticleEmitter before it.`);
50
+ return;
51
+ }
52
+ for (const state of Object.keys(machine.states)) {
53
+ if (state === '*')
54
+ continue;
55
+ const grounded = GROUND_STATES.includes(state);
56
+ machine.on(state, { onExit: () => (this.leftGround = grounded) });
57
+ }
58
+ for (const state of AIR_STATES) {
59
+ machine.on(state, {
60
+ onExit: () => {
61
+ if (this.entity.get(PlatformerMotor)?.grounded)
62
+ dust.emit(this.landCount);
63
+ },
64
+ });
65
+ }
66
+ machine.on('jump', {
67
+ onEnter: () => {
68
+ if (this.leftGround)
69
+ dust.emit(this.jumpCount);
70
+ },
71
+ });
72
+ }
73
+ }
@@ -0,0 +1,24 @@
1
+ import { Component } from '@waica/engine';
2
+ /**
3
+ * A soft dust trail while this entity walks. The
4
+ * ParticleEmitter on the same entity does the continuous emission (its
5
+ * `rate` and dust look are authored in the prefab, with `emitting: false`);
6
+ * this only switches `emitting` on when the StateMachine enters the walking
7
+ * state and off when it leaves it, so the trail stops the moment the body
8
+ * does and the dust already raised settles on its own.
9
+ *
10
+ * Its siblings must already be mounted when it is, so it goes after the
11
+ * StateMachine and the ParticleEmitter in the prefab.
12
+ */
13
+ export declare class DustTrail extends Component {
14
+ static componentName: string;
15
+ static displayName: string;
16
+ static params: {
17
+ state: {
18
+ label: string;
19
+ };
20
+ };
21
+ /** The state during which the trail is raised. */
22
+ state: string;
23
+ onReady(): void;
24
+ }
@@ -0,0 +1,38 @@
1
+ import { Component, ParticleEmitter, StateMachine } from '@waica/engine';
2
+ /**
3
+ * A soft dust trail while this entity walks. The
4
+ * ParticleEmitter on the same entity does the continuous emission (its
5
+ * `rate` and dust look are authored in the prefab, with `emitting: false`);
6
+ * this only switches `emitting` on when the StateMachine enters the walking
7
+ * state and off when it leaves it, so the trail stops the moment the body
8
+ * does and the dust already raised settles on its own.
9
+ *
10
+ * Its siblings must already be mounted when it is, so it goes after the
11
+ * StateMachine and the ParticleEmitter in the prefab.
12
+ */
13
+ export class DustTrail extends Component {
14
+ static componentName = 'DustTrail';
15
+ static displayName = 'Dust trail';
16
+ static params = {
17
+ state: { label: 'State' },
18
+ };
19
+ /** The state during which the trail is raised. */
20
+ state = 'walk';
21
+ onReady() {
22
+ const machine = this.entity.get(StateMachine);
23
+ const dust = this.entity.get(ParticleEmitter);
24
+ if (!machine || !dust) {
25
+ console.warn(`[waica] "${this.entity.name}": DustTrail needs a StateMachine and a ParticleEmitter before it.`);
26
+ return;
27
+ }
28
+ dust.emitting = machine.current === this.state;
29
+ machine.on(this.state, {
30
+ onEnter: () => {
31
+ dust.emitting = true;
32
+ },
33
+ onExit: () => {
34
+ dust.emitting = false;
35
+ },
36
+ });
37
+ }
38
+ }
package/dist/facing.d.ts CHANGED
@@ -3,9 +3,10 @@ import { type ProjectedPoint } from '@waica/engine';
3
3
  export type ScreenFacing = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
4
4
  export declare const SCREEN_FACINGS: readonly ScreenFacing[];
5
5
  /**
6
- * The eight-way facing a screen-relative input reads as — signs only, so a
7
- * tiny diagonal component still counts. Undefined for no input, which lets
8
- * the caller keep whatever it was facing.
6
+ * The eight-way facing a screen-relative input reads as: the 45° sector its
7
+ * angle falls in, so an almost-vertical stick faces `n`, not `ne`; every
8
+ * keyboard input (components in {-1, 0, 1}) lands on its sign pair exactly.
9
+ * Undefined for no input, which lets the caller keep whatever it was facing.
9
10
  */
10
11
  export declare function facingForInput(inputX: number, inputY: number): ScreenFacing | undefined;
11
12
  /** The screen sign pair behind a facing; undefined for a name the table lacks. */
package/dist/facing.js CHANGED
@@ -11,29 +11,19 @@ const FACING_VECTORS = {
11
11
  w: { x: -1, y: 0 },
12
12
  nw: { x: -1, y: 1 },
13
13
  };
14
+ /** Counter-clockwise from east, one per 45° sector of the input angle. */
15
+ const FACINGS_BY_SECTOR = ['e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se'];
14
16
  /**
15
- * The eight-way facing a screen-relative input reads as — signs only, so a
16
- * tiny diagonal component still counts. Undefined for no input, which lets
17
- * the caller keep whatever it was facing.
17
+ * The eight-way facing a screen-relative input reads as: the 45° sector its
18
+ * angle falls in, so an almost-vertical stick faces `n`, not `ne`; every
19
+ * keyboard input (components in {-1, 0, 1}) lands on its sign pair exactly.
20
+ * Undefined for no input, which lets the caller keep whatever it was facing.
18
21
  */
19
22
  export function facingForInput(inputX, inputY) {
20
23
  if (inputX === 0 && inputY === 0)
21
24
  return undefined;
22
- if (inputY > 0) {
23
- if (inputX > 0)
24
- return 'ne';
25
- if (inputX < 0)
26
- return 'nw';
27
- return 'n';
28
- }
29
- if (inputY < 0) {
30
- if (inputX > 0)
31
- return 'se';
32
- if (inputX < 0)
33
- return 'sw';
34
- return 's';
35
- }
36
- return inputX > 0 ? 'e' : 'w';
25
+ const sector = Math.round(Math.atan2(inputY, inputX) / (Math.PI / 4));
26
+ return FACINGS_BY_SECTOR[(sector + 8) % 8];
37
27
  }
38
28
  /** The screen sign pair behind a facing; undefined for a name the table lacks. */
39
29
  export function facingVector(facing) {
@@ -51,25 +51,25 @@ function gridPlayerGraph() {
51
51
  },
52
52
  };
53
53
  }
54
- /** The body update: keyboard input, else a live Move Order, then the motor step and move/stop signal. */
54
+ /** The body update: directional input (keys or stick), else a live Move Order, then the motor step and move/stop signal. */
55
55
  function gridPlayerUpdate(Motor) {
56
56
  return (ctx, dt) => {
57
57
  const { entity, game, fsm } = ctx;
58
58
  const motor = entity.get(Motor);
59
59
  if (!motor)
60
60
  return;
61
- const keyboardX = game.input.axis('left', 'right');
62
- const keyboardY = game.input.axis('down', 'up');
63
- if (keyboardX !== 0 || keyboardY !== 0) {
64
- // Keyboard movement always wins and cancels a live Move Order (CA-4).
61
+ const inputX = game.input.axis('left', 'right');
62
+ const inputY = game.input.axis('down', 'up');
63
+ if (inputX !== 0 || inputY !== 0) {
64
+ // Any directional input — keys or stick — wins and cancels a live Move Order (CA-4).
65
65
  entity.get(ClickToMove)?.cancel();
66
- motor.run(keyboardX, keyboardY, dt);
66
+ motor.run(inputX, inputY, dt);
67
67
  }
68
68
  else {
69
69
  const driven = driveClickToMove(ctx, motor);
70
70
  if (driven) {
71
71
  // driveClickToMove answers in logical space; motor.run expects the
72
- // same screen-relative input keyboard axes already are (IsoMotor
72
+ // same screen-relative input the directional axes already are (IsoMotor
73
73
  // converts internally, TopDownMotor's screen space is logical).
74
74
  const screenInput = game.projection === 'isometric' ? screenInputToLogical(driven.x, driven.y) : driven;
75
75
  motor.run(screenInput.x, screenInput.y, dt);
package/dist/index.d.ts CHANGED
@@ -23,3 +23,7 @@ export { buildNavigationGrid } from './navigation-grid.js';
23
23
  export type { GridCell, GridPoint, NavigationGrid } from './navigation-grid.js';
24
24
  export { findPath, nearestReachableCell, planPath, reachableCells } from './pathfinding.js';
25
25
  export type { PlannedPath } from './pathfinding.js';
26
+ export { SwingSparks } from './swing-sparks.js';
27
+ export { DamagePuff } from './damage-puff.js';
28
+ export { DustPuffs } from './dust-puffs.js';
29
+ export { DustTrail } from './dust-trail.js';
package/dist/index.js CHANGED
@@ -21,3 +21,7 @@ export { Lifetime } from './lifetime.js';
21
21
  export { ClickToMove, driveClickToMove } from './click-to-move.js';
22
22
  export { buildNavigationGrid } from './navigation-grid.js';
23
23
  export { findPath, nearestReachableCell, planPath, reachableCells } from './pathfinding.js';
24
+ export { SwingSparks } from './swing-sparks.js';
25
+ export { DamagePuff } from './damage-puff.js';
26
+ export { DustPuffs } from './dust-puffs.js';
27
+ export { DustTrail } from './dust-trail.js';
@@ -0,0 +1,32 @@
1
+ import { Component } from '@waica/engine';
2
+ /**
3
+ * A burst of sparks from this entity's own
4
+ * ParticleEmitter each time its StateMachine enters the swing state. The
5
+ * look (color, speed, lifetime) is authored on the emitter in the prefab;
6
+ * this only decides when, and how many. It hooks the state's onEnter
7
+ * instead of polling, so it fires exactly once per swing — right after
8
+ * the role's own onEnter has landed the strike.
9
+ *
10
+ * Its siblings must already be mounted when it is, so it goes after the
11
+ * StateMachine and the ParticleEmitter in the prefab.
12
+ */
13
+ export declare class SwingSparks extends Component {
14
+ static componentName: string;
15
+ static displayName: string;
16
+ static params: {
17
+ state: {
18
+ label: string;
19
+ };
20
+ count: {
21
+ label: string;
22
+ min: number;
23
+ max: number;
24
+ step: number;
25
+ };
26
+ };
27
+ /** The state whose entry is a swing. */
28
+ state: string;
29
+ /** Sparks per swing. */
30
+ count: number;
31
+ onReady(): void;
32
+ }
@@ -0,0 +1,33 @@
1
+ import { Component, ParticleEmitter, StateMachine } from '@waica/engine';
2
+ /**
3
+ * A burst of sparks from this entity's own
4
+ * ParticleEmitter each time its StateMachine enters the swing state. The
5
+ * look (color, speed, lifetime) is authored on the emitter in the prefab;
6
+ * this only decides when, and how many. It hooks the state's onEnter
7
+ * instead of polling, so it fires exactly once per swing — right after
8
+ * the role's own onEnter has landed the strike.
9
+ *
10
+ * Its siblings must already be mounted when it is, so it goes after the
11
+ * StateMachine and the ParticleEmitter in the prefab.
12
+ */
13
+ export class SwingSparks extends Component {
14
+ static componentName = 'SwingSparks';
15
+ static displayName = 'Swing sparks';
16
+ static params = {
17
+ state: { label: 'State' },
18
+ count: { label: 'Sparks', min: 1, max: 64, step: 1 },
19
+ };
20
+ /** The state whose entry is a swing. */
21
+ state = 'attack';
22
+ /** Sparks per swing. */
23
+ count = 14;
24
+ onReady() {
25
+ const machine = this.entity.get(StateMachine);
26
+ const sparks = this.entity.get(ParticleEmitter);
27
+ if (!machine || !sparks) {
28
+ console.warn(`[waica] "${this.entity.name}": SwingSparks needs a StateMachine and a ParticleEmitter before it.`);
29
+ return;
30
+ }
31
+ machine.on(this.state, { onEnter: () => sparks.emit(this.count) });
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/behaviors",
3
- "version": "0.20.0",
3
+ "version": "0.22.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.20.0"
22
+ "@waica/engine": "^0.22.0"
23
23
  }
24
24
  }