Initial commit
This commit is contained in:
257
src/scenes/GameScene.ts
Normal file
257
src/scenes/GameScene.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import Phaser from "phaser";
|
||||
import {
|
||||
type EntityId,
|
||||
type Vec2,
|
||||
type Action,
|
||||
type RunState,
|
||||
type World,
|
||||
TILE_SIZE
|
||||
} from "../game/types";
|
||||
import { inBounds, isBlocked, isPlayerOnExit } from "../game/world";
|
||||
import { findPathAStar } from "../game/pathfinding";
|
||||
import { applyAction, stepUntilPlayerTurn } from "../game/simulation";
|
||||
import { makeTestWorld } from "../game/generator";
|
||||
import { DungeonRenderer } from "./DungeonRenderer";
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private world!: World;
|
||||
private playerId!: EntityId;
|
||||
|
||||
private levelIndex = 1;
|
||||
|
||||
private runState: RunState = {
|
||||
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2 },
|
||||
inventory: { gold: 0, items: [] }
|
||||
};
|
||||
|
||||
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
|
||||
|
||||
private playerPath: Vec2[] = [];
|
||||
private awaitingPlayer = false;
|
||||
|
||||
// Sub-systems
|
||||
private dungeonRenderer!: DungeonRenderer;
|
||||
private isMenuOpen = false;
|
||||
|
||||
constructor() {
|
||||
super("GameScene");
|
||||
}
|
||||
|
||||
create() {
|
||||
this.cursors = this.input.keyboard!.createCursorKeys();
|
||||
|
||||
// Camera
|
||||
this.cameras.main.setZoom(2);
|
||||
|
||||
// Initialize Sub-systems
|
||||
this.dungeonRenderer = new DungeonRenderer(this);
|
||||
|
||||
// Launch UI Scene
|
||||
this.scene.launch("GameUI");
|
||||
|
||||
// Listen for Menu State
|
||||
this.events.on("menu-toggled", (isOpen: boolean) => {
|
||||
this.isMenuOpen = isOpen;
|
||||
});
|
||||
|
||||
// Load initial level
|
||||
this.loadLevel(1);
|
||||
|
||||
// Menu Inputs
|
||||
this.input.keyboard?.on("keydown-I", () => {
|
||||
this.events.emit("toggle-menu");
|
||||
// Force update UI in case it opened
|
||||
this.emitUIUpdate();
|
||||
});
|
||||
this.input.keyboard?.on("keydown-ESC", () => {
|
||||
this.events.emit("close-menu");
|
||||
});
|
||||
|
||||
// Mouse click -> compute path (only during player turn, and not while menu is open)
|
||||
this.input.on("pointerdown", (p: Phaser.Input.Pointer) => {
|
||||
if (!this.awaitingPlayer) return;
|
||||
if (this.isMenuOpen) return;
|
||||
|
||||
const tx = Math.floor(p.worldX / TILE_SIZE);
|
||||
const ty = Math.floor(p.worldY / TILE_SIZE);
|
||||
|
||||
if (!inBounds(this.world, tx, ty)) return;
|
||||
|
||||
// Exploration rule: cannot click-to-move into unseen tiles
|
||||
if (!this.dungeonRenderer.isSeen(tx, ty)) return;
|
||||
|
||||
// Check if clicking on an enemy
|
||||
const isEnemy = [...this.world.actors.values()].some(a => a.pos.x === tx && a.pos.y === ty && !a.isPlayer);
|
||||
|
||||
const player = this.world.actors.get(this.playerId)!;
|
||||
const path = findPathAStar(
|
||||
this.world,
|
||||
this.dungeonRenderer.seenArray,
|
||||
{ ...player.pos },
|
||||
{ x: tx, y: ty },
|
||||
{ ignoreBlockedTarget: isEnemy }
|
||||
);
|
||||
|
||||
if (path.length >= 2) this.playerPath = path;
|
||||
this.dungeonRenderer.render(this.playerPath);
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.awaitingPlayer) return;
|
||||
if (this.isMenuOpen) return;
|
||||
|
||||
// Auto-walk one step per turn
|
||||
if (this.playerPath.length >= 2) {
|
||||
const player = this.world.actors.get(this.playerId)!;
|
||||
const next = this.playerPath[1];
|
||||
const dx = next.x - player.pos.x;
|
||||
const dy = next.y - player.pos.y;
|
||||
|
||||
if (Math.abs(dx) + Math.abs(dy) !== 1) {
|
||||
this.playerPath = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlocked(this.world, next.x, next.y)) {
|
||||
// Check if it's an enemy at 'next'
|
||||
const targetId = [...this.world.actors.values()].find(
|
||||
a => a.pos.x === next.x && a.pos.y === next.y && !a.isPlayer
|
||||
)?.id;
|
||||
|
||||
if (targetId !== undefined) {
|
||||
this.commitPlayerAction({ type: "attack", targetId });
|
||||
this.playerPath = []; // Stop after attack
|
||||
return;
|
||||
} else {
|
||||
// Blocked by something else (friendly?)
|
||||
this.playerPath = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.commitPlayerAction({ type: "move", dx, dy });
|
||||
this.playerPath.shift();
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow keys
|
||||
let action: Action | null = null;
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
if (Phaser.Input.Keyboard.JustDown(this.cursors.left!)) dx = -1;
|
||||
else if (Phaser.Input.Keyboard.JustDown(this.cursors.right!)) dx = 1;
|
||||
else if (Phaser.Input.Keyboard.JustDown(this.cursors.up!)) dy = -1;
|
||||
else if (Phaser.Input.Keyboard.JustDown(this.cursors.down!)) dy = 1;
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
console.log("Input: ", dx, dy);
|
||||
const player = this.world.actors.get(this.playerId)!;
|
||||
const targetX = player.pos.x + dx;
|
||||
const targetY = player.pos.y + dy;
|
||||
console.log("Target: ", targetX, targetY);
|
||||
|
||||
// Check for enemy at target position
|
||||
const targetId = [...this.world.actors.values()].find(
|
||||
a => a.pos.x === targetX && a.pos.y === targetY && !a.isPlayer
|
||||
)?.id;
|
||||
console.log("Found Target ID:", targetId);
|
||||
|
||||
if (targetId !== undefined) {
|
||||
action = { type: "attack", targetId };
|
||||
} else {
|
||||
action = { type: "move", dx, dy };
|
||||
}
|
||||
}
|
||||
|
||||
if (action) {
|
||||
this.playerPath = [];
|
||||
this.commitPlayerAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
private emitUIUpdate() {
|
||||
this.events.emit("update-ui", {
|
||||
world: this.world,
|
||||
playerId: this.playerId,
|
||||
levelIndex: this.levelIndex
|
||||
});
|
||||
}
|
||||
|
||||
private commitPlayerAction(action: Action) {
|
||||
this.awaitingPlayer = false;
|
||||
|
||||
const playerEvents = applyAction(this.world, this.playerId, action);
|
||||
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId);
|
||||
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||
|
||||
// Process events for visual fx
|
||||
const allEvents = [...playerEvents, ...enemyStep.events];
|
||||
if (allEvents.length > 0) console.log("Events:", allEvents);
|
||||
for (const ev of allEvents) {
|
||||
if (ev.type === "damaged") {
|
||||
console.log("Showing damage:", ev.amount, "at", ev.x, ev.y);
|
||||
this.dungeonRenderer.showDamage(ev.x, ev.y, ev.amount);
|
||||
}
|
||||
}
|
||||
|
||||
// Level transition
|
||||
if (isPlayerOnExit(this.world, this.playerId)) {
|
||||
this.syncRunStateFromPlayer();
|
||||
this.loadLevel(this.levelIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dungeonRenderer.computeFov(this.playerId);
|
||||
this.centerCameraOnPlayer();
|
||||
this.dungeonRenderer.render(this.playerPath);
|
||||
this.emitUIUpdate();
|
||||
}
|
||||
|
||||
private loadLevel(level: number) {
|
||||
this.levelIndex = level;
|
||||
|
||||
const { world, playerId } = makeTestWorld(level, this.runState);
|
||||
this.world = world;
|
||||
this.playerId = playerId;
|
||||
|
||||
// Reset transient state
|
||||
this.playerPath = [];
|
||||
this.awaitingPlayer = false;
|
||||
|
||||
// Camera bounds for this level
|
||||
this.cameras.main.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE);
|
||||
|
||||
// Initialize Renderer for new level
|
||||
this.dungeonRenderer.initializeLevel(this.world);
|
||||
|
||||
// Step until player turn
|
||||
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId);
|
||||
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||
|
||||
this.dungeonRenderer.computeFov(this.playerId);
|
||||
this.centerCameraOnPlayer();
|
||||
this.dungeonRenderer.render(this.playerPath);
|
||||
this.emitUIUpdate();
|
||||
|
||||
}
|
||||
|
||||
private syncRunStateFromPlayer() {
|
||||
const p = this.world.actors.get(this.playerId);
|
||||
if (!p?.stats || !p.inventory) return;
|
||||
|
||||
this.runState = {
|
||||
stats: { ...p.stats },
|
||||
inventory: { gold: p.inventory.gold, items: [...p.inventory.items] }
|
||||
};
|
||||
}
|
||||
|
||||
private centerCameraOnPlayer() {
|
||||
const player = this.world.actors.get(this.playerId)!;
|
||||
this.cameras.main.centerOn(
|
||||
player.pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
player.pos.y * TILE_SIZE + TILE_SIZE / 2
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user