feat: display backstory in NPC info panel with fade-in transition

Add backstory element to the Status tab between the activity label and
diamond separator. When a backstory arrives from the LLM, it fades in
with italic styling. Empty backstories remain hidden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 16:54:56 +00:00
parent 8c793578ae
commit baeaafd372
+48 -19
View File
@@ -35,6 +35,7 @@ export class NpcInfoPanel {
private separatorEl: HTMLDivElement;
private needsBarsContainer: HTMLDivElement;
private needsBars: Map<string, { wrapper: HTMLDivElement; fill: HTMLDivElement; valueEl: HTMLDivElement }> = new Map();
private backstoryEl: HTMLDivElement;
private statsContainer: HTMLDivElement;
private statElements: Map<string, HTMLDivElement> = new Map();
private statusTab!: HTMLDivElement;
@@ -53,12 +54,12 @@ export class NpcInfoPanel {
position: fixed;
top: 16px;
right: 16px;
width: 280px;
width: 400px;
border-radius: 16px;
border: 3px solid ${EB.borderOuter};
background: ${EB.borderGap};
z-index: 1000;
transform: translateX(350px);
transform: translateX(470px);
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow:
0 0 20px rgba(80, 60, 160, 0.4),
@@ -154,7 +155,7 @@ export class NpcInfoPanel {
flex: 1;
text-align: center;
padding: 8px 0;
font-size: 9px;
font-size: 18px;
cursor: pointer;
color: ${EB.textPrimary};
border-bottom: 2px solid ${EB.borderOuter};
@@ -167,7 +168,7 @@ export class NpcInfoPanel {
flex: 1;
text-align: center;
padding: 8px 0;
font-size: 9px;
font-size: 18px;
cursor: pointer;
color: ${EB.textMuted};
user-select: none;
@@ -197,7 +198,7 @@ export class NpcInfoPanel {
// Name
this.nameEl = document.createElement('div');
this.nameEl.style.cssText = `
font-size: 13px;
font-size: 26px;
color: ${EB.textPrimary};
text-align: center;
padding: 2px 0 4px;
@@ -209,18 +210,34 @@ export class NpcInfoPanel {
// Activity
this.activityEl = document.createElement('div');
this.activityEl.style.cssText = `
font-size: 10px;
font-size: 20px;
color: ${EB.textSecondary};
text-align: center;
padding-bottom: 2px;
`;
this.statusContent.appendChild(this.activityEl);
// Backstory text
this.backstoryEl = document.createElement('div');
this.backstoryEl.style.cssText = `
font-size: 14px;
color: ${EB.textSecondary};
text-align: center;
padding: 2px 8px 4px;
font-family: 'Press Start 2P', monospace;
line-height: 1.6;
font-style: italic;
min-height: 0;
transition: min-height 0.3s ease, opacity 0.3s ease;
opacity: 0;
`;
this.statusContent.appendChild(this.backstoryEl);
// Decorative separator (EarthBound diamond pattern)
this.separatorEl = document.createElement('div');
this.separatorEl.style.cssText = `
text-align: center;
font-size: 7px;
font-size: 14px;
color: ${EB.textMuted};
padding: 4px 0;
letter-spacing: 6px;
@@ -242,7 +259,7 @@ export class NpcInfoPanel {
const statsSeparator = document.createElement('div');
statsSeparator.style.cssText = `
text-align: center;
font-size: 7px;
font-size: 14px;
color: ${EB.textMuted};
padding: 4px 0;
letter-spacing: 6px;
@@ -287,7 +304,7 @@ export class NpcInfoPanel {
hide(): void {
this.visible = false;
this.outerFrame.style.transform = 'translateX(350px)';
this.outerFrame.style.transform = 'translateX(470px)';
}
updateInfo(entity: EntityState): void {
@@ -301,6 +318,18 @@ export class NpcInfoPanel {
this.activityEl.textContent = goal ? (ACTIVITY_LABELS[goal] ?? goal) : 'Idle';
}
// Update backstory
const backstory = entity.backstory;
if (backstory && backstory.length > 0) {
this.backstoryEl.textContent = `"${backstory}"`;
this.backstoryEl.style.opacity = '1';
this.backstoryEl.style.minHeight = '40px';
} else {
this.backstoryEl.textContent = '';
this.backstoryEl.style.opacity = '0';
this.backstoryEl.style.minHeight = '0';
}
if (entity.needs) {
this.updateNeeds(entity.needs);
}
@@ -346,7 +375,7 @@ export class NpcInfoPanel {
const labelEl = document.createElement('div');
labelEl.style.cssText = `
font-size: 9px;
font-size: 18px;
color: ${EB.textSecondary};
text-transform: uppercase;
letter-spacing: 1px;
@@ -355,7 +384,7 @@ export class NpcInfoPanel {
const valueEl = document.createElement('div');
valueEl.style.cssText = `
font-size: 10px;
font-size: 20px;
color: ${EB.textPrimary};
text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
`;
@@ -437,7 +466,7 @@ export class NpcInfoPanel {
el.style.cssText = `
display: flex;
justify-content: space-between;
font-size: 9px;
font-size: 18px;
`;
const labelEl = document.createElement('span');
labelEl.style.cssText = `color: ${EB.textSecondary}; text-transform: uppercase; letter-spacing: 0.5px;`;
@@ -477,7 +506,7 @@ export class NpcInfoPanel {
if (relationships.length === 0) {
this.relationshipsContent.innerHTML = '';
const empty = document.createElement('div');
empty.style.cssText = `text-align: center; color: ${EB.textMuted}; font-size: 9px; padding: 16px 0;`;
empty.style.cssText = `text-align: center; color: ${EB.textMuted}; font-size: 18px; padding: 16px 0;`;
empty.textContent = 'No relationships yet';
this.relationshipsContent.appendChild(empty);
return;
@@ -520,7 +549,7 @@ export class NpcInfoPanel {
const tierLabel = document.createElement('div');
const icon = tierIcons[tier] ?? '';
tierLabel.style.cssText = `font-size: 8px; color: ${EB.textSecondary}; margin-bottom: 4px;`;
tierLabel.style.cssText = `font-size: 16px; color: ${EB.textSecondary}; margin-bottom: 4px;`;
tierLabel.textContent = `${icon} ${tier}`;
tierEl.appendChild(tierLabel);
@@ -539,7 +568,7 @@ export class NpcInfoPanel {
const toggle = document.createElement('div');
toggle.style.cssText = `
font-size: 7px;
font-size: 14px;
color: ${EB.borderOuter};
cursor: pointer;
user-select: none;
@@ -571,7 +600,7 @@ export class NpcInfoPanel {
const memEl = document.createElement('div');
memEl.style.cssText = 'margin-bottom: 8px;';
const memLabel = document.createElement('div');
memLabel.style.cssText = `font-size: 8px; color: ${EB.textMuted}; margin-bottom: 4px;`;
memLabel.style.cssText = `font-size: 16px; color: ${EB.textMuted}; margin-bottom: 4px;`;
memLabel.textContent = '\u25CB Memories';
memEl.appendChild(memLabel);
@@ -594,7 +623,7 @@ export class NpcInfoPanel {
const toggle = document.createElement('div');
toggle.style.cssText = `
font-size: 7px;
font-size: 14px;
color: ${EB.borderOuter};
cursor: pointer;
user-select: none;
@@ -630,11 +659,11 @@ export class NpcInfoPanel {
align-items: center;
gap: 4px;
margin-bottom: 3px;
font-size: 8px;
font-size: 16px;
`;
const nameEl = document.createElement('span');
nameEl.style.cssText = `color: ${EB.textPrimary}; min-width: 70px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`;
nameEl.style.cssText = `color: ${EB.textPrimary}; min-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`;
const bondIcon = bond === 'partner' ? ' \u{1F48D}' : bond === 'former_partner' ? ' \u{1F48D}\u{FE0E}' : '';
nameEl.textContent = name + bondIcon;