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"; import { GAME_CONFIG } from "../game/config/GameConfig"; export class GameScene extends Phaser.Scene { private world!: World; private playerId!: EntityId; private levelIndex = 1; private runState: RunState = { stats: { ...GAME_CONFIG.player.initialStats }, 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"); } preload() { this.load.spritesheet("warrior", "warrior.png", { frameWidth: 12, frameHeight: 15 }); this.load.spritesheet("rat", "rat.png", { frameWidth: 16, frameHeight: 15 }); this.load.spritesheet("bat", "bat.png", { frameWidth: 15, frameHeight: 15 }); } create() { this.cursors = this.input.keyboard!.createCursorKeys(); // Camera this.cameras.main.setZoom(GAME_CONFIG.rendering.cameraZoom); // 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", () => { // Close minimap if it's open if (this.dungeonRenderer.isMinimapVisible()) { this.dungeonRenderer.toggleMinimap(); } 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"); // Also close minimap if (this.dungeonRenderer.isMinimapVisible()) { this.dungeonRenderer.toggleMinimap(); } }); this.input.keyboard?.on("keydown-M", () => { // Close menu if it's open this.events.emit("close-menu"); this.dungeonRenderer.toggleMinimap(); }); // Listen for Map button click from UI this.events.on("toggle-minimap", () => { this.dungeonRenderer.toggleMinimap(); }); // Listen for UI update requests this.events.on("request-ui-update", () => { this.emitUIUpdate(); }); // Mouse click -> compute path (only during player turn, and not while menu/minimap is open) this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { if (!this.awaitingPlayer) return; if (this.isMenuOpen || this.dungeonRenderer.isMinimapVisible()) 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 || this.dungeonRenderer.isMinimapVisible()) 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); } else if (ev.type === "killed") { console.log("Showing corpse for:", ev.victimType, "at", ev.x, ev.y); this.dungeonRenderer.spawnCorpse(ev.x, ev.y, ev.victimType || "rat"); } } // 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 ); } }