ecspresso 0.14.0 → 0.14.2
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/README.md +3 -6
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/dist/plugins/ai/pathfinding.d.ts +163 -0
- package/dist/plugins/ai/pathfinding.js +4 -0
- package/dist/plugins/ai/pathfinding.js.map +10 -0
- package/dist/plugins/input/input.d.ts +105 -27
- package/dist/plugins/input/input.js +2 -2
- package/dist/plugins/input/input.js.map +3 -3
- package/dist/plugins/rendering/renderer3D.d.ts +23 -2
- package/dist/plugins/rendering/renderer3D.js +239 -184
- package/dist/plugins/rendering/renderer3D.js.map +5 -5
- package/dist/plugins/rendering/tilemap.d.ts +230 -0
- package/dist/plugins/rendering/tilemap.js +4 -0
- package/dist/plugins/rendering/tilemap.js.map +11 -0
- package/dist/plugins/spatial/camera3D.d.ts +29 -9
- package/dist/plugins/spatial/camera3D.js +2 -2
- package/dist/plugins/spatial/camera3D.js.map +3 -3
- package/dist/plugins/ui/ui.d.ts +116 -0
- package/dist/plugins/ui/ui.js +4 -0
- package/dist/plugins/ui/ui.js.map +11 -0
- package/dist/system-builder.d.ts +31 -0
- package/package.json +16 -4
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tilemap plugin for ECSpresso.
|
|
3
|
+
*
|
|
4
|
+
* Two ingestion paths share a common `LoadedTilemap` shape:
|
|
5
|
+
* - `registerAsset` — load a Tiled `.tmj` file via the asset manager
|
|
6
|
+
* - `registerRuntime` — pass a pre-built tile-id array (procedural)
|
|
7
|
+
*
|
|
8
|
+
* Query methods (`isSolid`, `isOpaque`, `isWalkable`) read from `tileMetadata`
|
|
9
|
+
* regardless of source.
|
|
10
|
+
*/
|
|
11
|
+
import { type BasePluginOptions } from 'ecspresso';
|
|
12
|
+
import type { WorldConfigFrom, EmptyConfig } from '../../type-utils';
|
|
13
|
+
import { type NavGrid } from '../ai/pathfinding';
|
|
14
|
+
import type { Vector2D } from '../../utils/math';
|
|
15
|
+
export declare const TILE_FLIP_HORIZONTAL = 2147483648;
|
|
16
|
+
export declare const TILE_FLIP_VERTICAL = 1073741824;
|
|
17
|
+
export declare const TILE_FLIP_DIAGONAL = 536870912;
|
|
18
|
+
export declare const TILE_GID_MASK = 536870911;
|
|
19
|
+
export interface DecodedGid {
|
|
20
|
+
id: number;
|
|
21
|
+
flipH: boolean;
|
|
22
|
+
flipV: boolean;
|
|
23
|
+
flipD: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function decodeGid(gid: number): DecodedGid;
|
|
26
|
+
/** The three tile flag keys the query API understands. Custom keys flow through unchanged. */
|
|
27
|
+
export type TileFlag = 'solid' | 'blocksSight' | 'walkable';
|
|
28
|
+
/** Tile metadata. Known flag keys drive query methods; arbitrary custom keys are preserved. */
|
|
29
|
+
export interface TileMetadata {
|
|
30
|
+
solid?: boolean;
|
|
31
|
+
blocksSight?: boolean;
|
|
32
|
+
walkable?: boolean;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface ObjectDef {
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
rotation: number;
|
|
43
|
+
properties: Record<string, string | number | boolean>;
|
|
44
|
+
}
|
|
45
|
+
export interface RuntimeTileset {
|
|
46
|
+
textureKey: string;
|
|
47
|
+
columns: number;
|
|
48
|
+
tileWidth: number;
|
|
49
|
+
tileHeight: number;
|
|
50
|
+
firstgid?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface RuntimeLayer {
|
|
53
|
+
name: string;
|
|
54
|
+
tiles: Uint32Array | Uint8Array | readonly number[];
|
|
55
|
+
parallax?: Vector2D;
|
|
56
|
+
opacity?: number;
|
|
57
|
+
visible?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface TilemapRuntimeData {
|
|
60
|
+
width: number;
|
|
61
|
+
height: number;
|
|
62
|
+
tileSize: number;
|
|
63
|
+
layers: readonly RuntimeLayer[];
|
|
64
|
+
tilesets: readonly RuntimeTileset[];
|
|
65
|
+
tileMetadata?: Record<number, TileMetadata>;
|
|
66
|
+
objectLayers?: readonly {
|
|
67
|
+
name: string;
|
|
68
|
+
objects: readonly ObjectDef[];
|
|
69
|
+
}[];
|
|
70
|
+
}
|
|
71
|
+
export interface LoadedLayer {
|
|
72
|
+
name: string;
|
|
73
|
+
tiles: Uint32Array;
|
|
74
|
+
parallax: Vector2D;
|
|
75
|
+
opacity: number;
|
|
76
|
+
visible: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface LoadedTileset {
|
|
79
|
+
textureKey: string;
|
|
80
|
+
columns: number;
|
|
81
|
+
tileWidth: number;
|
|
82
|
+
tileHeight: number;
|
|
83
|
+
firstgid: number;
|
|
84
|
+
}
|
|
85
|
+
export interface LoadedObjectLayer {
|
|
86
|
+
name: string;
|
|
87
|
+
objects: readonly ObjectDef[];
|
|
88
|
+
}
|
|
89
|
+
export interface LoadedTilemap {
|
|
90
|
+
readonly width: number;
|
|
91
|
+
readonly height: number;
|
|
92
|
+
readonly tileWidth: number;
|
|
93
|
+
readonly tileHeight: number;
|
|
94
|
+
readonly layers: readonly LoadedLayer[];
|
|
95
|
+
readonly tilesets: readonly LoadedTileset[];
|
|
96
|
+
readonly tileMetadata: ReadonlyMap<number, TileMetadata>;
|
|
97
|
+
readonly objectLayers: readonly LoadedObjectLayer[];
|
|
98
|
+
tileToWorld(tx: number, ty: number): Vector2D;
|
|
99
|
+
worldToTile(wx: number, wy: number): {
|
|
100
|
+
tx: number;
|
|
101
|
+
ty: number;
|
|
102
|
+
};
|
|
103
|
+
getTile(layerIndex: number, tx: number, ty: number): number;
|
|
104
|
+
isSolid(tx: number, ty: number): boolean;
|
|
105
|
+
isOpaque(tx: number, ty: number): boolean;
|
|
106
|
+
isWalkable(tx: number, ty: number): boolean;
|
|
107
|
+
buildNavGrid(layerIndex: number, costFn?: (tileId: number) => number): NavGrid;
|
|
108
|
+
getObjectLayer(name: string): readonly ObjectDef[];
|
|
109
|
+
getObjects(type: string): readonly ObjectDef[];
|
|
110
|
+
}
|
|
111
|
+
/** Subset of a Tiled `.tmj` (JSON) document we consume in v1. */
|
|
112
|
+
export interface TiledMap {
|
|
113
|
+
width: number;
|
|
114
|
+
height: number;
|
|
115
|
+
tilewidth: number;
|
|
116
|
+
tileheight: number;
|
|
117
|
+
tilesets: readonly TiledTileset[];
|
|
118
|
+
layers: readonly TiledLayer[];
|
|
119
|
+
}
|
|
120
|
+
export interface TiledTileset {
|
|
121
|
+
firstgid: number;
|
|
122
|
+
columns: number;
|
|
123
|
+
tilewidth: number;
|
|
124
|
+
tileheight: number;
|
|
125
|
+
image: string;
|
|
126
|
+
imagewidth: number;
|
|
127
|
+
imageheight: number;
|
|
128
|
+
tiles?: readonly TiledTileDef[];
|
|
129
|
+
}
|
|
130
|
+
export interface TiledTileDef {
|
|
131
|
+
id: number;
|
|
132
|
+
properties?: readonly TiledProperty[];
|
|
133
|
+
animation?: readonly {
|
|
134
|
+
tileid: number;
|
|
135
|
+
duration: number;
|
|
136
|
+
}[];
|
|
137
|
+
}
|
|
138
|
+
export interface TiledProperty {
|
|
139
|
+
name: string;
|
|
140
|
+
type: 'bool' | 'int' | 'float' | 'string' | 'color' | 'file' | 'object';
|
|
141
|
+
value: string | number | boolean;
|
|
142
|
+
}
|
|
143
|
+
export type TiledLayer = TiledTileLayer | TiledObjectLayer;
|
|
144
|
+
export interface TiledTileLayer {
|
|
145
|
+
type: 'tilelayer';
|
|
146
|
+
name: string;
|
|
147
|
+
width: number;
|
|
148
|
+
height: number;
|
|
149
|
+
data: readonly number[];
|
|
150
|
+
opacity: number;
|
|
151
|
+
visible: boolean;
|
|
152
|
+
parallaxx?: number;
|
|
153
|
+
parallaxy?: number;
|
|
154
|
+
}
|
|
155
|
+
export interface TiledObjectLayer {
|
|
156
|
+
type: 'objectgroup';
|
|
157
|
+
name: string;
|
|
158
|
+
objects: readonly TiledObject[];
|
|
159
|
+
}
|
|
160
|
+
export interface TiledObject {
|
|
161
|
+
id: number;
|
|
162
|
+
name?: string;
|
|
163
|
+
type?: string;
|
|
164
|
+
x: number;
|
|
165
|
+
y: number;
|
|
166
|
+
width?: number;
|
|
167
|
+
height?: number;
|
|
168
|
+
rotation?: number;
|
|
169
|
+
properties?: readonly TiledProperty[];
|
|
170
|
+
}
|
|
171
|
+
export interface ParseTiledOptions {
|
|
172
|
+
tilesetTextures: Record<string, string>;
|
|
173
|
+
}
|
|
174
|
+
export type TilemapCullingMode = 'viewport' | 'none';
|
|
175
|
+
export interface TilemapLayerComponent {
|
|
176
|
+
dataKey: string;
|
|
177
|
+
tilesetKey?: string;
|
|
178
|
+
layerIndex: number;
|
|
179
|
+
opacity: number;
|
|
180
|
+
parallax: Vector2D;
|
|
181
|
+
cullingMode: TilemapCullingMode;
|
|
182
|
+
cameraRef?: number;
|
|
183
|
+
tintFn?: (tx: number, ty: number) => number | null;
|
|
184
|
+
}
|
|
185
|
+
export interface TilemapColliderTag {
|
|
186
|
+
dataKey: string;
|
|
187
|
+
}
|
|
188
|
+
export interface TilemapComponentTypes {
|
|
189
|
+
tilemap: TilemapLayerComponent;
|
|
190
|
+
tilemapCollider: TilemapColliderTag;
|
|
191
|
+
}
|
|
192
|
+
export interface TilemapRegistry {
|
|
193
|
+
registerRuntime(dataKey: string, data: TilemapRuntimeData): LoadedTilemap;
|
|
194
|
+
registerAsset(dataKey: string, assetKey: string, options?: ParseTiledOptions): Promise<LoadedTilemap>;
|
|
195
|
+
get(dataKey: string): LoadedTilemap | undefined;
|
|
196
|
+
has(dataKey: string): boolean;
|
|
197
|
+
readonly entries: ReadonlyMap<string, LoadedTilemap>;
|
|
198
|
+
}
|
|
199
|
+
export interface TilemapResourceTypes {
|
|
200
|
+
tilemaps: TilemapRegistry;
|
|
201
|
+
}
|
|
202
|
+
export type TilemapWorldConfig = WorldConfigFrom<TilemapComponentTypes, EmptyConfig['events'], TilemapResourceTypes>;
|
|
203
|
+
export interface TilemapPluginOptions<G extends string = 'rendering'> extends BasePluginOptions<G> {
|
|
204
|
+
/** Optional collision layer name. When set, solid tiles auto-spawn `aabbCollider` strips. */
|
|
205
|
+
collisionLayer?: string;
|
|
206
|
+
/** Layers the auto-generated tile bodies collide with. */
|
|
207
|
+
collidesWith?: readonly string[];
|
|
208
|
+
}
|
|
209
|
+
export declare function createLoadedTilemap(data: TilemapRuntimeData): LoadedTilemap;
|
|
210
|
+
export declare function parseTiledJSON(map: TiledMap, options: ParseTiledOptions): LoadedTilemap;
|
|
211
|
+
export declare function createTilemapPlugin<G extends string = 'rendering'>(options?: TilemapPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithComponents<EmptyConfig, TilemapComponentTypes>, TilemapResourceTypes>, EmptyConfig, never, G, never, never>;
|
|
212
|
+
/**
|
|
213
|
+
* Create a `tilemap` layer component for spreading into `spawn()`.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* ecs.spawn({
|
|
218
|
+
* ...createTilemapLayer('dungeon', 0),
|
|
219
|
+
* ...createLocalTransform(0, 0),
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export declare function createTilemapLayer(dataKey: string, layerIndex: number, options?: {
|
|
224
|
+
tilesetKey?: string;
|
|
225
|
+
opacity?: number;
|
|
226
|
+
parallax?: Vector2D;
|
|
227
|
+
cullingMode?: TilemapCullingMode;
|
|
228
|
+
cameraRef?: number;
|
|
229
|
+
tintFn?: (tx: number, ty: number) => number | null;
|
|
230
|
+
}): Pick<TilemapComponentTypes, 'tilemap'>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var f=Object.defineProperty;var T=(Q)=>Q;function I(Q,V){this[Q]=T.bind(null,V)}var s=(Q,V)=>{for(var J in V)f(Q,J,{get:V[J],enumerable:!0,configurable:!0,set:I.bind(V,J)})};var r=(Q,V)=>()=>(Q&&(V=Q(Q=0)),V);var d=((Q)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(Q,{get:(V,J)=>(typeof require<"u"?require:V)[J]}):Q)(function(Q){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+Q+'" is not supported')});import{definePlugin as S}from"ecspresso";var K={neighbors(Q,V,J){let Z=V%Q.width,Y=(V-Z)/Q.width,X=0;if(Z>0)J[X++]=V-1;if(Z<Q.width-1)J[X++]=V+1;if(Y>0)J[X++]=V-Q.width;if(Y<Q.height-1)J[X++]=V+Q.width;return X},stepCost(Q,V,J){return Q.cells[J]??0},heuristic(Q,V,J){let Z=V%Q.width,Y=(V-Z)/Q.width,X=J%Q.width,q=(J-X)/Q.width;return Math.abs(Z-X)+Math.abs(Y-q)}},M=(Q)=>{let V=()=>{throw Error(`pathfinding: topology '${Q}' is not implemented in v1`)};return{neighbors:V,stepCost:V,heuristic:V}},L=Object.freeze({square4:K,square8:M("square8"),"hex-pointy":M("hex-pointy"),"hex-flat":M("hex-flat")});function C(Q){let V=Q.topology??"square4",J=Q.cellSize??32,Z=Q.originX??0,Y=Q.originY??0,{width:X,height:q}=Q,B=Q.defaultCost??1;if(!Number.isInteger(X)||X<=0)throw Error(`pathfinding: width must be a positive integer, got ${X}`);if(!Number.isInteger(q)||q<=0)throw Error(`pathfinding: height must be a positive integer, got ${q}`);if(J<=0)throw Error(`pathfinding: cellSize must be > 0, got ${J}`);if(B<0||B>255)throw Error(`pathfinding: defaultCost must be in 0–255, got ${B}`);let W=X*q,D=Q.cells??new Uint8Array(W).fill(B);if(D.length!==W)throw Error(`pathfinding: cells length ${D.length} does not match width*height ${W}`);let H=1/J;return{topology:V,width:X,height:q,cellSize:J,originX:Z,originY:Y,cells:D,worldToCell:(j,k)=>{let z=Math.floor((j-Z)*H),R=Math.floor((k-Y)*H),O=z<0?0:z>=X?X-1:z;return(R<0?0:R>=q?q-1:R)*X+O},cellToWorld:(j)=>{let k=j%X,z=(j-k)/X;return{x:Z+(k+0.5)*J,y:Y+(z+0.5)*J}},cellFromXY:(j,k)=>k*X+j,cellToXY:(j)=>{let k=j%X;return{x:k,y:(j-k)/X}}}}function i(Q){return{pathRequest:{target:{x:Q.x,y:Q.y}}}}function v(Q,V,J){let Z=Q.size;Q.size=Z+1;while(Z>0){let Y=Z-1>>1;if((Q.priorities[Y]??0)<=J)break;Q.ids[Z]=Q.ids[Y]??0,Q.priorities[Z]=Q.priorities[Y]??0,Z=Y}Q.ids[Z]=V,Q.priorities[Z]=J}function w(Q){let V=Q.ids[0]??-1,J=Q.size-1;if(Q.size=J,J<=0)return V;let Z=Q.ids[J]??0,Y=Q.priorities[J]??0,X=0,q=J>>1;while(X<q){let B=(X<<1)+1,W=B+1;if(W<J&&(Q.priorities[W]??0)<(Q.priorities[B]??0))B=W;if((Q.priorities[B]??0)>=Y)break;Q.ids[X]=Q.ids[B]??0,Q.priorities[X]=Q.priorities[B]??0,X=B}return Q.ids[X]=Z,Q.priorities[X]=Y,V}function m(Q,V){let J=1,Z=V;while((Q[Z]??-1)!==-1)J++,Z=Q[Z]??-1;let Y=Array(J);Z=V;for(let X=J-1;X>=0;X--)if(Y[X]=Z,X>0)Z=Q[Z]??-1;return Y}function u(Q,V,J,Z){let Y=Q.cells.length;if(V<0||V>=Y)return null;if(J<0||J>=Y)return null;let X=Z?.maxNodesExpanded??1e4,q=Z?.blockedCells,B=Z?.goalTolerance??0,W=L[Q.topology],D=new Float32Array(Y);D.fill(Number.POSITIVE_INFINITY);let H=new Int32Array(Y);H.fill(-1);let U=new Uint8Array(Y),$={ids:new Int32Array(Y),priorities:new Float32Array(Y),size:0},N=[];D[V]=0,v($,V,W.heuristic(Q,V,J));let G=0;while($.size>0){if(G>=X)return null;let j=w($);if(U[j])continue;if(U[j]=1,G++,W.heuristic(Q,j,J)<=B)return m(H,j);N.length=0;let k=W.neighbors(Q,j,N);for(let z=0;z<k;z++){let R=N[z]??-1;if(R<0||U[R])continue;if((Q.cells[R]??0)===0)continue;if(q&&q.has(R))continue;let E=(D[j]??Number.POSITIVE_INFINITY)+W.stepCost(Q,j,R);if(E<(D[R]??Number.POSITIVE_INFINITY))D[R]=E,H[R]=j,v($,R,E+W.heuristic(Q,R,J))}}return null}function e(Q){let{grid:V,systemGroup:J="ai",priority:Z=150,phase:Y="update",maxRequestsPerFrame:X=4,maxNodesExpanded:q=1e4}=Q;return S("pathfinding").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((B)=>{B.addResource("navGrid",V),B.addSystem("pathfinding-request").setPriority(Z).inPhase(Y).inGroup(J).addQuery("requests",{with:["pathRequest","worldTransform"]}).setProcess(({queries:W,ecs:D})=>{let H=D.getResource("navGrid"),U=0;for(let $ of W.requests){if(U>=X)break;U++;let{pathRequest:N,worldTransform:G}=$.components,j=H.worldToCell(G.x,G.y),k=H.worldToCell(N.target.x,N.target.y),z=u(H,j,k,{maxNodesExpanded:q});if(D.commands.removeComponent($.id,"pathRequest"),z===null){D.eventBus.publish("pathBlocked",{entityId:$.id});continue}let R=z.slice(1).map((b)=>H.cellToWorld(b));if(D.eventBus.publish("pathFound",{entityId:$.id,path:R}),R.length===0)continue;let O=D.getComponent($.id,"path");if(O)O.waypoints=R,O.currentIndex=0,D.markChanged($.id,"path");else D.addComponent($.id,"path",{waypoints:R,currentIndex:0});let E=R[0];if(!E)continue;let A=D.getComponent($.id,"moveTarget");if(A)A.x=E.x,A.y=E.y,D.markChanged($.id,"moveTarget");else D.addComponent($.id,"moveTarget",{x:E.x,y:E.y})}}),B.addSystem("pathfinding-waypoint-advance").inGroup(J).setEventHandlers({arriveAtTarget({data:W,ecs:D}){let H=D.getComponent(W.entityId,"path");if(!H)return;let U=H.currentIndex+1;if(U>=H.waypoints.length){D.commands.removeComponent(W.entityId,"path");return}H.currentIndex=U,D.markChanged(W.entityId,"path");let $=H.waypoints[U];if(!$)return;D.commands.addComponent(W.entityId,"moveTarget",{x:$.x,y:$.y})}})})}import{definePlugin as x}from"ecspresso";var g=2147483648,y=1073741824,c=536870912,_=536870911;function $Q(Q){return{id:(Q&_)>>>0,flipH:(Q&g)!==0,flipV:(Q&y)!==0,flipD:(Q&c)!==0}}function h(Q){return Uint32Array.from(Q)}function F(Q){if(!Q)return{};let V={};for(let J of Q)V[J.name]=J.value;return V}function n(Q){let{width:V,height:J,tileSize:Z,layers:Y,tilesets:X}=Q;if(!Number.isFinite(V)||V<=0)throw Error(`tilemap: width must be > 0, got ${V}`);if(!Number.isFinite(J)||J<=0)throw Error(`tilemap: height must be > 0, got ${J}`);if(!Number.isFinite(Z)||Z<=0)throw Error(`tilemap: tileSize must be > 0, got ${Z}`);if(X.length===0)throw Error("tilemap: at least one tileset is required");let q=V*J,B=Y.map((U)=>{let $=h(U.tiles);if($.length!==q)throw Error(`tilemap: layer "${U.name}" tile count ${$.length} does not match width*height ${q}`);return{name:U.name,tiles:$,parallax:U.parallax?{x:U.parallax.x,y:U.parallax.y}:{x:1,y:1},opacity:U.opacity??1,visible:U.visible??!0}}),W=X.map((U,$)=>{if(U.firstgid===void 0&&$>0)throw Error(`tilemap: runtime tileset at index ${$} ("${U.textureKey}") must specify an explicit firstgid`);return{textureKey:U.textureKey,columns:U.columns,tileWidth:U.tileWidth,tileHeight:U.tileHeight,firstgid:U.firstgid??1}}),D=new Map;if(Q.tileMetadata)for(let[U,$]of Object.entries(Q.tileMetadata))D.set(Number(U),{...$});let H=(Q.objectLayers??[]).map((U)=>({name:U.name,objects:U.objects.map(($)=>({...$,properties:{...$.properties}}))}));return P({width:V,height:J,tileWidth:Z,tileHeight:Z,layers:B,tilesets:W,tileMetadata:D,objectLayers:H})}function p(Q,V){let{width:J,height:Z,tilewidth:Y,tileheight:X}=Q,q=J*Z,B=Q.tilesets.map((U)=>{let $=V.tilesetTextures[U.image];if(!$)throw Error(`tilemap: no texture key registered for tileset image "${U.image}"`);return{textureKey:$,columns:U.columns,tileWidth:U.tilewidth,tileHeight:U.tileheight,firstgid:U.firstgid}}),W=new Map;for(let U of Q.tilesets){if(!U.tiles)continue;for(let $ of U.tiles)W.set(U.firstgid+$.id,F($.properties))}let D=[],H=[];for(let U of Q.layers)if(U.type==="tilelayer"){if(U.data.length!==q)throw Error(`tilemap: layer "${U.name}" data length ${U.data.length} does not match map ${q}`);D.push({name:U.name,tiles:Uint32Array.from(U.data),parallax:{x:U.parallaxx??1,y:U.parallaxy??1},opacity:U.opacity,visible:U.visible})}else H.push({name:U.name,objects:U.objects.map(($)=>({name:$.name??"",type:$.type??"",x:$.x,y:$.y,width:$.width??0,height:$.height??0,rotation:$.rotation??0,properties:F($.properties)}))});return P({width:J,height:Z,tileWidth:Y,tileHeight:X,layers:D,tilesets:B,tileMetadata:W,objectLayers:H})}function P(Q){let{width:V,height:J,tileWidth:Z,tileHeight:Y,layers:X,tilesets:q,tileMetadata:B,objectLayers:W}=Q,D=(U,$,N)=>{if(U<0||$<0||U>=V||$>=J)return!1;let G=$*V+U;for(let j=0;j<X.length;j++){let k=(X[j].tiles[G]??0)&_;if(k===0)continue;let z=B.get(k);if(z&&z[N]===!0)return!0}return!1},H=(U)=>{return B.get(U)?.walkable===!0?1:0};return{width:V,height:J,tileWidth:Z,tileHeight:Y,layers:X,tilesets:q,tileMetadata:B,objectLayers:W,tileToWorld(U,$){return{x:(U+0.5)*Z,y:($+0.5)*Y}},worldToTile(U,$){return{tx:Math.floor(U/Z),ty:Math.floor($/Y)}},getTile(U,$,N){let G=X[U];if(!G)return 0;if($<0||N<0||$>=V||N>=J)return 0;return(G.tiles[N*V+$]??0)&_},isSolid:(U,$)=>D(U,$,"solid"),isOpaque:(U,$)=>D(U,$,"blocksSight"),isWalkable:(U,$)=>D(U,$,"walkable"),buildNavGrid(U,$){let N=X[U];if(!N)throw Error(`tilemap: buildNavGrid — no layer at index ${U}`);let G=new Uint8Array(V*J),j=$??H;for(let k=0;k<G.length;k++){let z=(N.tiles[k]??0)&_,R=j(z)|0;G[k]=R<0?0:R>255?255:R}return C({width:V,height:J,cellSize:Z,cells:G})},getObjectLayer(U){return W.find(($)=>$.name===U)?.objects??[]},getObjects(U){let $=[];for(let N of W)for(let G of N.objects)if(G.type===U)$.push(G);return $}}}function l(Q){let V=[];for(let J=0;J<Q.height;J++){let Z=-1;for(let Y=0;Y<=Q.width;Y++){let X=Y<Q.width&&Q.isSolid(Y,J);if(X&&Z===-1)Z=Y;else if(!X&&Z!==-1)V.push({tx:Z,ty:J,tw:Y-Z,th:1}),Z=-1}}return V}function JQ(Q={}){let{collisionLayer:V,collidesWith:J}=Q;return x("tilemap").withComponentTypes().withResourceTypes().withLabels().withGroups().install((Z)=>{let Y=new Map,X=new Map,q=(H)=>{let U=X.get(H);if(!U)return;for(let $ of U)Z.removeEntity($);X.delete(H)},B=(H,U)=>{if(!V)return;let $=[];for(let N of l(U)){let G=(N.tx+N.tw/2)*U.tileWidth,j=(N.ty+N.th/2)*U.tileHeight,k={tilemapCollider:{dataKey:H},aabbCollider:{width:N.tw*U.tileWidth,height:N.th*U.tileHeight},collisionLayer:{layer:V,collidesWith:J??[]},localTransform:{x:G,y:j,rotation:0,scaleX:1,scaleY:1},worldTransform:{x:G,y:j,rotation:0,scaleX:1,scaleY:1}},z=Z.spawn(k);$.push(z.id)}if($.length>0)X.set(H,$)},W=(H,U)=>{return q(H),Y.set(H,U),B(H,U),U},D={entries:Y,registerRuntime(H,U){return W(H,n(U))},async registerAsset(H,U,$){let N=await Z.loadAsset(U);return W(H,p(N,$??{tilesetTextures:{}}))},get:(H)=>Y.get(H),has:(H)=>Y.has(H)};Z.addResource("tilemaps",D)})}function VQ(Q,V,J){return{tilemap:{dataKey:Q,layerIndex:V,tilesetKey:J?.tilesetKey,opacity:J?.opacity??1,parallax:J?.parallax??{x:1,y:1},cullingMode:J?.cullingMode??"viewport",cameraRef:J?.cameraRef,tintFn:J?.tintFn}}}export{p as parseTiledJSON,$Q as decodeGid,JQ as createTilemapPlugin,VQ as createTilemapLayer,n as createLoadedTilemap,_ as TILE_GID_MASK,y as TILE_FLIP_VERTICAL,g as TILE_FLIP_HORIZONTAL,c as TILE_FLIP_DIAGONAL};
|
|
2
|
+
|
|
3
|
+
//# debugId=1B20DC0974BC7FAF64756E2164756E21
|
|
4
|
+
//# sourceMappingURL=tilemap.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/ai/pathfinding.ts", "../src/plugins/rendering/tilemap.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Pathfinding Plugin for ECSpresso\n *\n * A* pathfinding on a weighted grid. Produces waypoint lists consumed by the\n * steering plugin — the pathfinding system writes the `path` component and\n * sets `moveTarget` to the first waypoint; the waypoint advancement handler\n * listens for `arriveAtTarget` and advances to the next waypoint.\n *\n * Exports the pure `findPath(grid, start, goal, options?)` function for\n * turn-based / non-realtime consumers that don't need the component dance.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { Vector2D } from '../../utils/math';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SteeringWorldConfig } from '../physics/steering';\n\n// ==================== Topology / Grid Types ====================\n\n/** Flat-indexed cell position in a `NavGrid`. Transparent alias, not branded. */\nexport type CellIndex = number;\n\n/**\n * Grid topology. v1 ships `square4`; other values are accepted at construction\n * but throw at `findPath` time.\n */\nexport type NavGridTopology = 'square4' | 'square8' | 'hex-pointy' | 'hex-flat';\n\n/**\n * Weighted navigation grid. Row-major storage (`idx = row * width + col`).\n * Cell value `0` = impassable, `1`–`255` = traversal cost into that cell.\n */\nexport interface NavGrid {\n\treadonly topology: NavGridTopology;\n\treadonly width: number;\n\treadonly height: number;\n\treadonly cellSize: number;\n\treadonly originX: number;\n\treadonly originY: number;\n\treadonly cells: Uint8Array;\n\tworldToCell(wx: number, wy: number): CellIndex;\n\tcellToWorld(idx: CellIndex): Vector2D;\n\tcellFromXY(x: number, y: number): CellIndex;\n\tcellToXY(idx: CellIndex): { x: number; y: number };\n}\n\n/** Options accepted by `createNavGrid`. */\nexport interface CreateNavGridOptions {\n\ttopology?: NavGridTopology;\n\twidth: number;\n\theight: number;\n\tcellSize?: number;\n\toriginX?: number;\n\toriginY?: number;\n\tcells?: Uint8Array;\n\tdefaultCost?: number;\n}\n\n// ==================== Component Types ====================\n\n/** Signals the pathfinding system to compute a route to `target`. */\nexport interface PathRequest {\n\ttarget: Vector2D;\n}\n\n/** Active route; waypoints are in world-space, advanced by `currentIndex`. */\nexport interface Path {\n\twaypoints: Vector2D[];\n\tcurrentIndex: number;\n}\n\n/** Component types provided by the pathfinding plugin. */\nexport interface PathfindingComponentTypes {\n\tpathRequest: PathRequest;\n\tpath: Path;\n}\n\n// ==================== Event Types ====================\n\n/** Fired when A* produces a route. `path` is empty when start is already at the goal. */\nexport interface PathFoundEvent {\n\tentityId: number;\n\tpath: Vector2D[];\n}\n\n/** Fired when no path exists to the target. */\nexport interface PathBlockedEvent {\n\tentityId: number;\n}\n\n/** Event types provided by the pathfinding plugin. */\nexport interface PathfindingEventTypes {\n\tpathFound: PathFoundEvent;\n\tpathBlocked: PathBlockedEvent;\n}\n\n// ==================== Resource Types ====================\n\n/** Resource types provided by the pathfinding plugin. */\nexport interface PathfindingResourceTypes {\n\tnavGrid: NavGrid;\n}\n\n// ==================== WorldConfig ====================\n\n/** WorldConfig representing the pathfinding plugin's provided types. */\nexport type PathfindingWorldConfig = WorldConfigFrom<\n\tPathfindingComponentTypes,\n\tPathfindingEventTypes,\n\tPathfindingResourceTypes\n>;\n\n// ==================== Plugin Options ====================\n\nexport interface PathfindingPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {\n\t/** The navigation grid. Construct via `createNavGrid`. */\n\tgrid: NavGrid;\n\t/** Max path requests processed per frame (default 4). */\n\tmaxRequestsPerFrame?: number;\n\t/** Default `maxNodesExpanded` passed to A* per request (default 10_000). */\n\tmaxNodesExpanded?: number;\n}\n\n// ==================== NavGrid Construction ====================\n\ninterface TopologyOps {\n\tneighbors(grid: NavGrid, idx: CellIndex, out: number[]): number;\n\tstepCost(grid: NavGrid, from: CellIndex, to: CellIndex): number;\n\theuristic(grid: NavGrid, a: CellIndex, b: CellIndex): number;\n}\n\nconst square4Ops: TopologyOps = {\n\tneighbors(grid, idx, out) {\n\t\tconst col = idx % grid.width;\n\t\tconst row = (idx - col) / grid.width;\n\t\tlet count = 0;\n\t\tif (col > 0) out[count++] = idx - 1;\n\t\tif (col < grid.width - 1) out[count++] = idx + 1;\n\t\tif (row > 0) out[count++] = idx - grid.width;\n\t\tif (row < grid.height - 1) out[count++] = idx + grid.width;\n\t\treturn count;\n\t},\n\tstepCost(grid, _from, to) {\n\t\treturn grid.cells[to] ?? 0;\n\t},\n\theuristic(grid, a, b) {\n\t\tconst ax = a % grid.width;\n\t\tconst ay = (a - ax) / grid.width;\n\t\tconst bx = b % grid.width;\n\t\tconst by = (b - bx) / grid.width;\n\t\treturn Math.abs(ax - bx) + Math.abs(ay - by);\n\t},\n};\n\nconst unimplementedOps = (topology: NavGridTopology): TopologyOps => {\n\tconst err = (): never => {\n\t\tthrow new Error(`pathfinding: topology '${topology}' is not implemented in v1`);\n\t};\n\treturn {\n\t\tneighbors: err,\n\t\tstepCost: err,\n\t\theuristic: err,\n\t};\n};\n\nconst topologyOps: Readonly<Record<NavGridTopology, TopologyOps>> = Object.freeze({\n\t'square4': square4Ops,\n\t'square8': unimplementedOps('square8'),\n\t'hex-pointy': unimplementedOps('hex-pointy'),\n\t'hex-flat': unimplementedOps('hex-flat'),\n});\n\n/**\n * Create a weighted navigation grid.\n *\n * @example\n * ```typescript\n * const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });\n * grid.cells[grid.cellFromXY(5, 5)] = 0; // block a cell\n * ```\n */\nexport function createNavGrid(options: CreateNavGridOptions): NavGrid {\n\tconst topology = options.topology ?? 'square4';\n\tconst cellSize = options.cellSize ?? 32;\n\tconst originX = options.originX ?? 0;\n\tconst originY = options.originY ?? 0;\n\tconst { width, height } = options;\n\tconst defaultCost = options.defaultCost ?? 1;\n\n\tif (!Number.isInteger(width) || width <= 0) {\n\t\tthrow new Error(`pathfinding: width must be a positive integer, got ${width}`);\n\t}\n\tif (!Number.isInteger(height) || height <= 0) {\n\t\tthrow new Error(`pathfinding: height must be a positive integer, got ${height}`);\n\t}\n\tif (cellSize <= 0) {\n\t\tthrow new Error(`pathfinding: cellSize must be > 0, got ${cellSize}`);\n\t}\n\tif (defaultCost < 0 || defaultCost > 255) {\n\t\tthrow new Error(`pathfinding: defaultCost must be in 0–255, got ${defaultCost}`);\n\t}\n\n\tconst expectedLen = width * height;\n\tconst cells = options.cells ?? new Uint8Array(expectedLen).fill(defaultCost);\n\tif (cells.length !== expectedLen) {\n\t\tthrow new Error(\n\t\t\t`pathfinding: cells length ${cells.length} does not match width*height ${expectedLen}`,\n\t\t);\n\t}\n\n\tconst invCellSize = 1 / cellSize;\n\n\tconst worldToCell = (wx: number, wy: number): CellIndex => {\n\t\tconst col = Math.floor((wx - originX) * invCellSize);\n\t\tconst row = Math.floor((wy - originY) * invCellSize);\n\t\tconst cCol = col < 0 ? 0 : col >= width ? width - 1 : col;\n\t\tconst cRow = row < 0 ? 0 : row >= height ? height - 1 : row;\n\t\treturn cRow * width + cCol;\n\t};\n\n\tconst cellToWorld = (idx: CellIndex): Vector2D => {\n\t\tconst col = idx % width;\n\t\tconst row = (idx - col) / width;\n\t\treturn {\n\t\t\tx: originX + (col + 0.5) * cellSize,\n\t\t\ty: originY + (row + 0.5) * cellSize,\n\t\t};\n\t};\n\n\tconst cellFromXY = (x: number, y: number): CellIndex => y * width + x;\n\n\tconst cellToXY = (idx: CellIndex): { x: number; y: number } => {\n\t\tconst x = idx % width;\n\t\treturn { x, y: (idx - x) / width };\n\t};\n\n\treturn {\n\t\ttopology, width, height, cellSize, originX, originY, cells,\n\t\tworldToCell, cellToWorld, cellFromXY, cellToXY,\n\t};\n}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a `pathRequest` component for spreading into `spawn()` / `addComponent()`.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createPathRequest({ x: 200, y: 300 }),\n * });\n * ```\n */\nexport function createPathRequest(target: Vector2D): Pick<PathfindingComponentTypes, 'pathRequest'> {\n\treturn { pathRequest: { target: { x: target.x, y: target.y } } };\n}\n\n// ==================== Pure A* API ====================\n\nexport interface FindPathOptions {\n\t/** Cap on A* node expansions; returns `null` if exceeded. Default 10_000. */\n\tmaxNodesExpanded?: number;\n\t/** Dynamic per-call obstacles layered on top of the static grid. */\n\tblockedCells?: Set<CellIndex>;\n\t/** Accept arrival within N cells of goal (topology-aware distance). Default 0. */\n\tgoalTolerance?: number;\n}\n\ninterface PathHeap {\n\tids: Int32Array;\n\tpriorities: Float32Array;\n\tsize: number;\n}\n\n// Why: parallel-typed-array heap keeps cells & priorities in cache without per-node allocations.\nfunction heapPush(heap: PathHeap, id: number, priority: number): void {\n\tlet i = heap.size;\n\theap.size = i + 1;\n\twhile (i > 0) {\n\t\tconst parent = (i - 1) >> 1;\n\t\tif ((heap.priorities[parent] ?? 0) <= priority) break;\n\t\theap.ids[i] = heap.ids[parent] ?? 0;\n\t\theap.priorities[i] = heap.priorities[parent] ?? 0;\n\t\ti = parent;\n\t}\n\theap.ids[i] = id;\n\theap.priorities[i] = priority;\n}\n\nfunction heapPop(heap: PathHeap): number {\n\tconst top = heap.ids[0] ?? -1;\n\tconst last = heap.size - 1;\n\theap.size = last;\n\tif (last <= 0) return top;\n\tconst movedId = heap.ids[last] ?? 0;\n\tconst movedPri = heap.priorities[last] ?? 0;\n\tlet i = 0;\n\tconst half = last >> 1;\n\twhile (i < half) {\n\t\tlet child = (i << 1) + 1;\n\t\tconst right = child + 1;\n\t\tif (right < last && (heap.priorities[right] ?? 0) < (heap.priorities[child] ?? 0)) child = right;\n\t\tif ((heap.priorities[child] ?? 0) >= movedPri) break;\n\t\theap.ids[i] = heap.ids[child] ?? 0;\n\t\theap.priorities[i] = heap.priorities[child] ?? 0;\n\t\ti = child;\n\t}\n\theap.ids[i] = movedId;\n\theap.priorities[i] = movedPri;\n\treturn top;\n}\n\nfunction reconstructPath(cameFrom: Int32Array, end: CellIndex): CellIndex[] {\n\t// Why: two-pass (count then fill) avoids unshift/reverse allocation.\n\tlet count = 1;\n\tlet cur = end;\n\twhile ((cameFrom[cur] ?? -1) !== -1) {\n\t\tcount++;\n\t\tcur = cameFrom[cur] ?? -1;\n\t}\n\tconst path = new Array<CellIndex>(count);\n\tcur = end;\n\tfor (let i = count - 1; i >= 0; i--) {\n\t\tpath[i] = cur;\n\t\tif (i > 0) cur = cameFrom[cur] ?? -1;\n\t}\n\treturn path;\n}\n\n/**\n * Compute a path through `grid` from `start` to `goal`.\n *\n * Returns a list of cell indices starting with `start` and ending at a cell\n * within `goalTolerance` of `goal`, or `null` if no such path exists within\n * `maxNodesExpanded` expansions.\n *\n * `start` is always treated as passable (even if its grid cell is 0 or the\n * cell is in `blockedCells`) — actors physics-pushed onto a wall still get a\n * valid origin.\n */\nexport function findPath(\n\tgrid: NavGrid,\n\tstart: CellIndex,\n\tgoal: CellIndex,\n\toptions?: FindPathOptions,\n): CellIndex[] | null {\n\tconst n = grid.cells.length;\n\tif (start < 0 || start >= n) return null;\n\tif (goal < 0 || goal >= n) return null;\n\n\tconst maxNodesExpanded = options?.maxNodesExpanded ?? 10_000;\n\tconst blockedCells = options?.blockedCells;\n\tconst goalTolerance = options?.goalTolerance ?? 0;\n\tconst ops = topologyOps[grid.topology];\n\n\t// Per-call allocations: ~n bytes × 5 (gScore, cameFrom, closed, heap ids, heap priorities).\n\t// For a 100×100 grid that's ~120 KB per search. Acceptable for v1 game-grid scales.\n\t// Deferred optimization: closure-scoped reusable pool keyed by `n`, reset per call.\n\tconst gScore = new Float32Array(n);\n\tgScore.fill(Number.POSITIVE_INFINITY);\n\tconst cameFrom = new Int32Array(n);\n\tcameFrom.fill(-1);\n\tconst closed = new Uint8Array(n);\n\tconst heap: PathHeap = {\n\t\tids: new Int32Array(n),\n\t\tpriorities: new Float32Array(n),\n\t\tsize: 0,\n\t};\n\tconst neighborBuf: number[] = [];\n\n\tgScore[start] = 0;\n\theapPush(heap, start, ops.heuristic(grid, start, goal));\n\n\tlet expanded = 0;\n\twhile (heap.size > 0) {\n\t\tif (expanded >= maxNodesExpanded) return null;\n\t\tconst current = heapPop(heap);\n\t\tif (closed[current]) continue;\n\t\tclosed[current] = 1;\n\t\texpanded++;\n\n\t\tif (ops.heuristic(grid, current, goal) <= goalTolerance) {\n\t\t\treturn reconstructPath(cameFrom, current);\n\t\t}\n\n\t\tneighborBuf.length = 0;\n\t\tconst count = ops.neighbors(grid, current, neighborBuf);\n\t\tfor (let k = 0; k < count; k++) {\n\t\t\tconst next = neighborBuf[k] ?? -1;\n\t\t\tif (next < 0 || closed[next]) continue;\n\t\t\tconst cellCost = grid.cells[next] ?? 0;\n\t\t\tif (cellCost === 0) continue;\n\t\t\tif (blockedCells && blockedCells.has(next)) continue;\n\n\t\t\tconst tentative = (gScore[current] ?? Number.POSITIVE_INFINITY) + ops.stepCost(grid, current, next);\n\t\t\tif (tentative < (gScore[next] ?? Number.POSITIVE_INFINITY)) {\n\t\t\t\tgScore[next] = tentative;\n\t\t\t\tcameFrom[next] = current;\n\t\t\t\theapPush(heap, next, tentative + ops.heuristic(grid, next, goal));\n\t\t\t}\n\t\t}\n\t}\n\treturn null;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a pathfinding plugin for ECSpresso.\n *\n * Requires the transform and steering plugins to be installed (entities need\n * `worldTransform` for start-cell detection and `moveTarget`/`moveSpeed` for\n * waypoint traversal).\n *\n * @example\n * ```typescript\n * const grid = createNavGrid({ width: 32, height: 32, cellSize: 16 });\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createSteeringPlugin())\n * .withPlugin(createPathfindingPlugin({ grid }))\n * .build();\n *\n * ecs.spawn({\n * ...createTransform(0, 0),\n * ...createMoveSpeed(100),\n * ...createPathRequest({ x: 500, y: 300 }),\n * });\n * ```\n */\nexport function createPathfindingPlugin<G extends string = 'ai'>(\n\toptions: PathfindingPluginOptions<G>,\n) {\n\tconst {\n\t\tgrid,\n\t\tsystemGroup = 'ai' as G,\n\t\tpriority = 150,\n\t\tphase = 'update',\n\t\tmaxRequestsPerFrame = 4,\n\t\tmaxNodesExpanded = 10_000,\n\t} = options;\n\n\treturn definePlugin('pathfinding')\n\t\t.withComponentTypes<PathfindingComponentTypes>()\n\t\t.withEventTypes<PathfindingEventTypes>()\n\t\t.withResourceTypes<PathfindingResourceTypes>()\n\t\t.withLabels<'pathfinding-request' | 'pathfinding-waypoint-advance'>()\n\t\t.withGroups<G>()\n\t\t.requires<TransformWorldConfig & SteeringWorldConfig>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('navGrid', grid);\n\n\t\t\tworld\n\t\t\t\t.addSystem('pathfinding-request')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('requests', {\n\t\t\t\t\twith: ['pathRequest', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst navGrid = ecs.getResource('navGrid');\n\t\t\t\t\tlet processed = 0;\n\t\t\t\t\tfor (const entity of queries.requests) {\n\t\t\t\t\t\tif (processed >= maxRequestsPerFrame) break;\n\t\t\t\t\t\tprocessed++;\n\t\t\t\t\t\tconst { pathRequest, worldTransform } = entity.components;\n\t\t\t\t\t\tconst startIdx = navGrid.worldToCell(worldTransform.x, worldTransform.y);\n\t\t\t\t\t\tconst goalIdx = navGrid.worldToCell(pathRequest.target.x, pathRequest.target.y);\n\t\t\t\t\t\tconst result = findPath(navGrid, startIdx, goalIdx, { maxNodesExpanded });\n\t\t\t\t\t\tecs.commands.removeComponent(entity.id, 'pathRequest');\n\n\t\t\t\t\t\tif (result === null) {\n\t\t\t\t\t\t\tecs.eventBus.publish('pathBlocked', { entityId: entity.id });\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst waypoints = result.slice(1).map(idx => navGrid.cellToWorld(idx));\n\t\t\t\t\t\tecs.eventBus.publish('pathFound', { entityId: entity.id, path: waypoints });\n\t\t\t\t\t\tif (waypoints.length === 0) continue;\n\n\t\t\t\t\t\tconst existingPath = ecs.getComponent(entity.id, 'path');\n\t\t\t\t\t\tif (existingPath) {\n\t\t\t\t\t\t\texistingPath.waypoints = waypoints;\n\t\t\t\t\t\t\texistingPath.currentIndex = 0;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'path');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'path', { waypoints, currentIndex: 0 });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst first = waypoints[0];\n\t\t\t\t\t\tif (!first) continue;\n\t\t\t\t\t\tconst existingMT = ecs.getComponent(entity.id, 'moveTarget');\n\t\t\t\t\t\tif (existingMT) {\n\t\t\t\t\t\t\texistingMT.x = first.x;\n\t\t\t\t\t\t\texistingMT.y = first.y;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'moveTarget');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'moveTarget', { x: first.x, y: first.y });\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('pathfinding-waypoint-advance')\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.setEventHandlers({\n\t\t\t\t\tarriveAtTarget({ data, ecs }) {\n\t\t\t\t\t\tconst path = ecs.getComponent(data.entityId, 'path');\n\t\t\t\t\t\tif (!path) return;\n\t\t\t\t\t\tconst next = path.currentIndex + 1;\n\t\t\t\t\t\tif (next >= path.waypoints.length) {\n\t\t\t\t\t\t\tecs.commands.removeComponent(data.entityId, 'path');\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpath.currentIndex = next;\n\t\t\t\t\t\tecs.markChanged(data.entityId, 'path');\n\t\t\t\t\t\tconst wp = path.waypoints[next];\n\t\t\t\t\t\tif (!wp) return;\n\t\t\t\t\t\t// Why: use command buffer so the add is queued AFTER steering's\n\t\t\t\t\t\t// queued `removeComponent('moveTarget')` in the same phase.\n\t\t\t\t\t\tecs.commands.addComponent(data.entityId, 'moveTarget', { x: wp.x, y: wp.y });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t});\n}\n",
|
|
6
|
+
"/**\n * Tilemap plugin for ECSpresso.\n *\n * Two ingestion paths share a common `LoadedTilemap` shape:\n * - `registerAsset` — load a Tiled `.tmj` file via the asset manager\n * - `registerRuntime` — pass a pre-built tile-id array (procedural)\n *\n * Query methods (`isSolid`, `isOpaque`, `isWalkable`) read from `tileMetadata`\n * regardless of source.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type ECSpresso from 'ecspresso';\nimport type { WorldConfigFrom, EmptyConfig } from '../../type-utils';\nimport { createNavGrid, type NavGrid } from '../ai/pathfinding';\nimport type { Vector2D } from '../../utils/math';\nimport type { LocalTransform, WorldTransform } from '../spatial/transform';\nimport type { AABBCollider, CollisionLayer } from '../physics/collision';\n\nexport const TILE_FLIP_HORIZONTAL = 0x80000000;\nexport const TILE_FLIP_VERTICAL = 0x40000000;\nexport const TILE_FLIP_DIAGONAL = 0x20000000;\nexport const TILE_GID_MASK = 0x1fffffff;\n\nexport interface DecodedGid {\n\tid: number;\n\tflipH: boolean;\n\tflipV: boolean;\n\tflipD: boolean;\n}\n\nexport function decodeGid(gid: number): DecodedGid {\n\treturn {\n\t\tid: (gid & TILE_GID_MASK) >>> 0,\n\t\tflipH: (gid & TILE_FLIP_HORIZONTAL) !== 0,\n\t\tflipV: (gid & TILE_FLIP_VERTICAL) !== 0,\n\t\tflipD: (gid & TILE_FLIP_DIAGONAL) !== 0,\n\t};\n}\n\n/** The three tile flag keys the query API understands. Custom keys flow through unchanged. */\nexport type TileFlag = 'solid' | 'blocksSight' | 'walkable';\n\n/** Tile metadata. Known flag keys drive query methods; arbitrary custom keys are preserved. */\nexport interface TileMetadata {\n\tsolid?: boolean;\n\tblocksSight?: boolean;\n\twalkable?: boolean;\n\t[key: string]: unknown;\n}\n\nexport interface ObjectDef {\n\tname: string;\n\ttype: string;\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\trotation: number;\n\tproperties: Record<string, string | number | boolean>;\n}\n\nexport interface RuntimeTileset {\n\ttextureKey: string;\n\tcolumns: number;\n\ttileWidth: number;\n\ttileHeight: number;\n\tfirstgid?: number;\n}\n\nexport interface RuntimeLayer {\n\tname: string;\n\ttiles: Uint32Array | Uint8Array | readonly number[];\n\tparallax?: Vector2D;\n\topacity?: number;\n\tvisible?: boolean;\n}\n\nexport interface TilemapRuntimeData {\n\twidth: number;\n\theight: number;\n\ttileSize: number;\n\tlayers: readonly RuntimeLayer[];\n\ttilesets: readonly RuntimeTileset[];\n\ttileMetadata?: Record<number, TileMetadata>;\n\tobjectLayers?: readonly { name: string; objects: readonly ObjectDef[] }[];\n}\n\nexport interface LoadedLayer {\n\tname: string;\n\ttiles: Uint32Array;\n\tparallax: Vector2D;\n\topacity: number;\n\tvisible: boolean;\n}\n\nexport interface LoadedTileset {\n\ttextureKey: string;\n\tcolumns: number;\n\ttileWidth: number;\n\ttileHeight: number;\n\tfirstgid: number;\n}\n\nexport interface LoadedObjectLayer {\n\tname: string;\n\tobjects: readonly ObjectDef[];\n}\n\nexport interface LoadedTilemap {\n\treadonly width: number;\n\treadonly height: number;\n\treadonly tileWidth: number;\n\treadonly tileHeight: number;\n\treadonly layers: readonly LoadedLayer[];\n\treadonly tilesets: readonly LoadedTileset[];\n\treadonly tileMetadata: ReadonlyMap<number, TileMetadata>;\n\treadonly objectLayers: readonly LoadedObjectLayer[];\n\n\ttileToWorld(tx: number, ty: number): Vector2D;\n\tworldToTile(wx: number, wy: number): { tx: number; ty: number };\n\tgetTile(layerIndex: number, tx: number, ty: number): number;\n\tisSolid(tx: number, ty: number): boolean;\n\tisOpaque(tx: number, ty: number): boolean;\n\tisWalkable(tx: number, ty: number): boolean;\n\tbuildNavGrid(layerIndex: number, costFn?: (tileId: number) => number): NavGrid;\n\tgetObjectLayer(name: string): readonly ObjectDef[];\n\tgetObjects(type: string): readonly ObjectDef[];\n}\n\n/** Subset of a Tiled `.tmj` (JSON) document we consume in v1. */\nexport interface TiledMap {\n\twidth: number;\n\theight: number;\n\ttilewidth: number;\n\ttileheight: number;\n\ttilesets: readonly TiledTileset[];\n\tlayers: readonly TiledLayer[];\n}\n\nexport interface TiledTileset {\n\tfirstgid: number;\n\tcolumns: number;\n\ttilewidth: number;\n\ttileheight: number;\n\timage: string;\n\timagewidth: number;\n\timageheight: number;\n\ttiles?: readonly TiledTileDef[];\n}\n\nexport interface TiledTileDef {\n\tid: number;\n\tproperties?: readonly TiledProperty[];\n\tanimation?: readonly { tileid: number; duration: number }[];\n}\n\nexport interface TiledProperty {\n\tname: string;\n\ttype: 'bool' | 'int' | 'float' | 'string' | 'color' | 'file' | 'object';\n\tvalue: string | number | boolean;\n}\n\nexport type TiledLayer = TiledTileLayer | TiledObjectLayer;\n\nexport interface TiledTileLayer {\n\ttype: 'tilelayer';\n\tname: string;\n\twidth: number;\n\theight: number;\n\tdata: readonly number[];\n\topacity: number;\n\tvisible: boolean;\n\tparallaxx?: number;\n\tparallaxy?: number;\n}\n\nexport interface TiledObjectLayer {\n\ttype: 'objectgroup';\n\tname: string;\n\tobjects: readonly TiledObject[];\n}\n\nexport interface TiledObject {\n\tid: number;\n\tname?: string;\n\ttype?: string;\n\tx: number;\n\ty: number;\n\twidth?: number;\n\theight?: number;\n\trotation?: number;\n\tproperties?: readonly TiledProperty[];\n}\n\nexport interface ParseTiledOptions {\n\ttilesetTextures: Record<string, string>;\n}\n\nexport type TilemapCullingMode = 'viewport' | 'none';\n\nexport interface TilemapLayerComponent {\n\tdataKey: string;\n\ttilesetKey?: string;\n\tlayerIndex: number;\n\topacity: number;\n\tparallax: Vector2D;\n\tcullingMode: TilemapCullingMode;\n\tcameraRef?: number;\n\ttintFn?: (tx: number, ty: number) => number | null;\n}\n\nexport interface TilemapColliderTag {\n\tdataKey: string;\n}\n\nexport interface TilemapComponentTypes {\n\ttilemap: TilemapLayerComponent;\n\ttilemapCollider: TilemapColliderTag;\n}\n\nexport interface TilemapRegistry {\n\tregisterRuntime(dataKey: string, data: TilemapRuntimeData): LoadedTilemap;\n\tregisterAsset(dataKey: string, assetKey: string, options?: ParseTiledOptions): Promise<LoadedTilemap>;\n\tget(dataKey: string): LoadedTilemap | undefined;\n\thas(dataKey: string): boolean;\n\treadonly entries: ReadonlyMap<string, LoadedTilemap>;\n}\n\nexport interface TilemapResourceTypes {\n\ttilemaps: TilemapRegistry;\n}\n\nexport type TilemapWorldConfig = WorldConfigFrom<TilemapComponentTypes, EmptyConfig['events'], TilemapResourceTypes>;\n\nexport interface TilemapPluginOptions<G extends string = 'rendering'> extends BasePluginOptions<G> {\n\t/** Optional collision layer name. When set, solid tiles auto-spawn `aabbCollider` strips. */\n\tcollisionLayer?: string;\n\t/** Layers the auto-generated tile bodies collide with. */\n\tcollidesWith?: readonly string[];\n}\n\nfunction toTilesArray(input: RuntimeLayer['tiles']): Uint32Array {\n\treturn Uint32Array.from(input);\n}\n\nfunction tiledPropsToRecord(props?: readonly TiledProperty[]): Record<string, string | number | boolean> {\n\tif (!props) return {};\n\tconst out: Record<string, string | number | boolean> = {};\n\tfor (const p of props) out[p.name] = p.value;\n\treturn out;\n}\n\nexport function createLoadedTilemap(data: TilemapRuntimeData): LoadedTilemap {\n\tconst { width, height, tileSize, layers, tilesets } = data;\n\n\tif (!Number.isFinite(width) || width <= 0) {\n\t\tthrow new Error(`tilemap: width must be > 0, got ${width}`);\n\t}\n\tif (!Number.isFinite(height) || height <= 0) {\n\t\tthrow new Error(`tilemap: height must be > 0, got ${height}`);\n\t}\n\tif (!Number.isFinite(tileSize) || tileSize <= 0) {\n\t\tthrow new Error(`tilemap: tileSize must be > 0, got ${tileSize}`);\n\t}\n\tif (tilesets.length === 0) {\n\t\tthrow new Error('tilemap: at least one tileset is required');\n\t}\n\n\tconst expectedLen = width * height;\n\n\tconst loadedLayers: LoadedLayer[] = layers.map((l) => {\n\t\tconst tiles = toTilesArray(l.tiles);\n\t\tif (tiles.length !== expectedLen) {\n\t\t\tthrow new Error(\n\t\t\t\t`tilemap: layer \"${l.name}\" tile count ${tiles.length} does not match width*height ${expectedLen}`,\n\t\t\t);\n\t\t}\n\t\treturn {\n\t\t\tname: l.name,\n\t\t\ttiles,\n\t\t\tparallax: l.parallax ? { x: l.parallax.x, y: l.parallax.y } : { x: 1, y: 1 },\n\t\t\topacity: l.opacity ?? 1,\n\t\t\tvisible: l.visible ?? true,\n\t\t};\n\t});\n\n\tconst loadedTilesets: LoadedTileset[] = tilesets.map((t, i) => {\n\t\tif (t.firstgid === undefined && i > 0) {\n\t\t\tthrow new Error(`tilemap: runtime tileset at index ${i} (\"${t.textureKey}\") must specify an explicit firstgid`);\n\t\t}\n\t\treturn {\n\t\t\ttextureKey: t.textureKey,\n\t\t\tcolumns: t.columns,\n\t\t\ttileWidth: t.tileWidth,\n\t\t\ttileHeight: t.tileHeight,\n\t\t\tfirstgid: t.firstgid ?? 1,\n\t\t};\n\t});\n\n\tconst metadataMap = new Map<number, TileMetadata>();\n\tif (data.tileMetadata) {\n\t\tfor (const [k, v] of Object.entries(data.tileMetadata)) {\n\t\t\tmetadataMap.set(Number(k), { ...v });\n\t\t}\n\t}\n\n\tconst objectLayers: LoadedObjectLayer[] = (data.objectLayers ?? []).map(l => ({\n\t\tname: l.name,\n\t\tobjects: l.objects.map(o => ({ ...o, properties: { ...o.properties } })),\n\t}));\n\n\treturn buildLoadedTilemap({\n\t\twidth,\n\t\theight,\n\t\ttileWidth: tileSize,\n\t\ttileHeight: tileSize,\n\t\tlayers: loadedLayers,\n\t\ttilesets: loadedTilesets,\n\t\ttileMetadata: metadataMap,\n\t\tobjectLayers,\n\t});\n}\n\nexport function parseTiledJSON(map: TiledMap, options: ParseTiledOptions): LoadedTilemap {\n\tconst { width, height, tilewidth, tileheight } = map;\n\tconst expectedLen = width * height;\n\n\tconst loadedTilesets: LoadedTileset[] = map.tilesets.map((t) => {\n\t\tconst textureKey = options.tilesetTextures[t.image];\n\t\tif (!textureKey) {\n\t\t\tthrow new Error(`tilemap: no texture key registered for tileset image \"${t.image}\"`);\n\t\t}\n\t\treturn {\n\t\t\ttextureKey,\n\t\t\tcolumns: t.columns,\n\t\t\ttileWidth: t.tilewidth,\n\t\t\ttileHeight: t.tileheight,\n\t\t\tfirstgid: t.firstgid,\n\t\t};\n\t});\n\n\tconst metadataMap = new Map<number, TileMetadata>();\n\tfor (const ts of map.tilesets) {\n\t\tif (!ts.tiles) continue;\n\t\tfor (const td of ts.tiles) {\n\t\t\tmetadataMap.set(ts.firstgid + td.id, tiledPropsToRecord(td.properties) as TileMetadata);\n\t\t}\n\t}\n\n\tconst loadedLayers: LoadedLayer[] = [];\n\tconst loadedObjectLayers: LoadedObjectLayer[] = [];\n\tfor (const l of map.layers) {\n\t\tif (l.type === 'tilelayer') {\n\t\t\tif (l.data.length !== expectedLen) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`tilemap: layer \"${l.name}\" data length ${l.data.length} does not match map ${expectedLen}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tloadedLayers.push({\n\t\t\t\tname: l.name,\n\t\t\t\ttiles: Uint32Array.from(l.data),\n\t\t\t\tparallax: { x: l.parallaxx ?? 1, y: l.parallaxy ?? 1 },\n\t\t\t\topacity: l.opacity,\n\t\t\t\tvisible: l.visible,\n\t\t\t});\n\t\t} else {\n\t\t\tloadedObjectLayers.push({\n\t\t\t\tname: l.name,\n\t\t\t\tobjects: l.objects.map(o => ({\n\t\t\t\t\tname: o.name ?? '',\n\t\t\t\t\ttype: o.type ?? '',\n\t\t\t\t\tx: o.x,\n\t\t\t\t\ty: o.y,\n\t\t\t\t\twidth: o.width ?? 0,\n\t\t\t\t\theight: o.height ?? 0,\n\t\t\t\t\trotation: o.rotation ?? 0,\n\t\t\t\t\tproperties: tiledPropsToRecord(o.properties),\n\t\t\t\t})),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn buildLoadedTilemap({\n\t\twidth,\n\t\theight,\n\t\ttileWidth: tilewidth,\n\t\ttileHeight: tileheight,\n\t\tlayers: loadedLayers,\n\t\ttilesets: loadedTilesets,\n\t\ttileMetadata: metadataMap,\n\t\tobjectLayers: loadedObjectLayers,\n\t});\n}\n\ninterface LoadedTilemapState {\n\twidth: number;\n\theight: number;\n\ttileWidth: number;\n\ttileHeight: number;\n\tlayers: LoadedLayer[];\n\ttilesets: LoadedTileset[];\n\ttileMetadata: Map<number, TileMetadata>;\n\tobjectLayers: LoadedObjectLayer[];\n}\n\nfunction buildLoadedTilemap(state: LoadedTilemapState): LoadedTilemap {\n\tconst { width, height, tileWidth, tileHeight, layers, tilesets, tileMetadata, objectLayers } = state;\n\n\t// Why: hot path. Inlining the bounds + cellIndex math here avoids\n\t// per-layer recomputation in the loop below (called by FOV / pathfinding consumers).\n\tconst flagAtAnyLayer = (tx: number, ty: number, key: TileFlag): boolean => {\n\t\tif (tx < 0 || ty < 0 || tx >= width || ty >= height) return false;\n\t\tconst idx = ty * width + tx;\n\t\tfor (let i = 0; i < layers.length; i++) {\n\t\t\tconst gid = (layers[i]!.tiles[idx] ?? 0) & TILE_GID_MASK;\n\t\t\tif (gid === 0) continue;\n\t\t\tconst meta = tileMetadata.get(gid);\n\t\t\tif (meta && meta[key] === true) return true;\n\t\t}\n\t\treturn false;\n\t};\n\n\tconst defaultCostFromMetadata = (gid: number): number => {\n\t\tconst meta = tileMetadata.get(gid);\n\t\treturn meta?.walkable === true ? 1 : 0;\n\t};\n\n\treturn {\n\t\twidth,\n\t\theight,\n\t\ttileWidth,\n\t\ttileHeight,\n\t\tlayers,\n\t\ttilesets,\n\t\ttileMetadata,\n\t\tobjectLayers,\n\n\t\ttileToWorld(tx, ty) {\n\t\t\treturn { x: (tx + 0.5) * tileWidth, y: (ty + 0.5) * tileHeight };\n\t\t},\n\n\t\tworldToTile(wx, wy) {\n\t\t\treturn { tx: Math.floor(wx / tileWidth), ty: Math.floor(wy / tileHeight) };\n\t\t},\n\n\t\tgetTile(layerIndex, tx, ty) {\n\t\t\tconst layer = layers[layerIndex];\n\t\t\tif (!layer) return 0;\n\t\t\tif (tx < 0 || ty < 0 || tx >= width || ty >= height) return 0;\n\t\t\treturn (layer.tiles[ty * width + tx] ?? 0) & TILE_GID_MASK;\n\t\t},\n\n\t\tisSolid: (tx, ty) => flagAtAnyLayer(tx, ty, 'solid'),\n\t\tisOpaque: (tx, ty) => flagAtAnyLayer(tx, ty, 'blocksSight'),\n\t\tisWalkable: (tx, ty) => flagAtAnyLayer(tx, ty, 'walkable'),\n\n\t\tbuildNavGrid(layerIndex, costFn) {\n\t\t\tconst layer = layers[layerIndex];\n\t\t\tif (!layer) {\n\t\t\t\tthrow new Error(`tilemap: buildNavGrid — no layer at index ${layerIndex}`);\n\t\t\t}\n\t\t\tconst cells = new Uint8Array(width * height);\n\t\t\tconst cost = costFn ?? defaultCostFromMetadata;\n\t\t\tfor (let i = 0; i < cells.length; i++) {\n\t\t\t\tconst gid = (layer.tiles[i] ?? 0) & TILE_GID_MASK;\n\t\t\t\tconst c = cost(gid) | 0;\n\t\t\t\tcells[i] = c < 0 ? 0 : c > 255 ? 255 : c;\n\t\t\t}\n\t\t\treturn createNavGrid({ width, height, cellSize: tileWidth, cells });\n\t\t},\n\n\t\tgetObjectLayer(name) {\n\t\t\treturn objectLayers.find(l => l.name === name)?.objects ?? [];\n\t\t},\n\n\t\tgetObjects(type) {\n\t\t\tconst out: ObjectDef[] = [];\n\t\t\tfor (const layer of objectLayers) {\n\t\t\t\tfor (const o of layer.objects) {\n\t\t\t\t\tif (o.type === type) out.push(o);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out;\n\t\t},\n\t};\n}\n\ninterface CollisionStrip {\n\ttx: number;\n\tty: number;\n\ttw: number;\n\tth: number;\n}\n\n/** Greedy row-first scan: each row produces N horizontal strips. No vertical merge in v1. */\nfunction buildCollisionStrips(map: LoadedTilemap): CollisionStrip[] {\n\tconst strips: CollisionStrip[] = [];\n\tfor (let ty = 0; ty < map.height; ty++) {\n\t\tlet runStart = -1;\n\t\tfor (let tx = 0; tx <= map.width; tx++) {\n\t\t\tconst solid = tx < map.width && map.isSolid(tx, ty);\n\t\t\tif (solid && runStart === -1) {\n\t\t\t\trunStart = tx;\n\t\t\t} else if (!solid && runStart !== -1) {\n\t\t\t\tstrips.push({ tx: runStart, ty, tw: tx - runStart, th: 1 });\n\t\t\t\trunStart = -1;\n\t\t\t}\n\t\t}\n\t}\n\treturn strips;\n}\n\n// Component shape for the optional collision-strip spawn. The plugin doesn't\n// declare a hard requirement on the collision/transform plugins so users who\n// don't enable `collisionLayer` aren't forced to install them; the cast at the\n// spawn site is the bridge. When `collisionLayer` is set the user has installed\n// both plugins by definition (otherwise the layer name would be meaningless).\ninterface CollisionSpawnShape {\n\ttilemapCollider: TilemapColliderTag;\n\taabbCollider: AABBCollider;\n\tcollisionLayer: CollisionLayer<string>;\n\tlocalTransform: LocalTransform;\n\tworldTransform: WorldTransform;\n}\n\ntype TilemapWorld = ECSpresso<TilemapWorldConfig>;\n\ntype TilemapLabels = never;\n\nexport function createTilemapPlugin<G extends string = 'rendering'>(\n\toptions: TilemapPluginOptions<G> = {},\n) {\n\tconst { collisionLayer, collidesWith } = options;\n\n\treturn definePlugin('tilemap')\n\t\t.withComponentTypes<TilemapComponentTypes>()\n\t\t.withResourceTypes<TilemapResourceTypes>()\n\t\t.withLabels<TilemapLabels>()\n\t\t.withGroups<G>()\n\t\t.install((world: TilemapWorld) => {\n\t\t\tconst entries = new Map<string, LoadedTilemap>();\n\t\t\tconst colliderEntitiesByKey = new Map<string, number[]>();\n\n\t\t\tconst despawnCollidersFor = (dataKey: string): void => {\n\t\t\t\tconst ids = colliderEntitiesByKey.get(dataKey);\n\t\t\t\tif (!ids) return;\n\t\t\t\tfor (const id of ids) world.removeEntity(id);\n\t\t\t\tcolliderEntitiesByKey.delete(dataKey);\n\t\t\t};\n\n\t\t\tconst spawnCollisionStripsFor = (dataKey: string, lt: LoadedTilemap): void => {\n\t\t\t\tif (!collisionLayer) return;\n\t\t\t\tconst ids: number[] = [];\n\t\t\t\tfor (const s of buildCollisionStrips(lt)) {\n\t\t\t\t\tconst cx = (s.tx + s.tw / 2) * lt.tileWidth;\n\t\t\t\t\tconst cy = (s.ty + s.th / 2) * lt.tileHeight;\n\t\t\t\t\tconst components: CollisionSpawnShape = {\n\t\t\t\t\t\ttilemapCollider: { dataKey },\n\t\t\t\t\t\taabbCollider: { width: s.tw * lt.tileWidth, height: s.th * lt.tileHeight },\n\t\t\t\t\t\tcollisionLayer: { layer: collisionLayer, collidesWith: collidesWith ?? [] },\n\t\t\t\t\t\tlocalTransform: { x: cx, y: cy, rotation: 0, scaleX: 1, scaleY: 1 },\n\t\t\t\t\t\tworldTransform: { x: cx, y: cy, rotation: 0, scaleX: 1, scaleY: 1 },\n\t\t\t\t\t};\n\t\t\t\t\tconst entity = (world as unknown as ECSpresso<WorldConfigFrom<CollisionSpawnShape>>).spawn(components);\n\t\t\t\t\tids.push(entity.id);\n\t\t\t\t}\n\t\t\t\tif (ids.length > 0) colliderEntitiesByKey.set(dataKey, ids);\n\t\t\t};\n\n\t\t\tconst ingest = (dataKey: string, lt: LoadedTilemap): LoadedTilemap => {\n\t\t\t\tdespawnCollidersFor(dataKey);\n\t\t\t\tentries.set(dataKey, lt);\n\t\t\t\tspawnCollisionStripsFor(dataKey, lt);\n\t\t\t\treturn lt;\n\t\t\t};\n\n\t\t\tconst registry: TilemapRegistry = {\n\t\t\t\tentries,\n\t\t\t\tregisterRuntime(dataKey, data) {\n\t\t\t\t\treturn ingest(dataKey, createLoadedTilemap(data));\n\t\t\t\t},\n\t\t\t\tasync registerAsset(dataKey, assetKey, parseOptions) {\n\t\t\t\t\tconst raw = await (world as unknown as ECSpresso<WorldConfigFrom<EmptyConfig['components'], EmptyConfig['events'], EmptyConfig['resources'], { [k: string]: TiledMap }>>).loadAsset(assetKey) as TiledMap;\n\t\t\t\t\treturn ingest(dataKey, parseTiledJSON(raw, parseOptions ?? { tilesetTextures: {} }));\n\t\t\t\t},\n\t\t\t\tget: (dataKey) => entries.get(dataKey),\n\t\t\t\thas: (dataKey) => entries.has(dataKey),\n\t\t\t};\n\n\t\t\tworld.addResource('tilemaps', registry);\n\t\t});\n}\n\n/**\n * Create a `tilemap` layer component for spreading into `spawn()`.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createTilemapLayer('dungeon', 0),\n * ...createLocalTransform(0, 0),\n * });\n * ```\n */\nexport function createTilemapLayer(\n\tdataKey: string,\n\tlayerIndex: number,\n\toptions?: {\n\t\ttilesetKey?: string;\n\t\topacity?: number;\n\t\tparallax?: Vector2D;\n\t\tcullingMode?: TilemapCullingMode;\n\t\tcameraRef?: number;\n\t\ttintFn?: (tx: number, ty: number) => number | null;\n\t},\n): Pick<TilemapComponentTypes, 'tilemap'> {\n\treturn {\n\t\ttilemap: {\n\t\t\tdataKey,\n\t\t\tlayerIndex,\n\t\t\ttilesetKey: options?.tilesetKey,\n\t\t\topacity: options?.opacity ?? 1,\n\t\t\tparallax: options?.parallax ?? { x: 1, y: 1 },\n\t\t\tcullingMode: options?.cullingMode ?? 'viewport',\n\t\t\tcameraRef: options?.cameraRef,\n\t\t\ttintFn: options?.tintFn,\n\t\t},\n\t};\n}\n"
|
|
7
|
+
],
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "1B20DC0974BC7FAF64756E2164756E21",
|
|
10
|
+
"names": []
|
|
11
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Camera 3D Plugin for ECSpresso
|
|
3
3
|
*
|
|
4
|
-
* Orbit/follow/shake camera controls for a Three.js PerspectiveCamera
|
|
5
|
-
* Purely resource-based (no camera
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Orbit/follow/shake camera controls for a Three.js PerspectiveCamera or
|
|
5
|
+
* OrthographicCamera managed by renderer3D. Purely resource-based (no camera
|
|
6
|
+
* entity). The renderer3D `camera` resource is the single camera target.
|
|
7
|
+
* Orbit via pointer drag + scroll wheel, follow via entity tracking, shake
|
|
8
|
+
* via trauma-based offsets.
|
|
9
|
+
*
|
|
10
|
+
* The plugin's `projection` option must match the underlying camera's kind;
|
|
11
|
+
* a mismatch throws at init. State is a discriminated union — perspective
|
|
12
|
+
* cameras expose `fov` / `setFov`, orthographic cameras expose `zoom` / `setZoom`.
|
|
8
13
|
*
|
|
9
14
|
* Import from 'ecspresso/plugins/spatial/camera3D'
|
|
10
15
|
*/
|
|
@@ -25,14 +30,13 @@ export interface Camera3DShakeOptions {
|
|
|
25
30
|
maxOffsetY?: number;
|
|
26
31
|
maxOffsetZ?: number;
|
|
27
32
|
}
|
|
28
|
-
export interface
|
|
33
|
+
export interface Camera3DBaseState {
|
|
29
34
|
targetX: number;
|
|
30
35
|
targetY: number;
|
|
31
36
|
targetZ: number;
|
|
32
37
|
azimuth: number;
|
|
33
38
|
elevation: number;
|
|
34
39
|
distance: number;
|
|
35
|
-
fov: number;
|
|
36
40
|
followTarget: number;
|
|
37
41
|
followSmoothing: number;
|
|
38
42
|
followOffsetX: number;
|
|
@@ -49,14 +53,24 @@ export interface Camera3DState {
|
|
|
49
53
|
setTarget(x: number, y: number, z: number): void;
|
|
50
54
|
setOrbit(azimuth: number, elevation: number, distance: number): void;
|
|
51
55
|
setDistance(distance: number): void;
|
|
52
|
-
setFov(fov: number): void;
|
|
53
56
|
addTrauma(amount: number): void;
|
|
54
57
|
}
|
|
58
|
+
export interface PerspectiveCamera3DState extends Camera3DBaseState {
|
|
59
|
+
projection: 'perspective';
|
|
60
|
+
fov: number;
|
|
61
|
+
setFov(fov: number): void;
|
|
62
|
+
}
|
|
63
|
+
export interface OrthographicCamera3DState extends Camera3DBaseState {
|
|
64
|
+
projection: 'orthographic';
|
|
65
|
+
zoom: number;
|
|
66
|
+
setZoom(zoom: number): void;
|
|
67
|
+
}
|
|
68
|
+
export type Camera3DState = PerspectiveCamera3DState | OrthographicCamera3DState;
|
|
55
69
|
export interface Camera3DResourceTypes {
|
|
56
70
|
camera3DState: Camera3DState;
|
|
57
71
|
}
|
|
58
72
|
export type Camera3DWorldConfig = WorldConfigFrom<{}, {}, Camera3DResourceTypes>;
|
|
59
|
-
export interface
|
|
73
|
+
export interface Camera3DBasePluginOptions<G extends string = 'camera3d'> {
|
|
60
74
|
systemGroup?: G;
|
|
61
75
|
phase?: SystemPhase;
|
|
62
76
|
azimuth?: number;
|
|
@@ -67,7 +81,6 @@ export interface Camera3DPluginOptions<G extends string = 'camera3d'> {
|
|
|
67
81
|
y: number;
|
|
68
82
|
z: number;
|
|
69
83
|
};
|
|
70
|
-
fov?: number;
|
|
71
84
|
minDistance?: number;
|
|
72
85
|
maxDistance?: number;
|
|
73
86
|
minElevation?: number;
|
|
@@ -78,6 +91,13 @@ export interface Camera3DPluginOptions<G extends string = 'camera3d'> {
|
|
|
78
91
|
shake?: boolean | Partial<Camera3DShakeOptions>;
|
|
79
92
|
randomFn?: () => number;
|
|
80
93
|
}
|
|
94
|
+
export type Camera3DPluginOptions<G extends string = 'camera3d'> = Camera3DBasePluginOptions<G> & ({
|
|
95
|
+
projection?: 'perspective';
|
|
96
|
+
fov?: number;
|
|
97
|
+
} | {
|
|
98
|
+
projection: 'orthographic';
|
|
99
|
+
zoom?: number;
|
|
100
|
+
});
|
|
81
101
|
export type Camera3DLabels = 'camera3d-init' | 'camera3d-follow' | 'camera3d-shake' | 'camera3d-sync';
|
|
82
102
|
/**
|
|
83
103
|
* Convert spherical coordinates to cartesian. Y-up convention (Three.js default).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var t=Object.defineProperty;var e=(J)=>J;function a(J,$){this[J]=e.bind(null,$)}var Jq=(J,$)=>{for(var R in $)t(J,R,{get:$[R],enumerable:!0,configurable:!0,set:a.bind($,R)})};var Qq=(J,$)=>()=>(J&&($=J(J=0)),$);var $q=((J)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(J,{get:($,R)=>(typeof require<"u"?require:$)[R]}):J)(function(J){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+J+'" is not supported')});import{definePlugin as qq}from"ecspresso";var G={smoothing:5,offsetX:0,offsetY:0,offsetZ:0},H={traumaDecay:1,maxOffsetX:0.3,maxOffsetY:0.3,maxOffsetZ:0.3},O=Math.PI/2,E=0.001,N={x:0,y:0,z:0};function K(J,$,R){return Math.max($,Math.min(R,J))}function Bq(J){if(J===!0)return{...H};return{traumaDecay:J.traumaDecay??H.traumaDecay,maxOffsetX:J.maxOffsetX??H.maxOffsetX,maxOffsetY:J.maxOffsetY??H.maxOffsetY,maxOffsetZ:J.maxOffsetZ??H.maxOffsetZ}}function L(J,$,R,W){let z=Math.cos($);W.x=R*z*Math.sin(J),W.y=R*Math.sin($),W.z=R*z*Math.cos(J)}function Vq(J){let{systemGroup:$="camera3d",phase:R="postUpdate",azimuth:W=0,elevation:z=0.5,distance:f=10,target:S,minDistance:k=1,maxDistance:x=100,minElevation:U=-O+E,maxElevation:u=O-E,orbitSensitivity:D=0.003,dollySensitivity:C=1.1,follow:b,shake:T,randomFn:w=Math.random}=J??{},p=J?.projection??"perspective",g=J?.projection!=="orthographic"?J?.fov??75:75,h=J?.projection==="orthographic"?J.zoom??1:1,y=T?Bq(T):H,l=y.traumaDecay,m=y.maxOffsetX,d=y.maxOffsetY,c=y.maxOffsetZ,n={targetX:S?.x??0,targetY:S?.y??0,targetZ:S?.z??0,azimuth:W,elevation:K(z,U,u),distance:K(f,k,x),followTarget:-1,followSmoothing:b?.smoothing??G.smoothing,followOffsetX:b?.offsetX??G.offsetX,followOffsetY:b?.offsetY??G.offsetY,followOffsetZ:b?.offsetZ??G.offsetZ,trauma:0,shakeOffsetX:0,shakeOffsetY:0,shakeOffsetZ:0},i={follow(j,B){let I=typeof j==="number"?j:j.id;this.followTarget=I,this.followSmoothing=B?.smoothing??b?.smoothing??G.smoothing,this.followOffsetX=B?.offsetX??b?.offsetX??G.offsetX,this.followOffsetY=B?.offsetY??b?.offsetY??G.offsetY,this.followOffsetZ=B?.offsetZ??b?.offsetZ??G.offsetZ},unfollow(){this.followTarget=-1},setTarget(j,B,I){this.targetX=j,this.targetY=B,this.targetZ=I},setOrbit(j,B,I){this.azimuth=j,this.elevation=K(B,U,u),this.distance=K(I,k,x)},setDistance(j){this.distance=K(j,k,x)},addTrauma(j){this.trauma=K(this.trauma+j,0,1)}};return qq("camera3d").withResourceTypes().withLabels().withGroups().requires().install((j)=>{let B={active:!1,prevX:0,prevY:0,pendingDolly:0,el:null},q={...n,...i,...p==="orthographic"?{projection:"orthographic",zoom:h,setZoom(Q){this.zoom=Q}}:{projection:"perspective",fov:g,setFov(Q){this.fov=Q}}};j.addResource("camera3DState",q);function _(Q){B.active=!0,B.prevX=Q.clientX,B.prevY=Q.clientY,B.el?.setPointerCapture(Q.pointerId)}function v(Q){if(!B.active)return;let Z=Q.clientX-B.prevX,M=Q.clientY-B.prevY;B.prevX=Q.clientX,B.prevY=Q.clientY,q.azimuth-=Z*D,q.elevation=K(q.elevation+M*D,U,u)}function P(Q){B.active=!1,B.el?.releasePointerCapture(Q.pointerId)}function F(Q){Q.preventDefault(),B.pendingDolly+=Math.sign(Q.deltaY)}let V=null,X=null,Y=null;j.addSystem("camera3d-init").inGroup($).setOnInitialize((Q)=>{let Z=Q.getResource("threeRenderer");if(V=Q.getResource("camera"),V.isPerspectiveCamera)X=V;else if(V.isOrthographicCamera)Y=V;if(q.projection==="perspective"&&!X)throw Error("createCamera3DPlugin: configured as 'perspective' but the renderer's camera is not a PerspectiveCamera.");if(q.projection==="orthographic"&&!Y)throw Error("createCamera3DPlugin: configured as 'orthographic' but the renderer's camera is not an OrthographicCamera.");if(q.projection==="perspective"&&X)q.fov=X.fov;else if(q.projection==="orthographic"&&Y)q.zoom=Y.zoom;B.el=Z.domElement,B.el.addEventListener("pointerdown",_),B.el.addEventListener("pointermove",v),B.el.addEventListener("pointerup",P),B.el.addEventListener("wheel",F,{passive:!1}),L(q.azimuth,q.elevation,q.distance,N),V.position.set(q.targetX+N.x,q.targetY+N.y,q.targetZ+N.z),V.lookAt(q.targetX,q.targetY,q.targetZ)}).setOnDetach(()=>{if(!B.el)return;B.el.removeEventListener("pointerdown",_),B.el.removeEventListener("pointermove",v),B.el.removeEventListener("pointerup",P),B.el.removeEventListener("wheel",F),B.el=null,V=null,X=null,Y=null}),j.addSystem("camera3d-follow").setPriority(400).inPhase(R).inGroup($).setProcess(({ecs:Q,dt:Z})=>{if(q.followTarget<0)return;let M;try{M=Q.getComponent(q.followTarget,"worldTransform3D")}catch{q.followTarget=-1;return}if(!M)return;let r=M.x+q.followOffsetX,o=M.y+q.followOffsetY,s=M.z+q.followOffsetZ,A=Math.min(1,q.followSmoothing*Z);q.targetX+=(r-q.targetX)*A,q.targetY+=(o-q.targetY)*A,q.targetZ+=(s-q.targetZ)*A}),j.addSystem("camera3d-shake").setPriority(390).inPhase(R).inGroup($).setProcess(({dt:Q})=>{if(q.trauma<=0){q.shakeOffsetX=0,q.shakeOffsetY=0,q.shakeOffsetZ=0;return}q.trauma=Math.max(0,q.trauma-l*Q);let Z=q.trauma*q.trauma;q.shakeOffsetX=m*Z*(w()*2-1),q.shakeOffsetY=d*Z*(w()*2-1),q.shakeOffsetZ=c*Z*(w()*2-1)}),j.addSystem("camera3d-sync").setPriority(380).inPhase(R).inGroup($).setProcess(()=>{if(!V)return;if(B.pendingDolly!==0)q.distance=K(q.distance*Math.pow(C,B.pendingDolly),k,x),B.pendingDolly=0;if(L(q.azimuth,q.elevation,q.distance,N),V.position.set(q.targetX+N.x+q.shakeOffsetX,q.targetY+N.y+q.shakeOffsetY,q.targetZ+N.z+q.shakeOffsetZ),V.lookAt(q.targetX+q.shakeOffsetX,q.targetY+q.shakeOffsetY,q.targetZ+q.shakeOffsetZ),q.projection==="perspective"&&X&&X.fov!==q.fov)X.fov=q.fov,X.updateProjectionMatrix();else if(q.projection==="orthographic"&&Y&&Y.zoom!==q.zoom)Y.zoom=q.zoom,Y.updateProjectionMatrix()})})}export{L as sphericalToCartesian,Vq as createCamera3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=E9143DDF484E288364756E2164756E21
|
|
4
4
|
//# sourceMappingURL=camera3D.js.map
|