diff --git a/src/core/config/GameConfig.ts b/src/core/config/GameConfig.ts index 6bd2a31..524fdc8 100644 --- a/src/core/config/GameConfig.ts +++ b/src/core/config/GameConfig.ts @@ -7,7 +7,13 @@ export const GAME_CONFIG = { defense: 2, level: 1, exp: 0, - expToNextLevel: 10 + expToNextLevel: 10, + statPoints: 0, + skillPoints: 0, + strength: 10, + dexterity: 10, + intelligence: 10, + passiveNodes: [] as string[] }, speed: 100, viewRadius: 8 @@ -56,7 +62,9 @@ export const GAME_CONFIG = { baseExpToNextLevel: 10, expMultiplier: 1.5, hpGainPerLevel: 5, - attackGainPerLevel: 1 + attackGainPerLevel: 1, + statPointsPerLevel: 5, + skillPointsPerLevel: 1 }, diff --git a/src/core/types.ts b/src/core/types.ts index 6596f34..f1f64cf 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -29,6 +29,14 @@ export type Stats = { level: number; exp: 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 }; diff --git a/src/engine/__tests__/generator.test.ts b/src/engine/__tests__/generator.test.ts index e7e4a3e..52e2c88 100644 --- a/src/engine/__tests__/generator.test.ts +++ b/src/engine/__tests__/generator.test.ts @@ -6,7 +6,10 @@ describe('World Generator', () => { describe('generateWorld', () => { it('should generate a world with correct dimensions', () => { 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: [] } }; @@ -19,7 +22,10 @@ describe('World Generator', () => { it('should place player actor', () => { 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: [] } }; @@ -34,7 +40,10 @@ describe('World Generator', () => { it('should create walkable rooms', () => { 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: [] } }; @@ -47,7 +56,10 @@ describe('World Generator', () => { it('should place exit in valid location', () => { 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: [] } }; @@ -60,7 +72,10 @@ describe('World Generator', () => { it('should create enemies', () => { 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: [] } }; @@ -83,7 +98,10 @@ describe('World Generator', () => { it('should generate deterministic maps for same level', () => { 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: [] } }; @@ -101,7 +119,10 @@ describe('World Generator', () => { it('should generate different maps for different levels', () => { 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: [] } }; @@ -114,7 +135,10 @@ describe('World Generator', () => { it('should scale enemy difficulty with level', () => { 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: [] } }; diff --git a/src/engine/__tests__/simulation.test.ts b/src/engine/__tests__/simulation.test.ts index ace18c7..0d2944c 100644 --- a/src/engine/__tests__/simulation.test.ts +++ b/src/engine/__tests__/simulation.test.ts @@ -10,6 +10,12 @@ describe('Combat Simulation', () => { actors, exit: { x: 9, y: 9 } }); + + const createTestStats = (overrides: Partial = {}) => ({ + 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', () => { it('should deal damage when player attacks enemy', () => { @@ -20,7 +26,7 @@ describe('Combat Simulation', () => { pos: { x: 3, y: 3 }, speed: 100, energy: 0, - stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } + stats: createTestStats() }); actors.set(2, { id: 2, @@ -28,7 +34,7 @@ describe('Combat Simulation', () => { pos: { x: 4, y: 3 }, speed: 100, 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); @@ -49,7 +55,7 @@ describe('Combat Simulation', () => { pos: { x: 3, y: 3 }, speed: 100, energy: 0, - stats: { maxHp: 20, hp: 20, attack: 50, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } + stats: createTestStats({ attack: 50 }) }); actors.set(2, { id: 2, @@ -57,7 +63,7 @@ describe('Combat Simulation', () => { pos: { x: 4, y: 3 }, speed: 100, 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); @@ -78,7 +84,7 @@ describe('Combat Simulation', () => { pos: { x: 3, y: 3 }, speed: 100, energy: 0, - stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } + stats: createTestStats() }); actors.set(2, { id: 2, @@ -86,7 +92,7 @@ describe('Combat Simulation', () => { pos: { x: 4, y: 3 }, speed: 100, 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); @@ -109,7 +115,7 @@ describe('Combat Simulation', () => { pos: { x: 3, y: 3 }, speed: 100, energy: 0, - stats: { maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10 } + stats: createTestStats() }); const world = createTestWorld(actors); diff --git a/src/engine/simulation/simulation.ts b/src/engine/simulation/simulation.ts index 70522b4..d6d0bf7 100644 --- a/src/engine/simulation/simulation.ts +++ b/src/engine/simulation/simulation.ts @@ -62,6 +62,9 @@ function checkLevelUp(player: Actor, events: SimEvent[]) { s.maxHp += GAME_CONFIG.leveling.hpGainPerLevel; s.hp = s.maxHp; // Heal on level up s.attack += GAME_CONFIG.leveling.attackGainPerLevel; + + s.statPoints += GAME_CONFIG.leveling.statPointsPerLevel; + s.skillPoints += GAME_CONFIG.leveling.skillPointsPerLevel; // Scale requirement s.expToNextLevel = Math.floor(s.expToNextLevel * GAME_CONFIG.leveling.expMultiplier); diff --git a/src/engine/world/generator.ts b/src/engine/world/generator.ts index 297d196..3d4c2d7 100644 --- a/src/engine/world/generator.ts +++ b/src/engine/world/generator.ts @@ -241,7 +241,13 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map defense: enemyDef.baseDefense, level: 0, exp: 0, - expToNextLevel: 0 + expToNextLevel: 0, + statPoints: 0, + skillPoints: 0, + strength: 0, + dexterity: 0, + intelligence: 0, + passiveNodes: [] } }); enemyId++; diff --git a/src/rendering/__tests__/DungeonRenderer.test.ts b/src/rendering/__tests__/DungeonRenderer.test.ts index 0663b80..5bacf61 100644 --- a/src/rendering/__tests__/DungeonRenderer.test.ts +++ b/src/rendering/__tests__/DungeonRenderer.test.ts @@ -193,7 +193,7 @@ describe('DungeonRenderer', () => { pos: { x: 2, y: 2 }, speed: 100, 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; diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index e0a5873..a450fd1 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -35,6 +35,7 @@ export class GameScene extends Phaser.Scene { private dungeonRenderer!: DungeonRenderer; private isMenuOpen = false; private isInventoryOpen = false; + private isCharacterOpen = false; constructor() { super("GameScene"); @@ -67,6 +68,9 @@ export class GameScene extends Phaser.Scene { 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); @@ -97,6 +101,9 @@ export class GameScene extends Phaser.Scene { // Toggle inventory 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; @@ -119,6 +126,14 @@ export class GameScene extends Phaser.Scene { 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) this.input.on("pointerdown", (p: Phaser.Input.Pointer) => { if (!this.awaitingPlayer) return; @@ -151,7 +166,7 @@ export class GameScene extends Phaser.Scene { update() { 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 if (this.playerPath.length >= 2) { @@ -339,4 +354,51 @@ export class GameScene extends Phaser.Scene { 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(); + } } diff --git a/src/ui/GameUI.ts b/src/ui/GameUI.ts index cdb8956..1a15338 100644 --- a/src/ui/GameUI.ts +++ b/src/ui/GameUI.ts @@ -17,12 +17,21 @@ export default class GameUI extends Phaser.Scene { private menuButton!: Phaser.GameObjects.Container; private mapButton!: Phaser.GameObjects.Container; private backpackButton!: Phaser.GameObjects.Container; + private characterButton!: Phaser.GameObjects.Container; // Inventory/Equipment Overlay private inventoryOpen = false; private invContainer!: Phaser.GameObjects.Container; private equipmentSlots: Map = new Map(); 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 private deathContainer!: Phaser.GameObjects.Container; @@ -37,6 +46,7 @@ export default class GameUI extends Phaser.Scene { this.createHud(); this.createMenu(); this.createInventoryOverlay(); + this.createCharacterOverlay(); this.createDeathScreen(); // 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-inventory", () => this.toggleInventory()); + gameScene.events.on("toggle-character", () => this.toggleCharacter()); gameScene.events.on("close-menu", () => { this.setMenuOpen(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()); + // 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); } @@ -382,6 +408,139 @@ export default class GameUI extends Phaser.Scene { 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) { this.updateHud(world, playerId, floorIndex); if (this.menuOpen) { @@ -390,6 +549,31 @@ export default class GameUI extends Phaser.Scene { if (this.inventoryOpen) { 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) {