Further refactoring

This commit is contained in:
Peter Stockings
2026-01-07 09:19:38 +11:00
parent f9b1abee6e
commit fcd31cce68
11 changed files with 910 additions and 220 deletions

View File

@@ -7,8 +7,6 @@ import {
type RunState,
type World,
type CombatantActor,
type Item,
type ItemDropActor,
type UIUpdatePayload
} from "../core/types";
import { TILE_SIZE } from "../core/constants";
@@ -16,14 +14,14 @@ import { inBounds, isBlocked, isPlayerOnExit, tryDestructTile } from "../engine/
import { findPathAStar } from "../engine/world/pathfinding";
import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation";
import { generateWorld } from "../engine/world/generator";
import { traceProjectile, getClosestVisibleEnemy } 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";
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;
@@ -40,22 +38,18 @@ export class GameScene extends Phaser.Scene {
private playerPath: Vec2[] = [];
private awaitingPlayer = false;
private followPlayer = true;
// 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();
// Targeting Mode
private isTargeting = false;
private targetingItem: string | null = null;
private targetCursor: { x: number, y: number } | null = null;
private targetingGraphics!: Phaser.GameObjects.Graphics;
private targetingSystem!: TargetingSystem;
private turnCount = 0; // Track turns for mana regen
@@ -72,7 +66,10 @@ export class GameScene extends Phaser.Scene {
// Initialize Sub-systems
this.dungeonRenderer = new DungeonRenderer(this);
this.targetingGraphics = this.add.graphics().setDepth(2000);
this.cameraController = new CameraController(this.cameras.main);
this.itemManager = new ItemManager(this.world, this.entityManager);
const targetingGraphics = this.add.graphics().setDepth(2000);
this.targetingSystem = new TargetingSystem(targetingGraphics);
// Launch UI Scene
this.scene.launch("GameUI");
@@ -177,82 +174,48 @@ export class GameScene extends Phaser.Scene {
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) {
// Check if already targeting this item -> verify intent to throw
if (this.isTargeting && this.targetingItem === item.id) {
if (this.targetCursor) {
this.executeThrow(this.targetCursor.x, this.targetCursor.y);
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(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y);
}
return;
}
this.targetingItem = item.id;
this.isTargeting = true;
// Auto-target closest visible enemy
const closest = getClosestVisibleEnemy(
this.world,
player.pos,
this.dungeonRenderer.seenArray,
this.targetingSystem.startTargeting(
item.id,
player.pos,
this.world,
this.dungeonRenderer.seenArray,
this.world.width
);
if (closest) {
this.targetCursor = closest;
} else {
// Default to player pos or null?
// If we default to mouse pos, we need current mouse pos.
// Let's default to null and wait for mouse move, OR default to player pos forward?
// Let's just default to null until mouse moves.
this.targetCursor = null;
}
this.drawTargetingLine();
console.log("Targeting Mode: ON");
this.emitUIUpdate();
}
});
// Right Clicks to cancel targeting
this.input.on('pointerdown', (p: Phaser.Input.Pointer) => {
if (p.rightButtonDown() && this.isTargeting) {
this.cancelTargeting();
if (p.rightButtonDown() && this.targetingSystem.isActive) {
this.targetingSystem.cancel();
this.emitUIUpdate();
}
});
// Zoom Control
this.input.on(
"wheel",
(
_pointer: Phaser.Input.Pointer,
_gameObjects: any,
_deltaX: number,
deltaY: number,
_deltaZ: number
) => {
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;
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);
}
);
this.cameraController.handleWheel(deltaY);
});
// Disable context menu for right-click panning
this.input.mouse?.disableContextMenu();
@@ -260,12 +223,13 @@ export class GameScene extends Phaser.Scene {
// 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) {
if (this.targetingSystem.isActive) {
const tx = Math.floor(p.worldX / TILE_SIZE);
const ty = Math.floor(p.worldY / TILE_SIZE);
// Only update if changed to avoid jitter if needed, but simple assignment is fine
this.targetCursor = { x: tx, y: ty };
this.drawTargetingLine();
const player = this.world.actors.get(this.playerId) as CombatantActor;
if (player) {
this.targetingSystem.updateCursor({ x: tx, y: ty }, player.pos);
}
}
return;
}
@@ -283,28 +247,27 @@ export class GameScene extends Phaser.Scene {
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;
this.cameraController.handlePan(dx, dy);
}
if (this.isTargeting) {
if (this.targetingSystem.isActive) {
const tx = Math.floor(p.worldX / TILE_SIZE);
const ty = Math.floor(p.worldY / TILE_SIZE);
this.targetCursor = { x: tx, y: ty };
this.drawTargetingLine();
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.isTargeting) {
if (this.targetingSystem.isActive) {
// Only Left Click throws
if (p.button === 0) {
if (this.targetCursor) {
this.executeThrow(this.targetCursor.x, this.targetCursor.y);
if (this.targetingSystem.cursorPos) {
this.executeThrow(this.targetingSystem.cursorPos.x, this.targetingSystem.cursorPos.y);
}
}
return;
@@ -313,7 +276,7 @@ export class GameScene extends Phaser.Scene {
// Movement Click
if (p.button !== 0) return;
this.followPlayer = true;
this.cameraController.enableFollowMode();
if (!this.awaitingPlayer) return;
if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return;
@@ -410,8 +373,9 @@ export class GameScene extends Phaser.Scene {
if (this.cursors.down!.isDown) dy += 1;
if (dx !== 0 || dy !== 0) {
if (this.isTargeting) {
this.cancelTargeting();
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;
@@ -443,7 +407,7 @@ export class GameScene extends Phaser.Scene {
playerId: this.playerId,
floorIndex: this.floorIndex,
uiState: {
targetingItemId: this.targetingItem
targetingItemId: this.targetingSystem.itemId
}
};
this.events.emit("update-ui", payload);
@@ -457,11 +421,15 @@ export class GameScene extends Phaser.Scene {
}
this.awaitingPlayer = false;
this.followPlayer = true;
this.cameraController.enableFollowMode();
// Check for pickups right after move (before enemy turn, so you get it efficiently)
if (action.type === "move") {
this.tryPickupItem();
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);
@@ -509,8 +477,8 @@ export class GameScene extends Phaser.Scene {
if (!this.world.actors.has(this.playerId)) {
this.syncRunStateFromPlayer();
const uiScene = this.scene.get("GameUI") as any;
if (uiScene) {
const uiScene = this.scene.get("GameUI") as GameUI;
if (uiScene && 'showDeathScreen' in uiScene) {
uiScene.showDeathScreen({
floor: this.floorIndex,
gold: this.runState.inventory.gold,
@@ -528,8 +496,9 @@ export class GameScene extends Phaser.Scene {
}
this.dungeonRenderer.computeFov(this.playerId);
if (this.followPlayer) {
this.centerCameraOnPlayer();
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();
@@ -537,18 +506,19 @@ export class GameScene extends Phaser.Scene {
private loadFloor(floor: number) {
this.floorIndex = floor;
this.followPlayer = true;
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.cameras.main.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE);
this.cameraController.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE);
this.dungeonRenderer.initializeFloor(this.world, this.playerId);
@@ -557,7 +527,8 @@ export class GameScene extends Phaser.Scene {
this.dungeonRenderer.computeFov(this.playerId);
this.centerCameraOnPlayer();
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();
@@ -584,139 +555,49 @@ export class GameScene extends Phaser.Scene {
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 drawTargetingLine() {
if (!this.world || !this.targetCursor) {
this.targetingGraphics.clear();
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 = this.targetCursor.x * TILE_SIZE + TILE_SIZE / 2;
const endY = this.targetCursor.y * TILE_SIZE + TILE_SIZE / 2;
this.targetingGraphics.lineStyle(2, 0xff0000, 0.7);
this.targetingGraphics.lineBetween(startX, startY, endX, endY);
this.targetingGraphics.strokeRect(this.targetCursor.x * TILE_SIZE, this.targetCursor.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
private cancelTargeting() {
this.isTargeting = false;
this.targetingItem = null;
this.targetCursor = null;
this.targetingGraphics.clear();
console.log("Targeting cancelled");
this.emitUIUpdate();
}
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,
() => {
private executeThrow(_targetX: number, _targetY: number) {
const success = this.targetingSystem.executeThrow(
this.world,
this.playerId,
this.entityManager,
(blockedPos, hitActorId, item) => {
if (hitActorId !== undefined) {
const victim = this.world.actors.get(hitActorId) as CombatantActor;
if (victim) {
const dmg = item.stats?.attack ?? 1; // Use item stats
const dmg = item.stats?.attack ?? 1;
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);
const player = this.world.actors.get(this.playerId) as CombatantActor;
this.dungeonRenderer.showProjectile(
player.pos,
blockedPos,
item.id,
() => {
// Drop the actual item at the landing spot
this.itemManager.spawnItem(item, blockedPos);
// "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);
}
// Trigger destruction/interaction
if (tryDestructTile(this.world, blockedPos.x, blockedPos.y)) {
this.dungeonRenderer.updateTile(blockedPos.x, blockedPos.y);
}
this.cancelTargeting();
this.commitPlayerAction({ type: "throw" });
this.emitUIUpdate();
this.targetingSystem.cancel();
this.commitPlayerAction({ type: "throw" });
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
if (!success) {
this.emitUIUpdate();
}
}