diff --git a/src/entities/ObstaclePlatformDisappearing.ts b/src/entities/ObstaclePlatformDisappearing.ts new file mode 100644 index 0000000..18da8cc --- /dev/null +++ b/src/entities/ObstaclePlatformDisappearing.ts @@ -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), + }); + } +} diff --git a/src/entities/ObstacleSpikeNormal.ts b/src/entities/ObstacleSpikeNormal.ts new file mode 100644 index 0000000..2c2042a --- /dev/null +++ b/src/entities/ObstacleSpikeNormal.ts @@ -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); + } +} diff --git a/src/entities/ObstacleSpikeTricky.ts b/src/entities/ObstacleSpikeTricky.ts new file mode 100644 index 0000000..107f085 --- /dev/null +++ b/src/entities/ObstacleSpikeTricky.ts @@ -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; + } + } +} diff --git a/src/levels/LevelLoader.ts b/src/levels/LevelLoader.ts index faaddb9..71d67d4 100644 --- a/src/levels/LevelLoader.ts +++ b/src/levels/LevelLoader.ts @@ -2,12 +2,18 @@ import Phaser from "phaser"; import { Player } from "../entities/Player"; import { Exit } from "../entities/Exit"; 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"; export interface LoadedLevel { player: Player; exit: Exit; platforms: Platform[]; + disappearing: ObstaclePlatformDisappearing[]; + spikes: ObstacleSpikeNormal[]; + trickySpikes: ObstacleSpikeTricky[]; } interface Cell { @@ -19,30 +25,50 @@ 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 }; + return { + player: this.buildPlayer(matrix), + exit: this.buildExit(matrix), + 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); + }), + }; } - /** - * 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[] = []; + private buildHorizontalRuns( + matrix: LevelMatrix, + tileType: TileType, + factory: (x: number, y: number, width: number, height: number) => T, + ): T[] { + const result: T[] = []; 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) { + const match = col < cols && matrix[row][col] === tileType; + if (match && runStart === -1) { runStart = col; - } else if (!isPlatform && runStart !== -1) { + } else if (!match && 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)); + result.push(factory(x, y, width, TILE_SIZE)); runStart = -1; } } @@ -50,6 +76,20 @@ export class LevelLoader { return result; } + private buildSingleCells( + 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 { const cell = this.findTile(matrix, TileType.Player); if (!cell) throw new Error("Level matrix has no Player tile"); diff --git a/src/levels/levels.ts b/src/levels/levels.ts index 4962d19..7926cff 100644 --- a/src/levels/levels.ts +++ b/src/levels/levels.ts @@ -7,6 +7,9 @@ export enum TileType { Platform = 1, Player = 2, Exit = 3, + DisappearingPlatform = 4, + Spike = 5, + TrickySpike = 6, } export type LevelMatrix = TileType[][]; @@ -21,6 +24,9 @@ const CHAR_TO_TILE: Record = { "#": TileType.Platform, "P": TileType.Player, "E": TileType.Exit, + "~": TileType.DisappearingPlatform, + "^": TileType.Spike, + "T": TileType.TrickySpike, }; function parseLevel(rows: string[]): LevelMatrix { @@ -40,7 +46,24 @@ function defineLevel(name: string, rows: string[]): 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", - "####################", - "####################", + "######~~~~##########", + "######^^^^##########", ]), - defineLevel("Ugorj át a szakadékokon", [ - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - "....................", - ".E.................P", - "####....####....####", - "####....####....####", - ]), - defineLevel("Mászd meg a lépcsőt", [ + defineLevel("Csaló tüske és lépcső", [ "....................", "....................", "....................", @@ -88,7 +94,7 @@ export const LEVELS: readonly LevelDef[] = [ "........#####.......", "............#####...", "...............#####", - "...................P", + ".........T.........P", "####################", ]), ]; diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 017d979..8555f6e 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -2,6 +2,9 @@ import Phaser from "phaser"; import { Player } from "../entities/Player"; import { Exit } from "../entities/Exit"; 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 { LevelLoader } from "../levels/LevelLoader"; @@ -17,7 +20,11 @@ export class GameScene extends Phaser.Scene { private player!: Player; private exit!: Exit; private platforms: Platform[] = []; + private disappearing: ObstaclePlatformDisappearing[] = []; + private spikes: ObstacleSpikeNormal[] = []; + private trickySpikes: ObstacleSpikeTricky[] = []; private levelCompleted = false; + private dying = false; private hudText!: Phaser.GameObjects.Text; private centerText!: Phaser.GameObjects.Text; @@ -28,7 +35,11 @@ export class GameScene extends Phaser.Scene { init(data: SceneData): void { this.levelIndex = data.levelIndex ?? 0; this.levelCompleted = false; + this.dying = false; this.platforms = []; + this.disappearing = []; + this.spikes = []; + this.trickySpikes = []; } create(): void { @@ -40,9 +51,15 @@ export class GameScene extends Phaser.Scene { this.player = loaded.player; this.exit = loaded.exit; 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.disappearing, undefined, Platform.canLand); 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( 12, @@ -63,16 +80,23 @@ export class GameScene extends Phaser.Scene { } override update(): void { - if (this.levelCompleted) return; + if (this.levelCompleted || this.dying) return; 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) { - this.restartLevel(); + this.die(); } } private completeLevel(): void { - if (this.levelCompleted) return; + if (this.levelCompleted || this.dying) return; this.levelCompleted = true; 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 }); } }