diff --git a/server/src/game/GameLoop.ts b/server/src/game/GameLoop.ts index 5c5576f..0fb9249 100644 --- a/server/src/game/GameLoop.ts +++ b/server/src/game/GameLoop.ts @@ -70,7 +70,7 @@ export class GameLoop { this.eventMemoryService, (summary) => this.onInventionCreated?.(summary), ); - this.desireGeneratorSystem = createDesireGeneratorSystem(this.llmService); + this.desireGeneratorSystem = createDesireGeneratorSystem(this.llmService, this.eventMemoryService); this.saveManager = new SaveManager(options?.savePath ?? 'saves/default.db'); @@ -279,7 +279,7 @@ export class GameLoop { socialSystem(this.world, this.eventMemoryService); narrationEmitter(this.world, this.narrationService, this.followedEntityIds, this.eventMemoryService); relationshipSystem(this.world, this.eventMemoryService); - desireFulfillmentSystem(this.world, this.tick); + desireFulfillmentSystem(this.world, this.tick, this.eventMemoryService); this.desireGeneratorSystem.update(this.world, this.tick); industrySystem(this.world, this.map); pickupSystem(this.world, this.tick, (entry) => this.onStockpileEvent?.(entry)); diff --git a/server/src/systems/desireFulfillmentSystem.ts b/server/src/systems/desireFulfillmentSystem.ts index bf7febf..6f54fe7 100644 --- a/server/src/systems/desireFulfillmentSystem.ts +++ b/server/src/systems/desireFulfillmentSystem.ts @@ -1,6 +1,7 @@ import type { Desire, FulfillmentCriteria } from '@dflike/shared'; import type { World } from '../ecs/World.js'; import type { StructureData } from './buildingSystem.js'; +import type { EventMemoryService } from '../llm/eventMemoryService.js'; import { classify } from './relationshipHelpers.js'; import { RecipeRegistry } from '../industry/recipeRegistry.js'; import { ItemRegistry } from '../industry/itemRegistry.js'; @@ -84,16 +85,29 @@ function checkFulfillment( } } -export function desireFulfillmentSystem(world: World, tick: number): void { +export function desireFulfillmentSystem( + world: World, + tick: number, + eventMemoryService?: EventMemoryService, +): void { if (tick % desireConfig.fulfillmentCheckInterval !== 0) return; for (const entityId of world.query('desires')) { const desires = world.getComponent(entityId, 'desires'); if (!desires || desires.length === 0) continue; - const remaining = desires.filter( - (desire) => !checkFulfillment(world, entityId, desire.fulfillment), - ); + const remaining: Desire[] = []; + for (const desire of desires) { + if (checkFulfillment(world, entityId, desire.fulfillment)) { + eventMemoryService?.record(entityId, { + type: 'desire_fulfilled', + tick, + detail: desire.description, + }); + } else { + remaining.push(desire); + } + } if (remaining.length !== desires.length) { world.addComponent(entityId, 'desires', remaining); diff --git a/server/src/systems/desireGeneratorSystem.ts b/server/src/systems/desireGeneratorSystem.ts index 639005e..474f73d 100644 --- a/server/src/systems/desireGeneratorSystem.ts +++ b/server/src/systems/desireGeneratorSystem.ts @@ -1,6 +1,8 @@ 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'; @@ -10,7 +12,10 @@ export interface DesireGeneratorSystem { triggerEvent(world: World, entityId: number, trigger: string, tick: number): void; } -export function createDesireGeneratorSystem(llmService: LlmService): DesireGeneratorSystem { +export function createDesireGeneratorSystem( + llmService: LlmService, + eventMemoryService?: EventMemoryService, +): DesireGeneratorSystem { const pendingEntities = new Set(); function canAddDesire(world: World, entityId: number): boolean { @@ -38,11 +43,22 @@ export function createDesireGeneratorSystem(llmService: LlmService): DesireGener 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'); + const recentInventions = timeline + ? timeline.getSummaries().slice(-5).map(s => s.name).join(', ') + : ''; + const variables: Record = { npcName: name, stats: formatStatsForPrompt(stats), backstory, currentDesires: currentDesireList, + recentEvents: recentEvents || 'none', + recentInventions: recentInventions || 'none', resourceTypes: context.resourceTypes, recipeList: context.recipeList, structureList: context.structureList, diff --git a/server/src/systems/industrySystem.ts b/server/src/systems/industrySystem.ts index 6794078..6270338 100644 --- a/server/src/systems/industrySystem.ts +++ b/server/src/systems/industrySystem.ts @@ -9,10 +9,11 @@ import type { CraftingState } from './craftingSystem.js'; import type { BuildingState, StructureData } from './buildingSystem.js'; import type { PickupState } from './pickupSystem.js'; import { RecipeRegistry, type Recipe } from '../industry/recipeRegistry.js'; +import { ItemRegistry } from '../industry/itemRegistry.js'; import { getEffectiveStat } from './statHelpers.js'; import { industryConfig } from '../config/industryConfig.js'; -function scoreCraftableRecipes(recipes: Recipe[], desires: Desire[]): Recipe { +function scoreCraftableRecipes(recipes: Recipe[], desires: Desire[], itemRegistry?: ItemRegistry): Recipe { if (recipes.length === 1 || desires.length === 0) return recipes[0]; let bestRecipe = recipes[0]; @@ -24,8 +25,11 @@ function scoreCraftableRecipes(recipes: Recipe[], desires: Desire[]): Recipe { const f = desire.fulfillment; if (f.type === 'own_item' && f.itemId === recipe.outputItemId) { score += desire.priority * 10; - } else if (f.type === 'own_item_category') { - score += desire.priority * 5; + } else if (f.type === 'own_item_category' && itemRegistry) { + const outputDef = itemRegistry.get(recipe.outputItemId); + if (outputDef && outputDef.category === f.category) { + score += desire.priority * 5; + } } } if (score > bestScore) { @@ -91,7 +95,8 @@ export function industrySystem(world: World, map: GameMap): void { const craftRecipes = registry.findCraftable(inv).filter(r => !r.id.startsWith('build_')); if (craftRecipes.length > 0) { const desires = world.getComponent(entity, 'desires') ?? []; - const recipe = scoreCraftableRecipes(craftRecipes, desires); + const itemRegistry = world.getSingleton('itemRegistry'); + const recipe = scoreCraftableRecipes(craftRecipes, desires, itemRegistry); const dex = getEffectiveStat(world, entity, 'dexterity'); const baseTicks = industryConfig.craftBaseTicks; const ticks = Math.max(1, Math.round(baseTicks * (1 - (dex - 10) * industryConfig.craftDexterityModifier)));