This commit is contained in:
2026-05-15 20:11:19 +02:00
parent 1722842e0a
commit c9f0b5d704
6 changed files with 195 additions and 40 deletions

View File

@@ -0,0 +1,37 @@
import Phaser from "phaser";
export class ObstaclePlatformDisappearing extends Phaser.GameObjects.Rectangle {
static readonly COLOR = 0x9966cc;
static readonly TRIGGER_DISTANCE = 110;
static readonly FADE_MS = 200;
declare body: Phaser.Physics.Arcade.StaticBody;
private vanished = false;
constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number) {
super(scene, x, y, width, height, ObstaclePlatformDisappearing.COLOR);
scene.add.existing(this);
scene.physics.add.existing(this, true);
}
react(playerX: number, playerY: number): void {
if (this.vanished) return;
const dx = this.x - playerX;
const dy = this.y - playerY;
if (Math.hypot(dx, dy) < ObstaclePlatformDisappearing.TRIGGER_DISTANCE) {
this.vanish();
}
}
private vanish(): void {
this.vanished = true;
this.body.enable = false;
this.scene.tweens.add({
targets: this,
alpha: 0,
duration: ObstaclePlatformDisappearing.FADE_MS,
onComplete: () => this.setVisible(false),
});
}
}

View File

@@ -0,0 +1,15 @@
import Phaser from "phaser";
export class ObstacleSpikeNormal extends Phaser.GameObjects.Rectangle {
static readonly WIDTH = 40;
static readonly HEIGHT = 20;
static readonly COLOR = 0xff3333;
declare body: Phaser.Physics.Arcade.StaticBody;
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, ObstacleSpikeNormal.WIDTH, ObstacleSpikeNormal.HEIGHT, ObstacleSpikeNormal.COLOR);
scene.add.existing(this);
scene.physics.add.existing(this, true);
}
}

View File

@@ -0,0 +1,30 @@
import Phaser from "phaser";
export class ObstacleSpikeTricky extends Phaser.GameObjects.Rectangle {
static readonly WIDTH = 40;
static readonly HEIGHT = 20;
static readonly COLOR = 0xff8800;
static readonly TRIGGER_DISTANCE = 140;
static readonly DISPLACEMENT = 80;
declare body: Phaser.Physics.Arcade.StaticBody;
private moved = false;
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, ObstacleSpikeTricky.WIDTH, ObstacleSpikeTricky.HEIGHT, ObstacleSpikeTricky.COLOR);
scene.add.existing(this);
scene.physics.add.existing(this, true);
}
react(playerX: number): void {
if (this.moved) return;
const dx = this.x - playerX;
if (Math.abs(dx) < ObstacleSpikeTricky.TRIGGER_DISTANCE) {
const direction = Math.sign(dx) || 1;
this.x += direction * ObstacleSpikeTricky.DISPLACEMENT;
this.body.updateFromGameObject();
this.moved = true;
}
}
}

View File

@@ -2,12 +2,18 @@ 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 { ObstaclePlatformDisappearing } from "../entities/ObstaclePlatformDisappearing";
import { ObstacleSpikeNormal } from "../entities/ObstacleSpikeNormal";
import { ObstacleSpikeTricky } from "../entities/ObstacleSpikeTricky";
import { LevelMatrix, TILE_SIZE, TileType } from "./levels"; import { LevelMatrix, TILE_SIZE, TileType } from "./levels";
export interface LoadedLevel { export interface LoadedLevel {
player: Player; player: Player;
exit: Exit; exit: Exit;
platforms: Platform[]; platforms: Platform[];
disappearing: ObstaclePlatformDisappearing[];
spikes: ObstacleSpikeNormal[];
trickySpikes: ObstacleSpikeTricky[];
} }
interface Cell { interface Cell {
@@ -19,30 +25,50 @@ export class LevelLoader {
constructor(private readonly scene: Phaser.Scene) {} constructor(private readonly scene: Phaser.Scene) {}
load(matrix: LevelMatrix): LoadedLevel { load(matrix: LevelMatrix): LoadedLevel {
const platforms = this.buildPlatforms(matrix); return {
const player = this.buildPlayer(matrix); player: this.buildPlayer(matrix),
const exit = this.buildExit(matrix); exit: this.buildExit(matrix),
return { player, exit, platforms }; platforms: this.buildHorizontalRuns(
matrix,
TileType.Platform,
(x, y, w, h) => new Platform(this.scene, x, y, w, h),
),
disappearing: this.buildHorizontalRuns(
matrix,
TileType.DisappearingPlatform,
(x, y, w, h) => new ObstaclePlatformDisappearing(this.scene, x, y, w, h),
),
spikes: this.buildSingleCells(matrix, TileType.Spike, (col, row) => {
const x = col * TILE_SIZE + TILE_SIZE / 2;
const y = (row + 1) * TILE_SIZE - ObstacleSpikeNormal.HEIGHT / 2;
return new ObstacleSpikeNormal(this.scene, x, y);
}),
trickySpikes: this.buildSingleCells(matrix, TileType.TrickySpike, (col, row) => {
const x = col * TILE_SIZE + TILE_SIZE / 2;
const y = (row + 1) * TILE_SIZE - ObstacleSpikeTricky.HEIGHT / 2;
return new ObstacleSpikeTricky(this.scene, x, y);
}),
};
} }
/** private buildHorizontalRuns<T>(
* Merge contiguous Platform tiles in each row into a single rectangle to matrix: LevelMatrix,
* avoid corner-catching issues between adjacent 1-tile static bodies. tileType: TileType,
*/ factory: (x: number, y: number, width: number, height: number) => T,
private buildPlatforms(matrix: LevelMatrix): Platform[] { ): T[] {
const result: Platform[] = []; const result: T[] = [];
for (let row = 0; row < matrix.length; row++) { for (let row = 0; row < matrix.length; row++) {
const cols = matrix[row].length; const cols = matrix[row].length;
let runStart = -1; let runStart = -1;
for (let col = 0; col <= cols; col++) { for (let col = 0; col <= cols; col++) {
const isPlatform = col < cols && matrix[row][col] === TileType.Platform; const match = col < cols && matrix[row][col] === tileType;
if (isPlatform && runStart === -1) { if (match && runStart === -1) {
runStart = col; runStart = col;
} else if (!isPlatform && runStart !== -1) { } else if (!match && runStart !== -1) {
const width = (col - runStart) * TILE_SIZE; const width = (col - runStart) * TILE_SIZE;
const x = runStart * TILE_SIZE + width / 2; const x = runStart * TILE_SIZE + width / 2;
const y = row * TILE_SIZE + TILE_SIZE / 2; const y = row * TILE_SIZE + TILE_SIZE / 2;
result.push(new Platform(this.scene, x, y, width, TILE_SIZE)); result.push(factory(x, y, width, TILE_SIZE));
runStart = -1; runStart = -1;
} }
} }
@@ -50,6 +76,20 @@ export class LevelLoader {
return result; return result;
} }
private buildSingleCells<T>(
matrix: LevelMatrix,
tileType: TileType,
factory: (col: number, row: number) => T,
): T[] {
const result: T[] = [];
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] === tileType) result.push(factory(col, row));
}
}
return result;
}
private buildPlayer(matrix: LevelMatrix): Player { private buildPlayer(matrix: LevelMatrix): Player {
const cell = this.findTile(matrix, TileType.Player); const cell = this.findTile(matrix, TileType.Player);
if (!cell) throw new Error("Level matrix has no Player tile"); if (!cell) throw new Error("Level matrix has no Player tile");

View File

@@ -7,6 +7,9 @@ export enum TileType {
Platform = 1, Platform = 1,
Player = 2, Player = 2,
Exit = 3, Exit = 3,
DisappearingPlatform = 4,
Spike = 5,
TrickySpike = 6,
} }
export type LevelMatrix = TileType[][]; export type LevelMatrix = TileType[][];
@@ -21,6 +24,9 @@ const CHAR_TO_TILE: Record<string, TileType> = {
"#": TileType.Platform, "#": TileType.Platform,
"P": TileType.Player, "P": TileType.Player,
"E": TileType.Exit, "E": TileType.Exit,
"~": TileType.DisappearingPlatform,
"^": TileType.Spike,
"T": TileType.TrickySpike,
}; };
function parseLevel(rows: string[]): LevelMatrix { function parseLevel(rows: string[]): LevelMatrix {
@@ -40,7 +46,24 @@ function defineLevel(name: string, rows: string[]): LevelDef {
} }
export const LEVELS: readonly LevelDef[] = [ export const LEVELS: readonly LevelDef[] = [
defineLevel("Sétálj át", [ defineLevel("Ugorj a tüskén át", [
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
".E........^........P",
"####################",
"####################",
]),
defineLevel("Csapda híd", [
"....................", "....................",
"....................", "....................",
"....................", "....................",
@@ -54,27 +77,10 @@ export const LEVELS: readonly LevelDef[] = [
"....................", "....................",
"....................", "....................",
".E.................P", ".E.................P",
"####################", "######~~~~##########",
"####################", "######^^^^##########",
]), ]),
defineLevel("Ugorj át a szakadékokon", [ defineLevel("Csaló tüske és lépcső", [
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
"....................",
".E.................P",
"####....####....####",
"####....####....####",
]),
defineLevel("Mászd meg a lépcsőt", [
"....................", "....................",
"....................", "....................",
"....................", "....................",
@@ -88,7 +94,7 @@ export const LEVELS: readonly LevelDef[] = [
"........#####.......", "........#####.......",
"............#####...", "............#####...",
"...............#####", "...............#####",
"...................P", ".........T.........P",
"####################", "####################",
]), ]),
]; ];

View File

@@ -2,6 +2,9 @@ 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 { ObstaclePlatformDisappearing } from "../entities/ObstaclePlatformDisappearing";
import { ObstacleSpikeNormal } from "../entities/ObstacleSpikeNormal";
import { ObstacleSpikeTricky } from "../entities/ObstacleSpikeTricky";
import { LEVELS } from "../levels/levels"; import { LEVELS } from "../levels/levels";
import { LevelLoader } from "../levels/LevelLoader"; import { LevelLoader } from "../levels/LevelLoader";
@@ -17,7 +20,11 @@ export class GameScene extends Phaser.Scene {
private player!: Player; private player!: Player;
private exit!: Exit; private exit!: Exit;
private platforms: Platform[] = []; private platforms: Platform[] = [];
private disappearing: ObstaclePlatformDisappearing[] = [];
private spikes: ObstacleSpikeNormal[] = [];
private trickySpikes: ObstacleSpikeTricky[] = [];
private levelCompleted = false; private levelCompleted = false;
private dying = false;
private hudText!: Phaser.GameObjects.Text; private hudText!: Phaser.GameObjects.Text;
private centerText!: Phaser.GameObjects.Text; private centerText!: Phaser.GameObjects.Text;
@@ -28,7 +35,11 @@ export class GameScene extends Phaser.Scene {
init(data: SceneData): void { init(data: SceneData): void {
this.levelIndex = data.levelIndex ?? 0; this.levelIndex = data.levelIndex ?? 0;
this.levelCompleted = false; this.levelCompleted = false;
this.dying = false;
this.platforms = []; this.platforms = [];
this.disappearing = [];
this.spikes = [];
this.trickySpikes = [];
} }
create(): void { create(): void {
@@ -40,9 +51,15 @@ export class GameScene extends Phaser.Scene {
this.player = loaded.player; this.player = loaded.player;
this.exit = loaded.exit; this.exit = loaded.exit;
this.platforms = loaded.platforms; this.platforms = loaded.platforms;
this.disappearing = loaded.disappearing;
this.spikes = loaded.spikes;
this.trickySpikes = loaded.trickySpikes;
this.physics.add.collider(this.player, this.platforms, undefined, Platform.canLand); this.physics.add.collider(this.player, this.platforms, undefined, Platform.canLand);
this.physics.add.collider(this.player, this.disappearing, undefined, Platform.canLand);
this.physics.add.overlap(this.player, this.exit, () => this.completeLevel()); this.physics.add.overlap(this.player, this.exit, () => this.completeLevel());
this.physics.add.overlap(this.player, this.spikes, () => this.die());
this.physics.add.overlap(this.player, this.trickySpikes, () => this.die());
this.hudText = this.add.text( this.hudText = this.add.text(
12, 12,
@@ -63,16 +80,23 @@ export class GameScene extends Phaser.Scene {
} }
override update(): void { override update(): void {
if (this.levelCompleted) return; if (this.levelCompleted || this.dying) return;
this.player.update(); this.player.update();
for (const platform of this.disappearing) {
platform.react(this.player.x, this.player.y);
}
for (const spike of this.trickySpikes) {
spike.react(this.player.x);
}
if (this.player.y > this.scale.height + GameScene.FALL_LIMIT_PADDING) { if (this.player.y > this.scale.height + GameScene.FALL_LIMIT_PADDING) {
this.restartLevel(); this.die();
} }
} }
private completeLevel(): void { private completeLevel(): void {
if (this.levelCompleted) return; if (this.levelCompleted || this.dying) return;
this.levelCompleted = true; this.levelCompleted = true;
this.player.freeze(); this.player.freeze();
@@ -88,7 +112,10 @@ export class GameScene extends Phaser.Scene {
}); });
} }
private restartLevel(): void { private die(): void {
if (this.levelCompleted || this.dying) return;
this.dying = true;
this.player.freeze();
this.scene.restart({ levelIndex: this.levelIndex }); this.scene.restart({ levelIndex: this.levelIndex });
} }
} }