import Player from '../entities/Player.js'; import ButtonBarrierSystem from '../systems/ButtonBarrierSystem.js'; import TilePropertyHelper, { FLOOR_GIDS, SOLID_FLOOR_GID, LADDER_TOP_GID, } from '../systems/TilePropertyHelper.js'; export default class Level1Scene extends Phaser.Scene { constructor() { super({ key: 'Level1Scene' }); } create() { // ------------------------- // Tilemap — patch external tileset reference // ------------------------- // levels-a.json references platforms.tsx as an external source. // Phaser cannot fetch .tsx files, so the tileset object in the JSON cache // has no `tiles` array, causing a crash when the parser tries tileset.tiles[n]. // We replace the external reference with the required metadata before parsing. const cachedMap = this.cache.tilemap.get('level1'); if (cachedMap) { // Fix 1: Replace external tileset source reference with embedded metadata. // Phaser cannot fetch .tsx files, so the JSON tileset object has no `tiles` // array, causing tileset.tiles[n] to throw during parsing. cachedMap.data.tilesets = cachedMap.data.tilesets.map(ts => { if (!ts.source) return ts; const name = ts.source.split('/').pop().replace(/\.\w+$/, ''); return { firstgid: ts.firstgid, name, columns: 10, tilecount: 100, tileheight: 128, tilewidth: 128, image: '../sprites/platforms.png', imageheight: 1280, imagewidth: 1280, margin: 0, spacing: 0, tiles: [], }; }); // Fix 2: Normalize startx/starty on every infinite-map layer. // Phaser places chunk data into the layer data array at index // [chunk.y - starty][chunk.x - startx] // then renders data[row][col] at world pixel (col*tw, row*th). // The floor layer has startx=-16, starty=-16 even though all chunks // sit at x>=0, y>=0 — this pushes floor tiles to data rows 30+ which // maps to world y ~3840px, far off-screen. Setting startx/starty to the // actual minimum chunk coordinates fixes the placement. for (const layer of cachedMap.data.layers) { if (layer.type !== 'tilelayer' || !layer.chunks || !layer.chunks.length) continue; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const chunk of layer.chunks) { minX = Math.min(minX, chunk.x); minY = Math.min(minY, chunk.y); maxX = Math.max(maxX, chunk.x + chunk.width); maxY = Math.max(maxY, chunk.y + chunk.height); } layer.startx = minX; layer.starty = minY; layer.width = maxX - minX; layer.height = maxY - minY; } } const map = this.make.tilemap({ key: 'level1' }); // 'platforms' = name Phaser derives from the external tileset source 'platforms.tsx' // If tiles render blank, verify with: console.log(map.tilesets[0].name) const tileset = map.addTilesetImage('platforms', 'platforms', 128, 128, 0, 0); this._floorLayer = map.createLayer('floor', tileset, 0, 0); this._platformsLayer = map.createLayer('platforms', tileset, 0, 0); this._laddersLayer = map.createLayer('ladders', tileset, 0, 0); this._barriersLayer = map.createLayer('barriers', tileset, 0, 0); // ------------------------- // Tile collision setup // ------------------------- // Floor layer: one-way — only collide when player falls down this._floorLayer.setCollision(FLOOR_GIDS); // Platforms layer: floor tiles (one-way) + the one solid wall tile this._platformsLayer.setCollision([...FLOOR_GIDS, SOLID_FLOOR_GID]); // Ladder layer: only the top rung acts as a one-way surface this._laddersLayer.setCollision([LADDER_TOP_GID]); // Barrier layer: initial collision set by ButtonBarrierSystem.init() // ------------------------- // Player // ------------------------- // Spawn at tile (3, 11) — center of tile in world pixels const spawnX = 3 * 128 + 64; // 448 const spawnY = 11 * 128 + 64; // 1472 this._player = new Player(this, spawnX, spawnY, this._laddersLayer); const playerSprite = this._player.sprite; // ------------------------- // Button / Barrier system // ------------------------- this._bbSystem = new ButtonBarrierSystem(this, this._barriersLayer); this._bbSystem.init(); // ------------------------- // Physics colliders // ------------------------- // Floor layer: one-way (pass through from below, land from above) this.physics.add.collider( playerSprite, this._floorLayer, null, (player, tile) => player.body.velocity.y >= 0, this ); // Platforms layer: one-way for floor tiles; full collision for solid wall tile this.physics.add.collider( playerSprite, this._platformsLayer, null, (player, tile) => { if (tile.index === SOLID_FLOOR_GID) return true; return player.body.velocity.y >= 0; }, this ); // Ladder layer: top rung as one-way floor; disabled while actively climbing this.physics.add.collider( playerSprite, this._laddersLayer, null, (player, tile) => !this._player.onLadder && player.body.velocity.y >= 0, this ); // Barriers layer: dynamic — processCallback checks current solid state per tile this.physics.add.collider( playerSprite, this._barriersLayer, null, (player, tile) => { const props = TilePropertyHelper.getPropsByGID(tile.index); return !!(props && props.solid); }, this ); // ------------------------- // Camera // ------------------------- const worldW = map.width * map.tileWidth; // 30 * 128 = 3840 const worldH = map.height * map.tileHeight; // 20 * 128 = 2560 this.cameras.main .setBounds(0, 0, worldW, worldH) .startFollow(playerSprite, true, 0.1, 0.1); this.physics.world.setBounds(0, 0, worldW, worldH); // ------------------------- // HUD // ------------------------- this._livesText = this.add .text(16, 16, 'Lives: 3', { fontFamily: 'monospace', fontSize: '28px', color: '#ffffff', stroke: '#000000', strokeThickness: 4, }) .setScrollFactor(0) .setDepth(100); // Track velocity from the previous frame for button-landing detection this._prevVelocityY = 0; } update(time, delta) { // Store velocity BEFORE player.update() so we know if player was falling this._prevVelocityY = this._player.body.velocity.y; this._player.update(time, delta); this._checkButtonPress(); this._livesText.setText('Lives: ' + this._player.lives); } /** * Detect when the player lands on top of an unpressed button tile and press it. * Uses prevVelocityY to distinguish a real landing from walking across the top. */ _checkButtonPress() { const body = this._player.body; // Player must be touching the ground this frame if (!body.blocked.down) return; // Must have been falling (not just standing; threshold filters walking-on-top) if (this._prevVelocityY < 50) return; const sprite = this._player.sprite; // Sample a point just below the player's feet center const feetX = sprite.x; const feetY = sprite.y + 120; // approx bottom of physics body const tile = this._barriersLayer.getTileAtWorldXY(feetX, feetY + 4); if (!tile || tile.index <= 0) return; if (this._bbSystem.isButtonTile(tile.index)) { this._bbSystem.pressButton(tile.x, tile.y); } } }