@@ -16,43 +16,89 @@ import { stepArcadeBody, moveRectWithTilemapCollision } from '/src/engine/system
1616import { createDemoSpriteSheet } from '../0301/demoSpriteFactory.js' ;
1717
1818const theme = new Theme ( ThemeTokens ) ;
19+ const TILEMAP_PRESET_PATH = '/samples/phase-03/0305/sample-0305-tile-map-editor.json' ;
20+
21+ const FALLBACK_TILE_ROWS = [
22+ [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] ,
23+ [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 , 0 , 0 , 0 , 0 , 1 ] ,
24+ [ 1 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 3 , 3 , 0 , 0 , 1 ] ,
25+ [ 1 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 3 , 3 , 0 , 0 , 1 ] ,
26+ [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ] ,
27+ [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 , 0 , 1 ] ,
28+ [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ]
29+ ] ;
30+
31+ const FALLBACK_TILE_METADATA_DEFINITIONS = Object . freeze ( {
32+ 0 : { label : 'floor' , color : '#1f2937' , solid : false } ,
33+ 1 : { label : 'wall' , color : '#4338ca' , solid : true } ,
34+ 2 : { label : 'hazard' , color : '#dc2626' , solid : false , hazard : true , respawnMessage : 'Hazard tile touched. Reset.' } ,
35+ 3 : { label : 'trigger' , color : '#059669' , solid : false , trigger : 'goal-flag' , message : 'Trigger tile activated.' } ,
36+ 4 : { label : 'slope' , color : '#a855f7' , solid : false , slope : 'placeholder-up-right' , message : 'Slope metadata placeholder.' }
37+ } ) ;
38+
39+ function cloneRows ( rows = [ ] ) {
40+ return rows . map ( ( row ) => Array . isArray ( row ) ? row . map ( ( value ) => Number . parseInt ( value , 10 ) || 0 ) : [ ] ) ;
41+ }
42+
43+ function buildCollisionRows ( rows = [ ] , definitions = { } ) {
44+ return rows . map ( ( row ) => row . map ( ( tileId ) => ( definitions [ tileId ] ?. solid ? 1 : 0 ) ) ) ;
45+ }
46+
47+ function createFallbackTilemapDocument ( ) {
48+ const rows = cloneRows ( FALLBACK_TILE_ROWS ) ;
49+ const definitions = JSON . parse ( JSON . stringify ( FALLBACK_TILE_METADATA_DEFINITIONS ) ) ;
50+ return {
51+ schema : 'toolbox.tilemap/1' ,
52+ version : 1 ,
53+ map : {
54+ name : 'sample-0305-tile-metadata' ,
55+ width : rows [ 0 ] ?. length || 14 ,
56+ height : rows . length || 7 ,
57+ tileSize : 48
58+ } ,
59+ tileset : Object . entries ( definitions ) . map ( ( [ id , entry ] ) => ( {
60+ id : Number ( id ) ,
61+ name : entry . label || `tile-${ id } ` ,
62+ color : entry . color || '#64748b'
63+ } ) ) ,
64+ layers : [
65+ {
66+ id : 'ground' ,
67+ name : 'Ground' ,
68+ kind : 'tile' ,
69+ visible : true ,
70+ locked : false ,
71+ data : rows
72+ } ,
73+ {
74+ id : 'collision' ,
75+ name : 'Collision' ,
76+ kind : 'collision' ,
77+ visible : true ,
78+ locked : false ,
79+ data : buildCollisionRows ( rows , definitions )
80+ } ,
81+ {
82+ id : 'data' ,
83+ name : 'Data' ,
84+ kind : 'data' ,
85+ visible : true ,
86+ locked : false ,
87+ data : rows . map ( ( row ) => row . map ( ( ) => 0 ) )
88+ }
89+ ] ,
90+ markers : [
91+ { id : 'spawn-1' , type : 'spawn' , name : 'player-start' , col : 12 , row : 5 , properties : { } }
92+ ] ,
93+ tileMetadataDefinitions : definitions
94+ } ;
95+ }
1996
2097export default class TileMetadataScene extends Scene {
2198 constructor ( ) {
2299 super ( ) ;
23100 this . screen = { x : 60 , y : 240 } ;
24101 this . loader = new ImageAssetLoader ( ) ;
25- this . tilemap = new Tilemap ( {
26- tileSize : 48 ,
27- tiles : [
28- [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] ,
29- [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 , 0 , 0 , 0 , 0 , 1 ] ,
30- [ 1 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 3 , 3 , 0 , 0 , 1 ] ,
31- [ 1 , 0 , 0 , 2 , 2 , 0 , 0 , 0 , 0 , 3 , 3 , 0 , 0 , 1 ] ,
32- [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 ] ,
33- [ 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 4 , 0 , 0 , 1 ] ,
34- [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] ,
35- ] ,
36- definitions : {
37- 0 : { label : 'floor' , color : '#1f2937' , solid : false } ,
38- 1 : { label : 'wall' , color : '#4338ca' , solid : true } ,
39- 2 : { label : 'hazard' , color : '#dc2626' , solid : false , hazard : true , respawnMessage : 'Hazard tile touched. Reset.' } ,
40- 3 : { label : 'trigger' , color : '#059669' , solid : false , trigger : 'goal-flag' , message : 'Trigger tile activated.' } ,
41- 4 : { label : 'slope' , color : '#a855f7' , solid : false , slope : 'placeholder-up-right' , message : 'Slope metadata placeholder.' } ,
42- } ,
43- palette : { 0 : '#1f2937' , 1 : '#4338ca' } ,
44- } ) ;
45- this . world = {
46- width : this . tilemap . width * this . tilemap . tileSize ,
47- height : this . tilemap . height * this . tilemap . tileSize ,
48- } ;
49- this . camera = new Camera2D ( {
50- viewportWidth : 860 ,
51- viewportHeight : 300 ,
52- worldWidth : this . world . width ,
53- worldHeight : this . world . height ,
54- } ) ;
55-
56102 this . atlas = new SpriteAtlas ( {
57103 frames : {
58104 idle_0 : { x : 0 , y : 0 , width : 16 , height : 16 } ,
@@ -61,7 +107,8 @@ export default class TileMetadataScene extends Scene {
61107 } ,
62108 } ) ;
63109 this . asset = { status : 'generated-loaded' , image : createDemoSpriteSheet ( ) } ;
64- //this.spawn = { x: 48, y: 48 };
110+ this . sampleStatus = 'Loading shared tilemap preset...' ;
111+ this . sampleError = '' ;
65112 this . spawn = { x : 582 , y : 246 } ;
66113 this . player = {
67114 x : this . spawn . x ,
@@ -87,6 +134,136 @@ export default class TileMetadataScene extends Scene {
87134 } ) ;
88135 this . metadataNote = 'Walk onto colored metadata tiles.' ;
89136 this . flags = { goalTriggered : false } ;
137+ this . applyTilemapDocument ( createFallbackTilemapDocument ( ) ) ;
138+ void this . loadTilemapPreset ( ) ;
139+ }
140+
141+ extractTileMapDocumentFromSamplePreset ( rawPreset ) {
142+ if ( ! rawPreset || typeof rawPreset !== 'object' ) {
143+ return null ;
144+ }
145+ const payload = rawPreset . payload ;
146+ if ( payload && typeof payload === 'object' ) {
147+ if ( payload . tilemapDocument && typeof payload . tilemapDocument === 'object' ) {
148+ return payload . tilemapDocument ;
149+ }
150+ if ( payload . tileMapDocument && typeof payload . tileMapDocument === 'object' ) {
151+ return payload . tileMapDocument ;
152+ }
153+ if ( payload . tilemap && typeof payload . tilemap === 'object' ) {
154+ return payload . tilemap ;
155+ }
156+ if ( payload . tileMap && typeof payload . tileMap === 'object' ) {
157+ return payload . tileMap ;
158+ }
159+ if ( typeof payload . tilemapDocumentPath === 'string' && payload . tilemapDocumentPath . trim ( ) ) {
160+ return payload . tilemapDocumentPath . trim ( ) ;
161+ }
162+ if ( typeof payload . tileMapDocumentPath === 'string' && payload . tileMapDocumentPath . trim ( ) ) {
163+ return payload . tileMapDocumentPath . trim ( ) ;
164+ }
165+ }
166+ return null ;
167+ }
168+
169+ applyTilemapDocument ( documentModel ) {
170+ const map = documentModel ?. map || { } ;
171+ const layers = Array . isArray ( documentModel ?. layers ) ? documentModel . layers : [ ] ;
172+ const tileLayer = layers . find ( ( layer ) => layer && layer . kind === 'tile' ) || layers [ 0 ] || null ;
173+ const tileRows = cloneRows ( tileLayer ?. data || [ ] ) ;
174+ if ( ! Array . isArray ( tileRows ) || tileRows . length === 0 ) {
175+ throw new Error ( 'Tilemap document did not include tile rows.' ) ;
176+ }
177+
178+ const rawDefinitions = documentModel ?. tileMetadataDefinitions
179+ && typeof documentModel . tileMetadataDefinitions === 'object'
180+ ? documentModel . tileMetadataDefinitions
181+ : FALLBACK_TILE_METADATA_DEFINITIONS ;
182+
183+ const definitions = { } ;
184+ const palette = { } ;
185+ Object . entries ( rawDefinitions ) . forEach ( ( [ key , raw ] ) => {
186+ const tileId = Number . parseInt ( key , 10 ) ;
187+ if ( ! Number . isInteger ( tileId ) || ! raw || typeof raw !== 'object' ) {
188+ return ;
189+ }
190+ definitions [ tileId ] = {
191+ label : typeof raw . label === 'string' ? raw . label : `tile-${ tileId } ` ,
192+ color : typeof raw . color === 'string' ? raw . color : '#64748b' ,
193+ solid : raw . solid === true ,
194+ hazard : raw . hazard === true ,
195+ trigger : typeof raw . trigger === 'string' ? raw . trigger : undefined ,
196+ message : typeof raw . message === 'string' ? raw . message : undefined ,
197+ respawnMessage : typeof raw . respawnMessage === 'string' ? raw . respawnMessage : undefined ,
198+ slope : typeof raw . slope === 'string' ? raw . slope : undefined
199+ } ;
200+ palette [ tileId ] = definitions [ tileId ] . color ;
201+ } ) ;
202+
203+ const tileSize = Number ( map . tileSize ) > 0 ? Number ( map . tileSize ) : 48 ;
204+ this . tilemap = new Tilemap ( {
205+ tileSize,
206+ tiles : tileRows ,
207+ definitions,
208+ palette
209+ } ) ;
210+
211+ this . world = {
212+ width : this . tilemap . width * this . tilemap . tileSize ,
213+ height : this . tilemap . height * this . tilemap . tileSize
214+ } ;
215+ this . camera = new Camera2D ( {
216+ viewportWidth : 860 ,
217+ viewportHeight : 300 ,
218+ worldWidth : this . world . width ,
219+ worldHeight : this . world . height
220+ } ) ;
221+
222+ const spawnMarker = Array . isArray ( documentModel ?. markers )
223+ ? documentModel . markers . find ( ( entry ) => entry && entry . type === 'spawn' )
224+ : null ;
225+ const spawnCol = Number . parseInt ( spawnMarker ?. col , 10 ) ;
226+ const spawnRow = Number . parseInt ( spawnMarker ?. row , 10 ) ;
227+ if ( Number . isInteger ( spawnCol ) && Number . isInteger ( spawnRow ) ) {
228+ this . spawn = {
229+ x : spawnCol * this . tilemap . tileSize + 6 ,
230+ y : spawnRow * this . tilemap . tileSize + 6
231+ } ;
232+ }
233+ this . resetPlayerToSpawn ( ) ;
234+ }
235+
236+ async loadTilemapPreset ( ) {
237+ try {
238+ const presetResponse = await fetch ( TILEMAP_PRESET_PATH , { cache : 'no-store' } ) ;
239+ if ( ! presetResponse . ok ) {
240+ throw new Error ( `Preset request failed (${ presetResponse . status } ).` ) ;
241+ }
242+ const rawPreset = await presetResponse . json ( ) ;
243+ const extracted = this . extractTileMapDocumentFromSamplePreset ( rawPreset ) ;
244+ let documentModel = null ;
245+
246+ if ( typeof extracted === 'string' && extracted . trim ( ) ) {
247+ const documentResponse = await fetch ( extracted . trim ( ) , { cache : 'no-store' } ) ;
248+ if ( ! documentResponse . ok ) {
249+ throw new Error ( `Tilemap document request failed (${ documentResponse . status } ).` ) ;
250+ }
251+ documentModel = await documentResponse . json ( ) ;
252+ } else if ( extracted && typeof extracted === 'object' ) {
253+ documentModel = extracted ;
254+ }
255+
256+ if ( ! documentModel || typeof documentModel !== 'object' ) {
257+ throw new Error ( 'Preset payload did not include a tilemap document.' ) ;
258+ }
259+
260+ this . applyTilemapDocument ( documentModel ) ;
261+ this . sampleStatus = 'Loaded tilemap preset from sample-0305-tile-map-editor.json' ;
262+ this . sampleError = '' ;
263+ } catch ( error ) {
264+ this . sampleStatus = 'Using fallback in-scene tilemap.' ;
265+ this . sampleError = error instanceof Error ? error . message : String ( error ) ;
266+ }
90267 }
91268
92269 getOverlappedMetadata ( ) {
@@ -169,10 +346,12 @@ export default class TileMetadataScene extends Scene {
169346 drawFrame ( renderer , theme , [
170347 'Engine sample 0305' ,
171348 'Extends the tilemap from solid/not-solid into hazard, trigger, and slope-style metadata' ,
349+ 'This sample and Tilemap Studio load the same sample-0305-tile-map-editor.json source' ,
172350 'Red tiles reset the actor (to spawn point)' ,
173351 'Green tiles trigger a flag,' ,
174352 'Purple tiles prove schema room for slopes' ,
175353 'Only blue wall tiles are solid blockers' ,
354+ this . sampleError ? `${ this . sampleStatus } (${ this . sampleError } )` : this . sampleStatus ,
176355 this . metadataNote ,
177356 ] ) ;
178357
0 commit comments