feat(memory): record spawned, social, proposal, and bond events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 21:56:42 +00:00
parent 2c4a0d1f75
commit ef7a792750
4 changed files with 86 additions and 5 deletions
+2 -2
View File
@@ -66,7 +66,7 @@ export class GameLoop {
private spawnInitialNPCs(count: number): void {
for (let i = 0; i < count; i++) {
const entity = spawnNPC(this.world, this.map);
const entity = spawnNPC(this.world, this.map, undefined, this.eventMemoryService);
this.generateNpcBackstory(entity);
}
}
@@ -95,7 +95,7 @@ export class GameLoop {
statModifierSystem(this.world);
needsDecaySystem(this.world);
npcBrainSystem(this.world, this.map);
socialSystem(this.world);
socialSystem(this.world, this.eventMemoryService);
narrationEmitter(this.world, this.narrationService, this.followedEntityIds);
relationshipSystem(this.world);
movementSystem(this.world);
+8 -1
View File
@@ -4,8 +4,9 @@ import { generateRandomAppearance } from '../spawner/appearanceGenerator.js';
import { generateName } from '../spawner/nameGenerator.js';
import { generateStats } from '../spawner/statGenerator.js';
import type { EntityId, Position, Needs, Movement, NPCBrain, Appearance, SocialState, Stats, StatModifiers, Relationships } from '@dflike/shared';
import type { EventMemoryService } from '../llm/eventMemoryService.js';
export function spawnNPC(world: World, map: GameMap, positionHint?: Position): EntityId {
export function spawnNPC(world: World, map: GameMap, positionHint?: Position, eventMemoryService?: EventMemoryService): EntityId {
const entity = world.createEntity();
const pos = positionHint && map.isWalkable(positionHint.x, positionHint.y)
? positionHint
@@ -46,5 +47,11 @@ export function spawnNPC(world: World, map: GameMap, positionHint?: Position): E
world.addComponent<Relationships>(entity, 'relationships', new Map());
world.addComponent<string>(entity, 'backstory', '');
eventMemoryService?.record(entity, {
type: 'spawned',
tick: 0,
detail: `${world.getComponent<string>(entity, 'name')} arrived in the village`,
});
return entity;
}
+1 -1
View File
@@ -98,7 +98,7 @@ export class SocketServer {
const target = map.findNearestWalkable(data.x, data.y, 3)
?? map.getRandomWalkable();
const npcEntity = spawnNPC(world, map, target);
const npcEntity = spawnNPC(world, map, target, this.gameLoop.eventMemoryService);
this.gameLoop.generateNpcBackstory(npcEntity);
});
+75 -1
View File
@@ -12,6 +12,7 @@ import { getEffectiveStat } from './statHelpers.js';
import type { BondRegistry } from './bondRegistry.js';
import { addBond } from './bondRegistry.js';
import { relationshipConfig as cfg } from '../config/relationshipConfig.js';
import type { EventMemoryService } from '../llm/eventMemoryService.js';
function directionTo(from: Position, to: Position): number {
const dx = to.x - from.x;
@@ -36,7 +37,7 @@ function isTargetBusy(world: World, target: EntityId): boolean {
return false;
}
export function socialSystem(world: World): void {
export function socialSystem(world: World, eventMemoryService?: EventMemoryService): void {
const entities = world.query('socialState', 'position', 'movement', 'npcBrain', 'needs');
const npcs = entities.filter(e => !world.getComponent(e, 'playerControlled'));
@@ -174,6 +175,29 @@ export function socialSystem(world: World): void {
tick: 0,
};
if (eventMemoryService) {
const nameA = world.getComponent<string>(e, 'name') ?? 'Unknown';
const nameB = world.getComponent<string>(partnerId!, 'name') ?? 'Unknown';
eventMemoryService.record(e, {
type: social.outcome === 'positive' ? 'social_positive' : 'social_negative',
tick: 0,
otherEntityId: partnerId!,
otherName: nameB,
detail: social.outcome === 'positive'
? `Had a positive interaction with ${nameB}`
: `Had a negative interaction with ${nameB}`,
});
eventMemoryService.record(partnerId!, {
type: partnerSocial.outcome === 'positive' ? 'social_positive' : 'social_negative',
tick: 0,
otherEntityId: e,
otherName: nameA,
detail: partnerSocial.outcome === 'positive'
? `Had a positive interaction with ${nameA}`
: `Had a negative interaction with ${nameA}`,
});
}
social.phase = 'none';
social.partnerId = null;
social.phaseTimer = 0;
@@ -267,6 +291,56 @@ export function socialSystem(world: World): void {
social.lastOutcome = { partnerId: partnerId!, outcome: social.outcome!, tick: 0 };
partnerSocial.lastOutcome = { partnerId: e, outcome: partnerSocial.outcome!, tick: 0 };
if (eventMemoryService) {
const nameInit = world.getComponent<string>(initiator, 'name') ?? 'Unknown';
const nameRec = world.getComponent<string>(receiver, 'name') ?? 'Unknown';
if (accepted) {
eventMemoryService.record(initiator, {
type: 'proposal_accepted',
tick: 0,
otherEntityId: receiver,
otherName: nameRec,
detail: `Proposal to ${nameRec} was accepted`,
});
eventMemoryService.record(receiver, {
type: 'proposal_accepted',
tick: 0,
otherEntityId: initiator,
otherName: nameInit,
detail: `Accepted ${nameInit}'s proposal`,
});
eventMemoryService.record(initiator, {
type: 'bond_formed',
tick: 0,
otherEntityId: receiver,
otherName: nameRec,
detail: `Became partners with ${nameRec}`,
});
eventMemoryService.record(receiver, {
type: 'bond_formed',
tick: 0,
otherEntityId: initiator,
otherName: nameInit,
detail: `Became partners with ${nameInit}`,
});
} else {
eventMemoryService.record(initiator, {
type: 'proposal_rejected',
tick: 0,
otherEntityId: receiver,
otherName: nameRec,
detail: `Proposal to ${nameRec} was rejected`,
});
eventMemoryService.record(receiver, {
type: 'proposal_rejected',
tick: 0,
otherEntityId: initiator,
otherName: nameInit,
detail: `Rejected ${nameInit}'s proposal`,
});
}
}
// Reset both entities
social.phase = 'none';
social.partnerId = null;