BreadBakingBonanza/src/scenes/Level1Scene.js

220 lines
8.5 KiB
JavaScript

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);
}
}
}