From e7b9df9e085bb4986826df0225ed292a9d1e36fa Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 02:18:25 +1200 Subject: [PATCH] React earlier to hardcore danger and audit native retreat outcomes --- ale/azerothai_survival.lua | 72 +++++++++++++++++++++++++++++++----- docs/survival-cooperation.md | 15 ++++++-- tests/survival-lua.test.py | 21 +++++++++-- 3 files changed, 93 insertions(+), 15 deletions(-) diff --git a/ale/azerothai_survival.lua b/ale/azerothai_survival.lua index b78af83..06585dd 100644 --- a/ale/azerothai_survival.lua +++ b/ale/azerothai_survival.lua @@ -1,6 +1,35 @@ -- Survival assistance for registered living bots only. No teleport, heal, revive or stat changes. local registry,observed,ownedMasters={}, {}, {} local registryAt=0 +AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_survival_audit ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + guid INT UNSIGNED,name VARCHAR(64),kind VARCHAR(32),detail VARCHAR(255))]]) +local function quoted(s)return "UNHEX('"..string.gsub(tostring(s),'.',function(c)return string.format('%02X',string.byte(c))end).."')" end +local function audit(p,kind,detail) + AuthDBExecute('INSERT INTO azerothai_survival_audit(guid,name,kind,detail) VALUES ('..p:GetGUIDLow()..','..quoted(p:GetName())..','..quoted(kind)..','..quoted(detail)..')') +end +local function dangerous(p,enemy) + if not enemy then return false end + local creature=enemy:ToCreature() + local rank=creature and creature:GetRank() or 0 + return enemy:GetLevel()>=p:GetLevel()+3 or rank==1 or rank==2 or rank==3 +end +local function dangerousSelection(p) + local target=p:GetSelection() + if not target or not dangerous(p,target) then return false end + for _,enemy in pairs(p:GetUnfriendlyUnitsInRange(40)) do + if enemy:GetGUID()==target:GetGUID() then return true end + end + return false +end +local function outcome(p,r,now) + local t=r.retreat + if not t then return end + local moved=p:GetMapId()~=t.map or (p:GetX()-t.x)^2+(p:GetY()-t.y)^2>=25 + t.moved=t.moved or moved + local result=p:IsDead() and 'died' or not p:IsInCombat() and 'out-of-combat' or now-t.at>=30 and (t.moved and 'moved-still-in-combat' or 'no-movement') + if result then audit(p,result,'Retreat observed for '..(now-t.at)..'s; movement='..tostring(t.moved==true));r.retreat=nil end +end local enabled,enabledAt=true,0 function AzerothAISetSurvivalEnabled(value) enabled=value==true;enabledAt=os.time() end local function refresh(now) @@ -28,41 +57,60 @@ end -- Prevent slower model intentions from undoing an immediate safety action. function AzerothAISurvivalOverride(p) local r=observed[p:GetGUIDLow()] - return r and os.time()-(r.actionAt or 0)<20 and (r.action=='retreat requested' or r.action=='recovering health' or r.action=='regroup requested') or false + return r and os.time()-(r.actionAt or 0)<20 and (r.action=='retreat requested' or r.action=='recovering health' or r.action=='regroup requested' or r.action=='waiting for companion' or r.action=='avoiding dangerous target') or false end local function sample() local now=os.time();refresh(now) for _,p in pairs(GetPlayersInWorld() or {}) do + local previous=observed[p:GetGUIDLow()] + if previous then outcome(p,previous,now) end if managed(p) then local id=p:GetGUIDLow();local ai=p:GetBotAI();local r=observed[id] or {};observed[id]=r local hp=p:GetHealthPct() - if p:IsInCombat() and (hp<40 or r.hp and r.hp-hp>=25) then r.dangerAt=now end + if p:IsInCombat() and (hp<55 or r.hp and r.hp-hp>=15) then r.dangerAt=now end r.hp=hp;r.seen=now local master=ai and ai:GetMaster() if ownedMasters[id] and (not p:GetGroup() or not master or not master:GetGroup() or master:GetGroup()~=p:GetGroup()) then if ai and master and master:IsPlayerbot() and master:GetGUIDLow()==ownedMasters[id] and not ai:HasGameClientMaster() then ai:SetMaster(nil) end ownedMasters[id]=nil end - if ai and not ai:HasGameClientMaster() and not p:IsTaxi() and not p:IsCasting() and not p:IsRooted() and not ai:IsMovementImpaired() then + if ai and not ai:HasGameClientMaster() and not p:IsTaxi() and not p:IsRooted() and not ai:IsMovementImpaired() then local g=safeGroup(p) -- Never intervene in a party containing humans, unregistered bots, or offline members. if not p:GetGroup() or g then local peers=g and g:GetMembers() or {p};local weak=0 for _,m in pairs(peers) do if m:GetMapId()==p:GetMapId() and p:GetDistance(m)<40 and m:GetHealthPct()<40 then weak=weak+1 end end local enemy=p:GetVictim() - local overmatched=enemy and enemy:GetLevel()>=p:GetLevel()+4 and hp<65 - if p:IsInCombat() and (hp<30 or overmatched or #peers>1 and weak>=math.max(2,math.ceil(#peers/2))) and now-(r.retreatAt or 0)>=20 then + local overmatched=dangerous(p,enemy) + if p:IsInCombat() and (hp<50 or overmatched or #peers>1 and weak>=math.max(2,math.ceil(#peers/2))) and now-(r.retreatAt or 0)>=8 then r.dangerAt=now;r.retreatAt=now;r.action='retreat requested';r.actionAt=now + if p:IsCasting() then p:StopSpellCast() end r.accepted=ai:DoSpecificAction('flee',true) - elseif not p:IsInCombat() and hp<65 and now-(r.dangerAt or 0)<600 and now-(r.restAt or 0)>=20 then + audit(p,'retreat-request', 'accepted='..tostring(r.accepted)..'; hp='..math.floor(hp)..'; dangerous='..tostring(overmatched)) + if not r.retreat then r.retreat={at=now,map=p:GetMapId(),x=p:GetX(),y=p:GetY()} end + elseif not p:IsInCombat() and dangerousSelection(p) and now-(r.avoidAt or 0)>=8 then + if p:IsCasting() then p:StopSpellCast() end + p:AttackStop();r.avoidAt=now;r.dangerAt=now;r.action='avoiding dangerous target';r.actionAt=now + r.accepted=ai:DoSpecificAction('flee',true) + audit(p,'avoid-request','accepted='..tostring(r.accepted)) + elseif not p:IsCasting() and not p:IsInCombat() and hp<80 and now-(r.dangerAt or 0)<600 and now-(r.restAt or 0)>=20 then r.restAt=now;r.action='recovering health';r.actionAt=now ai:DoSpecificAction('eat',true);ai:DoSpecificAction('drink',true) - elseif enabled and now-enabledAt<180 and g and not p:IsInCombat() then + elseif enabled and now-enabledAt<180 and g and not p:IsInCombat() and not p:IsCasting() then local leader=ai:GetGroupLeader() - if leader and leader~=p and managed(leader) and not leader:IsInCombat() and not leader:IsTaxi() and leader:GetMapId()==p:GetMapId() and leader:GetPhaseMask()==p:GetPhaseMask() and p:GetDistance(leader)>18 and p:GetDistance(leader)<90 and hp>=65 and now-(r.followAt or 0)>=20 then + if leader and leader~=p and managed(leader) and not leader:IsInCombat() and not leader:IsTaxi() and leader:GetMapId()==p:GetMapId() and leader:GetPhaseMask()==p:GetPhaseMask() and p:GetDistance(leader)>12 and p:GetDistance(leader)<200 and hp>=65 and now-(r.followAt or 0)>=8 then ai:SetMaster(leader);ownedMasters[id]=leader:GetGUIDLow() ai:ChangeStrategy('-stay',1) r.followAt=now;r.action='regroup requested';r.actionAt=now;r.accepted=ai:DoSpecificAction('follow',true) + elseif leader==p and hp>=80 then + for _,m in pairs(peers) do + if m~=p and not m:IsInCombat() and not m:IsTaxi() and m:GetMapId()==p:GetMapId() and m:GetPhaseMask()==p:GetPhaseMask() and p:GetDistance(m)>25 and p:GetDistance(m)<200 then + r.waitSince=r.waitSince or now + if now-r.waitSince<20 then p:MoveStop();r.action='waiting for companion';r.actionAt=now + elseif now-r.waitSince>60 then r.waitSince=nil end + break + end + end end end end @@ -71,5 +119,11 @@ local function sample() end for id,r in pairs(observed) do if now-(r.seen or 0)>600 then observed[id]=nil;ownedMasters[id]=nil end end end -CreateLuaEvent(function()local ok,err=pcall(sample);if not ok then PrintError('[AzerothAI survival] '..tostring(err)) end end,10000,0) +CreateLuaEvent(function()local ok,err=pcall(sample);if not ok then PrintError('[AzerothAI survival] '..tostring(err)) end end,2000,0) PrintInfo('[AzerothAI] Survival assistance enabled: danger memory, native retreat and nearby regrouping') + +CreateLuaEvent(function() AuthDBExecute("DELETE FROM azerothai_survival_audit WHERE at