diff --git a/apps/cryptothrone.com/src/components/game/scene/SandCity.tsx b/apps/cryptothrone.com/src/components/game/scene/SandCity.tsx index 08c575a67..acc48d35a 100644 --- a/apps/cryptothrone.com/src/components/game/scene/SandCity.tsx +++ b/apps/cryptothrone.com/src/components/game/scene/SandCity.tsx @@ -1,191 +1,212 @@ import { Scene } from 'phaser'; import Phaser from 'phaser'; import { - Quadtree, - type Bounds, - type Range, - PlayerController, - eventEmitterInstance as EventEmitter, - type CharacterEventData, - notificationType, - ULIDFactory, - npcDatabase, - mapDatabase, - Debug, + Quadtree, + type Bounds, + type Range, + PlayerController, + eventEmitterInstance as EventEmitter, + type CharacterEventData, + notificationType, + ULIDFactory, + npcDatabase, + mapDatabase, + Debug, } from '@kbve/laser'; declare global { - interface Window { - __GRID_ENGINE__?: any; - } + interface Window { + __GRID_ENGINE__?: any; + } } export class SandCity extends Scene { - cursor: Phaser.Types.Input.Keyboard.CursorKeys | undefined; - gridEngine: any; - quadtree: Quadtree | undefined; - playerController: PlayerController | undefined; - - constructor() { - super({ key: 'SandCity' }); - } - - preload() { - EventEmitter.emit('notification', { - title: 'Success', - message: `You arrived safely to SandCity Passport: ${ULIDFactory().toString()}`, - notificationType: notificationType.success, - }); - } - - async create() { - let cloudCityTilemap: Phaser.Tilemaps.Tilemap | null = null; - - try { - cloudCityTilemap = await mapDatabase.loadMap(this, 'cloud-city-map'); - } catch (error) { - Debug.error('Failed to load map:', error); - return; - } - - if (!cloudCityTilemap) { - Debug.error('Tilemap could not be loaded.'); - return; - } - - const bounds = await mapDatabase.getBounds('cloud-city-map'); - if (bounds) { - this.quadtree = new Quadtree(bounds); - } else { - Debug.error('Bounds could not be retrieved.'); - return; - } - - const playerSprite = this.add.sprite(0, 0, 'player'); - playerSprite.scale = 1.5; - - const playerBounds = playerSprite.getBounds(); - - const targetX = playerBounds.centerX + (playerSprite.width * 3); - const targetY = playerBounds.centerY + (playerSprite.height * 3); - - this.cameras.main.pan(targetX, targetY, 1000, 'Power2'); - - this.cameras.main.once('camerapancomplete', () => { - this.cameras.main.startFollow(playerSprite, true); - this.cameras.main.setFollowOffset( - -playerSprite.width, - -playerSprite.height, - ); - }); - - const gridEngineConfig = { - characters: [ - { - id: 'player', - sprite: playerSprite, - walkingAnimationMapping: 6, - startPosition: { x: 5, y: 12 }, - }, - ], - numberOfDirections: 8, - }; - - this.gridEngine.create(cloudCityTilemap, gridEngineConfig); - this.loadRanges(); - - this.playerController = new PlayerController( - this, - this.gridEngine, - this.quadtree, - ); - - // Retrieve NPCs from mapDatabase - const npcs = await mapDatabase.getNpcsFromTilesetKey('cloud-city-map'); - - if (npcs) { - for (const npc of npcs) { - try { - await npcDatabase.loadCharacter( - this, - npc.ulid, - npc.position.x, - npc.position.y, - ); - } catch (error) { - Debug.error(`Failed to load NPC with ULID: ${npc.ulid}`, error); - } - } - } - - // await npcDatabase.loadCharacter(this, '01J2DT4G871KJ0VNSHCNC5REDM', 6, 6); - // await npcDatabase.loadCharacter(this, '01J2HCTMQ58JBMJGW9YA3FBQCG', 8, 8); - // await npcDatabase.loadCharacter(this, '01J2HQJBMBGEEMWDBDWATRCY3T', 8, 15); - - window.__GRID_ENGINE__ = this.gridEngine; - } - - loadRanges() { - const ranges: Range[] = [ - { - name: 'well', - bounds: { xMin: 2, xMax: 5, yMin: 10, yMax: 14 }, - action: () => { - const eventData: CharacterEventData = { - message: - 'Seems like there are no fish in the sand pits. You know null, this area could be fixed up a bit too.', - }; - EventEmitter.emit('charEvent', eventData); - }, - }, - { - name: 'sign', - bounds: { xMin: 2, xMax: 5, yMin: 2, yMax: 5 }, - action: () => { - const eventData = { - message: 'Sign does not have much to say.', - character_name: 'Evee The BarKeep', - character_image: '/assets/npc/barkeep.webp', - background_image: '/assets/background/woodensign.webp', - }; - EventEmitter.emit('charEvent', eventData); - }, - }, - { - name: 'building', - bounds: { xMin: 13, xMax: 13, yMin: 6, yMax: 7 }, - action: () => { - const eventData: CharacterEventData = { - message: 'Sorry, we are closed!', - character_name: 'Evee The BarKeep', - character_image: '/assets/npc/barkeep.webp', - background_image: '/assets/background/animebar.webp', - }; - EventEmitter.emit('charEvent', eventData); - }, - }, - { - name: 'tombstone', - bounds: { xMin: 7, xMax: 10, yMin: 9, yMax: 10 }, - action: () => { - const eventData: CharacterEventData = { - message: - 'Samson the Great was an amazing sailer, died drinking dat drank.', - character_name: 'Samson Statue', - character_image: '/assets/npc/samson.png', - background_image: '/assets/background/animetombstone.webp', - }; - EventEmitter.emit('charEvent', eventData); - }, - }, - ]; - - for (const range of ranges) { - if (this.quadtree != undefined) this.quadtree.insert(range); - } - } - - update() { - this.playerController?.handleMovement(); - } + cursor: Phaser.Types.Input.Keyboard.CursorKeys | undefined; + gridEngine: any; + quadtree: Quadtree | undefined; + playerController: PlayerController | undefined; + + constructor() { + super({ key: 'SandCity' }); + } + + preload() { + EventEmitter.emit('notification', { + title: 'Success', + message: `You arrived safely to SandCity Passport: ${ULIDFactory().toString()}`, + notificationType: notificationType.success, + }); + } + + async create() { + let cloudCityTilemap: Phaser.Tilemaps.Tilemap | null = null; + + try { + await mapDatabase.prepareMapLoad('cloud-city-map'); + cloudCityTilemap = await mapDatabase.loadNewMap( + this, + 'cloud-city-map', + 5, + 12, + ); + } catch (error) { + Debug.error('Failed to load map:', error); + return; + } + + if (!cloudCityTilemap) { + Debug.error('Tilemap could not be loaded.'); + return; + } + + if (cloudCityTilemap) { + Debug.log('New Tilemap Loaded'); + } + + const bounds = await mapDatabase.getBounds('cloud-city-map'); + if (bounds) { + this.quadtree = new Quadtree(bounds); + } else { + Debug.error('Bounds could not be retrieved.'); + return; + } + + const playerSprite = this.add.sprite(0, 0, 'player'); + playerSprite.scale = 1.5; + + const playerBounds = playerSprite.getBounds(); + + const targetX = playerBounds.centerX + playerSprite.width * 3; + const targetY = playerBounds.centerY + playerSprite.height * 3; + + this.cameras.main.pan(targetX, targetY, 1000, 'Power2'); + + this.cameras.main.once('camerapancomplete', () => { + this.cameras.main.startFollow(playerSprite, true); + this.cameras.main.setFollowOffset( + -playerSprite.width, + -playerSprite.height, + ); + }); + + const gridEngineConfig = { + characters: [ + { + id: 'player', + sprite: playerSprite, + walkingAnimationMapping: 6, + startPosition: { x: 5, y: 12 }, + }, + ], + numberOfDirections: 8, + }; + + this.gridEngine.create(cloudCityTilemap, gridEngineConfig); + this.loadRanges(); + + this.playerController = new PlayerController( + this, + this.gridEngine, + this.quadtree, + ); + + // Retrieve NPCs from mapDatabase + const npcs = await mapDatabase.getNpcsFromTilesetKey('cloud-city-map'); + + if (npcs) { + for (const npc of npcs) { + try { + await npcDatabase.loadCharacter( + this, + npc.ulid, + npc.position.x, + npc.position.y, + ); + } catch (error) { + Debug.error( + `Failed to load NPC with ULID: ${npc.ulid}`, + error, + ); + } + } + } + + // await npcDatabase.loadCharacter(this, '01J2DT4G871KJ0VNSHCNC5REDM', 6, 6); + // await npcDatabase.loadCharacter(this, '01J2HCTMQ58JBMJGW9YA3FBQCG', 8, 8); + // await npcDatabase.loadCharacter(this, '01J2HQJBMBGEEMWDBDWATRCY3T', 8, 15); + + window.__GRID_ENGINE__ = this.gridEngine; + } + + loadRanges() { + const ranges: Range[] = [ + { + name: 'well', + bounds: { xMin: 2, xMax: 5, yMin: 10, yMax: 14 }, + action: () => { + const eventData: CharacterEventData = { + message: + 'Seems like there are no fish in the sand pits. You know null, this area could be fixed up a bit too.', + }; + EventEmitter.emit('charEvent', eventData); + }, + }, + { + name: 'sign', + bounds: { xMin: 2, xMax: 5, yMin: 2, yMax: 5 }, + action: () => { + const eventData = { + message: 'Sign does not have much to say.', + character_name: 'Evee The BarKeep', + character_image: '/assets/npc/barkeep.webp', + background_image: '/assets/background/woodensign.webp', + }; + EventEmitter.emit('charEvent', eventData); + }, + }, + { + name: 'building', + bounds: { xMin: 13, xMax: 13, yMin: 6, yMax: 7 }, + action: () => { + const eventData: CharacterEventData = { + message: 'Sorry, we are closed!', + character_name: 'Evee The BarKeep', + character_image: '/assets/npc/barkeep.webp', + background_image: '/assets/background/animebar.webp', + }; + EventEmitter.emit('charEvent', eventData); + }, + }, + { + name: 'tombstone', + bounds: { xMin: 7, xMax: 10, yMin: 9, yMax: 10 }, + action: () => { + const eventData: CharacterEventData = { + message: + 'Samson the Great was an amazing sailer, died drinking dat drank.', + character_name: 'Samson Statue', + character_image: '/assets/npc/samson.png', + background_image: + '/assets/background/animetombstone.webp', + }; + EventEmitter.emit('charEvent', eventData); + }, + }, + ]; + + for (const range of ranges) { + if (this.quadtree != undefined) this.quadtree.insert(range); + } + } + + update() { + this.playerController?.handleMovement(); + mapDatabase.updateVisibleChunks( + this, + 'cloud-city-map', + this.playerController?.getPlayerCoordsX() || 0, + this.playerController?.getPlayerCoordsY() || 0, + 1, + ); + } } diff --git a/packages/laser/src/lib/phaser/map/mapdatabase.ts b/packages/laser/src/lib/phaser/map/mapdatabase.ts index 02a713d52..46ed85189 100644 --- a/packages/laser/src/lib/phaser/map/mapdatabase.ts +++ b/packages/laser/src/lib/phaser/map/mapdatabase.ts @@ -3,7 +3,12 @@ import Dexie from 'dexie'; import axios from 'axios'; import { Debug } from '../../utils/debug'; -import { IMapData, type Bounds, type INPCObjectGPS } from '../../../types'; +import { + IMapData, + type Bounds, + type INPCObjectGPS, + type ITilemapJson, +} from '../../../types'; /** * Represents a Dexie-based database for managing maps, JSON files, and tileset images. @@ -18,18 +23,25 @@ class MapDatabase extends Dexie { tilemapKey: string; chunkX: number; chunkY: number; - jsonData: string; + jsonData: ITilemapJson; imageData?: Blob; }, [string, number, number] >; + //tileJsonData: Dexie.Table<{ tilemapKey: string; jsonContent: object }, string>; + tileJsonData: Dexie.Table< + { tilemapKey: string; jsonContent: ITilemapJson }, + string + >; // Map Settings - quick access to avoid calling dexie. - nbChunksX = 0; - nbChunksY = 0; - chunkWidth = 0; - chunkHeight = 0; - displayedChunks: Set = new Set(); + chunkSize = 10; + tileWidth = 32; + tileHeight = 32; + chunkWidth = this.chunkSize * this.tileWidth; + chunkHeight = this.chunkSize * this.tileHeight; + scale = 1; + displayedChunks: Set = new Set(); constructor() { super('MapDatabase'); @@ -38,21 +50,19 @@ class MapDatabase extends Dexie { jsonFiles: 'tilemapKey', tilesetImages: 'tilemapKey', chunks: '[tilemapKey+chunkX+chunkY]', + tileJsonData: 'tilemapKey', }); this.maps = this.table('maps'); this.jsonFiles = this.table('jsonFiles'); this.tilesetImages = this.table('tilesetImages'); this.chunks = this.table('chunks'); + this.tileJsonData = this.table('tileJsonData'); } /** * Resets map-related variables for safety before loading a new map. */ resetMapSettings() { - this.nbChunksX = 0; - this.nbChunksY = 0; - this.chunkWidth = 0; - this.chunkHeight = 0; this.displayedChunks.clear(); } @@ -477,21 +487,108 @@ class MapDatabase extends Dexie { } //** Map Chunking */ + //** vChunk */ + //** v0 - Still building out the core functions. */ + + /** + * Ensures all necessary assets for the given tilemap are available in the database. + * This includes map data, JSON data, and tileset image, and chunks the map if needed. + * + * @param {string} tilemapKey - The unique key identifying the map. + * @returns {Promise} Resolves when all assets are verified and available in Dexie. + */ + async prepareMapLoad(tilemapKey: string): Promise { + // Check and ensure map data is available + const mapData = await this.getMap(tilemapKey); + if (!mapData) { + throw new Error(`Map with key ${tilemapKey} not found`); + } + + // Check and ensure JSON data is available + const jsonData = await this.getJsonData(tilemapKey); + if (!jsonData) { + throw new Error(`JSON data for map ${tilemapKey} not found`); + } + + // Check and ensure tileset image is available + const tilesetImage = await this.getTilesetImage(tilemapKey); + if (!tilesetImage) { + throw new Error(`Tileset image for map ${tilemapKey} not found`); + } + + // Create URL for tileset image if not already set + let tilesetImageUrl: string | null = null; + try { + tilesetImageUrl = URL.createObjectURL(tilesetImage); + } catch (error) { + throw new Error( + `Failed to create object URL for tileset image: ${error}`, + ); + } + + if (!tilesetImageUrl) { + throw new Error( + `Tileset image URL for map ${tilemapKey} could not be created.`, + ); + } + + // Finally, chunk the map for efficient loading if it's not already chunked + await this.chunkMap(tilemapKey); + } + + /** + * Helper function to retrieve parsed JSON data for a tilemap. + * @param {string} tilemapKey - The unique key identifying the map. + * @returns {Promise} Parsed JSON data if available. + */ + async getParsedJsonData(tilemapKey: string): Promise { + // Check if parsed JSON data is already stored in `tileJsonData` + const tileJsonEntry = await this.tileJsonData.get(tilemapKey); + if (tileJsonEntry) { + return tileJsonEntry.jsonContent as ITilemapJson; + } + + // Fetch JSON data path from `jsonFiles` table + const jsonFileEntry = await this.jsonFiles.get(tilemapKey); + if (!jsonFileEntry) { + Debug.error(`JSON file path for ${tilemapKey} not found`); + return null; + } + + try { + // Fetch and parse the JSON data from the file path + const response = await axios.get(jsonFileEntry.jsonData); + const jsonData: ITilemapJson = response.data; + + // Store parsed JSON data in `tileJsonData` + await this.tileJsonData.put({ tilemapKey, jsonContent: jsonData }); + return jsonData; + } catch (error) { + Debug.error( + `Failed to fetch or parse JSON data for ${tilemapKey}:`, + error, + ); + return null; + } + } + /** * Adds a chunk to the database with a reference to its parent map. * @param {string} tilemapKey - The map identifier. * @param {number} chunkX - The X coordinate of the chunk. * @param {number} chunkY - The Y coordinate of the chunk. - * @param {string} jsonData - JSON data specific to the chunk. + * @param {ITilemapJson} jsonData - JSON data specific to the chunk. * @param {Blob} [imageData] - Optional image data for the chunk's tileset. */ async addChunk( tilemapKey: string, chunkX: number, chunkY: number, - jsonData: string, + jsonData: ITilemapJson, imageData?: Blob, ) { + Debug.log(`Adding chunk for (${chunkX}, ${chunkY}) of ${tilemapKey}`); + Debug.log(`Chunk data: ${jsonData}`); await this.chunks.put({ tilemapKey, chunkX, @@ -506,14 +603,18 @@ class MapDatabase extends Dexie { * @param {string} tilemapKey - The map identifier. * @param {number} chunkX - The X coordinate of the chunk. * @param {number} chunkY - The Y coordinate of the chunk. - * @returns {Promise<{ jsonData: string; imageData?: Blob } | undefined>} The chunk data if found. + * @returns {Promise<{ jsonData: ITilemapJson; imageData?: Blob } | undefined>} The chunk data if found. */ async getChunk( tilemapKey: string, chunkX: number, chunkY: number, - ): Promise<{ jsonData: string; imageData?: Blob } | undefined> { - return await this.chunks.get([tilemapKey, chunkX, chunkY]); + ): Promise<{ jsonData: ITilemapJson; imageData?: Blob } | undefined> { + const chunk = await this.chunks.get([tilemapKey, chunkX, chunkY]); + Debug.log( + `Retrieved chunk for (${chunkX}, ${chunkY}) of ${tilemapKey}: ${chunk}`, + ); + return chunk; } /** @@ -552,24 +653,20 @@ class MapDatabase extends Dexie { chunkX: number, chunkY: number, chunkSize: number, - ): Promise { - // Retrieve the full JSON data for the map from jsonFiles table - const jsonFileEntry = await this.jsonFiles.get(tilemapKey); - if (!jsonFileEntry) { - Debug.error(`JSON data for map ${tilemapKey} not found`); - return ''; + ): Promise { + // Use getParsedJsonData to ensure JSON data is fetched and cached properly + const fullTileData = await this.getParsedJsonData(tilemapKey); + if (!fullTileData) { + Debug.error(`Parsed JSON data for map ${tilemapKey} not found`); + return null; } - const fullTileData = JSON.parse(jsonFileEntry.jsonData); // Assume jsonData holds the entire map JSON - - // Calculate the start and end indices for the chunk const startX = chunkX * chunkSize; const startY = chunkY * chunkSize; - const endX = Math.min(startX + chunkSize, fullTileData.width); // Ensure we don't exceed map bounds + const endX = Math.min(startX + chunkSize, fullTileData.width); const endY = Math.min(startY + chunkSize, fullTileData.height); - // Extract the tile data within the chunk's bounds - const chunkTileData = []; + const chunkTileData: number[] = []; for (let y = startY; y < endY; y++) { const row = fullTileData.layers[0].data.slice( y * fullTileData.width + startX, @@ -578,8 +675,8 @@ class MapDatabase extends Dexie { chunkTileData.push(...row); } - // Construct a chunk-specific JSON structure - const chunkJson = { + return { + ...fullTileData, // Spread the full data structure width: endX - startX, height: endY - startY, layers: [ @@ -588,12 +685,7 @@ class MapDatabase extends Dexie { data: chunkTileData, }, ], - tilesets: fullTileData.tilesets, // Reference to the same tilesets - tilewidth: fullTileData.tilewidth, - tileheight: fullTileData.tileheight, }; - - return JSON.stringify(chunkJson); } /** @@ -604,20 +696,19 @@ class MapDatabase extends Dexie { async getMapDimensions( tilemapKey: string, ): Promise<{ width: number; height: number } | undefined> { - const mapData = await this.getMap(tilemapKey); - if (!mapData || !mapData.bounds) { - Debug.error(`Bounds data for map ${tilemapKey} not found`); + const jsonData = await this.getParsedJsonData(tilemapKey); + if (!jsonData) { + Debug.error(`Failed to retrieve JSON data for ${tilemapKey}`); return undefined; } - const { xMin, xMax, yMin, yMax } = mapData.bounds; - const width = xMax - xMin; - const height = yMax - yMin; + const width = jsonData.width; + const height = jsonData.height; if (width > 0 && height > 0) { return { width, height }; } else { - Debug.error(`Invalid bounds data for map ${tilemapKey}`); + Debug.error(`Invalid JSON data for map ${tilemapKey}`); return undefined; } } @@ -639,6 +730,7 @@ class MapDatabase extends Dexie { const numChunksX = Math.ceil(width / chunkSize); const numChunksY = Math.ceil(height / chunkSize); + Debug.log(`Starting chunkMap for ${tilemapKey}`); for (let chunkX = 0; chunkX < numChunksX; chunkX++) { for (let chunkY = 0; chunkY < numChunksY; chunkY++) { const jsonData = await this.extractChunkJsonData( @@ -647,9 +739,288 @@ class MapDatabase extends Dexie { chunkY, chunkSize, ); + + if (!jsonData) { + Debug.error( + `Failed to extract JSON data for chunk (${chunkX}, ${chunkY}) of ${tilemapKey}`, + ); + continue; // Skip this chunk if JSON data extraction failed + } + + Debug.log( + `Storing chunk (${chunkX}, ${chunkY}) for ${tilemapKey}`, + ); await this.addChunk(tilemapKey, chunkX, chunkY, jsonData); } } + Debug.log(`Finished chunkMap for ${tilemapKey}`); + } + + /** + * Loads a specific chunk into the Phaser scene. + * @param {Phaser.Scene} scene - The Phaser scene. + * @param {string} tilemapKey - The map identifier. + * @param {number} chunkX - The X coordinate of the chunk. + * @param {number} chunkY - The Y coordinate of the chunk. + */ +/** + * Loads a specific chunk into the Phaser scene. + * @param {Phaser.Scene} scene - The Phaser scene. + * @param {string} tilemapKey - The map identifier. + * @param {number} chunkX - The X coordinate of the chunk. + * @param {number} chunkY - The Y coordinate of the chunk. + */ +async loadChunkIntoScene( + scene: Phaser.Scene, + tilemapKey: string, + chunkX: number, + chunkY: number, +): Promise { + const chunkData = await this.getChunk(tilemapKey, chunkX, chunkY); + if (!chunkData) { + Debug.error(`Chunk data for (${chunkX}, ${chunkY}) not found`); + return; + } + + const chunkTilemapKey = `${tilemapKey}_${chunkX}_${chunkY}`; + + // Use the original tileset key from mapData + const mapData = await this.getMap(tilemapKey); + if (!mapData) { + Debug.error(`Map data not found for ${tilemapKey}`); + return; + } + + const tilesetName = chunkData.jsonData.tilesets[0].name; // Use tileset name from JSON + + // Ensure tileset image is loaded with the correct key before loading the chunk + if (!scene.textures.exists(tilesetName)) { + const tilesetImage = await this.getTilesetImage(tilemapKey); + if (tilesetImage) { + const tilesetImageUrl = URL.createObjectURL(tilesetImage); + scene.load.image(tilesetName, tilesetImageUrl); // Load the image with the correct name + + await new Promise((resolve) => scene.load.once('complete', resolve)); + scene.load.start(); + } else { + Debug.error(`Failed to load tileset image for ${tilesetName}`); + return; + } + } + + // Use tilemapTiledJSON to load chunk data as a Tiled map + scene.load.tilemapTiledJSON(chunkTilemapKey, chunkData.jsonData); + + await new Promise((resolve) => scene.load.once('complete', resolve)); + scene.load.start(); + + // Create tilemap and add all layers + const map = scene.make.tilemap({ key: chunkTilemapKey }); + const tileset = map.addTilesetImage(tilesetName); // Use tileset name for matching + if (tileset) { + + // Loop through each layer in the tilemap and create it + map.layers.forEach((layerData, index) => { + const layer = map.createLayer(index, tileset, 0, 0); + if (layer) { + layer.setScale(this.scale); + Debug.log(`Layer ${index} created for chunk (${chunkX}, ${chunkY}) with tileset ${tilesetName}.`); + } else { + Debug.error(`Layer ${index} could not be created for chunk (${chunkX}, ${chunkY}).`); + } + }); + } else { + Debug.error(`Tileset ${tilesetName} could not be added to tilemap.`); + } +} + + + // + /** + * Removes a specific chunk from the Phaser scene. + * @param {Phaser.Scene} scene - The Phaser scene. + * @param {string} tilemapKey - The map identifier. + * @param {number} chunkX - The X coordinate of the chunk. + * @param {number} chunkY - The Y coordinate of the chunk. + */ + removeChunkFromScene( + scene: Phaser.Scene, + tilemapKey: string, + chunkX: number, + chunkY: number, + ): void { + const chunkTilemapKey = `${tilemapKey}_${chunkX}_${chunkY}`; + + // Remove the layer and tilemap + const map = scene.make.tilemap({ key: chunkTilemapKey }); + if (map) { + map.destroy(); + } + + // Remove from cache + scene.cache.tilemap.remove(chunkTilemapKey); + } + + /** + * Updates the visible chunks based on the player's position. + * @param {Phaser.Scene} scene - The Phaser scene. + * @param {string} tilemapKey - The map identifier. + * @param {number} playerX - Player's X position in the world. + * @param {number} playerY - Player's Y position in the world. + * @param {number} viewRadius - Number of chunks to load around the player. + */ + async updateVisibleChunks( + scene: Phaser.Scene, + tilemapKey: string, + playerX: number, + playerY: number, + viewRadius: number, + ): Promise { + const mapData = await this.getMap(tilemapKey); + if (!mapData) { + console.error(`Map data for ${tilemapKey} not found`); + return; + } + + const tileWidth = this.tileWidth; + const tileHeight = this.tileHeight; + const chunkSize = this.chunkSize; + + const playerChunkX = Math.floor(playerX / (chunkSize * tileWidth)); + const playerChunkY = Math.floor(playerY / (chunkSize * tileHeight)); + + const newDisplayedChunks = new Set(); + + // Calculate chunks to display + for (let dx = -viewRadius; dx <= viewRadius; dx++) { + for (let dy = -viewRadius; dy <= viewRadius; dy++) { + const chunkX = playerChunkX + dx; + const chunkY = playerChunkY + dy; + const chunkKey = `${chunkX},${chunkY}`; + + // Load the chunk if it's within the map bounds + if (chunkX >= 0 && chunkY >= 0) { + await this.loadChunkIntoScene( + scene, + tilemapKey, + chunkX, + chunkY, + ); + newDisplayedChunks.add(chunkKey); + } + } + } + + // Remove chunks that are no longer in view + for (const chunkKey of this.displayedChunks) { + if (!newDisplayedChunks.has(chunkKey)) { + const [chunkX, chunkY] = chunkKey.split(',').map(Number); + this.removeChunkFromScene(scene, tilemapKey, chunkX, chunkY); + } + } + + // Update the displayed chunks + this.displayedChunks = newDisplayedChunks; + } + + /** + * Load the initial chunk of the map based on player's starting position. + * This serves as the starting map for the scene. + * @param {Phaser.Scene} scene - The Phaser scene + * @param {string} tilemapKey - The unique key identifying the map + * @param {number} startX - Player's starting X position in the world + * @param {number} startY - Player's starting Y position in the world + */ + async loadNewMap( + scene: Phaser.Scene, + tilemapKey: string, + startX: number, + startY: number, + ): Promise { + Debug.log(`Loading map with key: ${tilemapKey}`); + this.resetMapSettings(); + + const mapData = await this.getMap(tilemapKey); + if (!mapData) { + Debug.error(`Map data not found for ${tilemapKey}`); + return null; + } + + // Determine which chunk the player starts in + const playerChunkX = Math.floor(startX / (this.chunkWidth || 1)); + const playerChunkY = Math.floor(startY / (this.chunkHeight || 1)); + + // Load the initial chunk for player's starting position + const initialChunkData = await this.getChunk( + tilemapKey, + playerChunkX, + playerChunkY, + ); + if (!initialChunkData) { + Debug.error( + `Chunk (${playerChunkX}, ${playerChunkY}) not found for ${tilemapKey}`, + ); + return null; + } + + // Prepare tileset image if not loaded + const tilesetKey = mapData.tilesetKey; + if (!scene.textures.exists(tilesetKey)) { + const tilesetImage = await this.getTilesetImage(tilemapKey); + if (tilesetImage) { + const tilesetImageUrl = URL.createObjectURL(tilesetImage); + scene.load.image(tilesetKey, tilesetImageUrl); + await new Promise((resolve) => { + scene.load.once('complete', resolve); + scene.load.start(); + }); + } else { + Debug.error(`Failed to load tileset image for ${tilesetKey}`); + return null; + } + } + + // Use tilemapTiledJSON to add the initial chunk data to Phaser's cache + const initialChunkKey = `${tilemapKey}_${playerChunkX}_${playerChunkY}`; + scene.load.tilemapTiledJSON(initialChunkKey, initialChunkData.jsonData); + + // Wait for JSON data to load + await new Promise((resolve) => scene.load.once('complete', resolve)); + scene.load.start(); + + // Create the tilemap for the initial chunk + const map = scene.make.tilemap({ key: initialChunkKey }); + if (!map) { + Debug.error( + `Tilemap could not be created for chunk (${playerChunkX}, ${playerChunkY})`, + ); + return null; + } + + // Add tileset and layer to the scene + const tileset = map.addTilesetImage(mapData.tilesetName, tilesetKey); + if (!tileset) { + Debug.error(`Tileset ${tilesetKey} could not be added to tilemap.`); + return null; + } + + // Create layers for the initial chunk + for (let i = 0; i < map.layers.length; i++) { + const layer = map.createLayer( + i, + tileset, + 0, + 0, + ); + if (layer) { + layer.setScale(mapData.scale || this.scale); + Debug.log(`Layer ${i} created for initial chunk.`); + } else { + Debug.error(`Layer ${i} could not be created.`); + } + } + + return map; } } diff --git a/packages/laser/src/lib/phaser/player/playercontroller.ts b/packages/laser/src/lib/phaser/player/playercontroller.ts index 855b40bb3..dfed680fd 100644 --- a/packages/laser/src/lib/phaser/player/playercontroller.ts +++ b/packages/laser/src/lib/phaser/player/playercontroller.ts @@ -12,6 +12,7 @@ export class PlayerController { private cursor: Phaser.Types.Input.Keyboard.CursorKeys | undefined; private wasdKeys!: { [key: string]: Phaser.Input.Keyboard.Key; }; private tooltip: Phaser.GameObjects.Text; + private tileSize = 48; constructor(scene: Scene, gridEngine: any, quadtree: Quadtree) { @@ -25,6 +26,7 @@ export class PlayerController { font: '16px Arial', backgroundColor: '#000000', }).setDepth(4).setPadding(3,2,2,3).setVisible(false); + this.tileSize; } private initializeWASDKeys() { @@ -195,18 +197,30 @@ export class PlayerController { } private checkForNearbyObjects() { - const tileSize = 48; // Adjust this based on your game's tile size const playerPosition = this.gridEngine.getPosition('player') as Point; - const screenX = playerPosition.x * tileSize; - const screenY = playerPosition.y * tileSize; - + const screenX = playerPosition.x * this.tileSize; + const screenY = playerPosition.y * this.tileSize; + const foundRanges = this.quadtree.query(playerPosition); - + if (foundRanges.length > 0) { - this.tooltip.setPosition(screenX, screenY - 60).setVisible(true); + this.tooltip.setPosition(screenX, screenY - 60).setVisible(true); } else { - this.tooltip.setVisible(false); + this.tooltip.setVisible(false); } +} + + + // Method to get the player's X coordinate in the world + getPlayerCoordsX(): number { + const playerPosition = this.gridEngine.getPosition('player') as Point; + return playerPosition.x; + } + + // Method to get the player's Y coordinate in the world + getPlayerCoordsY(): number { + const playerPosition = this.gridEngine.getPosition('player') as Point; + return playerPosition.y; } handleMovement() { diff --git a/packages/laser/src/types.ts b/packages/laser/src/types.ts index 3505a6d66..221f9e6f0 100644 --- a/packages/laser/src/types.ts +++ b/packages/laser/src/types.ts @@ -565,6 +565,72 @@ export interface IMapData { jsonDataUrl: string; } +export interface ITilemapJson { + compressionlevel: number; + height: number; + infinite: boolean; + layers: Layer[]; + nextlayerid: number; + nextobjectid: number; + orientation: string; + renderorder: string; + tiledversion: string; + tileheight: number; + tilesets: Tileset[]; + tilewidth: number; + type: string; + version: string; + width: number; + properties?: Property[]; // Root-level properties, if any like 'ge_collide' + } + + export interface Layer { + data: number[]; + height: number; + id: number; + name: string; + opacity: number; + type: string; + visible: boolean; + width: number; + x: number; + y: number; + properties?: Property[]; // Layer-specific properties + } + + export interface Property { + name: string; + type: string; + value: boolean | number | string | any; + } + + export interface Tileset { + columns: number; + firstgid: number; + image: string; + imageheight: number; + imagewidth: number; + margin: number; + name: string; + spacing: number; + tilecount: number; + tileheight: number; + tilewidth: number; + tiles?: Tile[]; // Optional in case no specific tiles are defined + } + + export interface Tile { + id: number; + properties?: Property[]; // Tile-specific properties + } + + export interface ChunkedLayer { + chunks: Map; + width: number; + height: number; + chunkSize: number; + } + //* QuadTree */ export interface Bounds { xMin: number;