// 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 UIUpdatePayload } 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 { DungeonRenderer } from "../rendering/DungeonRenderer"; import { GAME_CONFIG } from "../core/config/GameConfig"; import { EntityManager } from "../engine/EntityManager"; import { ProgressionManager } from "../engine/ProgressionManager"; import GameUI from "../ui/GameUI"; import { CameraController } from "./systems/CameraController"; import { ItemManager } from "./systems/ItemManager"; import { TargetingSystem } from "./systems/TargetingSystem"; 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; // Sub-systems private dungeonRenderer!: DungeonRenderer; private cameraController!: CameraController; private itemManager!: ItemManager; private isMenuOpen = false; private isInventoryOpen = false; private isCharacterOpen = false; private entityManager!: EntityManager; private progressionManager: ProgressionManager = new ProgressionManager(); private targetingSystem!: TargetingSystem; 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.cameraController = new CameraController(this.cameras.main); this.itemManager = new ItemManager(this.world, this.entityManager); this.targetingSystem = new TargetingSystem(this); // 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]; // Ranged Weapon Logic if (item.type === "Weapon" && item.weaponType === "ranged") { // Check Ammo if (item.stats.currentAmmo <= 0) { // Try Reload const ammoId = `ammo_${item.stats.ammoType}`; const ammoItem = player.inventory.items.find(it => it.id === ammoId); // Simple check if (ammoItem && ammoItem.quantity && ammoItem.quantity > 0) { const needed = item.stats.magazineSize - item.stats.currentAmmo; const toTake = Math.min(needed, ammoItem.quantity); item.stats.currentAmmo += toTake; ammoItem.quantity -= toTake; if (ammoItem.quantity <= 0) { player.inventory.items = player.inventory.items.filter(it => it !== ammoItem); } this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Reloaded!", "#00ff00"); console.log("Reloaded. Ammo:", item.stats.currentAmmo); this.commitPlayerAction({ type: "wait" }); this.emitUIUpdate(); } else { this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "No Ammo!", "#ff0000"); console.log("No ammo found for", item.name); } return; } // Has ammo, start targeting if (this.targetingSystem.isActive && this.targetingSystem.itemId === item.id) { // Already targeting - execute shoot if (this.targetingSystem.cursorPos) { this.executeThrow(); } return; } const { x: tx, y: ty } = this.getPointerTilePos(this.input.activePointer); this.targetingSystem.startTargeting( item.id, player.pos, this.world, this.entityManager, this.playerId, this.dungeonRenderer.seenArray, this.world.width, { x: tx, y: ty } ); this.emitUIUpdate(); return; } const result = this.itemManager.handleUse(data.itemId, player); if (result.success && result.consumed) { const healAmount = player.stats.maxHp - player.stats.hp; // Already healed by manager const actualHeal = Math.min(healAmount, player.stats.hp); this.dungeonRenderer.showHeal(player.pos.x, player.pos.y, actualHeal); this.commitPlayerAction({ type: "wait" }); this.emitUIUpdate(); } else if (result.success && !result.consumed) { // Throwable item - start targeting if (this.targetingSystem.isActive && this.targetingSystem.itemId === item.id) { // Already targeting - execute throw if (this.targetingSystem.cursorPos) { this.executeThrow(); } return; } const { x: tx, y: ty } = this.getPointerTilePos(this.input.activePointer); this.targetingSystem.startTargeting( item.id, player.pos, this.world, this.entityManager, this.playerId, this.dungeonRenderer.seenArray, this.world.width, { x: tx, y: ty } ); this.emitUIUpdate(); } }); this.events.on("drop-item", (data: { itemId: string, pointerX: number, pointerY: number }) => { if (!this.awaitingPlayer) return; const player = this.world.actors.get(this.playerId) as CombatantActor; if (!player || !player.inventory) return; const item = this.itemManager.getItem(player, data.itemId); if (!item) return; // Determine drop position based on pointer or player pos let dropPos = { x: player.pos.x, y: player.pos.y }; if (data.pointerX !== undefined && data.pointerY !== undefined) { const tilePos = this.getPointerTilePos({ x: data.pointerX, y: data.pointerY } as Phaser.Input.Pointer); // Limit drop distance to 1 tile from player for balance/fairness const dx = Math.sign(tilePos.x - player.pos.x); const dy = Math.sign(tilePos.y - player.pos.y); const targetX = player.pos.x + dx; const targetY = player.pos.y + dy; if (inBounds(this.world, targetX, targetY) && !isBlocked(this.world, targetX, targetY, this.entityManager)) { dropPos = { x: targetX, y: targetY }; } } // Remove from inventory and spawn in world if (this.itemManager.removeFromInventory(player, data.itemId)) { this.itemManager.spawnItem(item, dropPos); const quantityText = (item.quantity && item.quantity > 1) ? ` x${item.quantity}` : ""; this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, `Dropped ${item.name}${quantityText}`, "#aaaaaa"); this.emitUIUpdate(); } }); // Right Clicks to cancel targeting this.input.on('pointerdown', (p: Phaser.Input.Pointer) => { if (p.rightButtonDown() && this.targetingSystem.isActive) { this.targetingSystem.cancel(); this.emitUIUpdate(); } }); // Zoom Control this.input.on("wheel", (_pointer: Phaser.Input.Pointer, _gameObjects: Phaser.GameObjects.GameObject[], _deltaX: number, deltaY: number, _deltaZ: number) => { if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; this.cameraController.handleWheel(deltaY); }); // 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.targetingSystem.isActive) { const tx = Math.floor(p.worldX / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE); const player = this.world.actors.get(this.playerId) as CombatantActor; if (player) { this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos); } } 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.cameraController.handlePan(dx, dy); } if (this.targetingSystem.isActive) { const tx = Math.floor(p.worldX / TILE_SIZE); const ty = Math.floor(p.worldY / TILE_SIZE); const player = this.world.actors.get(this.playerId) as CombatantActor; if (player) { this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos); } } }); // Mouse click -> this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { // Targeting Click if (this.targetingSystem.isActive) { // Only Left Click throws if (p.button === 0) { if (this.targetingSystem.cursorPos) { this.executeThrow(); } } return; } // Movement Click if (p.button !== 0) return; this.cameraController.enableFollowMode(); 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) { if (this.targetingSystem.isActive) { this.targetingSystem.cancel(); this.emitUIUpdate(); } 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() { const payload: UIUpdatePayload = { world: this.world, playerId: this.playerId, floorIndex: this.floorIndex, uiState: { targetingItemId: this.targetingSystem.itemId } }; this.events.emit("update-ui", payload); } private commitPlayerAction(action: Action) { const playerEvents = applyAction(this.world, this.playerId, action, this.entityManager); if (playerEvents.some(ev => ev.type === "move-blocked")) { return; } this.awaitingPlayer = false; this.cameraController.enableFollowMode(); // Check for pickups right after move (before enemy turn, so you get it efficiently) if (action.type === "move") { const player = this.world.actors.get(this.playerId) as CombatantActor; const pickedItem = this.itemManager.tryPickup(player); if (pickedItem) { this.emitUIUpdate(); } } 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 GameUI; if (uiScene && 'showDeathScreen' in 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.cameraController.isFollowing) { const player = this.world.actors.get(this.playerId) as CombatantActor; this.cameraController.centerOnTile(player.pos.x, player.pos.y); } this.dungeonRenderer.render(this.playerPath); this.emitUIUpdate(); } private loadFloor(floor: number) { this.floorIndex = floor; this.cameraController.enableFollowMode(); const { world, playerId } = generateWorld(floor, this.runState); this.world = world; this.playerId = playerId; this.entityManager = new EntityManager(this.world); this.itemManager.updateWorld(this.world, this.entityManager); this.playerPath = []; this.awaitingPlayer = false; this.cameraController.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); const player = this.world.actors.get(this.playerId) as CombatantActor; this.cameraController.centerOnTile(player.pos.x, player.pos.y); 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 executeThrow() { const success = this.targetingSystem.executeThrow( this.world, this.playerId, this.entityManager, (blockedPos, hitActorId, item) => { // Damage Logic if (hitActorId !== undefined) { const victim = this.world.actors.get(hitActorId) as CombatantActor; if (victim) { const stats = 'stats' in item ? item.stats : undefined; const dmg = (stats && 'attack' in stats) ? (stats.attack ?? 1) : 1; victim.stats.hp -= dmg; this.dungeonRenderer.showDamage(victim.pos.x, victim.pos.y, dmg); this.dungeonRenderer.shakeCamera(); } } const player = this.world.actors.get(this.playerId) as CombatantActor; // Projectile Visuals let projectileId = item.id; if (item.type === "Weapon" && item.weaponType === "ranged") { projectileId = `ammo_${item.stats.ammoType}`; // Show ammo sprite // Consume Ammo if (item.stats.currentAmmo > 0) { item.stats.currentAmmo--; } } this.dungeonRenderer.showProjectile( player.pos, blockedPos, projectileId, () => { // Only drop item if it acts as a thrown item (Consumable/Misc), NOT Weapon const shouldDrop = item.type !== "Weapon"; if (shouldDrop) { // Drop the actual item at the landing spot this.itemManager.spawnItem(item, blockedPos); } // Trigger destruction/interaction if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) { this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y); } this.targetingSystem.cancel(); this.commitPlayerAction({ type: "throw" }); // Or 'attack' if shooting? 'throw' is fine for now this.emitUIUpdate(); } ); } ); if (!success) { this.emitUIUpdate(); } } private getPointerTilePos(pointer: Phaser.Input.Pointer): { x: number, y: number } { const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y); return { x: Math.floor(worldPoint.x / TILE_SIZE), y: Math.floor(worldPoint.y / TILE_SIZE) }; } }