import { z } from 'zod'; import {classicCommand} from '../classic'; import {queueAleCommand} from '../ale-store'; import {runGM} from '../gm'; import type { GameConnector,Bot,Decision,RuntimeConfig } from '../types'; const deathSignal=z.object({ id:z.string().min(1).max(160), at:z.string().min(1).max(80), cause:z.string().min(1).max(300), killer:z.string().max(120).optional(), zone:z.string().max(120).optional(), verified:z.boolean() }).strict(); const observation=z.object({ zone:z.string().max(120), level:z.number().int().min(1).max(100), summary:z.string().max(2000), availableActions:z.array(z.enum(['explore','quest','socialize','trade','rest'])), alive:z.boolean().default(true), death:deathSignal.optional() }); export class SimulationConnector implements GameConnector { id='simulation'; async health(){return {ok:true,detail:'Simulation ready. No AzerothCore connection.'};} async observe(bot:Bot){return {zone:bot.zone,level:bot.level,availableActions:bot.personality.activities,summary:'Simulated world observation. Game mechanics are not executed.',alive:bot.status!=='dead'};} async lifecycle(){ /* Acknowledged only inside the simulation. */ } async decide(bot:Bot,d:Decision){return {summary:`${bot.name} chose to ${d.action}: ${d.goal}`,zone:bot.zone};} } export class AzerothCoreConnector implements GameConnector { id='azerothcore'; constructor(private config:RuntimeConfig){} private async request(path:string,body?:unknown){ if(!this.config.BRIDGE_URL||!this.config.BRIDGE_TOKEN)throw new Error('Configure BRIDGE_URL and BRIDGE_TOKEN on the server first.'); const url=new URL(this.config.BRIDGE_URL); if(!['http:','https:'].includes(url.protocol))throw new Error('Invalid bridge URL'); const response=await fetch(`${url.href.replace(/\/$/,'')}/v1${path}`,{method:body?'POST':'GET',headers:{Authorization:`Bearer ${this.config.BRIDGE_TOKEN}`,'Content-Type':'application/json'},...(body?{body:JSON.stringify(body)}:{}),signal:AbortSignal.timeout(12000),redirect:'error'}); if(!response.ok)throw new Error(`Game bridge returned HTTP ${response.status}`); return response.json(); } async health(){const r=z.object({ok:z.boolean(),detail:z.string().max(300)}).parse(await this.request('/health'));return r;} async observe(bot:Bot){return observation.parse(await this.request(`/bots/${encodeURIComponent(bot.id)}/observation`));} async lifecycle(bot:Bot,action:string,requestId:string){ const r=z.object({accepted:z.literal(true)}).parse(await this.request(`/bots/${encodeURIComponent(bot.id)}/lifecycle`,{action,identity:{id:bot.id,name:bot.name,race:bot.race,role:bot.role,tier:bot.tier,mortal:bot.mortal,plotArmorHpFloor:bot.plotArmorHpFloor},requestId})); void r; } async decide(bot:Bot,decision:Decision,requestId:string){return z.object({accepted:z.literal(true),summary:z.string().max(1000),zone:z.string().max(120).optional()}).parse(await this.request(`/bots/${encodeURIComponent(bot.id)}/intent`,{decision,requestId}));} } export class ClassicConnector implements GameConnector { id='cmangos-classic'; constructor(private config:RuntimeConfig){} async health(){const result=await runGM('server info',true,true,this.config);return {ok:true,detail:result.output};} async observe(bot:Bot){ if(!bot.world||Date.now()/1000-bot.world.at>30)throw new Error('Fresh Classic observation required'); return {zone:bot.zone,level:bot.level,alive:!bot.world.isDead&&!bot.world.isGhost,availableActions:bot.personality.activities,summary:JSON.stringify({zone:bot.zone,level:bot.level,health:bot.world.healthPct,mana:bot.world.manaPct,quests:bot.world.quests,combat:bot.world.inCombat,party:bot.world.groupNames})}; } async lifecycle(){throw new Error('Classic maintains the fixed 50-character roster automatically. Manual lifecycle changes are not enabled.');} async decide(bot:Bot,decision:Decision){ if(!this.config.DB)throw new Error('Classic command database is unavailable'); const queued=await queueAleCommand(this.config.DB,classicCommand('intent',bot,decision.action)); return {summary:`${bot.name}: requested ${decision.action}; native receipt and observed outcome are pending (${queued.id}).`,zone:bot.zone}; } } // Add a connector factory here; personality and memory code remains game-independent. export const connectors:RecordGameConnector>={simulation:()=>new SimulationConnector(),azerothcore:c=>c.GAME_BACKEND==='cmangos-classic'?new ClassicConnector(c):new AzerothCoreConnector(c)};