Add character overlay, where skills and passives (changing this) can be set
This commit is contained in:
@@ -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
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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: [] }
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ describe('Combat Simulation', () => {
|
||||
actors,
|
||||
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', () => {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -241,7 +241,13 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map<EntityId, Actor>
|
||||
defense: enemyDef.baseDefense,
|
||||
level: 0,
|
||||
exp: 0,
|
||||
expToNextLevel: 0
|
||||
expToNextLevel: 0,
|
||||
statPoints: 0,
|
||||
skillPoints: 0,
|
||||
strength: 0,
|
||||
dexterity: 0,
|
||||
intelligence: 0,
|
||||
passiveNodes: []
|
||||
}
|
||||
});
|
||||
enemyId++;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
184
src/ui/GameUI.ts
184
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<string, Phaser.GameObjects.Container> = 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) {
|
||||
|
||||
Reference in New Issue
Block a user