diff --git a/src/entities/Exit.ts b/src/entities/Exit.ts index 7897764..abdbfe5 100644 --- a/src/entities/Exit.ts +++ b/src/entities/Exit.ts @@ -7,8 +7,8 @@ export class Exit extends Phaser.GameObjects.Rectangle { declare body: Phaser.Physics.Arcade.StaticBody; - constructor(scene: Phaser.Scene, x: number, groundY: number) { - super(scene, x, groundY - Exit.HEIGHT / 2, Exit.WIDTH, Exit.HEIGHT, Exit.COLOR); + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, Exit.WIDTH, Exit.HEIGHT, Exit.COLOR); scene.add.existing(this); scene.physics.add.existing(this, true); } diff --git a/src/levels/LevelLoader.ts b/src/levels/LevelLoader.ts new file mode 100644 index 0000000..faaddb9 --- /dev/null +++ b/src/levels/LevelLoader.ts @@ -0,0 +1,77 @@ +import Phaser from "phaser"; +import { Player } from "../entities/Player"; +import { Exit } from "../entities/Exit"; +import { Platform } from "../entities/Platform"; +import { LevelMatrix, TILE_SIZE, TileType } from "./levels"; + +export interface LoadedLevel { + player: Player; + exit: Exit; + platforms: Platform[]; +} + +interface Cell { + row: number; + col: number; +} + +export class LevelLoader { + constructor(private readonly scene: Phaser.Scene) {} + + load(matrix: LevelMatrix): LoadedLevel { + const platforms = this.buildPlatforms(matrix); + const player = this.buildPlayer(matrix); + const exit = this.buildExit(matrix); + return { player, exit, platforms }; + } + + /** + * Merge contiguous Platform tiles in each row into a single rectangle to + * avoid corner-catching issues between adjacent 1-tile static bodies. + */ + private buildPlatforms(matrix: LevelMatrix): Platform[] { + const result: Platform[] = []; + for (let row = 0; row < matrix.length; row++) { + const cols = matrix[row].length; + let runStart = -1; + for (let col = 0; col <= cols; col++) { + const isPlatform = col < cols && matrix[row][col] === TileType.Platform; + if (isPlatform && runStart === -1) { + runStart = col; + } else if (!isPlatform && runStart !== -1) { + const width = (col - runStart) * TILE_SIZE; + const x = runStart * TILE_SIZE + width / 2; + const y = row * TILE_SIZE + TILE_SIZE / 2; + result.push(new Platform(this.scene, x, y, width, TILE_SIZE)); + runStart = -1; + } + } + } + return result; + } + + private buildPlayer(matrix: LevelMatrix): Player { + const cell = this.findTile(matrix, TileType.Player); + if (!cell) throw new Error("Level matrix has no Player tile"); + const x = cell.col * TILE_SIZE + TILE_SIZE / 2; + const y = (cell.row + 1) * TILE_SIZE - Player.SIZE / 2; + return new Player(this.scene, x, y); + } + + private buildExit(matrix: LevelMatrix): Exit { + const cell = this.findTile(matrix, TileType.Exit); + if (!cell) throw new Error("Level matrix has no Exit tile"); + const x = cell.col * TILE_SIZE + TILE_SIZE / 2; + const y = (cell.row + 1) * TILE_SIZE - Exit.HEIGHT / 2; + return new Exit(this.scene, x, y); + } + + private findTile(matrix: LevelMatrix, tile: TileType): Cell | null { + for (let row = 0; row < matrix.length; row++) { + for (let col = 0; col < matrix[row].length; col++) { + if (matrix[row][col] === tile) return { row, col }; + } + } + return null; + } +} diff --git a/src/levels/levels.ts b/src/levels/levels.ts new file mode 100644 index 0000000..4962d19 --- /dev/null +++ b/src/levels/levels.ts @@ -0,0 +1,94 @@ +export const TILE_SIZE = 40; +export const LEVEL_COLS = 20; +export const LEVEL_ROWS = 15; + +export enum TileType { + Empty = 0, + Platform = 1, + Player = 2, + Exit = 3, +} + +export type LevelMatrix = TileType[][]; + +export interface LevelDef { + name: string; + matrix: LevelMatrix; +} + +const CHAR_TO_TILE: Record = { + ".": TileType.Empty, + "#": TileType.Platform, + "P": TileType.Player, + "E": TileType.Exit, +}; + +function parseLevel(rows: string[]): LevelMatrix { + return rows.map((row, rowIdx) => + [...row].map((c, colIdx) => { + const tile = CHAR_TO_TILE[c]; + if (tile === undefined) { + throw new Error(`Invalid tile char '${c}' at row ${rowIdx}, col ${colIdx}`); + } + return tile; + }), + ); +} + +function defineLevel(name: string, rows: string[]): LevelDef { + return { name, matrix: parseLevel(rows) }; +} + +export const LEVELS: readonly LevelDef[] = [ + defineLevel("Sétálj át", [ + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + ".E.................P", + "####################", + "####################", + ]), + defineLevel("Ugorj át a szakadékokon", [ + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + ".E.................P", + "####....####....####", + "####....####....####", + ]), + defineLevel("Mászd meg a lépcsőt", [ + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + "....................", + ".E..................", + "#####...............", + "....#####...........", + "........#####.......", + "............#####...", + "...............#####", + "...................P", + "####################", + ]), +]; diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 9e5b586..503a4d0 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -2,36 +2,57 @@ import Phaser from "phaser"; import { Player } from "../entities/Player"; import { Exit } from "../entities/Exit"; import { Platform } from "../entities/Platform"; +import { LEVELS } from "../levels/levels"; +import { LevelLoader } from "../levels/LevelLoader"; + +interface SceneData { + levelIndex?: number; +} export class GameScene extends Phaser.Scene { - private static readonly FLOOR_TOP = 500; - private static readonly FLOOR_HEIGHT = 100; + private static readonly TRANSITION_DELAY_MS = 1200; + private static readonly FALL_LIMIT_PADDING = 80; + private levelIndex = 0; private player!: Player; private exit!: Exit; - private platform!: Platform; - private levelCompleteText!: Phaser.GameObjects.Text; + private platforms: Platform[] = []; private levelCompleted = false; + private hudText!: Phaser.GameObjects.Text; + private centerText!: Phaser.GameObjects.Text; constructor() { super({ key: "GameScene" }); } + init(data: SceneData): void { + this.levelIndex = data.levelIndex ?? 0; + this.levelCompleted = false; + this.platforms = []; + } + create(): void { - const width = this.scale.width; - const height = this.scale.height; - const floorTop = GameScene.FLOOR_TOP; - const floorH = GameScene.FLOOR_HEIGHT; + this.physics.world.setBounds(0, 0, this.scale.width, this.scale.height + 200); - this.platform = new Platform(this, width / 2, floorTop + floorH / 2, width, floorH); - this.exit = new Exit(this, 60, floorTop); - this.player = new Player(this, width - Player.SIZE - 40, floorTop - Player.SIZE / 2); + const def = LEVELS[this.levelIndex]; + const loader = new LevelLoader(this); + const loaded = loader.load(def.matrix); + this.player = loaded.player; + this.exit = loaded.exit; + this.platforms = loaded.platforms; - this.physics.add.collider(this.player, this.platform); + this.physics.add.collider(this.player, this.platforms); this.physics.add.overlap(this.player, this.exit, () => this.completeLevel()); - this.levelCompleteText = this.add - .text(width / 2, height / 2, "LEVEL COMPLETED!", { + this.hudText = this.add.text( + 12, + 10, + `Level ${this.levelIndex + 1}/${LEVELS.length} — ${def.name}`, + { fontFamily: "Arial", fontSize: "18px", color: "#ffffff" }, + ); + + this.centerText = this.add + .text(this.scale.width / 2, this.scale.height / 2, "", { fontFamily: "Arial", fontSize: "48px", color: "#ffffff", @@ -44,12 +65,30 @@ export class GameScene extends Phaser.Scene { override update(): void { if (this.levelCompleted) return; this.player.update(); + + if (this.player.y > this.scale.height + GameScene.FALL_LIMIT_PADDING) { + this.restartLevel(); + } } private completeLevel(): void { if (this.levelCompleted) return; this.levelCompleted = true; this.player.freeze(); - this.levelCompleteText.setVisible(true); + + const isLast = this.levelIndex >= LEVELS.length - 1; + this.centerText + .setText(isLast ? "ALL LEVELS COMPLETED!" : "LEVEL COMPLETED!") + .setVisible(true); + + if (isLast) return; + + this.time.delayedCall(GameScene.TRANSITION_DELAY_MS, () => { + this.scene.restart({ levelIndex: this.levelIndex + 1 }); + }); + } + + private restartLevel(): void { + this.scene.restart({ levelIndex: this.levelIndex }); } }