Hold independent work while companions regroup and leaders wait
Core tests / core (push) Successful in 36s

This commit is contained in:
Codex
2026-09-24 05:58:16 +12:00
parent 6b8ce612ad
commit 900b926b8e
3 changed files with 50 additions and 16 deletions
+34 -16
View File
@@ -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
+12
View File
@@ -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.
+4
View File
@@ -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')