@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AnimationContract } from './contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Directional animation contract: the archetype declares which facing
|
|
4
|
+
* directions exist, how clips are named per direction (`<state>-<dir>`),
|
|
5
|
+
* and how a missing direction degrades — including mirroring, so west can
|
|
6
|
+
* reuse east art flipped. Extends the base AnimationContract thesis to
|
|
7
|
+
* genres where characters face more than one way (top-down, isometric).
|
|
8
|
+
*/
|
|
9
|
+
export interface DirectionalFallback<Dir extends string = string> {
|
|
10
|
+
dir: Dir;
|
|
11
|
+
/** Mirror the resolved clip horizontally (e.g. west plays east flipped). */
|
|
12
|
+
flip?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface DirectionalAnimation<Dir extends string = string> {
|
|
15
|
+
/** Declared facing directions, e.g. ['n', 's', 'e', 'w'] — 8 for isometric. */
|
|
16
|
+
directions: readonly Dir[];
|
|
17
|
+
/** Directional degradation: a missing facing resolves to another, optionally mirrored. */
|
|
18
|
+
fallbacks?: Partial<Record<Dir, DirectionalFallback<Dir>>>;
|
|
19
|
+
/** State-level degradation chain applied when no directional clip resolves. */
|
|
20
|
+
contract: AnimationContract;
|
|
21
|
+
}
|
|
22
|
+
export interface ResolvedDirectionalClip {
|
|
23
|
+
clip: string | undefined;
|
|
24
|
+
flip: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves state × facing to a playable clip: the exact `<state>-<dir>`
|
|
28
|
+
* clip, else the declared directional fallback chain (accumulating flips).
|
|
29
|
+
* On a dead end, walks the base AnimationContract's state fallback chain
|
|
30
|
+
* (cycle-safe) and, for each candidate state in turn, retries the
|
|
31
|
+
* directional chain against it before trying its bare name. An invalid
|
|
32
|
+
* facing, or a chain that dead-ends everywhere, resolves to nothing rather
|
|
33
|
+
* than guessing — the caller's name-based fallback path handles that.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveDirectionalClip(animation: DirectionalAnimation, available: Iterable<string>, state: string, facing: string): ResolvedDirectionalClip;
|
|
36
|
+
/** Installs (or clears, with null) the active directional contract. */
|
|
37
|
+
export declare function installDirectionalAnimation(animation: DirectionalAnimation | null): void;
|
|
38
|
+
/** The active directional contract, if an archetype installed one. */
|
|
39
|
+
export declare function installedDirectionalAnimation(): DirectionalAnimation | null;
|
|
40
|
+
/**
|
|
41
|
+
* The explicit seam a driving component (a motor) opts into so StateMachine
|
|
42
|
+
* can resolve directional clips: it reports the entity's current facing as
|
|
43
|
+
* one of the contract's declared directions.
|
|
44
|
+
*/
|
|
45
|
+
export interface AnimationFacingProvider {
|
|
46
|
+
getAnimationFacing(): string;
|
|
47
|
+
}
|
|
48
|
+
export declare function isAnimationFacingProvider(value: unknown): value is AnimationFacingProvider;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walks the exact `<state>-<dir>` clip, then the declared directional
|
|
3
|
+
* fallback chain (accumulating flips), cycle-safe. Undefined at a dead end —
|
|
4
|
+
* callers decide what to try next, instead of this reaching for anything else.
|
|
5
|
+
*/
|
|
6
|
+
function directionalChain(animation, available, state, facing) {
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
let dir = facing;
|
|
9
|
+
let flip = false;
|
|
10
|
+
while (dir && !seen.has(dir)) {
|
|
11
|
+
const candidate = `${state}-${dir}`;
|
|
12
|
+
if (available.has(candidate))
|
|
13
|
+
return { clip: candidate, flip };
|
|
14
|
+
seen.add(dir);
|
|
15
|
+
const fallback = animation.fallbacks?.[dir];
|
|
16
|
+
if (!fallback)
|
|
17
|
+
break;
|
|
18
|
+
if (fallback.flip)
|
|
19
|
+
flip = !flip;
|
|
20
|
+
dir = fallback.dir;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves state × facing to a playable clip: the exact `<state>-<dir>`
|
|
26
|
+
* clip, else the declared directional fallback chain (accumulating flips).
|
|
27
|
+
* On a dead end, walks the base AnimationContract's state fallback chain
|
|
28
|
+
* (cycle-safe) and, for each candidate state in turn, retries the
|
|
29
|
+
* directional chain against it before trying its bare name. An invalid
|
|
30
|
+
* facing, or a chain that dead-ends everywhere, resolves to nothing rather
|
|
31
|
+
* than guessing — the caller's name-based fallback path handles that.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveDirectionalClip(animation, available, state, facing) {
|
|
34
|
+
if (!animation.directions.includes(facing))
|
|
35
|
+
return { clip: undefined, flip: false };
|
|
36
|
+
const set = new Set(available);
|
|
37
|
+
const direct = directionalChain(animation, set, state, facing);
|
|
38
|
+
if (direct)
|
|
39
|
+
return direct;
|
|
40
|
+
const seenStates = new Set();
|
|
41
|
+
let candidate = state;
|
|
42
|
+
while (candidate && !seenStates.has(candidate)) {
|
|
43
|
+
seenStates.add(candidate);
|
|
44
|
+
if (candidate !== state) {
|
|
45
|
+
const viaDirection = directionalChain(animation, set, candidate, facing);
|
|
46
|
+
if (viaDirection)
|
|
47
|
+
return viaDirection;
|
|
48
|
+
}
|
|
49
|
+
if (set.has(candidate))
|
|
50
|
+
return { clip: candidate, flip: false };
|
|
51
|
+
candidate = animation.contract.fallbacks[candidate];
|
|
52
|
+
}
|
|
53
|
+
return { clip: undefined, flip: false };
|
|
54
|
+
}
|
|
55
|
+
let installed = null;
|
|
56
|
+
/** Installs (or clears, with null) the active directional contract. */
|
|
57
|
+
export function installDirectionalAnimation(animation) {
|
|
58
|
+
installed = animation;
|
|
59
|
+
}
|
|
60
|
+
/** The active directional contract, if an archetype installed one. */
|
|
61
|
+
export function installedDirectionalAnimation() {
|
|
62
|
+
return installed;
|
|
63
|
+
}
|
|
64
|
+
export function isAnimationFacingProvider(value) {
|
|
65
|
+
return (typeof value === 'object' &&
|
|
66
|
+
value !== null &&
|
|
67
|
+
typeof value.getAnimationFacing === 'function');
|
|
68
|
+
}
|
package/dist/archetype.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DirectionalAnimation } from './animation/directional.js';
|
|
1
2
|
import type { InputBindings } from './input.js';
|
|
2
3
|
import type { PrefabJson, SceneEntityJson, SceneJson, SceneRegistry } from './scene.js';
|
|
3
4
|
import type { ArchetypeBundle } from './state/hooks.js';
|
|
@@ -30,6 +31,8 @@ export interface ArchetypeManifest {
|
|
|
30
31
|
bindings: Readonly<InputBindings>;
|
|
31
32
|
actionLabels: Readonly<Record<string, string>>;
|
|
32
33
|
bundle: ArchetypeBundle;
|
|
34
|
+
/** Directional animation contract, for genres where characters face around. */
|
|
35
|
+
animation?: DirectionalAnimation;
|
|
33
36
|
}
|
|
34
37
|
/** Browser manifest enriched with URLs produced by an asset-aware bundler. */
|
|
35
38
|
export interface BrowserArchetypeManifest extends ArchetypeManifest {
|
package/dist/camera.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface SceneCameraJson {
|
|
|
21
21
|
deadzoneWidth?: number;
|
|
22
22
|
deadzoneHeight?: number;
|
|
23
23
|
lookahead?: number;
|
|
24
|
+
/** Vertical lookahead in world units; 0 (the default) disables it. */
|
|
25
|
+
lookaheadY?: number;
|
|
24
26
|
smoothing?: number;
|
|
25
27
|
limits?: CameraLimitsJson;
|
|
26
28
|
}
|
|
@@ -31,6 +33,7 @@ export interface ResolvedSceneCamera {
|
|
|
31
33
|
deadzoneWidth: number;
|
|
32
34
|
deadzoneHeight: number;
|
|
33
35
|
lookahead: number;
|
|
36
|
+
lookaheadY: number;
|
|
34
37
|
smoothing: number;
|
|
35
38
|
limits: CameraLimitsJson | null;
|
|
36
39
|
}
|
|
@@ -41,10 +44,24 @@ export declare const CAMERA_DEFAULTS: {
|
|
|
41
44
|
readonly deadzoneWidth: 2;
|
|
42
45
|
readonly deadzoneHeight: 2.5;
|
|
43
46
|
readonly lookahead: 1.5;
|
|
47
|
+
readonly lookaheadY: 0;
|
|
44
48
|
readonly smoothing: 6;
|
|
45
49
|
};
|
|
46
50
|
/** Fills a scene's camera block with the engine defaults. */
|
|
47
51
|
export declare function resolveSceneCamera(json?: SceneCameraJson): ResolvedSceneCamera;
|
|
52
|
+
/** Two-axis velocity a followed entity reports for camera lookahead. */
|
|
53
|
+
export interface CameraVelocity {
|
|
54
|
+
vx: number;
|
|
55
|
+
vy: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The explicit seam a component opts into so the scene camera can read the
|
|
59
|
+
* followed entity's velocity — the engine never guesses from field names.
|
|
60
|
+
*/
|
|
61
|
+
export interface CameraVelocityProvider {
|
|
62
|
+
getCameraVelocity(): CameraVelocity;
|
|
63
|
+
}
|
|
64
|
+
export declare function isCameraVelocityProvider(value: unknown): value is CameraVelocityProvider;
|
|
48
65
|
export interface CameraStepInput {
|
|
49
66
|
/** Current camera center. */
|
|
50
67
|
x: number;
|
|
@@ -59,6 +76,8 @@ export interface CameraStepInput {
|
|
|
59
76
|
} | null;
|
|
60
77
|
/** Followed entity's horizontal velocity, for lookahead. */
|
|
61
78
|
vx: number;
|
|
79
|
+
/** Followed entity's vertical velocity, for lookaheadY; omitted = 0. */
|
|
80
|
+
vy?: number;
|
|
62
81
|
dt: number;
|
|
63
82
|
}
|
|
64
83
|
/**
|
package/dist/camera.js
CHANGED
|
@@ -6,6 +6,7 @@ export const CAMERA_DEFAULTS = {
|
|
|
6
6
|
deadzoneWidth: 2,
|
|
7
7
|
deadzoneHeight: 2.5,
|
|
8
8
|
lookahead: 1.5,
|
|
9
|
+
lookaheadY: 0,
|
|
9
10
|
smoothing: 6,
|
|
10
11
|
};
|
|
11
12
|
/** Fills a scene's camera block with the engine defaults. */
|
|
@@ -17,6 +18,7 @@ export function resolveSceneCamera(json) {
|
|
|
17
18
|
deadzoneWidth: json?.deadzoneWidth ?? CAMERA_DEFAULTS.deadzoneWidth,
|
|
18
19
|
deadzoneHeight: json?.deadzoneHeight ?? CAMERA_DEFAULTS.deadzoneHeight,
|
|
19
20
|
lookahead: json?.lookahead ?? CAMERA_DEFAULTS.lookahead,
|
|
21
|
+
lookaheadY: json?.lookaheadY ?? CAMERA_DEFAULTS.lookaheadY,
|
|
20
22
|
smoothing: json?.smoothing ?? CAMERA_DEFAULTS.smoothing,
|
|
21
23
|
limits: json?.limits ?? null,
|
|
22
24
|
};
|
|
@@ -28,6 +30,11 @@ function clampAxis(center, halfView, min, max) {
|
|
|
28
30
|
return (min + max) / 2;
|
|
29
31
|
return Math.min(Math.max(center, min + halfView), max - halfView);
|
|
30
32
|
}
|
|
33
|
+
export function isCameraVelocityProvider(value) {
|
|
34
|
+
return (typeof value === 'object' &&
|
|
35
|
+
value !== null &&
|
|
36
|
+
typeof value.getCameraVelocity === 'function');
|
|
37
|
+
}
|
|
31
38
|
/**
|
|
32
39
|
* One simulation step of the camera: deadzone-follow with lookahead and
|
|
33
40
|
* exponential smoothing, then limits. Pure — returns the next center.
|
|
@@ -48,6 +55,9 @@ export function stepSceneCamera(cam, input) {
|
|
|
48
55
|
wantY = input.target.y - Math.sign(dy) * halfDzH;
|
|
49
56
|
if (Math.abs(input.vx) > 1)
|
|
50
57
|
wantX += Math.sign(input.vx) * cam.lookahead;
|
|
58
|
+
const vy = input.vy ?? 0;
|
|
59
|
+
if (Math.abs(vy) > 1)
|
|
60
|
+
wantY += Math.sign(vy) * cam.lookaheadY;
|
|
51
61
|
x = THREE.MathUtils.damp(x, wantX, cam.smoothing, input.dt);
|
|
52
62
|
y = THREE.MathUtils.damp(y, wantY, cam.smoothing, input.dt);
|
|
53
63
|
}
|
package/dist/component.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export declare abstract class Component {
|
|
|
63
63
|
onReady?(): void;
|
|
64
64
|
/** Runs once per frame. */
|
|
65
65
|
onUpdate?(dt: number): void;
|
|
66
|
+
/** Runs after the scene changes between identity and projected rendering. */
|
|
67
|
+
onProjectionChange?(projection: 'isometric' | null): void;
|
|
66
68
|
/** Runs when this entity's Hitbox overlaps another one's. */
|
|
67
69
|
onCollide?(other: Entity): void;
|
|
68
70
|
/** Runs when this entity's DynamicBody physically contacts a Solid. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component } from '../component.js';
|
|
2
2
|
import { type ClipDef } from '../animation/clip-player.js';
|
|
3
3
|
import { type SheetCell, type SheetDef } from '../animation/sheet.js';
|
|
4
|
+
import type { YSortParticipant } from '../render-sort.js';
|
|
4
5
|
/**
|
|
5
6
|
* Sprite animated from one or more spritesheets. The main sheet is the
|
|
6
7
|
* top-level texture/cols/rows (or explicit cells); extraSheets append after
|
|
@@ -10,7 +11,7 @@ import { type SheetCell, type SheetDef } from '../animation/sheet.js';
|
|
|
10
11
|
* so the quad rescales per frame, anchored bottom-center — width/height size
|
|
11
12
|
* the sheet's largest frame and smaller ones keep their feet planted.
|
|
12
13
|
*/
|
|
13
|
-
export declare class AnimatedSprite extends Component {
|
|
14
|
+
export declare class AnimatedSprite extends Component implements YSortParticipant {
|
|
14
15
|
static componentName: string;
|
|
15
16
|
static updateAfter: readonly string[];
|
|
16
17
|
static params: {
|
|
@@ -20,6 +21,18 @@ export declare class AnimatedSprite extends Component {
|
|
|
20
21
|
offsetY: {
|
|
21
22
|
label: string;
|
|
22
23
|
};
|
|
24
|
+
anchorX: {
|
|
25
|
+
label: string;
|
|
26
|
+
min: number;
|
|
27
|
+
max: number;
|
|
28
|
+
step: number;
|
|
29
|
+
};
|
|
30
|
+
anchorY: {
|
|
31
|
+
label: string;
|
|
32
|
+
min: number;
|
|
33
|
+
max: number;
|
|
34
|
+
step: number;
|
|
35
|
+
};
|
|
23
36
|
layer: {
|
|
24
37
|
label: string;
|
|
25
38
|
min: number;
|
|
@@ -55,10 +68,22 @@ export declare class AnimatedSprite extends Component {
|
|
|
55
68
|
set offsetX(value: number);
|
|
56
69
|
get offsetY(): number;
|
|
57
70
|
set offsetY(value: number);
|
|
71
|
+
private _anchorX;
|
|
72
|
+
private _anchorY;
|
|
73
|
+
get anchorX(): number;
|
|
74
|
+
set anchorX(value: number);
|
|
75
|
+
get anchorY(): number;
|
|
76
|
+
set anchorY(value: number);
|
|
58
77
|
pixelArt: boolean;
|
|
59
78
|
private _layer;
|
|
60
79
|
get layer(): number;
|
|
61
80
|
set layer(value: number);
|
|
81
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
82
|
+
setSortZ(z: number): void;
|
|
83
|
+
/** Mirrored state, readable by Runtime Snapshots; write via setFlipX. */
|
|
84
|
+
flipX: boolean;
|
|
85
|
+
/** Mirrors the quad horizontally — directional clips reuse east art for west. */
|
|
86
|
+
setFlipX(value: boolean): void;
|
|
62
87
|
clips: Record<string, ClipDef>;
|
|
63
88
|
initialClip?: string;
|
|
64
89
|
/** Clip currently playing. */
|
|
@@ -76,7 +101,7 @@ export declare class AnimatedSprite extends Component {
|
|
|
76
101
|
onUpdate(dt: number): void;
|
|
77
102
|
onDestroy(): void;
|
|
78
103
|
private showFrame;
|
|
79
|
-
/** Repositions/rescales the
|
|
104
|
+
/** Repositions/rescales the displayed frame inside its anchored full-size box. */
|
|
80
105
|
private syncQuad;
|
|
81
106
|
private applyFrame;
|
|
82
107
|
}
|
|
@@ -2,7 +2,9 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { Component } from '../component.js';
|
|
3
3
|
import { ClipPlayer } from '../animation/clip-player.js';
|
|
4
4
|
import { locateFrame, sheetCell } from '../animation/sheet.js';
|
|
5
|
+
import { spritePlacement } from '../sprite-placement.js';
|
|
5
6
|
const loader = new THREE.TextureLoader();
|
|
7
|
+
const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
6
8
|
/**
|
|
7
9
|
* Sprite animated from one or more spritesheets. The main sheet is the
|
|
8
10
|
* top-level texture/cols/rows (or explicit cells); extraSheets append after
|
|
@@ -18,10 +20,13 @@ export class AnimatedSprite extends Component {
|
|
|
18
20
|
static params = {
|
|
19
21
|
offsetX: { label: 'x offset' },
|
|
20
22
|
offsetY: { label: 'y offset' },
|
|
23
|
+
anchorX: { label: 'x anchor', min: 0, max: 1, step: 0.25 },
|
|
24
|
+
anchorY: { label: 'y anchor', min: 0, max: 1, step: 0.25 },
|
|
21
25
|
layer: { label: 'layer', min: -5, max: 5, step: 1 },
|
|
22
26
|
};
|
|
23
27
|
static transient = [
|
|
24
28
|
'current',
|
|
29
|
+
'flipX',
|
|
25
30
|
'player',
|
|
26
31
|
'sheets',
|
|
27
32
|
'texs',
|
|
@@ -81,6 +86,22 @@ export class AnimatedSprite extends Component {
|
|
|
81
86
|
this._offsetY = value;
|
|
82
87
|
this.syncQuad();
|
|
83
88
|
}
|
|
89
|
+
_anchorX = 0.5;
|
|
90
|
+
_anchorY = 0.5;
|
|
91
|
+
get anchorX() {
|
|
92
|
+
return this._anchorX;
|
|
93
|
+
}
|
|
94
|
+
set anchorX(value) {
|
|
95
|
+
this._anchorX = clampAnchor(value);
|
|
96
|
+
this.syncQuad();
|
|
97
|
+
}
|
|
98
|
+
get anchorY() {
|
|
99
|
+
return this._anchorY;
|
|
100
|
+
}
|
|
101
|
+
set anchorY(value) {
|
|
102
|
+
this._anchorY = clampAnchor(value);
|
|
103
|
+
this.syncQuad();
|
|
104
|
+
}
|
|
84
105
|
pixelArt = true;
|
|
85
106
|
// Draw order among sprites: higher layers render in front. Same-layer
|
|
86
107
|
// sprites fall back to spawn order, so give overlap an explicit layer.
|
|
@@ -93,6 +114,20 @@ export class AnimatedSprite extends Component {
|
|
|
93
114
|
if (this.mesh)
|
|
94
115
|
this.mesh.position.z = value * 0.01;
|
|
95
116
|
}
|
|
117
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
118
|
+
setSortZ(z) {
|
|
119
|
+
if (this.mesh)
|
|
120
|
+
this.mesh.position.z = z;
|
|
121
|
+
}
|
|
122
|
+
/** Mirrored state, readable by Runtime Snapshots; write via setFlipX. */
|
|
123
|
+
flipX = false;
|
|
124
|
+
/** Mirrors the quad horizontally — directional clips reuse east art for west. */
|
|
125
|
+
setFlipX(value) {
|
|
126
|
+
if (this.flipX === value)
|
|
127
|
+
return;
|
|
128
|
+
this.flipX = value;
|
|
129
|
+
this.syncQuad();
|
|
130
|
+
}
|
|
96
131
|
clips = {};
|
|
97
132
|
initialClip;
|
|
98
133
|
/** Clip currently playing. */
|
|
@@ -167,14 +202,24 @@ export class AnimatedSprite extends Component {
|
|
|
167
202
|
this.frame = index;
|
|
168
203
|
this.applyFrame();
|
|
169
204
|
}
|
|
170
|
-
/** Repositions/rescales the
|
|
205
|
+
/** Repositions/rescales the displayed frame inside its anchored full-size box. */
|
|
171
206
|
syncQuad() {
|
|
172
207
|
if (!this.mesh)
|
|
173
208
|
return;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
const placement = spritePlacement({
|
|
210
|
+
width: this.width,
|
|
211
|
+
height: this.height,
|
|
212
|
+
offsetX: this.offsetX,
|
|
213
|
+
offsetY: this.offsetY,
|
|
214
|
+
anchorX: this.anchorX,
|
|
215
|
+
anchorY: this.anchorY,
|
|
216
|
+
flipX: this.flipX,
|
|
217
|
+
frameScaleX: this.frameScaleX,
|
|
218
|
+
frameScaleY: this.frameScaleY,
|
|
219
|
+
});
|
|
220
|
+
this.mesh.position.x = placement.x;
|
|
221
|
+
this.mesh.position.y = placement.y;
|
|
222
|
+
this.mesh.scale.set(placement.scaleX, placement.scaleY, 1);
|
|
178
223
|
}
|
|
179
224
|
applyFrame() {
|
|
180
225
|
const located = locateFrame(this.sheets, this.frame);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { COLLISION_SHAPES, collisionBounds, collisionOverlap, resolveCollisionPoints, } from '../collision-shape.js';
|
|
2
2
|
import { Component } from '../component.js';
|
|
3
|
+
import { sceneSolids } from '../scene-solids.js';
|
|
3
4
|
import { resolveSolidAxis } from '../solid-axis.js';
|
|
4
5
|
import { Solid } from './solid.js';
|
|
5
6
|
/** Stable tie order for equal-distance initial-overlap recovery. */
|
|
@@ -161,10 +162,7 @@ export class DynamicBody extends Component {
|
|
|
161
162
|
return { entity: solid.entity, solid, axis, normal };
|
|
162
163
|
}
|
|
163
164
|
solids() {
|
|
164
|
-
return this.game.
|
|
165
|
-
.filter((entity) => entity !== this.entity)
|
|
166
|
-
.map((entity) => entity.get(Solid))
|
|
167
|
-
.filter((solid) => solid !== undefined);
|
|
165
|
+
return sceneSolids(this.game, this.entity);
|
|
168
166
|
}
|
|
169
167
|
collisionBody() {
|
|
170
168
|
return {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Component } from '../component.js';
|
|
2
|
+
import type { YSortParticipant } from '../render-sort.js';
|
|
2
3
|
export type SpriteShape = 'rectangle' | 'circle';
|
|
3
4
|
/**
|
|
4
5
|
* Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
|
|
5
6
|
* plane in front of the orthographic camera (see DESIGN.md §6, decision 2).
|
|
6
7
|
*/
|
|
7
|
-
export declare class Sprite extends Component {
|
|
8
|
+
export declare class Sprite extends Component implements YSortParticipant {
|
|
8
9
|
static componentName: string;
|
|
9
10
|
static params: {
|
|
10
11
|
offsetX: {
|
|
@@ -13,6 +14,18 @@ export declare class Sprite extends Component {
|
|
|
13
14
|
offsetY: {
|
|
14
15
|
label: string;
|
|
15
16
|
};
|
|
17
|
+
anchorX: {
|
|
18
|
+
label: string;
|
|
19
|
+
min: number;
|
|
20
|
+
max: number;
|
|
21
|
+
step: number;
|
|
22
|
+
};
|
|
23
|
+
anchorY: {
|
|
24
|
+
label: string;
|
|
25
|
+
min: number;
|
|
26
|
+
max: number;
|
|
27
|
+
step: number;
|
|
28
|
+
};
|
|
16
29
|
layer: {
|
|
17
30
|
label: string;
|
|
18
31
|
min: number;
|
|
@@ -36,17 +49,26 @@ export declare class Sprite extends Component {
|
|
|
36
49
|
set offsetX(value: number);
|
|
37
50
|
get offsetY(): number;
|
|
38
51
|
set offsetY(value: number);
|
|
52
|
+
private _anchorX;
|
|
53
|
+
private _anchorY;
|
|
54
|
+
get anchorX(): number;
|
|
55
|
+
set anchorX(value: number);
|
|
56
|
+
get anchorY(): number;
|
|
57
|
+
set anchorY(value: number);
|
|
39
58
|
/** Optional texture URL; with pixelArt on it filters in nearest. */
|
|
40
59
|
texture?: string;
|
|
41
60
|
pixelArt: boolean;
|
|
42
61
|
private _layer;
|
|
43
62
|
get layer(): number;
|
|
44
63
|
set layer(value: number);
|
|
64
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
65
|
+
setSortZ(z: number): void;
|
|
45
66
|
private _shape;
|
|
46
67
|
get shape(): SpriteShape;
|
|
47
68
|
set shape(value: SpriteShape);
|
|
48
69
|
private mesh?;
|
|
49
70
|
onReady(): void;
|
|
50
71
|
onDestroy(): void;
|
|
72
|
+
private syncQuad;
|
|
51
73
|
private createGeometry;
|
|
52
74
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { Component } from '../component.js';
|
|
3
|
+
import { spritePlacement } from '../sprite-placement.js';
|
|
3
4
|
const loader = new THREE.TextureLoader();
|
|
5
|
+
const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
4
6
|
/**
|
|
5
7
|
* Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
|
|
6
8
|
* plane in front of the orthographic camera (see DESIGN.md §6, decision 2).
|
|
@@ -10,6 +12,8 @@ export class Sprite extends Component {
|
|
|
10
12
|
static params = {
|
|
11
13
|
offsetX: { label: 'x offset' },
|
|
12
14
|
offsetY: { label: 'y offset' },
|
|
15
|
+
anchorX: { label: 'x anchor', min: 0, max: 1, step: 0.25 },
|
|
16
|
+
anchorY: { label: 'y anchor', min: 0, max: 1, step: 0.25 },
|
|
13
17
|
layer: { label: 'layer', min: -5, max: 5, step: 1 },
|
|
14
18
|
};
|
|
15
19
|
static transient = ['mesh'];
|
|
@@ -22,14 +26,14 @@ export class Sprite extends Component {
|
|
|
22
26
|
}
|
|
23
27
|
set width(value) {
|
|
24
28
|
this._width = value;
|
|
25
|
-
this.
|
|
29
|
+
this.syncQuad();
|
|
26
30
|
}
|
|
27
31
|
get height() {
|
|
28
32
|
return this._height;
|
|
29
33
|
}
|
|
30
34
|
set height(value) {
|
|
31
35
|
this._height = value;
|
|
32
|
-
this.
|
|
36
|
+
this.syncQuad();
|
|
33
37
|
}
|
|
34
38
|
_color = 0xffffff;
|
|
35
39
|
get color() {
|
|
@@ -46,16 +50,30 @@ export class Sprite extends Component {
|
|
|
46
50
|
}
|
|
47
51
|
set offsetX(value) {
|
|
48
52
|
this._offsetX = value;
|
|
49
|
-
|
|
50
|
-
this.mesh.position.x = value;
|
|
53
|
+
this.syncQuad();
|
|
51
54
|
}
|
|
52
55
|
get offsetY() {
|
|
53
56
|
return this._offsetY;
|
|
54
57
|
}
|
|
55
58
|
set offsetY(value) {
|
|
56
59
|
this._offsetY = value;
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
this.syncQuad();
|
|
61
|
+
}
|
|
62
|
+
_anchorX = 0.5;
|
|
63
|
+
_anchorY = 0.5;
|
|
64
|
+
get anchorX() {
|
|
65
|
+
return this._anchorX;
|
|
66
|
+
}
|
|
67
|
+
set anchorX(value) {
|
|
68
|
+
this._anchorX = clampAnchor(value);
|
|
69
|
+
this.syncQuad();
|
|
70
|
+
}
|
|
71
|
+
get anchorY() {
|
|
72
|
+
return this._anchorY;
|
|
73
|
+
}
|
|
74
|
+
set anchorY(value) {
|
|
75
|
+
this._anchorY = clampAnchor(value);
|
|
76
|
+
this.syncQuad();
|
|
59
77
|
}
|
|
60
78
|
/** Optional texture URL; with pixelArt on it filters in nearest. */
|
|
61
79
|
texture;
|
|
@@ -71,6 +89,11 @@ export class Sprite extends Component {
|
|
|
71
89
|
if (this.mesh)
|
|
72
90
|
this.mesh.position.z = value * 0.01;
|
|
73
91
|
}
|
|
92
|
+
/** Y-sort pass hook: overrides the layer-derived z for this frame. */
|
|
93
|
+
setSortZ(z) {
|
|
94
|
+
if (this.mesh)
|
|
95
|
+
this.mesh.position.z = z;
|
|
96
|
+
}
|
|
74
97
|
_shape = 'rectangle';
|
|
75
98
|
get shape() {
|
|
76
99
|
return this._shape;
|
|
@@ -96,8 +119,8 @@ export class Sprite extends Component {
|
|
|
96
119
|
material.color.set(0xffffff);
|
|
97
120
|
}
|
|
98
121
|
this.mesh = new THREE.Mesh(this.createGeometry(), material);
|
|
99
|
-
this.mesh.
|
|
100
|
-
this.
|
|
122
|
+
this.mesh.position.z = this.layer * 0.01;
|
|
123
|
+
this.syncQuad();
|
|
101
124
|
this.entity.node.add(this.mesh);
|
|
102
125
|
}
|
|
103
126
|
onDestroy() {
|
|
@@ -105,6 +128,24 @@ export class Sprite extends Component {
|
|
|
105
128
|
this.mesh?.geometry.dispose();
|
|
106
129
|
this.mesh?.material.dispose();
|
|
107
130
|
}
|
|
131
|
+
syncQuad() {
|
|
132
|
+
if (!this.mesh)
|
|
133
|
+
return;
|
|
134
|
+
const placement = spritePlacement({
|
|
135
|
+
width: this.width,
|
|
136
|
+
height: this.height,
|
|
137
|
+
offsetX: this.offsetX,
|
|
138
|
+
offsetY: this.offsetY,
|
|
139
|
+
anchorX: this.anchorX,
|
|
140
|
+
anchorY: this.anchorY,
|
|
141
|
+
flipX: false,
|
|
142
|
+
frameScaleX: 1,
|
|
143
|
+
frameScaleY: 1,
|
|
144
|
+
});
|
|
145
|
+
this.mesh.position.x = placement.x;
|
|
146
|
+
this.mesh.position.y = placement.y;
|
|
147
|
+
this.mesh.scale.set(placement.scaleX, placement.scaleY, 1);
|
|
148
|
+
}
|
|
108
149
|
createGeometry() {
|
|
109
150
|
return this._shape === 'circle'
|
|
110
151
|
? new THREE.CircleGeometry(0.5, 32)
|