feat: add backstory generation helper for NPC LLM backstories

Adds formatStatsForPrompt to convert Stats into prompt-friendly abbreviations
and generateBackstory to call the LLM service with the backstory template,
writing the result back to the entity's backstory component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 16:49:32 +00:00
parent 180f056570
commit aa3f5651dd
2 changed files with 127 additions and 0 deletions
@@ -0,0 +1,89 @@
import { describe, it, expect, vi } from 'vitest';
import { World } from '../../ecs/World.js';
import { formatStatsForPrompt, generateBackstory } from '../backstoryGenerator.js';
import type { Stats } from '@dflike/shared';
import type { LlmService } from '../llmService.js';
describe('formatStatsForPrompt', () => {
it('formats stats into a readable string', () => {
const stats: Stats = {
strength: 15, dexterity: 12, constitution: 14,
intelligence: 8, perception: 10,
sociability: 6, courage: 16, curiosity: 11,
empathy: 7, temperament: 13,
};
const result = formatStatsForPrompt(stats);
expect(result).toBe(
'STR:15, DEX:12, CON:14, INT:8, PER:10, SOC:6, COU:16, CUR:11, EMP:7, TMP:13'
);
});
});
describe('generateBackstory', () => {
it('calls LLM service with correct template and variables', async () => {
const world = new World();
const entity = world.createEntity();
world.addComponent(entity, 'name', 'Brynn');
world.addComponent(entity, 'backstory', '');
world.addComponent<Stats>(entity, 'stats', {
strength: 15, dexterity: 12, constitution: 14,
intelligence: 8, perception: 10,
sociability: 6, courage: 16, curiosity: 11,
empathy: 7, temperament: 13,
});
const mockService: LlmService = {
generate: vi.fn().mockResolvedValue('A brave warrior from the north.'),
queueDepth: () => 0,
clear: () => {},
};
await generateBackstory(world, entity, mockService);
expect(mockService.generate).toHaveBeenCalledWith('backstory', {
npcName: 'Brynn',
stats: 'STR:15, DEX:12, CON:14, INT:8, PER:10, SOC:6, COU:16, CUR:11, EMP:7, TMP:13',
});
expect(world.getComponent<string>(entity, 'backstory')).toBe(
'A brave warrior from the north.'
);
});
it('leaves backstory empty when LLM returns null', async () => {
const world = new World();
const entity = world.createEntity();
world.addComponent(entity, 'name', 'Thom');
world.addComponent(entity, 'backstory', '');
world.addComponent<Stats>(entity, 'stats', {
strength: 10, dexterity: 10, constitution: 10,
intelligence: 10, perception: 10,
sociability: 10, courage: 10, curiosity: 10,
empathy: 10, temperament: 10,
});
const mockService: LlmService = {
generate: vi.fn().mockResolvedValue(null),
queueDepth: () => 0,
clear: () => {},
};
await generateBackstory(world, entity, mockService);
expect(world.getComponent<string>(entity, 'backstory')).toBe('');
});
it('does not crash if entity is missing components', async () => {
const world = new World();
const entity = world.createEntity();
const mockService: LlmService = {
generate: vi.fn().mockResolvedValue('text'),
queueDepth: () => 0,
clear: () => {},
};
await generateBackstory(world, entity, mockService);
expect(mockService.generate).not.toHaveBeenCalled();
});
});
+38
View File
@@ -0,0 +1,38 @@
import type { Stats } from '@dflike/shared';
import type { World } from '../ecs/World.js';
import type { LlmService } from './llmService.js';
const STAT_KEYS: (keyof Stats)[] = [
'strength', 'dexterity', 'constitution', 'intelligence', 'perception',
'sociability', 'courage', 'curiosity', 'empathy', 'temperament',
];
const STAT_ABBREVS: Record<keyof Stats, string> = {
strength: 'STR', dexterity: 'DEX', constitution: 'CON',
intelligence: 'INT', perception: 'PER',
sociability: 'SOC', courage: 'COU', curiosity: 'CUR',
empathy: 'EMP', temperament: 'TMP',
};
export function formatStatsForPrompt(stats: Stats): string {
return STAT_KEYS.map(k => `${STAT_ABBREVS[k]}:${Math.floor(stats[k])}`).join(', ');
}
export async function generateBackstory(
world: World,
entityId: number,
llmService: LlmService,
): Promise<void> {
const name = world.getComponent<string>(entityId, 'name');
const stats = world.getComponent<Stats>(entityId, 'stats');
if (!name || !stats) return;
const result = await llmService.generate('backstory', {
npcName: name,
stats: formatStatsForPrompt(stats),
});
if (result) {
world.addComponent<string>(entityId, 'backstory', result);
}
}