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>
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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<Desire[]>(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);
|
||||
|
||||
@@ -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<EntityId>();
|
||||
|
||||
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>('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,
|
||||
|
||||
@@ -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<Desire[]>(entity, 'desires') ?? [];
|
||||
const recipe = scoreCraftableRecipes(craftRecipes, desires);
|
||||
const itemRegistry = world.getSingleton<ItemRegistry>('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)));
|
||||
|
||||
Reference in New Issue
Block a user