diff --git a/server/src/game/GameLoop.ts b/server/src/game/GameLoop.ts index e44ab5f..0cbdb03 100644 --- a/server/src/game/GameLoop.ts +++ b/server/src/game/GameLoop.ts @@ -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); diff --git a/server/src/game/spawner.ts b/server/src/game/spawner.ts index 40f2bff..9ad2367 100644 --- a/server/src/game/spawner.ts +++ b/server/src/game/spawner.ts @@ -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(entity, 'relationships', new Map()); world.addComponent(entity, 'backstory', ''); + eventMemoryService?.record(entity, { + type: 'spawned', + tick: 0, + detail: `${world.getComponent(entity, 'name')} arrived in the village`, + }); + return entity; } diff --git a/server/src/network/SocketServer.ts b/server/src/network/SocketServer.ts index 0d022f6..9cb9c49 100644 --- a/server/src/network/SocketServer.ts +++ b/server/src/network/SocketServer.ts @@ -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); }); diff --git a/server/src/systems/socialSystem.ts b/server/src/systems/socialSystem.ts index 97bac5a..a0096fe 100644 --- a/server/src/systems/socialSystem.ts +++ b/server/src/systems/socialSystem.ts @@ -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(e, 'name') ?? 'Unknown'; + const nameB = world.getComponent(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(initiator, 'name') ?? 'Unknown'; + const nameRec = world.getComponent(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;