obscales
This commit is contained in:
37
src/entities/ObstaclePlatformDisappearing.ts
Normal file
37
src/entities/ObstaclePlatformDisappearing.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
}
|
||||
15
src/entities/ObstacleSpikeNormal.ts
Normal file
15
src/entities/ObstacleSpikeNormal.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
30
src/entities/ObstacleSpikeTricky.ts
Normal file
30
src/entities/ObstacleSpikeTricky.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T>(
|
||||
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<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 {
|
||||
const cell = this.findTile(matrix, TileType.Player);
|
||||
if (!cell) throw new Error("Level matrix has no Player tile");
|
||||
|
||||
@@ -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<string, TileType> = {
|
||||
"#": 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",
|
||||
"####################",
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user