// Reading types.ts to verify actor structure before next step import Phaser from "phaser"; import { type EntityId, type Vec2, type Action, type RunState, type World, type CombatantActor, type Item, type ItemDropActor } from "../core/types"; import { TILE_SIZE } from "../core/constants"; import { inBounds, isBlocked, isPlayerOnExit, tryDestructTile } from "../engine/world/world-logic"; import { findPathAStar } from "../engine/world/pathfinding"; import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation"; import { generateWorld } from "../engine/world/generator"; import { traceProjectile } from "../engine/gameplay/CombatLogic"; import { DungeonRenderer } from "../rendering/DungeonRenderer"; import { GAME_CONFIG } from "../core/config/GameConfig"; import { EntityManager } from "../engine/EntityManager"; import { ProgressionManager } from "../engine/ProgressionManager"; export class GameScene extends Phaser.Scene { private world!: World; private playerId!: EntityId; private floorIndex = 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; private followPlayer = true; // Sub-systems private dungeonRenderer!: DungeonRenderer; private isMenuOpen = false; private isInventoryOpen = false; private isCharacterOpen = false; private entityManager!: EntityManager; private progressionManager: ProgressionManager = new ProgressionManager(); // Targeting Mode private isTargeting = false; private targetingItem: string | null = null; private targetingGraphics!: Phaser.GameObjects.Graphics; private turnCount = 0; // Track turns for mana regen constructor() { super("GameScene"); } create() { this.cursors = this.input.keyboard!.createCursorKeys(); // Camera this.cameras.main.setZoom(GAME_CONFIG.rendering.cameraZoom); this.cameras.main.fadeIn(1000, 0, 0, 0); // Initialize Sub-systems this.dungeonRenderer = new DungeonRenderer(this); this.targetingGraphics = this.add.graphics().setDepth(2000); // Launch UI Scene this.scene.launch("GameUI"); // Listen for Menu State this.events.on("menu-toggled", (isOpen: boolean) => { this.isMenuOpen = isOpen; }); this.events.on("inventory-toggled", (isOpen: boolean) => { this.isInventoryOpen = isOpen; }); this.events.on("character-toggled", (isOpen: boolean) => { this.isCharacterOpen = isOpen; }); // Load initial floor this.loadFloor(1); // Menu Inputs this.input.keyboard?.on("keydown-I", () => { if (this.dungeonRenderer.isMinimapVisible()) { this.dungeonRenderer.toggleMinimap(); } this.events.emit("toggle-menu"); this.emitUIUpdate(); }); this.input.keyboard?.on("keydown-ESC", () => { this.events.emit("close-menu"); if (this.dungeonRenderer.isMinimapVisible()) { this.dungeonRenderer.toggleMinimap(); } }); this.input.keyboard?.on("keydown-M", () => { this.events.emit("close-menu"); this.dungeonRenderer.toggleMinimap(); }); this.input.keyboard?.on("keydown-B", () => { this.events.emit("toggle-inventory"); }); this.input.keyboard?.on("keydown-C", () => { this.events.emit("toggle-character"); }); this.input.keyboard?.on("keydown-SPACE", () => { if (!this.awaitingPlayer) return; if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; this.commitPlayerAction({ type: "wait" }); }); // 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(); }); // Listen for game restart this.events.on("restart-game", () => { this.restartGame(); }); this.events.on("allocate-stat", (statName: string) => { const player = this.world.actors.get(this.playerId) as CombatantActor; if (player) { this.progressionManager.allocateStat(player, statName); this.emitUIUpdate(); } }); this.events.on("allocate-passive", (nodeId: string) => { const player = this.world.actors.get(this.playerId) as CombatantActor; if (player) { this.progressionManager.allocatePassive(player, nodeId); this.emitUIUpdate(); } }); this.events.on("player-wait", () => { if (!this.awaitingPlayer) return; if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; this.commitPlayerAction({ type: "wait" }); }); this.events.on("player-search", () => { if (!this.awaitingPlayer) return; if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; console.log("Player searching..."); this.commitPlayerAction({ type: "wait" }); }); this.events.on("use-item", (data: { itemId: string }) => { if (!this.awaitingPlayer) return; const player = this.world.actors.get(this.playerId) as CombatantActor; if (!player || !player.inventory) return; const itemIdx = player.inventory.items.findIndex(it => it.id === data.itemId); if (itemIdx === -1) return; const item = player.inventory.items[itemIdx]; if (item.stats && item.stats.hp && item.stats.hp > 0) { const healAmount = item.stats.hp; if (player.stats.hp < player.stats.maxHp) { player.stats.hp = Math.min(player.stats.hp + healAmount, player.stats.maxHp); // Remove item after use player.inventory.items.splice(itemIdx, 1); this.dungeonRenderer.showHeal(player.pos.x, player.pos.y, healAmount); this.commitPlayerAction({ type: "wait" }); this.emitUIUpdate(); } } else if (item.throwable) { this.targetingItem = item.id; this.isTargeting = true; console.log("Targeting Mode: ON"); } }); // Right Clicks to cancel targeting this.input.on('pointerdown', (p: Phaser.Input.Pointer) => { if (p.rightButtonDown() && this.isTargeting) { this.cancelTargeting(); } }); // Zoom Control this.input.on( "wheel", ( _pointer: Phaser.Input.Pointer, _gameObjects: any, _deltaX: number, deltaY: number, _deltaZ: number ) => { if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; const zoomDir = deltaY > 0 ? -1 : 1; const newZoom = Phaser.Math.Clamp( this.cameras.main.zoom + zoomDir * GAME_CONFIG.rendering.zoomStep, GAME_CONFIG.rendering.minZoom, GAME_CONFIG.rendering.maxZoom ); this.cameras.main.setZoom(newZoom); } ); // Disable context menu for right-click panning this.input.mouse?.disableContextMenu(); // Camera Panning this.input.on("pointermove", (p: Phaser.Input.Pointer) => { if (!p.isDown) { // Even if not down, we might need to update targeting line if (this.isTargeting) { this.updateTargetingLine(p); } return; } if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; const isRightDrag = p.rightButtonDown(); const isMiddleDrag = p.middleButtonDown(); const isShiftDrag = p.isDown && p.event.shiftKey; if (isRightDrag || isMiddleDrag || isShiftDrag) { const { x, y } = p.position; const { x: prevX, y: prevY } = p.prevPosition; const dx = (x - prevX) / this.cameras.main.zoom; const dy = (y - prevY) / this.cameras.main.zoom; this.cameras.main.scrollX -= dx; this.cameras.main.scrollY -= dy; this.followPlayer = false; } if (this.isTargeting) { this.updateTargetingLine(p); } }); // Mouse click -> this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { // Targeting Click if (this.isTargeting) { // Only Left Click throws if (p.button === 0) { const tx = Math.floor(p.worldX / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE); this.executeThrow(tx, ty); } return; } // Movement Click if (p.button !== 0) return; this.followPlayer = true; if (!this.awaitingPlayer) return; if (this.isMenuOpen || this.isInventoryOpen || 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; if (!this.dungeonRenderer.isSeen(tx, ty)) return; const isEnemy = [...this.world.actors.values()].some(a => a.category === "combatant" && a.pos.x === tx && a.pos.y === ty && !a.isPlayer ); const player = this.world.actors.get(this.playerId) as CombatantActor; const dx = tx - player.pos.x; const dy = ty - player.pos.y; const isDiagonalNeighbor = Math.abs(dx) === 1 && Math.abs(dy) === 1; if (isEnemy && isDiagonalNeighbor) { const targetId = [...this.world.actors.values()].find( a => a.category === "combatant" && a.pos.x === tx && a.pos.y === ty && !a.isPlayer )?.id; if (targetId !== undefined) { this.commitPlayerAction({ type: "attack", targetId }); return; } } 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.isInventoryOpen || this.isCharacterOpen || this.dungeonRenderer.isMinimapVisible()) return; // Auto-walk one step per turn if (this.playerPath.length >= 2) { const player = this.world.actors.get(this.playerId) as CombatantActor; 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, this.entityManager)) { const targetId = [...this.world.actors.values()].find( a => a.category === "combatant" && a.pos.x === next.x && a.pos.y === next.y && !a.isPlayer )?.id; if (targetId !== undefined) { this.commitPlayerAction({ type: "attack", targetId }); this.playerPath = []; return; } else { this.playerPath = []; return; } } this.commitPlayerAction({ type: "move", dx, dy }); this.playerPath.shift(); return; } let action: Action | null = null; let dx = 0; let dy = 0; const anyJustDown = Phaser.Input.Keyboard.JustDown(this.cursors.left!) || Phaser.Input.Keyboard.JustDown(this.cursors.right!) || Phaser.Input.Keyboard.JustDown(this.cursors.up!) || Phaser.Input.Keyboard.JustDown(this.cursors.down!); if (anyJustDown) { dx = 0; dy = 0; if (this.cursors.left!.isDown) dx -= 1; if (this.cursors.right!.isDown) dx += 1; if (this.cursors.up!.isDown) dy -= 1; if (this.cursors.down!.isDown) dy += 1; if (dx !== 0 || dy !== 0) { const player = this.world.actors.get(this.playerId) as CombatantActor; const targetX = player.pos.x + dx; const targetY = player.pos.y + dy; const targetId = [...this.world.actors.values()].find( a => a.category === "combatant" && a.pos.x === targetX && a.pos.y === targetY && !a.isPlayer )?.id; if (targetId !== undefined) { action = { type: "attack", targetId }; } else { if (Math.abs(dx) + Math.abs(dy) === 1) { 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, floorIndex: this.floorIndex }); } private commitPlayerAction(action: Action) { this.awaitingPlayer = false; this.followPlayer = true; const playerEvents = applyAction(this.world, this.playerId, action, this.entityManager); // Check for pickups right after move (before enemy turn, so you get it efficiently) if (action.type === "move") { this.tryPickupItem(); } const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityManager); this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId; this.turnCount++; if (this.turnCount % GAME_CONFIG.mana.regenInterval === 0) { const player = this.world.actors.get(this.playerId) as CombatantActor; if (player && player.stats.mana < player.stats.maxMana) { const regenAmount = Math.min( GAME_CONFIG.mana.regenPerTurn, player.stats.maxMana - player.stats.mana ); player.stats.mana += regenAmount; } } const allEvents = [...playerEvents, ...enemyStep.events]; for (const ev of allEvents) { if (ev.type === "damaged") { this.dungeonRenderer.showDamage(ev.x, ev.y, ev.amount, ev.isCrit, ev.isBlock); } else if (ev.type === "dodged") { this.dungeonRenderer.showDodge(ev.x, ev.y); } else if (ev.type === "healed") { this.dungeonRenderer.showHeal(ev.x, ev.y, ev.amount); } else if (ev.type === "killed") { this.dungeonRenderer.spawnCorpse(ev.x, ev.y, ev.victimType || "rat"); } else if (ev.type === "waited" && ev.actorId === this.playerId) { const player = this.world.actors.get(this.playerId) as CombatantActor; if (player) { this.dungeonRenderer.showWait(player.pos.x, player.pos.y); } } else if (ev.type === "orb-spawned") { this.dungeonRenderer.spawnOrb(ev.orbId, ev.x, ev.y); } else if (ev.type === "exp-collected" && ev.actorId === this.playerId) { this.dungeonRenderer.collectOrb(ev.actorId, ev.amount, ev.x, ev.y); } else if (ev.type === "leveled-up" && ev.actorId === this.playerId) { this.dungeonRenderer.showLevelUp(ev.x, ev.y); } else if (ev.type === "enemy-alerted") { this.dungeonRenderer.showAlert(ev.x, ev.y); } } if (!this.world.actors.has(this.playerId)) { this.syncRunStateFromPlayer(); const uiScene = this.scene.get("GameUI") as any; if (uiScene) { uiScene.showDeathScreen({ floor: this.floorIndex, gold: this.runState.inventory.gold, stats: this.runState.stats }); } return; } if (isPlayerOnExit(this.world, this.playerId)) { this.syncRunStateFromPlayer(); this.floorIndex++; this.loadFloor(this.floorIndex); return; } this.dungeonRenderer.computeFov(this.playerId); if (this.followPlayer) { this.centerCameraOnPlayer(); } this.dungeonRenderer.render(this.playerPath); this.emitUIUpdate(); } private loadFloor(floor: number) { this.floorIndex = floor; this.followPlayer = true; const { world, playerId } = generateWorld(floor, this.runState); this.world = world; this.playerId = playerId; this.entityManager = new EntityManager(this.world); this.playerPath = []; this.awaitingPlayer = false; this.cameras.main.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE); this.dungeonRenderer.initializeFloor(this.world, this.playerId); const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityManager); this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId; this.dungeonRenderer.computeFov(this.playerId); this.centerCameraOnPlayer(); this.dungeonRenderer.render(this.playerPath); this.emitUIUpdate(); // Create daggers for testing if none exist (redundant if generator does it, but good for safety) // Removed to rely on generator.ts } private syncRunStateFromPlayer() { const p = this.world.actors.get(this.playerId) as CombatantActor; if (!p || p.category !== "combatant" || !p.stats || !p.inventory) return; this.runState = { stats: { ...p.stats }, inventory: { gold: p.inventory.gold, items: [...p.inventory.items] } }; } private restartGame() { this.runState = { stats: { ...GAME_CONFIG.player.initialStats }, inventory: { gold: 0, items: [] } }; this.floorIndex = 1; this.loadFloor(this.floorIndex); } private centerCameraOnPlayer() { const player = this.world.actors.get(this.playerId) as CombatantActor; this.cameras.main.centerOn( player.pos.x * TILE_SIZE + TILE_SIZE / 2, player.pos.y * TILE_SIZE + TILE_SIZE / 2 ); } private updateTargetingLine(p: Phaser.Input.Pointer) { if (!this.world) return; this.targetingGraphics.clear(); const player = this.world.actors.get(this.playerId) as CombatantActor; if (!player) return; const startX = player.pos.x * TILE_SIZE + TILE_SIZE / 2; const startY = player.pos.y * TILE_SIZE + TILE_SIZE / 2; const endX = p.worldX; const endY = p.worldY; this.targetingGraphics.lineStyle(2, 0xff0000, 0.7); this.targetingGraphics.lineBetween(startX, startY, endX, endY); const tx = Math.floor(endX / TILE_SIZE); const ty = Math.floor(endY / TILE_SIZE); this.targetingGraphics.strokeRect(tx * TILE_SIZE, ty * TILE_SIZE, TILE_SIZE, TILE_SIZE); } private cancelTargeting() { this.isTargeting = false; this.targetingItem = null; this.targetingGraphics.clear(); console.log("Targeting cancelled"); } private executeThrow(targetX: number, targetY: number) { const player = this.world.actors.get(this.playerId) as CombatantActor; if (!player) return; const itemArg = this.targetingItem; if (!itemArg) return; const itemIdx = player.inventory!.items.findIndex(it => it.id === itemArg); if (itemIdx === -1) { console.log("Item not found!"); this.cancelTargeting(); return; } const item = player.inventory!.items[itemIdx]; player.inventory!.items.splice(itemIdx, 1); const start = player.pos; const end = { x: targetX, y: targetY }; const result = traceProjectile(this.world, start, end, this.entityManager, this.playerId); const { blockedPos, hitActorId } = result; this.dungeonRenderer.showProjectile( start, blockedPos, item.id, () => { if (hitActorId !== undefined) { const victim = this.world.actors.get(hitActorId) as CombatantActor; if (victim) { const dmg = item.stats?.attack ?? 1; // Use item stats victim.stats.hp -= dmg; this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, dmg); this.dungeonRenderer.shakeCamera(); if (victim.stats.hp <= 0) { // Force kill handled by simulation } } } // Drop the actual item at the landing spot this.spawnItem(item, blockedPos.x, blockedPos.y); // "Count as walking over the tile" -> Trigger destruction/interaction // e.g. breaking grass, opening items if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) { this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y); } this.cancelTargeting(); this.commitPlayerAction({ type: "wait" }); this.emitUIUpdate(); } ); } private spawnItem(item: Item, x: number, y: number) { if (!this.world || !this.entityManager) return; const id = this.entityManager.getNextId(); const drop: ItemDropActor = { id, pos: { x, y }, category: "item_drop", item: { ...item } // Clone item }; this.entityManager.addActor(drop); // Ensure renderer knows? Renderer iterates world.actors, so it should pick it up if we handle "item_drop" } private tryPickupItem() { const player = this.world.actors.get(this.playerId) as CombatantActor; if (!player) return; const actors = this.entityManager.getActorsAt(player.pos.x, player.pos.y); const itemActor = actors.find(a => (a as any).category === "item_drop"); // Safe check if (itemActor) { const drop = itemActor as any; // Cast to ItemDropActor const item = drop.item; // Add to inventory player.inventory!.items.push(item); // Remove from world this.entityManager.removeActor(drop.id); console.log("Picked up:", item.name); // Show FX? // this.dungeonRenderer.showPickup(player.pos.x, player.pos.y); -> need to implement this.emitUIUpdate(); } } }