729 lines
26 KiB
TypeScript
729 lines
26 KiB
TypeScript
// 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)
|
|
};
|
|
}
|
|
|
|
}
|