Enable bounded proximity conversations and confirmed social memories
This commit is contained in:
+28
-6
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { queueAleCommand } from './ale-store';
|
||||
import { luaString } from './ale';
|
||||
import { cleanupConversations,completeConversation,failConversation,leaseConversation,awaitDelivery,pendingDeliveries,type ConversationRequest } from './conversation-store';
|
||||
import { queueSocialReply,cleanupConversations,completeConversation,failConversation,leaseConversation,awaitDelivery,pendingDeliveries,type ConversationRequest } from './conversation-store';
|
||||
import type { Bot,RealmEvent,RuntimeConfig,State,StateStore } from './types';
|
||||
|
||||
const replySchema=z.object({reply:z.string().min(1).max(500)}).strict();
|
||||
@@ -20,7 +20,7 @@ function byName(state:State,name:string){
|
||||
if(!lower)return undefined;
|
||||
return state.bots.find(bot=>
|
||||
bot.autonomy?.managed===true&&
|
||||
bot.status!=='dead'&&bot.status!=='retired'&&
|
||||
bot.status==='online'&&
|
||||
bot.name.toLowerCase()===lower
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ async function modelReply(bot:Bot,request:ConversationRequest,state:State,config
|
||||
content:JSON.stringify({
|
||||
speaker:request.speakerName,
|
||||
channel:request.channel,
|
||||
message:request.message,
|
||||
message:request.socialTurn===0?'Start a conversation with '+request.speakerName+' about your current surroundings or goals. They have not spoken yet.':request.message,
|
||||
currentZone:request.zoneName||bot.zone,
|
||||
identity:{name:bot.name,race:bot.race,role:bot.role,tier:bot.tier},
|
||||
personality:bot.personality,
|
||||
@@ -105,11 +105,15 @@ async function modelReply(bot:Bot,request:ConversationRequest,state:State,config
|
||||
return cleanReply(replySchema.parse(JSON.parse(raw)).reply);
|
||||
}
|
||||
|
||||
function compileReply(botName:string,speakerName:string,reply:string,channel:'say'|'whisper'){
|
||||
function compileReply(botName:string,speakerName:string,reply:string,channel:'say'|'whisper',social=false){
|
||||
const source=[
|
||||
`local bot=GetPlayerByName(${luaString(botName)})`,
|
||||
'if not bot then error("Conversation bot is not online") end'
|
||||
];
|
||||
if(social){
|
||||
source.push(`local peer=GetPlayerByName(${luaString(speakerName)})`);
|
||||
source.push('if not peer or bot:IsDead() or peer:IsDead() or bot:IsInCombat() or peer:IsInCombat() or bot:GetMapId()~=peer:GetMapId() or bot:GetPhaseMask()~=peer:GetPhaseMask() or bot:GetTeam()~=peer:GetTeam() or bot:GetDistance(peer)>25 then error("Social pair is no longer available nearby") end');
|
||||
}
|
||||
if(channel==='whisper'){
|
||||
source.push(`local receiver=GetPlayerByName(${luaString(speakerName)})`);
|
||||
source.push('if not receiver then error("Conversation player is not online") end');
|
||||
@@ -136,6 +140,19 @@ async function remember(store:StateStore,botId:string,request:ConversationReques
|
||||
at:new Date().toISOString()
|
||||
});
|
||||
bot.memories=bot.memories.slice(-100);
|
||||
if(request.socialTurn===1){
|
||||
const peer=byName(state,request.speakerName);
|
||||
if(peer){
|
||||
peer.memories.push({id:request.id,text:`I said to ${bot.name}: "${request.message.slice(0,180)}" They replied: "${reply.slice(0,180)}"`,importance:6,source:'observation',at:new Date().toISOString()});
|
||||
peer.memories=peer.memories.slice(-100);
|
||||
for(const [from,to] of [[bot,peer],[peer,bot]]){
|
||||
const existing=from.relationships.find(r=>r.targetId===to.id);
|
||||
if(existing)existing.note='We have spoken together in person.';
|
||||
else from.relationships.push({targetId:to.id,affinity:0,note:'We have spoken together in person.'});
|
||||
from.relationships=from.relationships.slice(-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
bot.simulationBand='foreground';
|
||||
realmEvent(state,'memory',`${bot.name} had a live conversation with ${request.speakerName}.`,bot.id);
|
||||
try{
|
||||
@@ -150,16 +167,21 @@ async function remember(store:StateStore,botId:string,request:ConversationReques
|
||||
export async function runConversationTick(store:StateStore,db:D1Database,config:RuntimeConfig){
|
||||
for(const delivery of await pendingDeliveries(db)){
|
||||
if(delivery.status==='completed'){
|
||||
await remember(store,delivery.botId,delivery.request,delivery.reply);
|
||||
if(delivery.request.socialTurn!==0)await remember(store,delivery.botId,delivery.request,delivery.reply);
|
||||
const state=await store.load();
|
||||
const speaker=state.bots.find(b=>b.id===delivery.botId);
|
||||
if(speaker)await queueSocialReply(db,delivery.request,speaker.name,delivery.reply);
|
||||
await completeConversation(db,delivery.request.id,JSON.stringify({commandId:delivery.commandId,delivered:true}));
|
||||
}else if(delivery.status==='failed'){
|
||||
await failConversation(db,delivery.request.id,delivery.error||'Game rejected conversation delivery');
|
||||
}
|
||||
}
|
||||
await cleanupConversations(db);
|
||||
const request=await leaseConversation(db);
|
||||
if(!request)return {ok:true,ran:false,reason:'No player conversation waiting'};
|
||||
|
||||
try{
|
||||
if(request.socialTurn!==undefined&&Date.now()-Date.parse(request.createdAt)>120_000)throw new Error('Social encounter expired');
|
||||
const state=await store.load();
|
||||
const bot=chooseTarget(state,request);
|
||||
if(!bot){
|
||||
@@ -171,7 +193,7 @@ export async function runConversationTick(store:StateStore,db:D1Database,config:
|
||||
reply=cleanReply(reply);
|
||||
if(!reply)throw new Error('Conversation reply was empty');
|
||||
|
||||
const command=await queueAleCommand(db,compileReply(bot.name,request.speakerName,reply,request.channel));
|
||||
const command=await queueAleCommand(db,compileReply(bot.name,request.speakerName,reply,request.channel,request.socialTurn!==undefined));
|
||||
await awaitDelivery(db,request,bot.id,reply,command.id);
|
||||
await cleanupConversations(db);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user