@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,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
|
@@ -4,7 +4,7 @@ import type { Component } from './component.js';
|
|
|
4
4
|
import { Entity } from './entity.js';
|
|
5
5
|
import { Emitter } from './events.js';
|
|
6
6
|
import { Input, type InputBindings } from './input.js';
|
|
7
|
-
import { type SceneRegistry } from './scene.js';
|
|
7
|
+
import { type SceneRegistry, type SceneRenderJson } from './scene.js';
|
|
8
8
|
import { Stats, type StatValue } from './stats.js';
|
|
9
9
|
import { GameUi } from './ui.js';
|
|
10
10
|
/** Fixed game resolution: the view keeps this aspect, letterboxed. */
|
|
@@ -61,6 +61,8 @@ export declare class Game {
|
|
|
61
61
|
private readonly resolution;
|
|
62
62
|
private viewHeight;
|
|
63
63
|
private sceneCamera;
|
|
64
|
+
private renderSort;
|
|
65
|
+
private sceneProjection;
|
|
64
66
|
private lastTime;
|
|
65
67
|
private runtimeBridge;
|
|
66
68
|
constructor(options: GameOptions);
|
|
@@ -76,6 +78,11 @@ export declare class Game {
|
|
|
76
78
|
applyParamOverrides(entity: Entity, component: Component): void;
|
|
77
79
|
/** Registers a function that runs once per frame. Returns the unsubscribe. */
|
|
78
80
|
onUpdate(fn: UpdateFn): () => void;
|
|
81
|
+
/**
|
|
82
|
+
* Adopts a scene's render block. Called by loadScene; without a block the
|
|
83
|
+
* draw order stays layer-banded with spawn-order ties.
|
|
84
|
+
*/
|
|
85
|
+
setSceneRender(json?: SceneRenderJson): void;
|
|
79
86
|
/**
|
|
80
87
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
81
88
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
@@ -85,6 +92,8 @@ export declare class Game {
|
|
|
85
92
|
stop(): void;
|
|
86
93
|
/** Internal: called by Entity.destroy(). */
|
|
87
94
|
removeEntity(entity: Entity): void;
|
|
95
|
+
/** The scene's render projection; null keeps logical and render space identical. */
|
|
96
|
+
get projection(): 'isometric' | null;
|
|
88
97
|
/** Visible world height (2D camera zoom). */
|
|
89
98
|
get view(): number;
|
|
90
99
|
setViewHeight(value: number): void;
|
|
@@ -94,9 +103,12 @@ export declare class Game {
|
|
|
94
103
|
private tick;
|
|
95
104
|
private runFrame;
|
|
96
105
|
private unregisterRuntimeBridge;
|
|
106
|
+
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
107
|
+
private applyYSort;
|
|
97
108
|
private renderSurface;
|
|
98
109
|
private componentUpdateSchedule;
|
|
99
110
|
private updateSceneCamera;
|
|
111
|
+
private renderPoint;
|
|
100
112
|
private dispatchCollisions;
|
|
101
113
|
private resize;
|
|
102
114
|
}
|
package/dist/game.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { collisionOverlap } from './collision-shape.js';
|
|
3
|
-
import { resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
3
|
+
import { isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera, } from './camera.js';
|
|
4
4
|
import { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
5
5
|
import { Hitbox } from './components/hitbox.js';
|
|
6
6
|
import { Entity } from './entity.js';
|
|
@@ -8,6 +8,8 @@ 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';
|
|
12
|
+
import { isYSortParticipant, ySortZ } from './render-sort.js';
|
|
11
13
|
import { registryEntry, spawnFromJson } from './scene.js';
|
|
12
14
|
import { Stats } from './stats.js';
|
|
13
15
|
import { GameUi } from './ui.js';
|
|
@@ -40,6 +42,8 @@ export class Game {
|
|
|
40
42
|
resolution;
|
|
41
43
|
viewHeight;
|
|
42
44
|
sceneCamera = null;
|
|
45
|
+
renderSort = null;
|
|
46
|
+
sceneProjection = null;
|
|
43
47
|
lastTime = 0;
|
|
44
48
|
runtimeBridge = null;
|
|
45
49
|
constructor(options) {
|
|
@@ -61,6 +65,7 @@ export class Game {
|
|
|
61
65
|
/** Creates a live entity in the scene. */
|
|
62
66
|
spawn(name) {
|
|
63
67
|
const entity = new Entity(this, name);
|
|
68
|
+
entity.setProjected(this.sceneProjection === 'isometric');
|
|
64
69
|
this.entities.push(entity);
|
|
65
70
|
this.scene.add(entity.node);
|
|
66
71
|
return entity;
|
|
@@ -107,6 +112,22 @@ export class Game {
|
|
|
107
112
|
this.updateFns.add(fn);
|
|
108
113
|
return () => this.updateFns.delete(fn);
|
|
109
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Adopts a scene's render block. Called by loadScene; without a block the
|
|
117
|
+
* draw order stays layer-banded with spawn-order ties.
|
|
118
|
+
*/
|
|
119
|
+
setSceneRender(json) {
|
|
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
|
+
}
|
|
130
|
+
}
|
|
110
131
|
/**
|
|
111
132
|
* Adopts a scene's camera block: jumps to its framing and, while
|
|
112
133
|
* simulating, follows/clamps per its settings. Called by loadScene.
|
|
@@ -121,11 +142,11 @@ export class Game {
|
|
|
121
142
|
// With a follow target the declared position is moot: start centered on
|
|
122
143
|
// the target so play begins framed like the editor shows it.
|
|
123
144
|
const followed = this.sceneCamera.follow ? this.find(this.sceneCamera.follow) : undefined;
|
|
124
|
-
const
|
|
125
|
-
?
|
|
126
|
-
: this.sceneCamera.position;
|
|
127
|
-
this.camera.position.x = x;
|
|
128
|
-
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;
|
|
129
150
|
this.setViewHeight(this.sceneCamera.zoom);
|
|
130
151
|
}
|
|
131
152
|
start() {
|
|
@@ -159,6 +180,10 @@ export class Game {
|
|
|
159
180
|
if (i !== -1)
|
|
160
181
|
this.entities.splice(i, 1);
|
|
161
182
|
}
|
|
183
|
+
/** The scene's render projection; null keeps logical and render space identical. */
|
|
184
|
+
get projection() {
|
|
185
|
+
return this.sceneProjection;
|
|
186
|
+
}
|
|
162
187
|
/** Visible world height (2D camera zoom). */
|
|
163
188
|
get view() {
|
|
164
189
|
return this.viewHeight;
|
|
@@ -216,7 +241,32 @@ export class Game {
|
|
|
216
241
|
this.runtimeBridge?.unregister();
|
|
217
242
|
this.runtimeBridge = null;
|
|
218
243
|
};
|
|
244
|
+
/** Under y-sort, re-derives every participant's z from layer band + entity Y. */
|
|
245
|
+
applyYSort() {
|
|
246
|
+
const participants = [];
|
|
247
|
+
const entries = [];
|
|
248
|
+
for (const entity of this.entities) {
|
|
249
|
+
for (const component of entity.components) {
|
|
250
|
+
if (isYSortParticipant(component)) {
|
|
251
|
+
participants.push(component);
|
|
252
|
+
entries.push({ layer: component.layer, y: entity.node.position.y });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const z = ySortZ(entries);
|
|
257
|
+
for (const [index, participant] of participants.entries())
|
|
258
|
+
participant.setSortZ(z[index]);
|
|
259
|
+
}
|
|
219
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
|
+
}
|
|
268
|
+
if (this.renderSort === 'y')
|
|
269
|
+
this.applyYSort();
|
|
220
270
|
this.ui.setActive(this.simulate);
|
|
221
271
|
if (this.resolution) {
|
|
222
272
|
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
@@ -262,19 +312,30 @@ export class Game {
|
|
|
262
312
|
if (!cam)
|
|
263
313
|
return;
|
|
264
314
|
const followed = cam.follow ? this.find(cam.follow) : undefined;
|
|
265
|
-
const
|
|
315
|
+
const provider = followed?.components.find((c) => isCameraVelocityProvider(c));
|
|
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 };
|
|
266
323
|
const next = stepSceneCamera(cam, {
|
|
267
324
|
x: this.camera.position.x,
|
|
268
325
|
y: this.camera.position.y,
|
|
269
326
|
halfW: (this.camera.right - this.camera.left) / 2,
|
|
270
327
|
halfH: this.viewHeight / 2,
|
|
271
|
-
target
|
|
272
|
-
vx:
|
|
328
|
+
target,
|
|
329
|
+
vx: renderVelocity.x,
|
|
330
|
+
vy: renderVelocity.y,
|
|
273
331
|
dt,
|
|
274
332
|
});
|
|
275
333
|
this.camera.position.x = next.x;
|
|
276
334
|
this.camera.position.y = next.y;
|
|
277
335
|
}
|
|
336
|
+
renderPoint(x, y) {
|
|
337
|
+
return this.sceneProjection === 'isometric' ? projectIsometric(x, y) : { x, y };
|
|
338
|
+
}
|
|
278
339
|
dispatchCollisions() {
|
|
279
340
|
const boxed = this.entities.filter((e) => e.has(Hitbox));
|
|
280
341
|
for (let i = 0; i < boxed.length; i++) {
|