From 900b926b8efe36fa55b6f9822c451188ec8b2bff Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 05:58:16 +1200 Subject: [PATCH] Hold independent work while companions regroup and leaders wait --- ale/azerothai_survival.lua | 50 ++++++++++++++++++++++++------------ docs/survival-cooperation.md | 12 +++++++++ tests/survival-lua.test.py | 4 +++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/ale/azerothai_survival.lua b/ale/azerothai_survival.lua index 94ef1b2..16cd512 100644 --- a/ale/azerothai_survival.lua +++ b/ale/azerothai_survival.lua @@ -136,7 +136,30 @@ local function sample() if hp<85 or mana<35 then r.resting=true end if hp>=95 and mana>=65 then r.resting=false end local resting=not p:IsInCombat() and (r.resting or now<(r.recoverUntil or 0)) - if unsafe or avoid or resting then holdPlans(p,ai,r,now) else releasePlans(p,r) end + -- Decide cohesion before restoring work flags, so native RPG cannot undo a wait/follow. + local leader=g and ai:GetGroupLeader() or nil + local regroup,waiting=false,false + local cohesion=enabled and now-enabledAt<180 and g and not p:IsInCombat() and not p:IsCasting() and not unsafe and not avoid and not resting + if cohesion and leader and managed(leader) and not leader:IsTaxi() then + if leader~=p and leader:GetMapId()==p:GetMapId() and leader:GetPhaseMask()==p:GetPhaseMask() then + local distance=p:GetDistance(leader) + -- Hysteresis prevents alternating independent work and follow at the boundary. + if distance<=6 or distance>=200 then r.catchingUp=false + elseif distance>12 then r.catchingUp=true end + regroup=r.catchingUp and hp>=95 and mana>=65 and not leader:IsInCombat() + elseif leader==p then + local separated=false + for _,m in pairs(peers) do + if m~=p and not m:IsTaxi() and m:GetMapId()==p:GetMapId() and m:GetPhaseMask()==p:GetPhaseMask() and p:GetDistance(m)<200 and (p:GetDistance(m)>15 or m:GetHealthPct()<85 or (m:GetPowerType()==0 and m:GetPowerPct(0)<35)) then separated=true;break end + end + if separated then + r.waitSince=r.waitSince or now + waiting=now-r.waitSince<20 + if now-r.waitSince>60 then r.waitSince=now;waiting=true end + else r.waitSince=nil end + end + else r.catchingUp=false;r.waitSince=nil end + if unsafe or avoid or resting or regroup or waiting then holdPlans(p,ai,r,now) else releasePlans(p,r) end if unsafe 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 @@ -158,22 +181,17 @@ local function sample() ai:DoSpecificAction('eat',true);ai:DoSpecificAction('drink',true) end end - 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)>8 and p:GetDistance(leader)<200 and hp>=95 and mana>=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>=95 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)>15 or m:GetHealthPct()<85) 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 + elseif regroup then + ai:SetMaster(leader);ownedMasters[id]=leader:GetGUIDLow() + ai:ChangeStrategy('-stay',1) + r.action='regroup requested';r.actionAt=now + if now-(r.followAt or 0)>=8 then + r.followAt=now;r.accepted=ai:DoSpecificAction('follow',true) + if now-(r.cohesionAuditAt or 0)>=30 then audit(p,'regroup-request','accepted='..tostring(r.accepted)..'; distance='..math.floor(p:GetDistance(leader)));r.cohesionAuditAt=now end end + elseif waiting then + p:MoveStop();r.action='waiting for companion';r.actionAt=now + end end end diff --git a/docs/survival-cooperation.md b/docs/survival-cooperation.md index 01e2430..c82d7f6 100644 --- a/docs/survival-cooperation.md +++ b/docs/survival-cooperation.md @@ -72,3 +72,15 @@ native AI to flee if that hostile target is selected again within range. This memory is local to the running Lua process, expires, and is not a pathfinding barrier or a guarantee that the bot can escape. No automatic gear, talent, or level boosts are applied. + +## Cohesion holds + +Catch-up begins beyond 12 yards and remains active until within 6 yards, with a +200-yard maximum. During catch-up, the existing durable strategy lease pauses +independent questing, grinding, gathering and looting; following and defensive +combat remain native. Leaders use the same lease during their bounded 20-second +wait, rather than only stopping movement while RPG immediately restarts it. +A reunion resets the wait timer. Nearby low-mana companions also trigger a wait. +Safety/recovery still wins, incomplete or human parties are excluded, and no +teleport or forced cross-map reunion is attempted. Regroup requests record the +native acceptance result and distance at most once per 30 seconds per bot. diff --git a/tests/survival-lua.test.py b/tests/survival-lua.test.py index e6d98ff..584b4d1 100644 --- a/tests/survival-lua.test.py +++ b/tests/survival-lua.test.py @@ -61,3 +61,7 @@ l=new();l.execute("A.hp=80;step(0);A.hp=100;local query=AuthDBQuery;function Aut print('Readiness holds, mana recovery, outnumbered/rapid-loss retreat, and original-strategy restoration passed') l=new();l.execute("A.combat=true;A.hp=75;A.attackers={1,2};A.enemy={GetGUID=function()return 999 end,GetLevel=function()return 5 end,ToCreature=function()return nil end};step(0);A.selected=A.enemy;A.combat=false;A.hp=100;A.attackers={};step(20);assert(A.attackStopped and calls[#calls]=='flee');A.attackStopped=false;step(601);assert(not A.attackStopped)") print('Dangerous encounter target avoidance expires after ten minutes') +l=new();l.execute("group();A.distance=50;B.distance=50;A.strategies={rpg=true,grind=true};B.strategies={rpg=true,gather=true};step(0);assert(A.waited and not A.strategies.rpg and not B.strategies.gather);assert(AzerothAISurvivalOverride(A) and AzerothAISurvivalOverride(B));B.distance=9;step(8);assert(AzerothAISurvivalOverride(B));B.distance=5;A.distance=5;step(30);assert(not AzerothAISurvivalOverride(B),'Caught-up bot must release the hold')") +l=new();l.execute("group();B.distance=50;B.combat=true;B.hp=20;step(0);assert(calls[1]=='flee','Survival outranks regrouping');assert(not B.master)") +l=new();l.execute("group();A.distance=40;step(0);A.waited=false;step(22);assert(not A.waited,'Leader waiting must remain bounded');A.distance=5;step(2);A.distance=40;step(2);assert(A.waited,'A fresh separation starts a fresh wait')") +print('Cohesion suppresses competing work, keeps catch-up hysteresis, yields to danger, and resets bounded waits')