220 lines
8.5 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|