feat: display NPC stats in info panel with EarthBound styling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-07 14:28:04 +00:00
parent 0fc186b4f3
commit 304dc78778
+70 -2
View File
@@ -35,6 +35,8 @@ export class NpcInfoPanel {
private separatorEl: HTMLDivElement;
private needsBarsContainer: HTMLDivElement;
private needsBars: Map<string, { wrapper: HTMLDivElement; fill: HTMLDivElement; valueEl: HTMLDivElement }> = new Map();
private statsContainer: HTMLDivElement;
private statElements: Map<string, HTMLDivElement> = new Map();
private visible = false;
constructor() {
@@ -119,8 +121,8 @@ export class NpcInfoPanel {
this.portraitImg = document.createElement('img');
this.portraitImg.style.cssText = `
width: 96px;
height: 96px;
width: 192px;
height: 192px;
image-rendering: pixelated;
display: block;
opacity: 1;
@@ -188,6 +190,29 @@ export class NpcInfoPanel {
`;
infoSection.appendChild(this.needsBarsContainer);
// Stats separator
const statsSeparator = document.createElement('div');
statsSeparator.style.cssText = `
text-align: center;
font-size: 5px;
color: ${EB.textMuted};
padding: 4px 0;
letter-spacing: 6px;
user-select: none;
`;
statsSeparator.textContent = '\u25C6\u25C6\u25C6';
infoSection.appendChild(statsSeparator);
// Stats container - two columns
this.statsContainer = document.createElement('div');
this.statsContainer.style.cssText = `
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3px 8px;
font-family: 'Press Start 2P', monospace;
`;
infoSection.appendChild(this.statsContainer);
this.container.appendChild(infoSection);
this.outerFrame.appendChild(this.container);
document.body.appendChild(this.outerFrame);
@@ -219,6 +244,10 @@ export class NpcInfoPanel {
if (entity.needs) {
this.updateNeeds(entity.needs);
}
if (entity.stats) {
this.updateStats(entity.stats);
}
}
updateNeeds(needs: { hunger: number; energy: number }): void {
@@ -322,6 +351,45 @@ export class NpcInfoPanel {
}
}
updateStats(stats: Record<string, number>): void {
const physicalKeys = ['STR', 'DEX', 'CON', 'INT', 'PER'];
const personalityKeys = ['SOC', 'COU', 'CUR', 'EMP', 'TMP'];
const mapping: Record<string, string> = {
STR: 'strength', DEX: 'dexterity', CON: 'constitution', INT: 'intelligence', PER: 'perception',
SOC: 'sociability', COU: 'courage', CUR: 'curiosity', EMP: 'empathy', TMP: 'temperament',
};
for (const key of [...physicalKeys, ...personalityKeys]) {
const value = stats[mapping[key]];
if (value !== undefined) {
this.setStatDisplay(key, value);
}
}
}
private setStatDisplay(label: string, value: number): void {
let el = this.statElements.get(label);
if (!el) {
el = document.createElement('div');
el.style.cssText = `
display: flex;
justify-content: space-between;
font-size: 6px;
`;
const labelEl = document.createElement('span');
labelEl.style.cssText = `color: ${EB.textSecondary}; text-transform: uppercase; letter-spacing: 0.5px;`;
labelEl.textContent = label;
const valueEl = document.createElement('span');
valueEl.style.cssText = `color: ${EB.textPrimary}; text-shadow: 1px 1px 0 rgba(0,0,0,0.5);`;
valueEl.dataset.role = 'value';
el.appendChild(labelEl);
el.appendChild(valueEl);
this.statsContainer.appendChild(el);
this.statElements.set(label, el);
}
const valueEl = el.querySelector('[data-role="value"]') as HTMLSpanElement;
valueEl.textContent = String(value);
}
isVisible(): boolean {
return this.visible;
}