From 304dc787789374368a33ba84d027f2d77f06750f Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Mar 2026 14:28:04 +0000 Subject: [PATCH] feat: display NPC stats in info panel with EarthBound styling Co-Authored-By: Claude Opus 4.6 --- client/src/ui/NpcInfoPanel.ts | 72 ++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/client/src/ui/NpcInfoPanel.ts b/client/src/ui/NpcInfoPanel.ts index 2da5dab..904290c 100644 --- a/client/src/ui/NpcInfoPanel.ts +++ b/client/src/ui/NpcInfoPanel.ts @@ -35,6 +35,8 @@ export class NpcInfoPanel { private separatorEl: HTMLDivElement; private needsBarsContainer: HTMLDivElement; private needsBars: Map = new Map(); + private statsContainer: HTMLDivElement; + private statElements: Map = 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): void { + const physicalKeys = ['STR', 'DEX', 'CON', 'INT', 'PER']; + const personalityKeys = ['SOC', 'COU', 'CUR', 'EMP', 'TMP']; + const mapping: Record = { + 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; }