level builder

This commit is contained in:
2026-05-15 19:26:09 +02:00
parent 0e06d5eefa
commit 5d869b12f1
4 changed files with 227 additions and 17 deletions

View File

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

77
src/levels/LevelLoader.ts Normal file
View File

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

94
src/levels/levels.ts Normal file
View File

@@ -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<string, TileType> = {
".": 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",
"####################",
]),
];

View File

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