@waica/engine 0.7.0 → 0.9.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/component.d.ts +2 -0
- package/dist/components/animated-sprite.d.ts +19 -1
- package/dist/components/animated-sprite.js +35 -7
- package/dist/components/dynamic-body.js +2 -4
- package/dist/components/sprite.d.ts +19 -0
- package/dist/components/sprite.js +44 -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 +4 -0
- package/dist/game.js +41 -9
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/projection.d.ts +14 -0
- package/dist/projection.js +30 -0
- package/dist/scene-solids.d.ts +16 -0
- package/dist/scene-solids.js +28 -0
- package/dist/scene.d.ts +3 -1
- package/dist/solid-axis.js +2 -4
- package/dist/sprite-placement.d.ts +22 -0
- package/dist/sprite-placement.js +14 -0
- package/dist/tilemap-grid.d.ts +26 -0
- package/dist/tilemap-grid.js +41 -0
- package/package.json +1 -1
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. */
|
|
@@ -21,6 +21,18 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
21
21
|
offsetY: {
|
|
22
22
|
label: string;
|
|
23
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
|
+
};
|
|
24
36
|
layer: {
|
|
25
37
|
label: string;
|
|
26
38
|
min: number;
|
|
@@ -56,6 +68,12 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
56
68
|
set offsetX(value: number);
|
|
57
69
|
get offsetY(): number;
|
|
58
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);
|
|
59
77
|
pixelArt: boolean;
|
|
60
78
|
private _layer;
|
|
61
79
|
get layer(): number;
|
|
@@ -83,7 +101,7 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
83
101
|
onUpdate(dt: number): void;
|
|
84
102
|
onDestroy(): void;
|
|
85
103
|
private showFrame;
|
|
86
|
-
/** Repositions/rescales the
|
|
104
|
+
/** Repositions/rescales the displayed frame inside its anchored full-size box. */
|
|
87
105
|
private syncQuad;
|
|
88
106
|
private applyFrame;
|
|
89
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,6 +20,8 @@ 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 = [
|
|
@@ -82,6 +86,22 @@ export class AnimatedSprite extends Component {
|
|
|
82
86
|
this._offsetY = value;
|
|
83
87
|
this.syncQuad();
|
|
84
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
|
+
}
|
|
85
105
|
pixelArt = true;
|
|
86
106
|
// Draw order among sprites: higher layers render in front. Same-layer
|
|
87
107
|
// sprites fall back to spawn order, so give overlap an explicit layer.
|
|
@@ -182,16 +202,24 @@ export class AnimatedSprite extends Component {
|
|
|
182
202
|
this.frame = index;
|
|
183
203
|
this.applyFrame();
|
|
184
204
|
}
|
|
185
|
-
/** Repositions/rescales the
|
|
205
|
+
/** Repositions/rescales the displayed frame inside its anchored full-size box. */
|
|
186
206
|
syncQuad() {
|
|
187
207
|
if (!this.mesh)
|
|
188
208
|
return;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|
|
195
223
|
}
|
|
196
224
|
applyFrame() {
|
|
197
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 {
|
|
@@ -14,6 +14,18 @@ export declare class Sprite extends Component implements YSortParticipant {
|
|
|
14
14
|
offsetY: {
|
|
15
15
|
label: string;
|
|
16
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
|
+
};
|
|
17
29
|
layer: {
|
|
18
30
|
label: string;
|
|
19
31
|
min: number;
|
|
@@ -37,6 +49,12 @@ export declare class Sprite extends Component implements YSortParticipant {
|
|
|
37
49
|
set offsetX(value: number);
|
|
38
50
|
get offsetY(): number;
|
|
39
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);
|
|
40
58
|
/** Optional texture URL; with pixelArt on it filters in nearest. */
|
|
41
59
|
texture?: string;
|
|
42
60
|
pixelArt: boolean;
|
|
@@ -51,5 +69,6 @@ export declare class Sprite extends Component implements YSortParticipant {
|
|
|
51
69
|
private mesh?;
|
|
52
70
|
onReady(): void;
|
|
53
71
|
onDestroy(): void;
|
|
72
|
+
private syncQuad;
|
|
54
73
|
private createGeometry;
|
|
55
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;
|
|
@@ -101,8 +119,8 @@ export class Sprite extends Component {
|
|
|
101
119
|
material.color.set(0xffffff);
|
|
102
120
|
}
|
|
103
121
|
this.mesh = new THREE.Mesh(this.createGeometry(), material);
|
|
104
|
-
this.mesh.
|
|
105
|
-
this.
|
|
122
|
+
this.mesh.position.z = this.layer * 0.01;
|
|
123
|
+
this.syncQuad();
|
|
106
124
|
this.entity.node.add(this.mesh);
|
|
107
125
|
}
|
|
108
126
|
onDestroy() {
|
|
@@ -110,6 +128,24 @@ export class Sprite extends Component {
|
|
|
110
128
|
this.mesh?.geometry.dispose();
|
|
111
129
|
this.mesh?.material.dispose();
|
|
112
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
|
+
}
|
|
113
149
|
createGeometry() {
|
|
114
150
|
return this._shape === 'circle'
|
|
115
151
|
? new THREE.CircleGeometry(0.5, 32)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Component } from '../component.js';
|
|
2
|
+
import { SOLID_SOURCE_SYMBOL, type SolidSource } from '../scene-solids.js';
|
|
3
|
+
import { type TilemapCell, type TilemapCellBounds } from '../tilemap-grid.js';
|
|
4
|
+
import { Solid } from './solid.js';
|
|
5
|
+
/** One authorable cell map rendered as a single merged geometry. */
|
|
6
|
+
export declare class Tilemap extends Component implements SolidSource {
|
|
7
|
+
static componentName: string;
|
|
8
|
+
static params: {
|
|
9
|
+
color: {
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
cols: {
|
|
13
|
+
label: string;
|
|
14
|
+
min: number;
|
|
15
|
+
step: number;
|
|
16
|
+
};
|
|
17
|
+
rows: {
|
|
18
|
+
label: string;
|
|
19
|
+
min: number;
|
|
20
|
+
step: number;
|
|
21
|
+
};
|
|
22
|
+
mapWidth: {
|
|
23
|
+
label: string;
|
|
24
|
+
min: number;
|
|
25
|
+
step: number;
|
|
26
|
+
};
|
|
27
|
+
mapHeight: {
|
|
28
|
+
label: string;
|
|
29
|
+
min: number;
|
|
30
|
+
step: number;
|
|
31
|
+
};
|
|
32
|
+
cellSize: {
|
|
33
|
+
label: string;
|
|
34
|
+
min: number;
|
|
35
|
+
step: number;
|
|
36
|
+
};
|
|
37
|
+
layer: {
|
|
38
|
+
label: string;
|
|
39
|
+
min: number;
|
|
40
|
+
max: number;
|
|
41
|
+
step: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
static transient: string[];
|
|
45
|
+
readonly [SOLID_SOURCE_SYMBOL] = true;
|
|
46
|
+
private _texture;
|
|
47
|
+
get texture(): string;
|
|
48
|
+
set texture(value: string);
|
|
49
|
+
private _color;
|
|
50
|
+
get color(): number;
|
|
51
|
+
set color(value: number);
|
|
52
|
+
private _cols;
|
|
53
|
+
get cols(): number;
|
|
54
|
+
set cols(value: number);
|
|
55
|
+
private _rows;
|
|
56
|
+
get rows(): number;
|
|
57
|
+
set rows(value: number);
|
|
58
|
+
private _gridOffsetX;
|
|
59
|
+
get gridOffsetX(): number;
|
|
60
|
+
set gridOffsetX(value: number);
|
|
61
|
+
private _gridOffsetY;
|
|
62
|
+
get gridOffsetY(): number;
|
|
63
|
+
set gridOffsetY(value: number);
|
|
64
|
+
private _spacingX;
|
|
65
|
+
get spacingX(): number;
|
|
66
|
+
set spacingX(value: number);
|
|
67
|
+
private _spacingY;
|
|
68
|
+
get spacingY(): number;
|
|
69
|
+
set spacingY(value: number);
|
|
70
|
+
private _cellWidth;
|
|
71
|
+
get cellWidth(): number;
|
|
72
|
+
set cellWidth(value: number);
|
|
73
|
+
private _cellHeight;
|
|
74
|
+
get cellHeight(): number;
|
|
75
|
+
set cellHeight(value: number);
|
|
76
|
+
private _pixelArt;
|
|
77
|
+
get pixelArt(): boolean;
|
|
78
|
+
set pixelArt(value: boolean);
|
|
79
|
+
private _mapWidth;
|
|
80
|
+
get mapWidth(): number;
|
|
81
|
+
set mapWidth(value: number);
|
|
82
|
+
private _mapHeight;
|
|
83
|
+
get mapHeight(): number;
|
|
84
|
+
set mapHeight(value: number);
|
|
85
|
+
private _cellSize;
|
|
86
|
+
get cellSize(): number;
|
|
87
|
+
set cellSize(value: number);
|
|
88
|
+
private _cells;
|
|
89
|
+
get cells(): number[];
|
|
90
|
+
set cells(value: number[]);
|
|
91
|
+
private _solidTiles;
|
|
92
|
+
get solidTiles(): number[];
|
|
93
|
+
set solidTiles(value: number[]);
|
|
94
|
+
private _layer;
|
|
95
|
+
get layer(): number;
|
|
96
|
+
set layer(value: number);
|
|
97
|
+
private mesh?;
|
|
98
|
+
private loadedTexture?;
|
|
99
|
+
private derivedSolids;
|
|
100
|
+
onReady(): void;
|
|
101
|
+
onProjectionChange(): void;
|
|
102
|
+
onDestroy(): void;
|
|
103
|
+
solids(): readonly Solid[];
|
|
104
|
+
cellIndex(column: number, row: number): number | null;
|
|
105
|
+
cellAt(logicalX: number, logicalY: number): TilemapCell | null;
|
|
106
|
+
cellBounds(column: number, row: number): TilemapCellBounds | null;
|
|
107
|
+
private gridSpec;
|
|
108
|
+
private rebuildMap;
|
|
109
|
+
private rebuildMaterial;
|
|
110
|
+
private makeMaterial;
|
|
111
|
+
private rebuildGeometry;
|
|
112
|
+
private rebuildSolids;
|
|
113
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { sheetCell } from '../animation/sheet.js';
|
|
3
|
+
import { Component } from '../component.js';
|
|
4
|
+
import { projectIsometric } from '../projection.js';
|
|
5
|
+
import { SOLID_SOURCE_SYMBOL } from '../scene-solids.js';
|
|
6
|
+
import { cellAt as gridCellAt, cellBounds as gridCellBounds, cellIndex as gridCellIndex, } from '../tilemap-grid.js';
|
|
7
|
+
import { Solid } from './solid.js';
|
|
8
|
+
const loader = new THREE.TextureLoader();
|
|
9
|
+
/** One authorable cell map rendered as a single merged geometry. */
|
|
10
|
+
export class Tilemap extends Component {
|
|
11
|
+
static componentName = 'Tilemap';
|
|
12
|
+
static params = {
|
|
13
|
+
color: { label: 'color' },
|
|
14
|
+
cols: { label: 'tileset columns', min: 1, step: 1 },
|
|
15
|
+
rows: { label: 'tileset rows', min: 1, step: 1 },
|
|
16
|
+
mapWidth: { label: 'map width', min: 1, step: 1 },
|
|
17
|
+
mapHeight: { label: 'map height', min: 1, step: 1 },
|
|
18
|
+
cellSize: { label: 'cell size', min: 0.05, step: 0.25 },
|
|
19
|
+
layer: { label: 'layer', min: -5, max: 5, step: 1 },
|
|
20
|
+
};
|
|
21
|
+
static transient = ['mesh', 'loadedTexture', 'derivedSolids'];
|
|
22
|
+
[SOLID_SOURCE_SYMBOL] = true;
|
|
23
|
+
_texture = '';
|
|
24
|
+
get texture() {
|
|
25
|
+
return this._texture;
|
|
26
|
+
}
|
|
27
|
+
set texture(value) {
|
|
28
|
+
this._texture = value;
|
|
29
|
+
this.rebuildMaterial();
|
|
30
|
+
}
|
|
31
|
+
_color = 0xffffff;
|
|
32
|
+
get color() {
|
|
33
|
+
return this._color;
|
|
34
|
+
}
|
|
35
|
+
set color(value) {
|
|
36
|
+
this._color = value;
|
|
37
|
+
this.rebuildMaterial();
|
|
38
|
+
}
|
|
39
|
+
_cols = 1;
|
|
40
|
+
get cols() {
|
|
41
|
+
return this._cols;
|
|
42
|
+
}
|
|
43
|
+
set cols(value) {
|
|
44
|
+
this._cols = value;
|
|
45
|
+
this.rebuildGeometry();
|
|
46
|
+
}
|
|
47
|
+
_rows = 1;
|
|
48
|
+
get rows() {
|
|
49
|
+
return this._rows;
|
|
50
|
+
}
|
|
51
|
+
set rows(value) {
|
|
52
|
+
this._rows = value;
|
|
53
|
+
this.rebuildGeometry();
|
|
54
|
+
}
|
|
55
|
+
_gridOffsetX = 0;
|
|
56
|
+
get gridOffsetX() {
|
|
57
|
+
return this._gridOffsetX;
|
|
58
|
+
}
|
|
59
|
+
set gridOffsetX(value) {
|
|
60
|
+
this._gridOffsetX = value;
|
|
61
|
+
this.rebuildGeometry();
|
|
62
|
+
}
|
|
63
|
+
_gridOffsetY = 0;
|
|
64
|
+
get gridOffsetY() {
|
|
65
|
+
return this._gridOffsetY;
|
|
66
|
+
}
|
|
67
|
+
set gridOffsetY(value) {
|
|
68
|
+
this._gridOffsetY = value;
|
|
69
|
+
this.rebuildGeometry();
|
|
70
|
+
}
|
|
71
|
+
_spacingX = 0;
|
|
72
|
+
get spacingX() {
|
|
73
|
+
return this._spacingX;
|
|
74
|
+
}
|
|
75
|
+
set spacingX(value) {
|
|
76
|
+
this._spacingX = value;
|
|
77
|
+
this.rebuildGeometry();
|
|
78
|
+
}
|
|
79
|
+
_spacingY = 0;
|
|
80
|
+
get spacingY() {
|
|
81
|
+
return this._spacingY;
|
|
82
|
+
}
|
|
83
|
+
set spacingY(value) {
|
|
84
|
+
this._spacingY = value;
|
|
85
|
+
this.rebuildGeometry();
|
|
86
|
+
}
|
|
87
|
+
_cellWidth = 0;
|
|
88
|
+
get cellWidth() {
|
|
89
|
+
return this._cellWidth;
|
|
90
|
+
}
|
|
91
|
+
set cellWidth(value) {
|
|
92
|
+
this._cellWidth = value;
|
|
93
|
+
this.rebuildGeometry();
|
|
94
|
+
}
|
|
95
|
+
_cellHeight = 0;
|
|
96
|
+
get cellHeight() {
|
|
97
|
+
return this._cellHeight;
|
|
98
|
+
}
|
|
99
|
+
set cellHeight(value) {
|
|
100
|
+
this._cellHeight = value;
|
|
101
|
+
this.rebuildGeometry();
|
|
102
|
+
}
|
|
103
|
+
_pixelArt = true;
|
|
104
|
+
get pixelArt() {
|
|
105
|
+
return this._pixelArt;
|
|
106
|
+
}
|
|
107
|
+
set pixelArt(value) {
|
|
108
|
+
this._pixelArt = value;
|
|
109
|
+
this.rebuildMaterial();
|
|
110
|
+
}
|
|
111
|
+
_mapWidth = 1;
|
|
112
|
+
get mapWidth() {
|
|
113
|
+
return this._mapWidth;
|
|
114
|
+
}
|
|
115
|
+
set mapWidth(value) {
|
|
116
|
+
this._mapWidth = value;
|
|
117
|
+
this.rebuildMap();
|
|
118
|
+
}
|
|
119
|
+
_mapHeight = 1;
|
|
120
|
+
get mapHeight() {
|
|
121
|
+
return this._mapHeight;
|
|
122
|
+
}
|
|
123
|
+
set mapHeight(value) {
|
|
124
|
+
this._mapHeight = value;
|
|
125
|
+
this.rebuildMap();
|
|
126
|
+
}
|
|
127
|
+
_cellSize = 1;
|
|
128
|
+
get cellSize() {
|
|
129
|
+
return this._cellSize;
|
|
130
|
+
}
|
|
131
|
+
set cellSize(value) {
|
|
132
|
+
this._cellSize = value;
|
|
133
|
+
this.rebuildMap();
|
|
134
|
+
}
|
|
135
|
+
_cells = [];
|
|
136
|
+
get cells() {
|
|
137
|
+
return this._cells;
|
|
138
|
+
}
|
|
139
|
+
set cells(value) {
|
|
140
|
+
this._cells = [...value];
|
|
141
|
+
this.rebuildMap();
|
|
142
|
+
}
|
|
143
|
+
_solidTiles = [];
|
|
144
|
+
get solidTiles() {
|
|
145
|
+
return this._solidTiles;
|
|
146
|
+
}
|
|
147
|
+
set solidTiles(value) {
|
|
148
|
+
this._solidTiles = [...value];
|
|
149
|
+
this.rebuildSolids();
|
|
150
|
+
}
|
|
151
|
+
_layer = 0;
|
|
152
|
+
get layer() {
|
|
153
|
+
return this._layer;
|
|
154
|
+
}
|
|
155
|
+
set layer(value) {
|
|
156
|
+
this._layer = value;
|
|
157
|
+
this.rebuildGeometry();
|
|
158
|
+
}
|
|
159
|
+
mesh;
|
|
160
|
+
loadedTexture;
|
|
161
|
+
derivedSolids = [];
|
|
162
|
+
onReady() {
|
|
163
|
+
this.mesh = new THREE.Mesh(new THREE.BufferGeometry(), this.makeMaterial());
|
|
164
|
+
this.entity.node.add(this.mesh);
|
|
165
|
+
this.rebuildMaterial();
|
|
166
|
+
this.rebuildMap();
|
|
167
|
+
}
|
|
168
|
+
onProjectionChange() {
|
|
169
|
+
this.rebuildGeometry();
|
|
170
|
+
}
|
|
171
|
+
onDestroy() {
|
|
172
|
+
this.mesh?.removeFromParent();
|
|
173
|
+
this.mesh?.geometry.dispose();
|
|
174
|
+
this.mesh?.material.dispose();
|
|
175
|
+
this.loadedTexture?.dispose();
|
|
176
|
+
this.loadedTexture = undefined;
|
|
177
|
+
this.derivedSolids = [];
|
|
178
|
+
}
|
|
179
|
+
solids() {
|
|
180
|
+
return this.derivedSolids;
|
|
181
|
+
}
|
|
182
|
+
cellIndex(column, row) {
|
|
183
|
+
return gridCellIndex(this.mapWidth, this.mapHeight, column, row);
|
|
184
|
+
}
|
|
185
|
+
cellAt(logicalX, logicalY) {
|
|
186
|
+
return gridCellAt(this.gridSpec(), logicalX, logicalY);
|
|
187
|
+
}
|
|
188
|
+
cellBounds(column, row) {
|
|
189
|
+
return gridCellBounds(this.gridSpec(), column, row);
|
|
190
|
+
}
|
|
191
|
+
gridSpec() {
|
|
192
|
+
return {
|
|
193
|
+
mapWidth: this.mapWidth,
|
|
194
|
+
mapHeight: this.mapHeight,
|
|
195
|
+
cellSize: this.cellSize,
|
|
196
|
+
originX: this.entity?.position.x ?? 0,
|
|
197
|
+
originY: this.entity?.position.y ?? 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
rebuildMap() {
|
|
201
|
+
this.rebuildGeometry();
|
|
202
|
+
this.rebuildSolids();
|
|
203
|
+
}
|
|
204
|
+
rebuildMaterial() {
|
|
205
|
+
const mesh = this.mesh;
|
|
206
|
+
if (!mesh)
|
|
207
|
+
return;
|
|
208
|
+
this.loadedTexture?.dispose();
|
|
209
|
+
this.loadedTexture = undefined;
|
|
210
|
+
mesh.material.dispose();
|
|
211
|
+
mesh.material = this.makeMaterial();
|
|
212
|
+
if (!this.texture)
|
|
213
|
+
return;
|
|
214
|
+
const requested = this.texture;
|
|
215
|
+
const texture = loader.load(requested, () => {
|
|
216
|
+
if (this.loadedTexture === texture && this.texture === requested)
|
|
217
|
+
this.rebuildGeometry();
|
|
218
|
+
});
|
|
219
|
+
if (this.pixelArt) {
|
|
220
|
+
texture.magFilter = THREE.NearestFilter;
|
|
221
|
+
texture.minFilter = THREE.NearestFilter;
|
|
222
|
+
}
|
|
223
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
224
|
+
this.loadedTexture = texture;
|
|
225
|
+
mesh.material.map = texture;
|
|
226
|
+
mesh.material.color.set(0xffffff);
|
|
227
|
+
mesh.material.needsUpdate = true;
|
|
228
|
+
}
|
|
229
|
+
makeMaterial() {
|
|
230
|
+
return new THREE.MeshBasicMaterial({
|
|
231
|
+
color: this.color,
|
|
232
|
+
transparent: true,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
rebuildGeometry() {
|
|
236
|
+
const mesh = this.mesh;
|
|
237
|
+
if (!mesh)
|
|
238
|
+
return;
|
|
239
|
+
const positions = [];
|
|
240
|
+
const uvs = [];
|
|
241
|
+
const indices = [];
|
|
242
|
+
const width = Math.max(0, Math.floor(this.mapWidth));
|
|
243
|
+
const height = Math.max(0, Math.floor(this.mapHeight));
|
|
244
|
+
const size = this.cellSize;
|
|
245
|
+
if (Number.isFinite(size) && size > 0) {
|
|
246
|
+
const image = this.loadedTexture?.image;
|
|
247
|
+
const imageWidth = image?.width && image.width > 0 ? image.width : Math.max(1, this.cols);
|
|
248
|
+
const imageHeight = image?.height && image.height > 0 ? image.height : Math.max(1, this.rows);
|
|
249
|
+
const frameParams = {
|
|
250
|
+
gridOffsetX: this.gridOffsetX,
|
|
251
|
+
gridOffsetY: this.gridOffsetY,
|
|
252
|
+
spacingX: this.spacingX,
|
|
253
|
+
spacingY: this.spacingY,
|
|
254
|
+
cellWidth: this.cellWidth,
|
|
255
|
+
cellHeight: this.cellHeight,
|
|
256
|
+
};
|
|
257
|
+
for (let index = 0; index < width * height; index++) {
|
|
258
|
+
const tile = this.cells[index] ?? -1;
|
|
259
|
+
if (!Number.isFinite(tile) || tile < 0)
|
|
260
|
+
continue;
|
|
261
|
+
const column = index % width;
|
|
262
|
+
const row = Math.floor(index / width);
|
|
263
|
+
const logicalCenterX = (column + 0.5) * size;
|
|
264
|
+
const logicalCenterY = (row + 0.5) * size;
|
|
265
|
+
const center = this.game.projection === 'isometric'
|
|
266
|
+
? projectIsometric(logicalCenterX, logicalCenterY)
|
|
267
|
+
: { x: logicalCenterX, y: logicalCenterY };
|
|
268
|
+
const halfWidth = this.game.projection === 'isometric' ? size : size / 2;
|
|
269
|
+
const halfHeight = size / 2;
|
|
270
|
+
const z = this.layer * 0.01;
|
|
271
|
+
positions.push(center.x - halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y + halfHeight, z, center.x - halfWidth, center.y + halfHeight, z);
|
|
272
|
+
const frame = sheetCell(imageWidth, imageHeight, this.cols, this.rows, tile, frameParams);
|
|
273
|
+
const left = frame.x / imageWidth;
|
|
274
|
+
const right = (frame.x + frame.width) / imageWidth;
|
|
275
|
+
const bottom = 1 - (frame.y + frame.height) / imageHeight;
|
|
276
|
+
const top = 1 - frame.y / imageHeight;
|
|
277
|
+
uvs.push(left, bottom, right, bottom, right, top, left, top);
|
|
278
|
+
const vertex = positions.length / 3 - 4;
|
|
279
|
+
indices.push(vertex, vertex + 1, vertex + 2, vertex, vertex + 2, vertex + 3);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const geometry = new THREE.BufferGeometry();
|
|
283
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
284
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
285
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
286
|
+
mesh.geometry.dispose();
|
|
287
|
+
mesh.geometry = geometry;
|
|
288
|
+
}
|
|
289
|
+
rebuildSolids() {
|
|
290
|
+
if (!this.entity || !this.game)
|
|
291
|
+
return;
|
|
292
|
+
const solids = new Set(this.solidTiles);
|
|
293
|
+
const width = Math.max(0, Math.floor(this.mapWidth));
|
|
294
|
+
const height = Math.max(0, Math.floor(this.mapHeight));
|
|
295
|
+
const next = [];
|
|
296
|
+
for (let index = 0; index < width * height; index++) {
|
|
297
|
+
const tile = this.cells[index] ?? -1;
|
|
298
|
+
if (!solids.has(tile))
|
|
299
|
+
continue;
|
|
300
|
+
const column = index % width;
|
|
301
|
+
const row = Math.floor(index / width);
|
|
302
|
+
const bounds = this.cellBounds(column, row);
|
|
303
|
+
if (!bounds)
|
|
304
|
+
continue;
|
|
305
|
+
const solid = new Solid();
|
|
306
|
+
solid.entity = this.entity;
|
|
307
|
+
solid.game = this.game;
|
|
308
|
+
solid.width = this.cellSize;
|
|
309
|
+
solid.height = this.cellSize;
|
|
310
|
+
solid.offsetX = bounds.centerX - this.entity.position.x;
|
|
311
|
+
solid.offsetY = bounds.centerY - this.entity.position.y;
|
|
312
|
+
next.push(solid);
|
|
313
|
+
}
|
|
314
|
+
this.derivedSolids = next;
|
|
315
|
+
}
|
|
316
|
+
}
|
package/dist/entity.d.ts
CHANGED
|
@@ -10,10 +10,13 @@ export declare class Entity {
|
|
|
10
10
|
readonly name: string;
|
|
11
11
|
readonly node: THREE.Group<THREE.Object3DEventMap>;
|
|
12
12
|
readonly components: Component[];
|
|
13
|
+
private logicalPosition;
|
|
13
14
|
private destroyed;
|
|
14
15
|
get alive(): boolean;
|
|
15
16
|
constructor(game: Game, name: string);
|
|
16
17
|
get position(): THREE.Vector3;
|
|
18
|
+
/** Keeps identity scenes zero-copy while projected scenes own a logical transform. */
|
|
19
|
+
setProjected(projected: boolean): void;
|
|
17
20
|
get scale(): THREE.Vector3;
|
|
18
21
|
add<T extends Component>(Class: ComponentClass<T>, props?: Partial<T>): T;
|
|
19
22
|
get<T extends Component>(Class: ComponentClass<T>): T | undefined;
|
package/dist/entity.js
CHANGED
|
@@ -8,6 +8,7 @@ export class Entity {
|
|
|
8
8
|
name;
|
|
9
9
|
node = new THREE.Group();
|
|
10
10
|
components = [];
|
|
11
|
+
logicalPosition = null;
|
|
11
12
|
destroyed = false;
|
|
12
13
|
get alive() {
|
|
13
14
|
return !this.destroyed;
|
|
@@ -17,7 +18,18 @@ export class Entity {
|
|
|
17
18
|
this.name = name;
|
|
18
19
|
}
|
|
19
20
|
get position() {
|
|
20
|
-
return this.node.position;
|
|
21
|
+
return this.logicalPosition ?? this.node.position;
|
|
22
|
+
}
|
|
23
|
+
/** Keeps identity scenes zero-copy while projected scenes own a logical transform. */
|
|
24
|
+
setProjected(projected) {
|
|
25
|
+
if (projected === (this.logicalPosition !== null))
|
|
26
|
+
return;
|
|
27
|
+
if (projected) {
|
|
28
|
+
this.logicalPosition = this.node.position.clone();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.node.position.copy(this.logicalPosition);
|
|
32
|
+
this.logicalPosition = null;
|
|
21
33
|
}
|
|
22
34
|
get scale() {
|
|
23
35
|
return this.node.scale;
|
package/dist/game.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare class Game {
|
|
|
62
62
|
private viewHeight;
|
|
63
63
|
private sceneCamera;
|
|
64
64
|
private renderSort;
|
|
65
|
+
private sceneProjection;
|
|
65
66
|
private lastTime;
|
|
66
67
|
private runtimeBridge;
|
|
67
68
|
constructor(options: GameOptions);
|
|
@@ -91,6 +92,8 @@ export declare class Game {
|
|
|
91
92
|
stop(): void;
|
|
92
93
|
/** Internal: called by Entity.destroy(). */
|
|
93
94
|
removeEntity(entity: Entity): void;
|
|
95
|
+
/** The scene's render projection; null keeps logical and render space identical. */
|
|
96
|
+
get projection(): 'isometric' | null;
|
|
94
97
|
/** Visible world height (2D camera zoom). */
|
|
95
98
|
get view(): number;
|
|
96
99
|
setViewHeight(value: number): void;
|
|
@@ -105,6 +108,7 @@ export declare class Game {
|
|
|
105
108
|
private renderSurface;
|
|
106
109
|
private componentUpdateSchedule;
|
|
107
110
|
private updateSceneCamera;
|
|
111
|
+
private renderPoint;
|
|
108
112
|
private dispatchCollisions;
|
|
109
113
|
private resize;
|
|
110
114
|
}
|
package/dist/game.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Emitter } from './events.js';
|
|
|
8
8
|
import { Input } from './input.js';
|
|
9
9
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
10
10
|
import { RuntimeInspector } from './runtime-inspection.js';
|
|
11
|
+
import { projectIsometric } from './projection.js';
|
|
11
12
|
import { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
12
13
|
import { registryEntry, spawnFromJson } from './scene.js';
|
|
13
14
|
import { Stats } from './stats.js';
|
|
@@ -42,6 +43,7 @@ export class Game {
|
|
|
42
43
|
viewHeight;
|
|
43
44
|
sceneCamera = null;
|
|
44
45
|
renderSort = null;
|
|
46
|
+
sceneProjection = null;
|
|
45
47
|
lastTime = 0;
|
|
46
48
|
runtimeBridge = null;
|
|
47
49
|
constructor(options) {
|
|
@@ -63,6 +65,7 @@ export class Game {
|
|
|
63
65
|
/** Creates a live entity in the scene. */
|
|
64
66
|
spawn(name) {
|
|
65
67
|
const entity = new Entity(this, name);
|
|
68
|
+
entity.setProjected(this.sceneProjection === 'isometric');
|
|
66
69
|
this.entities.push(entity);
|
|
67
70
|
this.scene.add(entity.node);
|
|
68
71
|
return entity;
|
|
@@ -115,6 +118,15 @@ export class Game {
|
|
|
115
118
|
*/
|
|
116
119
|
setSceneRender(json) {
|
|
117
120
|
this.renderSort = json?.sort === 'y' ? 'y' : null;
|
|
121
|
+
const projection = json?.projection === 'isometric' ? 'isometric' : null;
|
|
122
|
+
if (projection === this.sceneProjection)
|
|
123
|
+
return;
|
|
124
|
+
this.sceneProjection = projection;
|
|
125
|
+
for (const entity of this.entities) {
|
|
126
|
+
entity.setProjected(projection === 'isometric');
|
|
127
|
+
for (const component of entity.components)
|
|
128
|
+
component.onProjectionChange?.(projection);
|
|
129
|
+
}
|
|
118
130
|
}
|
|
119
131
|
/**
|
|
120
132
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
@@ -130,11 +142,11 @@ export class Game {
|
|
|
130
142
|
// With a follow target the declared position is moot: start centered on
|
|
131
143
|
// the target so play begins framed like the editor shows it.
|
|
132
144
|
const followed = this.sceneCamera.follow ? this.find(this.sceneCamera.follow) : undefined;
|
|
133
|
-
const
|
|
134
|
-
?
|
|
135
|
-
: this.sceneCamera.position;
|
|
136
|
-
this.camera.position.x = x;
|
|
137
|
-
this.camera.position.y = y;
|
|
145
|
+
const center = followed
|
|
146
|
+
? this.renderPoint(followed.position.x, followed.position.y)
|
|
147
|
+
: { x: this.sceneCamera.position[0], y: this.sceneCamera.position[1] };
|
|
148
|
+
this.camera.position.x = center.x;
|
|
149
|
+
this.camera.position.y = center.y;
|
|
138
150
|
this.setViewHeight(this.sceneCamera.zoom);
|
|
139
151
|
}
|
|
140
152
|
start() {
|
|
@@ -168,6 +180,10 @@ export class Game {
|
|
|
168
180
|
if (i !== -1)
|
|
169
181
|
this.entities.splice(i, 1);
|
|
170
182
|
}
|
|
183
|
+
/** The scene's render projection; null keeps logical and render space identical. */
|
|
184
|
+
get projection() {
|
|
185
|
+
return this.sceneProjection;
|
|
186
|
+
}
|
|
171
187
|
/** Visible world height (2D camera zoom). */
|
|
172
188
|
get view() {
|
|
173
189
|
return this.viewHeight;
|
|
@@ -233,7 +249,7 @@ export class Game {
|
|
|
233
249
|
for (const component of entity.components) {
|
|
234
250
|
if (isYSortParticipant(component)) {
|
|
235
251
|
participants.push(component);
|
|
236
|
-
entries.push({ layer: component.layer, y: entity.position.y });
|
|
252
|
+
entries.push({ layer: component.layer, y: entity.node.position.y });
|
|
237
253
|
}
|
|
238
254
|
}
|
|
239
255
|
}
|
|
@@ -242,6 +258,13 @@ export class Game {
|
|
|
242
258
|
participant.setSortZ(z[index]);
|
|
243
259
|
}
|
|
244
260
|
renderSurface() {
|
|
261
|
+
if (this.sceneProjection === 'isometric') {
|
|
262
|
+
for (const entity of this.entities) {
|
|
263
|
+
const projected = projectIsometric(entity.position.x, entity.position.y);
|
|
264
|
+
entity.node.position.x = projected.x;
|
|
265
|
+
entity.node.position.y = projected.y;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
245
268
|
if (this.renderSort === 'y')
|
|
246
269
|
this.applyYSort();
|
|
247
270
|
this.ui.setActive(this.simulate);
|
|
@@ -291,19 +314,28 @@ export class Game {
|
|
|
291
314
|
const followed = cam.follow ? this.find(cam.follow) : undefined;
|
|
292
315
|
const provider = followed?.components.find((c) => isCameraVelocityProvider(c));
|
|
293
316
|
const velocity = provider?.getCameraVelocity();
|
|
317
|
+
const target = followed
|
|
318
|
+
? this.renderPoint(followed.position.x, followed.position.y)
|
|
319
|
+
: null;
|
|
320
|
+
const renderVelocity = velocity
|
|
321
|
+
? this.renderPoint(velocity.vx, velocity.vy)
|
|
322
|
+
: { x: 0, y: 0 };
|
|
294
323
|
const next = stepSceneCamera(cam, {
|
|
295
324
|
x: this.camera.position.x,
|
|
296
325
|
y: this.camera.position.y,
|
|
297
326
|
halfW: (this.camera.right - this.camera.left) / 2,
|
|
298
327
|
halfH: this.viewHeight / 2,
|
|
299
|
-
target
|
|
300
|
-
vx:
|
|
301
|
-
vy:
|
|
328
|
+
target,
|
|
329
|
+
vx: renderVelocity.x,
|
|
330
|
+
vy: renderVelocity.y,
|
|
302
331
|
dt,
|
|
303
332
|
});
|
|
304
333
|
this.camera.position.x = next.x;
|
|
305
334
|
this.camera.position.y = next.y;
|
|
306
335
|
}
|
|
336
|
+
renderPoint(x, y) {
|
|
337
|
+
return this.sceneProjection === 'isometric' ? projectIsometric(x, y) : { x, y };
|
|
338
|
+
}
|
|
307
339
|
dispatchCollisions() {
|
|
308
340
|
const boxed = this.entities.filter((e) => e.has(Hitbox));
|
|
309
341
|
for (let i = 0; i < boxed.length; i++) {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export { installDirectionalAnimation, installedDirectionalAnimation, isAnimation
|
|
|
4
4
|
export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
|
|
5
5
|
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
6
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';
|
|
7
11
|
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
8
12
|
export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
9
13
|
export { Entity } from './entity.js';
|
|
@@ -29,9 +33,14 @@ export { Solid } from './components/solid.js';
|
|
|
29
33
|
export { Hitbox } from './components/hitbox.js';
|
|
30
34
|
export { DynamicBody } from './components/dynamic-body.js';
|
|
31
35
|
export { AnimatedSprite } from './components/animated-sprite.js';
|
|
36
|
+
export { Tilemap } from './components/tilemap.js';
|
|
32
37
|
export { aabbOverlap } from './aabb.js';
|
|
33
38
|
export { resolveSolidAxis } from './solid-axis.js';
|
|
34
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';
|
|
35
44
|
export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
|
|
36
45
|
export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
|
|
37
46
|
export { Emitter } from './events.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
2
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
3
3
|
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
4
|
+
export { projectIsometric, screenInputToLogical, unprojectIsometric } from './projection.js';
|
|
5
|
+
export { spritePlacement } from './sprite-placement.js';
|
|
4
6
|
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
5
7
|
export { Entity } from './entity.js';
|
|
6
8
|
export { Component } from './component.js';
|
|
@@ -17,8 +19,11 @@ export { Solid } from './components/solid.js';
|
|
|
17
19
|
export { Hitbox } from './components/hitbox.js';
|
|
18
20
|
export { DynamicBody } from './components/dynamic-body.js';
|
|
19
21
|
export { AnimatedSprite } from './components/animated-sprite.js';
|
|
22
|
+
export { Tilemap } from './components/tilemap.js';
|
|
20
23
|
export { aabbOverlap } from './aabb.js';
|
|
21
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';
|
|
22
27
|
export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
|
|
23
28
|
export { Emitter } from './events.js';
|
|
24
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,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
|
@@ -31,10 +31,12 @@ export interface PrefabJson {
|
|
|
31
31
|
/** Scene-wide render options (v3). */
|
|
32
32
|
export interface SceneRenderJson {
|
|
33
33
|
/**
|
|
34
|
-
* 'y' orders same-layer sprites by their
|
|
34
|
+
* 'y' orders same-layer sprites by their render-space Y — lower Y renders
|
|
35
35
|
* in front (top-down depth). Absent: spawn order breaks same-layer ties.
|
|
36
36
|
*/
|
|
37
37
|
sort?: 'y';
|
|
38
|
+
/** Logical positions are projected to the fixed 2:1 render lattice. */
|
|
39
|
+
projection?: 'isometric';
|
|
38
40
|
}
|
|
39
41
|
export interface SceneJson {
|
|
40
42
|
waicaScene: 1 | 2 | 3;
|
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
|
+
}
|
|
@@ -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
|
+
}
|