Add character overlay, where skills and passives (changing this) can be set

This commit is contained in:
Peter Stockings
2026-01-04 21:12:07 +11:00
parent f67f488764
commit 171abb681a
9 changed files with 321 additions and 20 deletions

View File

@@ -7,7 +7,13 @@ export const GAME_CONFIG = {
defense: 2, defense: 2,
level: 1, level: 1,
exp: 0, exp: 0,
expToNextLevel: 10 expToNextLevel: 10,
statPoints: 0,
skillPoints: 0,
strength: 10,
dexterity: 10,
intelligence: 10,
passiveNodes: [] as string[]
}, },
speed: 100, speed: 100,
viewRadius: 8 viewRadius: 8
@@ -56,7 +62,9 @@ export const GAME_CONFIG = {
baseExpToNextLevel: 10, baseExpToNextLevel: 10,
expMultiplier: 1.5, expMultiplier: 1.5,
hpGainPerLevel: 5, hpGainPerLevel: 5,
attackGainPerLevel: 1 attackGainPerLevel: 1,
statPointsPerLevel: 5,
skillPointsPerLevel: 1
}, },

View File

@@ -29,6 +29,14 @@ export type Stats = {
level: number; level: number;
exp: number; exp: number;
expToNextLevel: number; expToNextLevel: number;
// New Progression Fields
statPoints: number;
skillPoints: number;
strength: number;
dexterity: number;
intelligence: number;
passiveNodes: string[]; // List of IDs for allocated passive nodes
}; };

View File

@@ -6,7 +6,10 @@ describe('World Generator', () => {
describe('generateWorld', () => { describe('generateWorld', () => {
it('should generate a world with correct dimensions', () => { it('should generate a world with correct dimensions', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -19,7 +22,10 @@ describe('World Generator', () => {
it('should place player actor', () => { it('should place player actor', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -34,7 +40,10 @@ describe('World Generator', () => {
it('should create walkable rooms', () => { it('should create walkable rooms', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -47,7 +56,10 @@ describe('World Generator', () => {
it('should place exit in valid location', () => { it('should place exit in valid location', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -60,7 +72,10 @@ describe('World Generator', () => {
it('should create enemies', () => { it('should create enemies', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -83,7 +98,10 @@ describe('World Generator', () => {
it('should generate deterministic maps for same level', () => { it('should generate deterministic maps for same level', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -101,7 +119,10 @@ describe('World Generator', () => {
it('should generate different maps for different levels', () => { it('should generate different maps for different levels', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };
@@ -114,7 +135,10 @@ describe('World Generator', () => {
it('should scale enemy difficulty with level', () => { it('should scale enemy difficulty with level', () => {
const runState = { const runState = {
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 }, stats: {
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: []
},
inventory: { gold: 0, items: [] } inventory: { gold: 0, items: [] }
}; };

View File

@@ -11,6 +11,12 @@ describe('Combat Simulation', () => {
exit: { x: 9, y: 9 } exit: { x: 9, y: 9 }
}); });
const createTestStats = (overrides: Partial<any> = {}) => ({
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: [],
...overrides
});
describe('applyAction - attack', () => { describe('applyAction - attack', () => {
it('should deal damage when player attacks enemy', () => { it('should deal damage when player attacks enemy', () => {
const actors = new Map<EntityId, Actor>(); const actors = new Map<EntityId, Actor>();
@@ -20,7 +26,7 @@ describe('Combat Simulation', () => {
pos: { x: 3, y: 3 }, pos: { x: 3, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats()
}); });
actors.set(2, { actors.set(2, {
id: 2, id: 2,
@@ -28,7 +34,7 @@ describe('Combat Simulation', () => {
pos: { x: 4, y: 3 }, pos: { x: 4, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1, level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 })
}); });
const world = createTestWorld(actors); const world = createTestWorld(actors);
@@ -49,7 +55,7 @@ describe('Combat Simulation', () => {
pos: { x: 3, y: 3 }, pos: { x: 3, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 20, hp: 20, attack: 50, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats({ attack: 50 })
}); });
actors.set(2, { actors.set(2, {
id: 2, id: 2,
@@ -57,7 +63,7 @@ describe('Combat Simulation', () => {
pos: { x: 4, y: 3 }, pos: { x: 4, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 1,level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 })
}); });
const world = createTestWorld(actors); const world = createTestWorld(actors);
@@ -78,7 +84,7 @@ describe('Combat Simulation', () => {
pos: { x: 3, y: 3 }, pos: { x: 3, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats()
}); });
actors.set(2, { actors.set(2, {
id: 2, id: 2,
@@ -86,7 +92,7 @@ describe('Combat Simulation', () => {
pos: { x: 4, y: 3 }, pos: { x: 4, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 10, hp: 10, attack: 3, defense: 3, level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 3 })
}); });
const world = createTestWorld(actors); const world = createTestWorld(actors);
@@ -109,7 +115,7 @@ describe('Combat Simulation', () => {
pos: { x: 3, y: 3 }, pos: { x: 3, y: 3 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } stats: createTestStats()
}); });
const world = createTestWorld(actors); const world = createTestWorld(actors);

View File

@@ -63,6 +63,9 @@ function checkLevelUp(player: Actor, events: SimEvent[]) {
s.hp = s.maxHp; // Heal on level up s.hp = s.maxHp; // Heal on level up
s.attack += GAME_CONFIG.leveling.attackGainPerLevel; s.attack += GAME_CONFIG.leveling.attackGainPerLevel;
s.statPoints += GAME_CONFIG.leveling.statPointsPerLevel;
s.skillPoints += GAME_CONFIG.leveling.skillPointsPerLevel;
// Scale requirement // Scale requirement
s.expToNextLevel = Math.floor(s.expToNextLevel * GAME_CONFIG.leveling.expMultiplier); s.expToNextLevel = Math.floor(s.expToNextLevel * GAME_CONFIG.leveling.expMultiplier);

View File

@@ -241,7 +241,13 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map<EntityId, Actor>
defense: enemyDef.baseDefense, defense: enemyDef.baseDefense,
level: 0, level: 0,
exp: 0, exp: 0,
expToNextLevel: 0 expToNextLevel: 0,
statPoints: 0,
skillPoints: 0,
strength: 0,
dexterity: 0,
intelligence: 0,
passiveNodes: []
} }
}); });
enemyId++; enemyId++;

View File

@@ -193,7 +193,7 @@ describe('DungeonRenderer', () => {
pos: { x: 2, y: 2 }, pos: { x: 2, y: 2 },
speed: 100, speed: 100,
energy: 0, energy: 0,
stats: { hp: 10, maxHp: 10, attack: 2, defense: 0, level: 1, exp: 0, expToNextLevel: 0 } stats: { hp: 10, maxHp: 10, attack: 2, defense: 0, level: 1, exp: 0, expToNextLevel: 0, statPoints: 0, skillPoints: 0, strength: 0, dexterity: 0, intelligence: 0, passiveNodes: [] }
}); });
(renderer as any).visible[2 * mockWorld.width + 2] = 1; (renderer as any).visible[2 * mockWorld.width + 2] = 1;

View File

@@ -35,6 +35,7 @@ export class GameScene extends Phaser.Scene {
private dungeonRenderer!: DungeonRenderer; private dungeonRenderer!: DungeonRenderer;
private isMenuOpen = false; private isMenuOpen = false;
private isInventoryOpen = false; private isInventoryOpen = false;
private isCharacterOpen = false;
constructor() { constructor() {
super("GameScene"); super("GameScene");
@@ -67,6 +68,9 @@ export class GameScene extends Phaser.Scene {
this.events.on("inventory-toggled", (isOpen: boolean) => { this.events.on("inventory-toggled", (isOpen: boolean) => {
this.isInventoryOpen = isOpen; this.isInventoryOpen = isOpen;
}); });
this.events.on("character-toggled", (isOpen: boolean) => {
this.isCharacterOpen = isOpen;
});
// Load initial floor // Load initial floor
this.loadFloor(1); this.loadFloor(1);
@@ -97,6 +101,9 @@ export class GameScene extends Phaser.Scene {
// Toggle inventory // Toggle inventory
this.events.emit("toggle-inventory"); this.events.emit("toggle-inventory");
}); });
this.input.keyboard?.on("keydown-C", () => {
this.events.emit("toggle-character");
});
this.input.keyboard?.on("keydown-SPACE", () => { this.input.keyboard?.on("keydown-SPACE", () => {
if (!this.awaitingPlayer) return; if (!this.awaitingPlayer) return;
@@ -119,6 +126,14 @@ export class GameScene extends Phaser.Scene {
this.restartGame(); this.restartGame();
}); });
this.events.on("allocate-stat", (statName: string) => {
this.allocateStat(statName);
});
this.events.on("allocate-passive", (nodeId: string) => {
this.allocatePassive(nodeId);
});
// Mouse click -> compute path (only during player turn, and not while menu/minimap is open) // Mouse click -> compute path (only during player turn, and not while menu/minimap is open)
this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { this.input.on("pointerdown", (p: Phaser.Input.Pointer) => {
if (!this.awaitingPlayer) return; if (!this.awaitingPlayer) return;
@@ -151,7 +166,7 @@ export class GameScene extends Phaser.Scene {
update() { update() {
if (!this.awaitingPlayer) return; if (!this.awaitingPlayer) return;
if (this.isMenuOpen || this.isInventoryOpen || this.dungeonRenderer.isMinimapVisible()) return; if (this.isMenuOpen || this.isInventoryOpen || this.isCharacterOpen || this.dungeonRenderer.isMinimapVisible()) return;
// Auto-walk one step per turn // Auto-walk one step per turn
if (this.playerPath.length >= 2) { if (this.playerPath.length >= 2) {
@@ -339,4 +354,51 @@ export class GameScene extends Phaser.Scene {
player.pos.y * TILE_SIZE + TILE_SIZE / 2 player.pos.y * TILE_SIZE + TILE_SIZE / 2
); );
} }
private allocateStat(statName: string) {
const p = this.world.actors.get(this.playerId);
if (!p || !p.stats || p.stats.statPoints <= 0) return;
p.stats.statPoints--;
if (statName === "strength") {
p.stats.strength++;
p.stats.maxHp += 2;
p.stats.hp += 2;
p.stats.attack += 0.2; // Small boost per Str
} else if (statName === "dexterity") {
p.stats.dexterity++;
p.speed += 1;
} else if (statName === "intelligence") {
p.stats.intelligence++;
// Maybe defense every 5 points?
if (p.stats.intelligence % 5 === 0) {
p.stats.defense++;
}
}
this.emitUIUpdate();
}
private allocatePassive(nodeId: string) {
const p = this.world.actors.get(this.playerId);
if (!p || !p.stats || p.stats.skillPoints <= 0) return;
if (p.stats.passiveNodes.includes(nodeId)) return;
p.stats.skillPoints--;
p.stats.passiveNodes.push(nodeId);
// Apply bonuses
if (nodeId === "off_1") p.stats.attack += 2;
else if (nodeId === "off_2") p.stats.attack += 4;
else if (nodeId === "def_1") {
p.stats.maxHp += 10;
p.stats.hp += 10;
}
else if (nodeId === "def_2") p.stats.defense += 2;
else if (nodeId === "util_1") p.speed += 5;
else if (nodeId === "util_2") p.stats.expToNextLevel = Math.floor(p.stats.expToNextLevel * 0.9);
this.emitUIUpdate();
}
} }

View File

@@ -17,6 +17,7 @@ export default class GameUI extends Phaser.Scene {
private menuButton!: Phaser.GameObjects.Container; private menuButton!: Phaser.GameObjects.Container;
private mapButton!: Phaser.GameObjects.Container; private mapButton!: Phaser.GameObjects.Container;
private backpackButton!: Phaser.GameObjects.Container; private backpackButton!: Phaser.GameObjects.Container;
private characterButton!: Phaser.GameObjects.Container;
// Inventory/Equipment Overlay // Inventory/Equipment Overlay
private inventoryOpen = false; private inventoryOpen = false;
@@ -24,6 +25,14 @@ export default class GameUI extends Phaser.Scene {
private equipmentSlots: Map<string, Phaser.GameObjects.Container> = new Map(); private equipmentSlots: Map<string, Phaser.GameObjects.Container> = new Map();
private backpackSlots: Phaser.GameObjects.Container[] = []; private backpackSlots: Phaser.GameObjects.Container[] = [];
// Character Overlay
private characterOpen = false;
private charContainer!: Phaser.GameObjects.Container;
private attrText!: Phaser.GameObjects.Text;
private skillPointsText!: Phaser.GameObjects.Text;
private statPointsText!: Phaser.GameObjects.Text;
private charStatsText!: Phaser.GameObjects.Text;
// Death Screen // Death Screen
private deathContainer!: Phaser.GameObjects.Container; private deathContainer!: Phaser.GameObjects.Container;
private deathText!: Phaser.GameObjects.Text; private deathText!: Phaser.GameObjects.Text;
@@ -37,6 +46,7 @@ export default class GameUI extends Phaser.Scene {
this.createHud(); this.createHud();
this.createMenu(); this.createMenu();
this.createInventoryOverlay(); this.createInventoryOverlay();
this.createCharacterOverlay();
this.createDeathScreen(); this.createDeathScreen();
// Listen for updates from GameScene // Listen for updates from GameScene
@@ -47,9 +57,11 @@ export default class GameUI extends Phaser.Scene {
gameScene.events.on("toggle-menu", () => this.toggleMenu()); gameScene.events.on("toggle-menu", () => this.toggleMenu());
gameScene.events.on("toggle-inventory", () => this.toggleInventory()); gameScene.events.on("toggle-inventory", () => this.toggleInventory());
gameScene.events.on("toggle-character", () => this.toggleCharacter());
gameScene.events.on("close-menu", () => { gameScene.events.on("close-menu", () => {
this.setMenuOpen(false); this.setMenuOpen(false);
this.setInventoryOpen(false); this.setInventoryOpen(false);
this.setCharacterOpen(false);
}); });
} }
@@ -140,6 +152,20 @@ export default class GameUI extends Phaser.Scene {
bpBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleInventory()); bpBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleInventory());
// Character Button (Right of Backpack)
const charBtnBg = this.add.rectangle(0, 0, btnW, btnH, 0x000000, 0.6).setStrokeStyle(1, 0xffffff, 0.8);
const charBtnLabel = this.add.text(0, 0, "Character", { fontSize: "14px", color: "#ffffff" }).setOrigin(0.5);
this.characterButton = this.add.container(0, 0, [charBtnBg, charBtnLabel]);
this.characterButton.setDepth(1000);
const placeCharButton = () => {
this.characterButton.setPosition(btnW / 2 + 10 + btnW + 5, cam.height - btnH / 2 - 10);
};
placeCharButton();
this.scale.on("resize", placeCharButton);
charBtnBg.setInteractive({ useHandCursor: true }).on("pointerdown", () => this.toggleCharacter());
this.setMenuOpen(false); this.setMenuOpen(false);
} }
@@ -382,6 +408,139 @@ export default class GameUI extends Phaser.Scene {
gameScene.events.emit("inventory-toggled", open); gameScene.events.emit("inventory-toggled", open);
} }
private toggleCharacter() {
this.setCharacterOpen(!this.characterOpen);
if (this.characterOpen) {
this.setMenuOpen(false);
this.setInventoryOpen(false);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("request-ui-update");
}
}
private setCharacterOpen(open: boolean) {
this.characterOpen = open;
this.charContainer.setVisible(open);
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("character-toggled", open);
}
private createCharacterOverlay() {
const cam = this.cameras.main;
const panelW = 850;
const panelH = 550;
const bg = this.add.graphics();
bg.fillStyle(0x000000, 0.9);
bg.fillRect(-panelW / 2, -panelH / 2, panelW, panelH);
// Capture clicks
const hitArea = new Phaser.Geom.Rectangle(-panelW / 2, -panelH / 2, panelW, panelH);
this.add.zone(0, 0, panelW, panelH).setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);
bg.lineStyle(3, 0x443322, 1);
bg.strokeRect(-panelW / 2, -panelH / 2, panelW, panelH);
bg.lineStyle(1, 0x887766, 0.3);
bg.strokeRect(-panelW / 2 + 5, -panelH / 2 + 5, panelW - 10, panelH - 10);
const title = this.add.text(0, -panelH / 2 + 25, "CHARACTER", {
fontSize: "28px",
color: "#d4af37",
fontStyle: "bold",
shadow: { blur: 2, color: "#000000", fill: true, offsetY: 2 }
}).setOrigin(0.5);
this.charContainer = this.add.container(0, 0, [bg, title]);
this.charContainer.setDepth(1001);
// --- Attributes Section ---
const attrX = -300;
const attrY = -145;
const treeX = 50;
const treeY = 0;
const attrTitle = this.add.text(attrX, attrY - 50, "ATTRIBUTES", { fontSize: "20px", color: "#d4af37", fontStyle: "bold" }).setOrigin(0.5);
this.charContainer.add(attrTitle);
this.attrText = this.add.text(attrX - 20, attrY + 30, "", { fontSize: "16px", color: "#ffffff", lineSpacing: 40 }).setOrigin(1, 0.5);
this.charContainer.add(this.attrText);
// Stat allocation buttons
const statsNames = ["strength", "dexterity", "intelligence"];
statsNames.forEach((name, i) => {
const btn = this.add.text(attrX + 50, attrY - 25 + i * 56, "[ + ]", { fontSize: "16px", color: "#00ff00" }).setOrigin(0, 0.5);
btn.setInteractive({ useHandCursor: true }).on("pointerdown", () => {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("allocate-stat", name);
});
this.charContainer.add(btn);
});
this.statPointsText = this.add.text(attrX, attrY + 150, "Stat Points: 0", { fontSize: "16px", color: "#d4af37" }).setOrigin(0.5);
this.charContainer.add(this.statPointsText);
this.skillPointsText = this.add.text(treeX, panelH / 2 - 40, "Skill Points: 0", { fontSize: "20px", color: "#d4af37", fontStyle: "bold" }).setOrigin(0.5);
this.charContainer.add(this.skillPointsText);
// Derived Stats
this.charStatsText = this.add.text(-attrX, 0, "", { fontSize: "14px", color: "#ffffff", lineSpacing: 10 }).setOrigin(0.5);
this.charContainer.add(this.charStatsText);
// --- Skill Tree Section ---
const treeTitle = this.add.text(treeX, -panelH / 2 + 80, "PASSIVE SKILL TREE", { fontSize: "20px", color: "#d4af37", fontStyle: "bold" }).setOrigin(0.5);
this.charContainer.add(treeTitle);
// Simple Grid for Tree
const nodes = [
{ id: "off_1", label: "Martial Arts", x: treeX - 100, y: treeY - 100, color: 0xff4444 },
{ id: "off_2", label: "Brutality", x: treeX - 100, y: treeY + 100, color: 0xcc0000 },
{ id: "def_1", label: "Thick Skin", x: treeX + 100, y: treeY - 100, color: 0x44ff44 },
{ id: "def_2", label: "Juggernaut", x: treeX + 100, y: treeY + 100, color: 0x00cc00 },
{ id: "util_1", label: "Fleetfoot", x: treeX, y: treeY - 150, color: 0x4444ff },
{ id: "util_2", label: "Cunning", x: treeX, y: treeY + 150, color: 0x0000cc },
];
// Connections
const connections = [
["off_1", "off_2"], ["def_1", "def_2"],
["util_1", "off_1"], ["util_1", "def_1"],
["util_2", "off_2"], ["util_2", "def_2"]
];
const treeLines = this.add.graphics();
treeLines.lineStyle(2, 0x333333, 1);
connections.forEach(conn => {
const n1 = nodes.find(n => n.id === conn[0])!;
const n2 = nodes.find(n => n.id === conn[1])!;
treeLines.lineBetween(n1.x, n1.y, n2.x, n2.y);
});
this.charContainer.add(treeLines);
treeLines.setDepth(-1); // Behind nodes
nodes.forEach(n => {
const circle = this.add.circle(n.x, n.y, 25, 0x1a1a1a).setStrokeStyle(2, n.color);
const label = this.add.text(n.x, n.y + 35, n.label, { fontSize: "12px", color: "#ffffff" }).setOrigin(0.5);
circle.setInteractive({ useHandCursor: true }).on("pointerdown", () => {
const gameScene = this.scene.get("GameScene");
gameScene.events.emit("allocate-passive", n.id);
});
this.charContainer.add([circle, label]);
});
const placeChar = () => {
this.charContainer.setPosition(cam.width / 2, cam.height / 2);
};
placeChar();
this.scale.on("resize", placeChar);
this.setCharacterOpen(false);
}
private updateUI(world: World, playerId: EntityId, floorIndex: number) { private updateUI(world: World, playerId: EntityId, floorIndex: number) {
this.updateHud(world, playerId, floorIndex); this.updateHud(world, playerId, floorIndex);
if (this.menuOpen) { if (this.menuOpen) {
@@ -390,6 +549,31 @@ export default class GameUI extends Phaser.Scene {
if (this.inventoryOpen) { if (this.inventoryOpen) {
this.updateInventoryUI(world, playerId); this.updateInventoryUI(world, playerId);
} }
if (this.characterOpen) {
this.updateCharacterUI(world, playerId);
}
}
private updateCharacterUI(world: World, playerId: EntityId) {
const p = world.actors.get(playerId);
if (!p || !p.stats) return;
const s = p.stats;
this.attrText.setText(`STR: ${s.strength}\nDEX: ${s.dexterity}\nINT: ${s.intelligence}`);
this.statPointsText.setText(`Unspent Points: ${s.statPoints}`);
this.skillPointsText.setText(`PASSIVE SKILL POINTS: ${s.skillPoints}`);
const statsLines = [
"SECONDARY STATS",
"",
`Max HP: ${s.maxHp}`,
`Attack: ${s.attack}`,
`Defense: ${s.defense}`,
`Speed: ${p.speed}`,
"",
`Passive Nodes: ${s.passiveNodes.length > 0 ? s.passiveNodes.join(", ") : "(none)"}`
];
this.charStatsText.setText(statsLines.join("\n"));
} }
private updateInventoryUI(world: World, playerId: EntityId) { private updateInventoryUI(world: World, playerId: EntityId) {