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,
|
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
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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: [] }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ describe('Combat Simulation', () => {
|
|||||||
actors,
|
actors,
|
||||||
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', () => {
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ function checkLevelUp(player: Actor, events: SimEvent[]) {
|
|||||||
s.maxHp += GAME_CONFIG.leveling.hpGainPerLevel;
|
s.maxHp += GAME_CONFIG.leveling.hpGainPerLevel;
|
||||||
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);
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 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;
|
||||||
private invContainer!: Phaser.GameObjects.Container;
|
private invContainer!: Phaser.GameObjects.Container;
|
||||||
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;
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user