Files
dflike/server/src/systems/desireGeneratorSystem.ts
T
root a8f27a869d fix: emit desire_fulfilled events, populate LLM context, fix category scoring
- desireFulfillmentSystem records memory events when desires are fulfilled
- desireGeneratorSystem populates recentEvents and recentInventions template vars
- industrySystem own_item_category scoring now checks recipe output category

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 00:14:35 +00:00

138 lines
4.4 KiB
TypeScript

import type { EntityId, Desire, Stats } from '@dflike/shared';
import type { World } from '../ecs/World.js';
import type { LlmService } from '../llm/llmService.js';
import type { EventMemoryService } from '../llm/eventMemoryService.js';
import type { InventionTimeline } from '../industry/inventionTimeline.js';
import { desireConfig } from '../config/desireConfig.js';
import { getEffectiveStat } from './statHelpers.js';
import { formatStatsForPrompt, getWorldContext, validateDesire } from '../llm/backstoryGenerator.js';
export interface DesireGeneratorSystem {
update(world: World, tick: number): void;
triggerEvent(world: World, entityId: number, trigger: string, tick: number): void;
}
export function createDesireGeneratorSystem(
llmService: LlmService,
eventMemoryService?: EventMemoryService,
): DesireGeneratorSystem {
const pendingEntities = new Set<EntityId>();
function canAddDesire(world: World, entityId: number): boolean {
const desires = world.getComponent<Desire[]>(entityId, 'desires');
if (!desires) return false;
return desires.length < desireConfig.maxDesires;
}
function fireDesireRequest(
world: World,
entityId: number,
tick: number,
source: 'periodic' | 'event',
trigger?: string,
): void {
if (pendingEntities.has(entityId)) return;
const name = world.getComponent<string>(entityId, 'name') ?? 'Unknown';
const stats = world.getComponent<Stats>(entityId, 'stats');
const backstory = world.getComponent<string>(entityId, 'backstory') ?? '';
const currentDesires = world.getComponent<Desire[]>(entityId, 'desires') ?? [];
if (!stats) return;
const context = getWorldContext(world);
const currentDesireList = currentDesires.map(d => d.description).join('; ') || 'none';
const recentEvents = eventMemoryService
? eventMemoryService.getRecentEvents(entityId, 5).map(e => e.detail).join('; ')
: '';
const timeline = world.getSingleton<InventionTimeline>('inventionTimeline');
const recentInventions = timeline
? timeline.getSummaries().slice(-5).map(s => s.name).join(', ')
: '';
const variables: Record<string, string> = {
npcName: name,
stats: formatStatsForPrompt(stats),
backstory,
currentDesires: currentDesireList,
recentEvents: recentEvents || 'none',
recentInventions: recentInventions || 'none',
resourceTypes: context.resourceTypes,
recipeList: context.recipeList,
structureList: context.structureList,
};
if (trigger) {
variables.trigger = trigger;
}
pendingEntities.add(entityId);
llmService.generate('desireGeneration', variables).then(response => {
pendingEntities.delete(entityId);
if (!response) return;
let parsed: unknown;
try {
parsed = JSON.parse(response);
} catch {
return;
}
const desire = validateDesire(parsed, tick);
if (!desire) return;
desire.source = source;
if (trigger) {
desire.sourceDetail = trigger;
}
// Re-check capacity (may have changed during async request)
if (!canAddDesire(world, entityId)) return;
const existingDesires = world.getComponent<Desire[]>(entityId, 'desires');
if (!existingDesires) return;
world.addComponent(entityId, 'desires', [...existingDesires, desire]);
}).catch(() => {
pendingEntities.delete(entityId);
});
}
return {
update(world: World, tick: number): void {
if (tick % desireConfig.periodicCheckInterval !== 0) return;
const npcs = world.query('desires');
for (const entityId of npcs) {
if (!canAddDesire(world, entityId)) continue;
if (pendingEntities.has(entityId)) continue;
const curiosity = getEffectiveStat(world, entityId, 'curiosity');
const intelligence = getEffectiveStat(world, entityId, 'intelligence');
const composite = curiosity + intelligence;
const chance = Math.max(
0.05,
Math.min(
0.8,
desireConfig.periodicBaseChance + (composite - 20) * desireConfig.periodicStatScale,
),
);
if (Math.random() >= chance) continue;
fireDesireRequest(world, entityId, tick, 'periodic');
}
},
triggerEvent(world: World, entityId: number, trigger: string, tick: number): void {
if (!canAddDesire(world, entityId)) return;
fireDesireRequest(world, entityId, tick, 'event', trigger);
},
};
}