@waica/engine 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/directional.d.ts +48 -0
- package/dist/animation/directional.js +68 -0
- package/dist/archetype.d.ts +3 -0
- package/dist/camera.d.ts +19 -0
- package/dist/camera.js +10 -0
- package/dist/component.d.ts +2 -0
- package/dist/components/animated-sprite.d.ts +27 -2
- package/dist/components/animated-sprite.js +50 -5
- package/dist/components/dynamic-body.js +2 -4
- package/dist/components/sprite.d.ts +23 -1
- package/dist/components/sprite.js +49 -8
- package/dist/components/tilemap.d.ts +113 -0
- package/dist/components/tilemap.js +316 -0
- package/dist/entity.d.ts +3 -0
- package/dist/entity.js +13 -1
- package/dist/game.d.ts +13 -1
- package/dist/game.js +70 -9
- package/dist/index.d.ts +16 -3
- package/dist/index.js +8 -1
- package/dist/projection.d.ts +14 -0
- package/dist/projection.js +30 -0
- package/dist/render-sort.d.ts +31 -0
- package/dist/render-sort.js +39 -0
- package/dist/scene-solids.d.ts +16 -0
- package/dist/scene-solids.js +28 -0
- package/dist/scene.d.ts +12 -0
- package/dist/scene.js +1 -0
- package/dist/solid-axis.js +2 -4
- package/dist/sprite-placement.d.ts +22 -0
- package/dist/sprite-placement.js +14 -0
- package/dist/state/hooks.d.ts +1 -1
- package/dist/state/hooks.js +3 -1
- package/dist/state/state-machine.d.ts +18 -0
- package/dist/state/state-machine.js +57 -1
- package/dist/tilemap-grid.d.ts +26 -0
- package/dist/tilemap-grid.js +41 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
2
|
export type { GameOptions, GameResolution, SpawnPrefabOptions, UpdateFn, ParamOverrides, } from './game.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
4
|
+
export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
|
|
5
|
+
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
6
|
+
export type { YSortEntry, YSortParticipant } from './render-sort.js';
|
|
7
|
+
export { projectIsometric, screenInputToLogical, unprojectIsometric } from './projection.js';
|
|
8
|
+
export type { ProjectedPoint } from './projection.js';
|
|
9
|
+
export { spritePlacement } from './sprite-placement.js';
|
|
10
|
+
export type { SpritePlacement, SpritePlacementInput } from './sprite-placement.js';
|
|
11
|
+
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
12
|
+
export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
5
13
|
export { Entity } from './entity.js';
|
|
6
14
|
export { Component } from './component.js';
|
|
7
15
|
export { authoringDefaults } from './authoring-defaults.js';
|
|
@@ -25,14 +33,19 @@ export { Solid } from './components/solid.js';
|
|
|
25
33
|
export { Hitbox } from './components/hitbox.js';
|
|
26
34
|
export { DynamicBody } from './components/dynamic-body.js';
|
|
27
35
|
export { AnimatedSprite } from './components/animated-sprite.js';
|
|
36
|
+
export { Tilemap } from './components/tilemap.js';
|
|
28
37
|
export { aabbOverlap } from './aabb.js';
|
|
29
38
|
export { resolveSolidAxis } from './solid-axis.js';
|
|
30
39
|
export type { CollisionAxis, SolidAxisOptions } from './solid-axis.js';
|
|
40
|
+
export { isSolidSource, sceneSolids, SOLID_SOURCE_SYMBOL } from './scene-solids.js';
|
|
41
|
+
export type { SolidSource } from './scene-solids.js';
|
|
42
|
+
export { cellAt, cellBounds, cellIndex } from './tilemap-grid.js';
|
|
43
|
+
export type { TilemapCell, TilemapCellBounds, TilemapGridSpec, } from './tilemap-grid.js';
|
|
31
44
|
export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
|
|
32
45
|
export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
|
|
33
46
|
export { Emitter } from './events.js';
|
|
34
47
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
35
|
-
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, PrefabJson } from './scene.js';
|
|
48
|
+
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, PrefabJson, } from './scene.js';
|
|
36
49
|
export { ClipPlayer } from './animation/clip-player.js';
|
|
37
50
|
export type { ClipDef } from './animation/clip-player.js';
|
|
38
51
|
export { sheetCell, sheetFrameCount, locateFrame } from './animation/sheet.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
|
-
export {
|
|
2
|
+
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
3
|
+
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
4
|
+
export { projectIsometric, screenInputToLogical, unprojectIsometric } from './projection.js';
|
|
5
|
+
export { spritePlacement } from './sprite-placement.js';
|
|
6
|
+
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
3
7
|
export { Entity } from './entity.js';
|
|
4
8
|
export { Component } from './component.js';
|
|
5
9
|
export { authoringDefaults } from './authoring-defaults.js';
|
|
@@ -15,8 +19,11 @@ export { Solid } from './components/solid.js';
|
|
|
15
19
|
export { Hitbox } from './components/hitbox.js';
|
|
16
20
|
export { DynamicBody } from './components/dynamic-body.js';
|
|
17
21
|
export { AnimatedSprite } from './components/animated-sprite.js';
|
|
22
|
+
export { Tilemap } from './components/tilemap.js';
|
|
18
23
|
export { aabbOverlap } from './aabb.js';
|
|
19
24
|
export { resolveSolidAxis } from './solid-axis.js';
|
|
25
|
+
export { isSolidSource, sceneSolids, SOLID_SOURCE_SYMBOL } from './scene-solids.js';
|
|
26
|
+
export { cellAt, cellBounds, cellIndex } from './tilemap-grid.js';
|
|
20
27
|
export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
|
|
21
28
|
export { Emitter } from './events.js';
|
|
22
29
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ProjectedPoint {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Maps screen-relative input into the logical square-grid world used by
|
|
7
|
+
* isometric simulation. The orthogonal rotation/reflection preserves input
|
|
8
|
+
* magnitude and angles; vectors longer than one are clamped after mapping.
|
|
9
|
+
*/
|
|
10
|
+
export declare function screenInputToLogical(ix: number, iy: number): ProjectedPoint;
|
|
11
|
+
/** Fixed 2:1 map from logical world coordinates to render coordinates. */
|
|
12
|
+
export declare function projectIsometric(lx: number, ly: number): ProjectedPoint;
|
|
13
|
+
/** Exact inverse of projectIsometric. */
|
|
14
|
+
export declare function unprojectIsometric(x: number, y: number): ProjectedPoint;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const cleanZero = (value) => (value === 0 ? 0 : value);
|
|
2
|
+
/**
|
|
3
|
+
* Maps screen-relative input into the logical square-grid world used by
|
|
4
|
+
* isometric simulation. The orthogonal rotation/reflection preserves input
|
|
5
|
+
* magnitude and angles; vectors longer than one are clamped after mapping.
|
|
6
|
+
*/
|
|
7
|
+
export function screenInputToLogical(ix, iy) {
|
|
8
|
+
let x = (ix - iy) / Math.SQRT2;
|
|
9
|
+
let y = (-ix - iy) / Math.SQRT2;
|
|
10
|
+
const length = Math.hypot(x, y);
|
|
11
|
+
if (length > 1) {
|
|
12
|
+
x /= length;
|
|
13
|
+
y /= length;
|
|
14
|
+
}
|
|
15
|
+
return { x: cleanZero(x), y: cleanZero(y) };
|
|
16
|
+
}
|
|
17
|
+
/** Fixed 2:1 map from logical world coordinates to render coordinates. */
|
|
18
|
+
export function projectIsometric(lx, ly) {
|
|
19
|
+
return {
|
|
20
|
+
x: cleanZero(lx - ly),
|
|
21
|
+
y: cleanZero(-(lx + ly) / 2),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Exact inverse of projectIsometric. */
|
|
25
|
+
export function unprojectIsometric(x, y) {
|
|
26
|
+
return {
|
|
27
|
+
x: cleanZero(x / 2 - y),
|
|
28
|
+
y: cleanZero(-x / 2 - y),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Y-sort draw ordering: an opt-in render mode (scene JSON `render.sort: 'y'`)
|
|
3
|
+
* for top-down scenes where "lower on screen" means "closer to the camera".
|
|
4
|
+
*/
|
|
5
|
+
export interface YSortEntry {
|
|
6
|
+
/** The sprite's layer — the primary draw-order band, exactly as without y-sort. */
|
|
7
|
+
layer: number;
|
|
8
|
+
/** The owning entity's world Y — the sort key. Sprite offsets don't shift it. */
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The explicit seam a component opts into to participate in y-sort: it
|
|
13
|
+
* exposes its draw-order layer and accepts the per-frame z the pass derives.
|
|
14
|
+
* Both stock sprite classes implement it; a custom renderable can too.
|
|
15
|
+
*/
|
|
16
|
+
export interface YSortParticipant {
|
|
17
|
+
readonly layer: number;
|
|
18
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
19
|
+
setSortZ(z: number): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function isYSortParticipant(value: unknown): value is YSortParticipant;
|
|
22
|
+
/**
|
|
23
|
+
* Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
|
|
24
|
+
* lower Y gets a higher z (renders in front), and exact Y ties keep input
|
|
25
|
+
* order. Offsets stay strictly inside (layer, layer + 1) × 0.01 for integer
|
|
26
|
+
* layers — but a fractional layer (e.g. 0.5) can sit closer than that to the
|
|
27
|
+
* next one present, so each band is capped at the gap to the next distinct
|
|
28
|
+
* layer above it, never wider than 0.01. Integer layers are always >= 1
|
|
29
|
+
* apart, so their band is exactly the old fixed 0.01.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ySortZ(entries: readonly YSortEntry[]): number[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function isYSortParticipant(value) {
|
|
2
|
+
return (typeof value === 'object' &&
|
|
3
|
+
value !== null &&
|
|
4
|
+
typeof value.layer === 'number' &&
|
|
5
|
+
typeof value.setSortZ === 'function');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
|
|
9
|
+
* lower Y gets a higher z (renders in front), and exact Y ties keep input
|
|
10
|
+
* order. Offsets stay strictly inside (layer, layer + 1) × 0.01 for integer
|
|
11
|
+
* layers — but a fractional layer (e.g. 0.5) can sit closer than that to the
|
|
12
|
+
* next one present, so each band is capped at the gap to the next distinct
|
|
13
|
+
* layer above it, never wider than 0.01. Integer layers are always >= 1
|
|
14
|
+
* apart, so their band is exactly the old fixed 0.01.
|
|
15
|
+
*/
|
|
16
|
+
export function ySortZ(entries) {
|
|
17
|
+
const byLayer = new Map();
|
|
18
|
+
for (const [index, entry] of entries.entries()) {
|
|
19
|
+
const group = byLayer.get(entry.layer);
|
|
20
|
+
if (group)
|
|
21
|
+
group.push(index);
|
|
22
|
+
else
|
|
23
|
+
byLayer.set(entry.layer, [index]);
|
|
24
|
+
}
|
|
25
|
+
const layers = [...byLayer.keys()].sort((a, b) => a - b);
|
|
26
|
+
const z = new Array(entries.length);
|
|
27
|
+
for (const [i, layer] of layers.entries()) {
|
|
28
|
+
const indices = byLayer.get(layer);
|
|
29
|
+
const next = layers[i + 1];
|
|
30
|
+
const width = next === undefined ? 0.01 : Math.min(0.01, (next - layer) * 0.01);
|
|
31
|
+
// Stable sort: back-to-front is descending Y, ties keep input order.
|
|
32
|
+
const ordered = [...indices].sort((a, b) => entries[b].y - entries[a].y);
|
|
33
|
+
const step = width / (ordered.length + 1);
|
|
34
|
+
for (const [rank, index] of ordered.entries()) {
|
|
35
|
+
z[index] = layer * 0.01 + (rank + 1) * step;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return z;
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Solid } from './components/solid.js';
|
|
2
|
+
import type { Entity } from './entity.js';
|
|
3
|
+
import type { Game } from './game.js';
|
|
4
|
+
/** Explicit opt-in brand for components that derive collision Solids. */
|
|
5
|
+
export declare const SOLID_SOURCE_SYMBOL: unique symbol;
|
|
6
|
+
/** Component seam for geometry that derives one or more collision Solids. */
|
|
7
|
+
export interface SolidSource {
|
|
8
|
+
readonly [SOLID_SOURCE_SYMBOL]: true;
|
|
9
|
+
solids(): readonly Solid[];
|
|
10
|
+
}
|
|
11
|
+
export declare function isSolidSource(value: unknown): value is SolidSource;
|
|
12
|
+
/**
|
|
13
|
+
* All static collision geometry in stable entity/component order. Without a
|
|
14
|
+
* SolidSource this is exactly one direct Solid per entity, as before.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sceneSolids(game: Game, except?: Entity): Solid[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Solid } from './components/solid.js';
|
|
2
|
+
/** Explicit opt-in brand for components that derive collision Solids. */
|
|
3
|
+
export const SOLID_SOURCE_SYMBOL = Symbol('waica.solidSource');
|
|
4
|
+
export function isSolidSource(value) {
|
|
5
|
+
if (value === null || typeof value !== 'object')
|
|
6
|
+
return false;
|
|
7
|
+
const candidate = value;
|
|
8
|
+
return candidate[SOLID_SOURCE_SYMBOL] === true && typeof candidate.solids === 'function';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* All static collision geometry in stable entity/component order. Without a
|
|
12
|
+
* SolidSource this is exactly one direct Solid per entity, as before.
|
|
13
|
+
*/
|
|
14
|
+
export function sceneSolids(game, except) {
|
|
15
|
+
const result = [];
|
|
16
|
+
for (const entity of game.entities) {
|
|
17
|
+
if (entity === except)
|
|
18
|
+
continue;
|
|
19
|
+
const direct = entity.get(Solid);
|
|
20
|
+
if (direct)
|
|
21
|
+
result.push(direct);
|
|
22
|
+
for (const component of entity.components ?? []) {
|
|
23
|
+
if (isSolidSource(component))
|
|
24
|
+
result.push(...component.solids());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
package/dist/scene.d.ts
CHANGED
|
@@ -28,10 +28,22 @@ export interface PrefabJson {
|
|
|
28
28
|
type: 'character' | 'object' | 'tile';
|
|
29
29
|
components: SceneComponentJson[];
|
|
30
30
|
}
|
|
31
|
+
/** Scene-wide render options (v3). */
|
|
32
|
+
export interface SceneRenderJson {
|
|
33
|
+
/**
|
|
34
|
+
* 'y' orders same-layer sprites by their render-space Y — lower Y renders
|
|
35
|
+
* in front (top-down depth). Absent: spawn order breaks same-layer ties.
|
|
36
|
+
*/
|
|
37
|
+
sort?: 'y';
|
|
38
|
+
/** Logical positions are projected to the fixed 2:1 render lattice. */
|
|
39
|
+
projection?: 'isometric';
|
|
40
|
+
}
|
|
31
41
|
export interface SceneJson {
|
|
32
42
|
waicaScene: 1 | 2 | 3;
|
|
33
43
|
/** The scene's built-in camera (v3); absent = the host keeps control. */
|
|
34
44
|
camera?: SceneCameraJson;
|
|
45
|
+
/** Draw-order policy (v3); absent = layer bands with spawn-order ties. */
|
|
46
|
+
render?: SceneRenderJson;
|
|
35
47
|
entities: SceneEntityJson[];
|
|
36
48
|
/** UI pieces (src/ui/*.html) mounted visible when the scene loads. */
|
|
37
49
|
ui?: string[];
|
package/dist/scene.js
CHANGED
|
@@ -82,6 +82,7 @@ export function spawnFromJson(game, json, registry) {
|
|
|
82
82
|
/** Loads a full scene into the game. */
|
|
83
83
|
export function loadScene(game, scene, registry) {
|
|
84
84
|
game.registry = registry;
|
|
85
|
+
game.setSceneRender(scene.render);
|
|
85
86
|
for (const entityJson of scene.entities)
|
|
86
87
|
spawnFromJson(game, entityJson, registry);
|
|
87
88
|
// After the spawns: with a follow target, the camera starts centered on it.
|
package/dist/solid-axis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { collisionBounds, collisionOverlap } from './collision-shape.js';
|
|
2
2
|
import { Solid } from './components/solid.js';
|
|
3
|
+
import { sceneSolids } from './scene-solids.js';
|
|
3
4
|
const CONTACT_TOLERANCE = 1e-7;
|
|
4
5
|
function bodyFor(solid) {
|
|
5
6
|
return {
|
|
@@ -19,10 +20,7 @@ function bodyFor(solid) {
|
|
|
19
20
|
export function resolveSolidAxis({ entity, axis, previous, body, onContact, }) {
|
|
20
21
|
const position = entity.position;
|
|
21
22
|
const target = position[axis];
|
|
22
|
-
const solids = entity.game
|
|
23
|
-
.filter((other) => other !== entity)
|
|
24
|
-
.map((other) => other.get(Solid))
|
|
25
|
-
.filter((solid) => solid !== undefined);
|
|
23
|
+
const solids = sceneSolids(entity.game, entity);
|
|
26
24
|
position[axis] = previous;
|
|
27
25
|
// Preserve the established spawn-inside-wall bail: pre-existing overlaps
|
|
28
26
|
// are ignored for this move rather than becoming arbitrary push-out rules.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SpritePlacementInput {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
offsetX: number;
|
|
5
|
+
offsetY: number;
|
|
6
|
+
anchorX: number;
|
|
7
|
+
anchorY: number;
|
|
8
|
+
flipX: boolean;
|
|
9
|
+
frameScaleX: number;
|
|
10
|
+
frameScaleY: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SpritePlacement {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
scaleX: number;
|
|
16
|
+
scaleY: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Places a displayed frame inside its full-size sprite box. The declared
|
|
20
|
+
* anchor belongs to that box; smaller animation frames retain its floor.
|
|
21
|
+
*/
|
|
22
|
+
export declare function spritePlacement(input: SpritePlacementInput): SpritePlacement;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Places a displayed frame inside its full-size sprite box. The declared
|
|
3
|
+
* anchor belongs to that box; smaller animation frames retain its floor.
|
|
4
|
+
*/
|
|
5
|
+
export function spritePlacement(input) {
|
|
6
|
+
const boxX = input.offsetX + (0.5 - input.anchorX) * input.width;
|
|
7
|
+
const floorY = input.offsetY - input.anchorY * input.height;
|
|
8
|
+
return {
|
|
9
|
+
x: input.flipX ? -boxX : boxX,
|
|
10
|
+
y: floorY + (input.height * input.frameScaleY) / 2,
|
|
11
|
+
scaleX: input.width * input.frameScaleX * (input.flipX ? -1 : 1),
|
|
12
|
+
scaleY: input.height * input.frameScaleY,
|
|
13
|
+
};
|
|
14
|
+
}
|
package/dist/state/hooks.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface ArchetypeBundle {
|
|
|
63
63
|
/** Extra named logic sets that are not themselves roles. */
|
|
64
64
|
logicSets?: Readonly<Record<string, StateLogic>>;
|
|
65
65
|
}
|
|
66
|
-
/** Clears role definitions
|
|
66
|
+
/** Clears role definitions, every named logic set and the directional contract. */
|
|
67
67
|
export declare function resetRegistries(): void;
|
|
68
68
|
/**
|
|
69
69
|
* Replaces the active registry contents with one archetype's complete bundle.
|
package/dist/state/hooks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installDirectionalAnimation } from '../animation/directional.js';
|
|
1
2
|
const sets = new Map();
|
|
2
3
|
/**
|
|
3
4
|
* Registers state code under a logic-set name. A role's name is also its
|
|
@@ -8,10 +9,11 @@ export function defineStates(name, states) {
|
|
|
8
9
|
sets.set(name, { ...sets.get(name), ...states });
|
|
9
10
|
}
|
|
10
11
|
const roles = new Map();
|
|
11
|
-
/** Clears role definitions
|
|
12
|
+
/** Clears role definitions, every named logic set and the directional contract. */
|
|
12
13
|
export function resetRegistries() {
|
|
13
14
|
roles.clear();
|
|
14
15
|
sets.clear();
|
|
16
|
+
installDirectionalAnimation(null);
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Replaces the active registry contents with one archetype's complete bundle.
|
|
@@ -66,6 +66,8 @@ export declare class StateMachine extends Component {
|
|
|
66
66
|
private readonly instanceHooks;
|
|
67
67
|
private readonly signals;
|
|
68
68
|
private readonly warnedClips;
|
|
69
|
+
/** Facing last used to resolve a directional clip — detects a turn mid-state. */
|
|
70
|
+
private lastFacing;
|
|
69
71
|
onReady(): void;
|
|
70
72
|
/** Adds instance-level hooks on top of the logic set — the escape hatch. */
|
|
71
73
|
on(state: string, hooks: StateHooks): void;
|
|
@@ -81,4 +83,20 @@ export declare class StateMachine extends Component {
|
|
|
81
83
|
private runCollision;
|
|
82
84
|
private enter;
|
|
83
85
|
private playClip;
|
|
86
|
+
/**
|
|
87
|
+
* Directional resolution: with an installed contract AND a sibling that
|
|
88
|
+
* reports facing, plays state × facing (mirroring included). Returns false
|
|
89
|
+
* to keep the name-based path — no contract, no facing, no playable clip.
|
|
90
|
+
*/
|
|
91
|
+
private playDirectionalClip;
|
|
92
|
+
private facingProvider;
|
|
93
|
+
/**
|
|
94
|
+
* playDirectionalClip only runs at enter() time (via playClip), so a
|
|
95
|
+
* facing change that doesn't cross a state boundary — turning while
|
|
96
|
+
* still walking, say — never re-resolves the clip on its own. Catch that
|
|
97
|
+
* here every frame: same state, new facing, re-run the resolution.
|
|
98
|
+
* AnimatedSprite.play() no-ops on an unchanged clip name, so this is
|
|
99
|
+
* cheap when facing hasn't moved the resolved clip.
|
|
100
|
+
*/
|
|
101
|
+
private reresolveOnFacingChange;
|
|
84
102
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from '../animation/directional.js';
|
|
1
2
|
import { Component } from '../component.js';
|
|
2
3
|
import { AnimatedSprite } from '../components/animated-sprite.js';
|
|
3
4
|
import { closestLogicSet, logicSet, registeredLogicSets, } from './hooks.js';
|
|
@@ -47,7 +48,14 @@ export class StateMachine extends Component {
|
|
|
47
48
|
static params = {
|
|
48
49
|
role: { label: 'Role' },
|
|
49
50
|
};
|
|
50
|
-
static transient = [
|
|
51
|
+
static transient = [
|
|
52
|
+
'current',
|
|
53
|
+
'elapsed',
|
|
54
|
+
'instanceHooks',
|
|
55
|
+
'signals',
|
|
56
|
+
'warnedClips',
|
|
57
|
+
'lastFacing',
|
|
58
|
+
];
|
|
51
59
|
/** The character's role — names the logic set providing its state code. */
|
|
52
60
|
role = '';
|
|
53
61
|
/** Starting state; defaults to the first declared state. */
|
|
@@ -60,6 +68,8 @@ export class StateMachine extends Component {
|
|
|
60
68
|
instanceHooks = new Map();
|
|
61
69
|
signals = new Set();
|
|
62
70
|
warnedClips = new Set();
|
|
71
|
+
/** Facing last used to resolve a directional clip — detects a turn mid-state. */
|
|
72
|
+
lastFacing;
|
|
63
73
|
onReady() {
|
|
64
74
|
if (this.role && !logicSet(this.role)) {
|
|
65
75
|
const sets = registeredLogicSets();
|
|
@@ -111,6 +121,7 @@ export class StateMachine extends Component {
|
|
|
111
121
|
this.enter(edge.to);
|
|
112
122
|
}
|
|
113
123
|
this.signals.clear();
|
|
124
|
+
this.reresolveOnFacingChange();
|
|
114
125
|
}
|
|
115
126
|
onCollide(other) {
|
|
116
127
|
// The state that was active when the contact happened owns it: a
|
|
@@ -167,6 +178,8 @@ export class StateMachine extends Component {
|
|
|
167
178
|
if (!sprite)
|
|
168
179
|
return;
|
|
169
180
|
const clip = this.states[state]?.clip ?? state;
|
|
181
|
+
if (this.playDirectionalClip(sprite, clip))
|
|
182
|
+
return;
|
|
170
183
|
if (sprite.clips[clip]) {
|
|
171
184
|
sprite.play(clip);
|
|
172
185
|
}
|
|
@@ -176,4 +189,47 @@ export class StateMachine extends Component {
|
|
|
176
189
|
`keeping "${sprite.current ?? 'none'}"`);
|
|
177
190
|
}
|
|
178
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Directional resolution: with an installed contract AND a sibling that
|
|
194
|
+
* reports facing, plays state × facing (mirroring included). Returns false
|
|
195
|
+
* to keep the name-based path — no contract, no facing, no playable clip.
|
|
196
|
+
*/
|
|
197
|
+
playDirectionalClip(sprite, clip) {
|
|
198
|
+
const animation = installedDirectionalAnimation();
|
|
199
|
+
if (!animation)
|
|
200
|
+
return false;
|
|
201
|
+
const provider = this.facingProvider();
|
|
202
|
+
if (!provider)
|
|
203
|
+
return false;
|
|
204
|
+
const facing = provider.getAnimationFacing();
|
|
205
|
+
this.lastFacing = facing;
|
|
206
|
+
const resolved = resolveDirectionalClip(animation, Object.keys(sprite.clips), clip, facing);
|
|
207
|
+
if (!resolved.clip || !sprite.clips[resolved.clip])
|
|
208
|
+
return false;
|
|
209
|
+
sprite.setFlipX(resolved.flip);
|
|
210
|
+
sprite.play(resolved.clip);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
facingProvider() {
|
|
214
|
+
return this.entity.components.find((c) => isAnimationFacingProvider(c));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* playDirectionalClip only runs at enter() time (via playClip), so a
|
|
218
|
+
* facing change that doesn't cross a state boundary — turning while
|
|
219
|
+
* still walking, say — never re-resolves the clip on its own. Catch that
|
|
220
|
+
* here every frame: same state, new facing, re-run the resolution.
|
|
221
|
+
* AnimatedSprite.play() no-ops on an unchanged clip name, so this is
|
|
222
|
+
* cheap when facing hasn't moved the resolved clip.
|
|
223
|
+
*/
|
|
224
|
+
reresolveOnFacingChange() {
|
|
225
|
+
if (!this.current || !installedDirectionalAnimation())
|
|
226
|
+
return;
|
|
227
|
+
const sprite = this.entity.get(AnimatedSprite);
|
|
228
|
+
if (!sprite)
|
|
229
|
+
return;
|
|
230
|
+
const provider = this.facingProvider();
|
|
231
|
+
if (!provider || provider.getAnimationFacing() === this.lastFacing)
|
|
232
|
+
return;
|
|
233
|
+
this.playDirectionalClip(sprite, this.states[this.current]?.clip ?? this.current);
|
|
234
|
+
}
|
|
179
235
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TilemapGridSpec {
|
|
2
|
+
mapWidth: number;
|
|
3
|
+
mapHeight: number;
|
|
4
|
+
cellSize: number;
|
|
5
|
+
originX?: number;
|
|
6
|
+
originY?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TilemapCell {
|
|
9
|
+
column: number;
|
|
10
|
+
row: number;
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TilemapCellBounds {
|
|
14
|
+
left: number;
|
|
15
|
+
right: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
top: number;
|
|
18
|
+
centerX: number;
|
|
19
|
+
centerY: number;
|
|
20
|
+
}
|
|
21
|
+
/** Row-major index for a map cell, or null outside a valid map. */
|
|
22
|
+
export declare function cellIndex(mapWidth: number, mapHeight: number, column: number, row: number): number | null;
|
|
23
|
+
/** Cell containing a logical point, relative to the Tilemap entity origin. */
|
|
24
|
+
export declare function cellAt(spec: TilemapGridSpec, logicalX: number, logicalY: number): TilemapCell | null;
|
|
25
|
+
/** Logical bounds and center of one map cell. */
|
|
26
|
+
export declare function cellBounds(spec: TilemapGridSpec, column: number, row: number): TilemapCellBounds | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function dimension(value) {
|
|
2
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
3
|
+
}
|
|
4
|
+
/** Row-major index for a map cell, or null outside a valid map. */
|
|
5
|
+
export function cellIndex(mapWidth, mapHeight, column, row) {
|
|
6
|
+
const width = dimension(mapWidth);
|
|
7
|
+
const height = dimension(mapHeight);
|
|
8
|
+
if (!Number.isInteger(column) || !Number.isInteger(row))
|
|
9
|
+
return null;
|
|
10
|
+
if (column < 0 || row < 0 || column >= width || row >= height)
|
|
11
|
+
return null;
|
|
12
|
+
return row * width + column;
|
|
13
|
+
}
|
|
14
|
+
/** Cell containing a logical point, relative to the Tilemap entity origin. */
|
|
15
|
+
export function cellAt(spec, logicalX, logicalY) {
|
|
16
|
+
const size = spec.cellSize;
|
|
17
|
+
if (!Number.isFinite(size) || size <= 0)
|
|
18
|
+
return null;
|
|
19
|
+
const column = Math.floor((logicalX - (spec.originX ?? 0)) / size);
|
|
20
|
+
const row = Math.floor((logicalY - (spec.originY ?? 0)) / size);
|
|
21
|
+
const index = cellIndex(spec.mapWidth, spec.mapHeight, column, row);
|
|
22
|
+
return index === null ? null : { column, row, index };
|
|
23
|
+
}
|
|
24
|
+
/** Logical bounds and center of one map cell. */
|
|
25
|
+
export function cellBounds(spec, column, row) {
|
|
26
|
+
if (cellIndex(spec.mapWidth, spec.mapHeight, column, row) === null)
|
|
27
|
+
return null;
|
|
28
|
+
const size = spec.cellSize;
|
|
29
|
+
if (!Number.isFinite(size) || size <= 0)
|
|
30
|
+
return null;
|
|
31
|
+
const left = (spec.originX ?? 0) + column * size;
|
|
32
|
+
const bottom = (spec.originY ?? 0) + row * size;
|
|
33
|
+
return {
|
|
34
|
+
left,
|
|
35
|
+
right: left + size,
|
|
36
|
+
bottom,
|
|
37
|
+
top: bottom + size,
|
|
38
|
+
centerX: left + size / 2,
|
|
39
|
+
centerY: bottom + size / 2,
|
|
40
|
+
};
|
|
41
|
+
}
|