level builder
This commit is contained in:
@@ -7,8 +7,8 @@ export class Exit extends Phaser.GameObjects.Rectangle {
|
|||||||
|
|
||||||
declare body: Phaser.Physics.Arcade.StaticBody;
|
declare body: Phaser.Physics.Arcade.StaticBody;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, groundY: number) {
|
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||||
super(scene, x, groundY - Exit.HEIGHT / 2, Exit.WIDTH, Exit.HEIGHT, Exit.COLOR);
|
super(scene, x, y, Exit.WIDTH, Exit.HEIGHT, Exit.COLOR);
|
||||||
scene.add.existing(this);
|
scene.add.existing(this);
|
||||||
scene.physics.add.existing(this, true);
|
scene.physics.add.existing(this, true);
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/levels/LevelLoader.ts
Normal file
77
src/levels/LevelLoader.ts
Normal 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
94
src/levels/levels.ts
Normal 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",
|
||||||
|
"####################",
|
||||||
|
]),
|
||||||
|
];
|
||||||
@@ -2,36 +2,57 @@ import Phaser from "phaser";
|
|||||||
import { Player } from "../entities/Player";
|
import { Player } from "../entities/Player";
|
||||||
import { Exit } from "../entities/Exit";
|
import { Exit } from "../entities/Exit";
|
||||||
import { Platform } from "../entities/Platform";
|
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 {
|
export class GameScene extends Phaser.Scene {
|
||||||
private static readonly FLOOR_TOP = 500;
|
private static readonly TRANSITION_DELAY_MS = 1200;
|
||||||
private static readonly FLOOR_HEIGHT = 100;
|
private static readonly FALL_LIMIT_PADDING = 80;
|
||||||
|
|
||||||
|
private levelIndex = 0;
|
||||||
private player!: Player;
|
private player!: Player;
|
||||||
private exit!: Exit;
|
private exit!: Exit;
|
||||||
private platform!: Platform;
|
private platforms: Platform[] = [];
|
||||||
private levelCompleteText!: Phaser.GameObjects.Text;
|
|
||||||
private levelCompleted = false;
|
private levelCompleted = false;
|
||||||
|
private hudText!: Phaser.GameObjects.Text;
|
||||||
|
private centerText!: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: "GameScene" });
|
super({ key: "GameScene" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(data: SceneData): void {
|
||||||
|
this.levelIndex = data.levelIndex ?? 0;
|
||||||
|
this.levelCompleted = false;
|
||||||
|
this.platforms = [];
|
||||||
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
const width = this.scale.width;
|
this.physics.world.setBounds(0, 0, this.scale.width, this.scale.height + 200);
|
||||||
const height = this.scale.height;
|
|
||||||
const floorTop = GameScene.FLOOR_TOP;
|
|
||||||
const floorH = GameScene.FLOOR_HEIGHT;
|
|
||||||
|
|
||||||
this.platform = new Platform(this, width / 2, floorTop + floorH / 2, width, floorH);
|
const def = LEVELS[this.levelIndex];
|
||||||
this.exit = new Exit(this, 60, floorTop);
|
const loader = new LevelLoader(this);
|
||||||
this.player = new Player(this, width - Player.SIZE - 40, floorTop - Player.SIZE / 2);
|
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.physics.add.overlap(this.player, this.exit, () => this.completeLevel());
|
||||||
|
|
||||||
this.levelCompleteText = this.add
|
this.hudText = this.add.text(
|
||||||
.text(width / 2, height / 2, "LEVEL COMPLETED!", {
|
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",
|
fontFamily: "Arial",
|
||||||
fontSize: "48px",
|
fontSize: "48px",
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
@@ -44,12 +65,30 @@ export class GameScene extends Phaser.Scene {
|
|||||||
override update(): void {
|
override update(): void {
|
||||||
if (this.levelCompleted) return;
|
if (this.levelCompleted) return;
|
||||||
this.player.update();
|
this.player.update();
|
||||||
|
|
||||||
|
if (this.player.y > this.scale.height + GameScene.FALL_LIMIT_PADDING) {
|
||||||
|
this.restartLevel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private completeLevel(): void {
|
private completeLevel(): void {
|
||||||
if (this.levelCompleted) return;
|
if (this.levelCompleted) return;
|
||||||
this.levelCompleted = true;
|
this.levelCompleted = true;
|
||||||
this.player.freeze();
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user