React earlier to hardcore danger and audit native retreat outcomes
Core tests / core (push) Successful in 48s
Core tests / core (push) Successful in 48s
This commit is contained in:
@@ -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<NOW()-INTERVAL 7 DAY LIMIT 2000") end,3600000,0)
|
||||
|
||||
local function deathResult(_,killer,p) local r=observed[p:GetGUIDLow()];if r and r.retreat then audit(p,'died','Died after retreat request');r.retreat=nil end end
|
||||
RegisterPlayerEvent(8,deathResult)
|
||||
RegisterPlayerEvent(6,deathResult)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Survival and cooperation
|
||||
|
||||
Native survival assistance samples registered, living bots every ten seconds. Health below 40% in combat or a drop of at least 25 percentage points records ten minutes of recent danger. Party selection gives those residents priority; recent danger bypasses sociability reluctance, but never hostility, faction/level compatibility, proximity or human-control checks. Candidate ranking favors shared active quest IDs and complementary native healer/tank roles. It does not change class, spec, gear or quest state.
|
||||
Native survival assistance samples registered, living bots every two seconds. Health below 55% in combat or a drop of at least 15 percentage points records ten minutes of recent danger. Party selection gives those residents priority; recent danger bypasses sociability reluctance, but never hostility, faction/level compatibility, proximity or human-control checks. Candidate ranking favors shared active quest IDs and complementary native healer/tank roles. It does not change class, spec, gear or quest state.
|
||||
|
||||
Party actions remain receipt-confirmed, one per community tick. Participants wait two minutes after a completed action or one minute after a rejected command before another invitation; pending commands remain excluded. Separated parties now have twenty minutes before automatic departure, giving native regrouping a chance. Declined candidate pairs no longer prevent the same tick from considering a better match.
|
||||
|
||||
The native guard requests the existing `flee` action at most every twenty seconds when health falls below 30%, an enemy is at least four levels higher and health is below 65%, or at least two members and at least half the party are nearby and below 40%. This is a movement request, not guaranteed escape. After danger, injured bots request normal eating/drinking actions; food, mana and other game requirements still apply. No health, level or resurrection changes occur.
|
||||
The native guard requests the existing `flee` action at most every eight seconds when health falls below 50%, an enemy is at least three levels higher or is elite/rare-elite/world-boss, or at least two members and at least half the party are nearby and below 40%. This is a movement request, not guaranteed escape. After danger, injured bots request normal eating/drinking actions; food, mana and other game requirements still apply. No health, level or resurrection changes occur.
|
||||
|
||||
Healthy, noncombat followers 18–90 yards from their bot leader on the same map/phase can request normal follow movement. Human-led, mixed, raid and partly offline parties are excluded. The guard does not teleport or force cross-map travel. A master relationship assigned for following is cleared when that party relationship ends. Slower model commands yield to a recent native survival request rather than immediately replacing it. An accepted request does not establish shared quest completion or successful escape; existing gameplay and cooperation counters remain the outcome evidence.
|
||||
Healthy, noncombat followers 12–200 yards from their bot leader on the same map/phase can request normal follow movement. Human-led, mixed, raid and partly offline parties are excluded. The guard does not teleport or force cross-map travel. A master relationship assigned for following is cleared when that party relationship ends. Slower model commands yield to a recent native survival request rather than immediately replacing it. An accepted request does not establish shared quest completion or successful escape; existing gameplay and cooperation counters remain the outcome evidence.
|
||||
|
||||
Grouping assistance uses the existing parties-enabled setting, refreshed by the operations collector with a three-minute expiry. Danger detection and emergency self-preservation remain active for eligible bots. Bot Activity displays recent survival requests explicitly as requests. The ALE bridge supplies native combat role and up to 25 active quest IDs for companion matching.
|
||||
|
||||
@@ -19,3 +19,12 @@ Install `ale/azerothai_parties.lua` beside the bridge and reload ALE before depl
|
||||
Party invitations now carry both exact native identities and expire 45 seconds after creation. The native guard retries every two seconds while a participant is busy or out of range, allowing a meeting within 60 yards on the same map/phase. Every attempt rechecks identity, lifecycle, faction, levels, human control and group capacity. There is no teleport or forced combat interruption. A deferred bridge command receives its completion receipt only when membership is confirmed or the invitation expires/fails. Pending participants remain reserved so another invitation cannot race the first. ALE reloads lose in-memory retries; the durable lane eventually replays the expired request as a failure rather than applying an old invitation.
|
||||
|
||||
Every ten seconds, the party guard checks incomplete groups against the complete persisted character membership and hardcore registry. It removes only a verified permanently dead member, after validating every identity and protecting human, unknown, active-but-offline, raid and combat parties. One mutation per group avoids touching a Group object after native disbanding. The audit table `acore_auth.azerothai_party_audit` records actual removals and invitation outcomes; it is covered by the full auth database backup.
|
||||
|
||||
|
||||
## Hardcore survival revision
|
||||
|
||||
The native guard now samples every two seconds. Retreat is requested below 50% health, against targets at least three levels higher or elite/rare-elite/world-boss targets, or when nearby party health collapses. Emergency casts are stopped before requesting flee; human-controlled, mixed/incomplete parties, taxi and movement restrictions remain protected. Requests are limited to one per eight seconds. Dangerous selected targets must also appear in the native unfriendly list within 40 yards before pre-combat avoidance is requested. These are native movement requests, not guaranteed avoidance or escape.
|
||||
|
||||
Healthy followers can regroup over 12–200 yards on the same map/phase. Healthy noncombat leaders pause for at most 20 seconds per minute when a noncombat companion is 25–200 yards behind. Recovery requests now extend to 80% health after recent danger. No teleport, resurrection, population or stat changes are made.
|
||||
|
||||
The auth table `azerothai_survival_audit` records request acceptance and observed outcomes: out of combat, movement while still in combat, no movement after 30 seconds, or death during observation. Movement means at least five yards from the starting position (or a map change); it does not prove successful escape. Audit records expire after seven days in bounded hourly batches. Snapshots continue to expose the latest action and timestamp.
|
||||
|
||||
@@ -4,7 +4,9 @@ setup='''
|
||||
now=1000;os.time=function()return now end;calls={};people={}
|
||||
function AuthDBQuery(sql)local i=1;return {GetUInt32=function(_,n)return n==0 and i or 10+i end,GetString=function()return i==1 and 'A' or 'B'end,NextRow=function()i=i+1;return i<=2 end} end
|
||||
function GetPlayersInWorld()return people end
|
||||
function CreateLuaEvent(fn)tick=fn end
|
||||
function CreateLuaEvent(fn,ms)if ms==2000 then tick=fn end end
|
||||
function RegisterPlayerEvent()end
|
||||
auditRows={};function AuthDBExecute(sql)table.insert(auditRows,sql)end
|
||||
function PrintInfo()end
|
||||
function PrintError(s)error(s)end
|
||||
function make(id,name)
|
||||
@@ -13,6 +15,9 @@ function make(id,name)
|
||||
function p:IsPlayerbot()return not self.human end;function p:IsDead()return self.dead end;function p:GetBotAI()return self.ai end
|
||||
function p:GetHealthPct()return self.hp end;function p:IsInCombat()return self.combat end
|
||||
function p:IsTaxi()return self.taxi end;function p:IsCasting()return self.cast end;function p:IsRooted()return false end
|
||||
function p:GetX()return self.x or 0 end;function p:GetY()return 0 end
|
||||
function p:GetUnfriendlyUnitsInRange()return self.friendly and {} or self.selected and {self.selected} or {} end;function p:GetSelection()return self.selected end;function p:StopSpellCast()self.cast=false;self.interrupted=true end
|
||||
function p:AttackStop()self.attackStopped=true end;function p:MoveStop()self.waited=true end
|
||||
function p:GetGroup()return self.group end;function p:GetVictim()return self.enemy end;function p:GetLevel()return 5 end
|
||||
function p:GetDistance()return self.distance end;function p:GetMapId()return 0 end;function p:GetPhaseMask()return 1 end
|
||||
function p.ai:GetMaster()return p.master end;function p.ai:SetMaster(m)p.master=m end
|
||||
@@ -27,11 +32,21 @@ function step(n)now=now+n;tick()end
|
||||
'''
|
||||
def new():
|
||||
l=LuaRuntime();l.execute(setup);l.execute((Path(__file__).resolve().parents[1]/'ale/azerothai_survival.lua').read_text());l.execute('AzerothAISetSurvivalEnabled(true)');return l
|
||||
l=new();l.execute("A.combat=true;A.hp=20;step(0);assert(calls[1]=='flee');step(10);assert(#calls==1);step(10);assert(#calls==2)")
|
||||
l=new();l.execute("A.combat=true;A.hp=20;step(0);assert(calls[1]=='flee');step(2);assert(#calls==1);step(6);assert(#calls==2)")
|
||||
l=new();l.execute("group();A.human=true;B.combat=true;B.hp=20;step(0);assert(#calls==0)")
|
||||
l=new();l.execute("group();step(0);assert(calls[1]=='follow' and B.master==A)")
|
||||
l=new();l.execute("group();B.distance=100;step(0);assert(#calls==0)")
|
||||
l=new();l.execute("group();B.distance=250;step(0);assert(#calls==0)")
|
||||
l=new();l.execute("A.combat=true;A.hp=20;A.controlled=true;step(0);assert(#calls==0)")
|
||||
l=new();l.execute("A.combat=true;A.hp=20;step(0);A.combat=false;step(20);assert(calls[2]=='eat' and calls[3]=='drink')")
|
||||
l=new();l.execute("group();AzerothAISetSurvivalEnabled(false);step(0);assert(#calls==0)")
|
||||
print('Native survival: retreat cooldown, recovery, regroup range, human-party protection, and disabled grouping passed')
|
||||
|
||||
l=new();l.execute("A.combat=true;A.hp=45;A.cast=true;step(0);assert(A.interrupted and calls[1]=='flee')")
|
||||
l=new();l.execute("A.combat=true;A.hp=100;A.enemy={GetLevel=function()return 8 end,ToCreature=function()return nil end,IsHostileTo=function()return true end};step(0);assert(calls[1]=='flee')")
|
||||
l=new();l.execute("A.selected={GetGUID=function()return 99 end,GetLevel=function()return 5 end,ToCreature=function(self)return self end,GetRank=function()return 1 end,IsHostileTo=function()return true end};step(0);assert(A.attackStopped and calls[1]=='flee')")
|
||||
l=new();l.execute("A.friendly=true;A.selected={GetLevel=function()return 80 end,ToCreature=function()return nil end};step(0);assert(#calls==0)")
|
||||
l=new();l.execute("A.combat=true;A.hp=40;step(0);A.x=10;step(2);A.combat=false;step(2);assert(string.find(auditRows[#auditRows], '6F75742D6F662D636F6D626174'))")
|
||||
l=new();l.execute("A.combat=true;A.hp=40;step(0);A.hp=70;step(30);assert(string.find(auditRows[#auditRows], '6E6F2D6D6F76656D656E74'))")
|
||||
l=new();l.execute("group();B.distance=150;step(0);assert(B.master==A)")
|
||||
l=new();l.execute("group();A.distance=40;step(0);assert(A.waited);A.waited=false;step(22);assert(not A.waited)")
|
||||
print('Earlier retreat, casting interruption, hostile/elite avoidance, bounded leader waiting, longer regroup and observed retreat outcomes passed')
|
||||
|
||||
Reference in New Issue
Block a user