-- 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 -- Persist only strategy flags this guard suppresses; reloads cannot strand a pause. local holdStrategies={'rpg','grind','quest','gather','loot'} AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_survival_holds ( guid INT UNSIGNED PRIMARY KEY,name VARCHAR(64),account_id INT UNSIGNED,expires BIGINT UNSIGNED, rpg TINYINT,grind TINYINT,quest TINYINT,gather TINYINT,loot TINYINT)]]) local holdScan=0 local function restoreHoldRow(q) local guid,name,account=q:GetUInt32(0),q:GetString(1),q:GetUInt32(2) local p=GetPlayerByName(name) if not p or not p:IsPlayerbot() or p:GetGUIDLow()~=guid or p:GetAccountId()~=account or not p:GetBotAI() or p:GetBotAI():HasGameClientMaster() then return end for i,key in ipairs(holdStrategies) do if q:GetUInt32(i+2)==1 then p:GetBotAI():ChangeStrategy('+'..key,1) end end AuthDBExecute('DELETE FROM azerothai_survival_holds WHERE guid='..guid) if observed[guid] then observed[guid].holding=false end end local function restoreExpiredHolds(now) if now-holdScan<10 then return end;holdScan=now local q=AuthDBQuery('SELECT guid,name,account_id,rpg,grind,quest,gather,loot FROM azerothai_survival_holds WHERE expires<='..now) if q then repeat restoreHoldRow(q) until not q:NextRow() end end local function holdPlans(p,ai,r,now) local flags,changed={},false for _,key in ipairs(holdStrategies) do local on=ai:HasStrategy(key,1);flags[#flags+1]=on and 1 or 0 if on then changed=true;ai:ChangeStrategy('-'..key,1) end end if not r.holding or changed or now-(r.holdSaved or 0)>=30 then AuthDBExecute('INSERT INTO azerothai_survival_holds VALUES ('..p:GetGUIDLow()..','..quoted(p:GetName())..','..p:GetAccountId()..','..(now+90)..','..table.concat(flags,',')..') ON DUPLICATE KEY UPDATE expires=VALUES(expires),rpg=GREATEST(rpg,VALUES(rpg)),grind=GREATEST(grind,VALUES(grind)),quest=GREATEST(quest,VALUES(quest)),gather=GREATEST(gather,VALUES(gather)),loot=GREATEST(loot,VALUES(loot))') r.holdSaved=now end r.holding=true end local function releasePlans(p,r) if not r.holding then return end local q=AuthDBQuery('SELECT guid,name,account_id,rpg,grind,quest,gather,loot FROM azerothai_survival_holds WHERE guid='..p:GetGUIDLow()) if q then restoreHoldRow(q) end r.holding=false 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()+2 or rank==1 or rank==2 or rank==3 end local function dangerousSelection(p,r,now) local target=p:GetSelection() if not target then return false end local key=tostring(target:GetGUID()) if not dangerous(p,target) and not (AzerothAILearnedDanger and AzerothAILearnedDanger(p,target)) and not (r.avoidTargets and (r.avoidTargets[key] or 0)>now) 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) if now-registryAt<60 then return end local q=AuthDBQuery("SELECT guid,name,account_id FROM azerothai_lifecycle WHERE state='active'") if not q then registry={};return end local rows={};repeat rows[q:GetUInt32(0)]={name=q:GetString(1),account=q:GetUInt32(2)} until not q:NextRow() registry=rows;registryAt=now end local function managed(p) if not p or not p:IsPlayerbot() or p:IsDead() then return false end local r=registry[p:GetGUIDLow()] return r and r.name==p:GetName() and r.account==p:GetAccountId() end local function safeGroup(p) local g=p:GetGroup();if not g or g:IsRaidGroup() or #g:GetMembers()~=g:GetMembersCount() then return nil end for _,m in pairs(g:GetMembers()) do if not managed(m) or not m:GetBotAI() or m:GetBotAI():HasGameClientMaster() then return nil end end return g end function AzerothAISurvivalState(p) local r=observed[p:GetGUIDLow()] if not r then return 0,'',0 end return r.dangerAt or 0,r.action or '',r.actionAt or 0,r.actionSince or r.actionAt or 0 end -- Prevent slower model intentions from undoing an immediate safety action. function AzerothAISurvivalOverride(p) local r=observed[p:GetGUIDLow()] return r and (r.holding or 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 r.action=='recovering readiness')) or false end local function sample() local now=os.time();refresh(now);restoreExpiredHolds(now) for _,p in pairs(GetPlayersInWorld() or {}) do local previous=observed[p:GetGUIDLow()] if previous then if previous.nativeFleeUntil and now>=previous.nativeFleeUntil then if p:GetMovementType()==9 or p:GetMovementType()==13 then p:MoveExpire() end previous.nativeFleeUntil=nil end 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 previousAction,previousAt=r.action,r.actionAt or 0 local hp=p:GetHealthPct() local falling=r.hp and r.hp-hp>=20 local mana=p:GetPowerType()==0 and p:GetPowerPct(0) or 100 if mana~=mana then mana=100 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:HasUnitState(0x00000100) and not p:IsRooted() and not ai:IsMovementImpaired() then local g=safeGroup(p) if (not p:GetGroup() or g) and hp>=95 and mana>=65 and ai:HasStrategy('stay',1) then ai:ChangeStrategy('-stay',1) end -- 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,support=0,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 for _,m in pairs(peers) do if m~=p and m:GetMapId()==p:GetMapId() and m:GetPhaseMask()==p:GetPhaseMask() and p:GetDistance(m)<=35 and m:GetHealthPct()>=70 then support=support+1 end end local attackers=0;for _ in pairs(p:GetAttackers()) do attackers=attackers+1 end local enemy=p:GetVictim() local overmatched=dangerous(p,enemy) or (AzerothAILearnedDanger and AzerothAILearnedDanger(p,enemy)) r.avoidTargets=r.avoidTargets or {} for key,expires in pairs(r.avoidTargets) do if expires<=now then r.avoidTargets[key]=nil end end if enemy and (overmatched or attackers>support+1 or falling and hp<80) then r.avoidTargets[tostring(enemy:GetGUID())]=now+600 end local outnumbered=attackers>support+1 and hp<80 local unsafe=p:IsInCombat() and (hp<(support==0 and 55 or 50) or overmatched or outnumbered or falling and hp<80 or #peers>1 and weak>=math.max(2,math.ceil(#peers/2))) local avoid=not p:IsInCombat() and dangerousSelection(p,r,now) if unsafe then r.recoverUntil=now+15 end 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)) -- 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=not (AzerothAITrainingOverride and AzerothAITrainingOverride(p)) and 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:HasUnitState(0x00000100) 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;r.catchSince=nil;r.catchDistance=nil elseif distance>12 and now>=(r.catchBlockedUntil or 0) then r.catchingUp=true;r.catchSince=r.catchSince or now if not r.catchDistance or distance=120 or now-(r.catchProgress or now)>=30 then r.catchingUp=false;r.catchSince=nil;r.catchDistance=nil;r.catchBlockedUntil=now+120 if ownedMasters[id] and master and master:GetGUIDLow()==ownedMasters[id] then ai:SetMaster(nil);ownedMasters[id]=nil end audit(p,'regroup-backoff','No useful reunion within bounded attempt; resume own activities') end 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:HasUnitState(0x00000100) 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 r.accepted=ai:DoSpecificAction('flee',true) -- The bot action may reject fleeing when its target/context is missing. -- Bound a native pathfinding attempt; never teleport or clear combat/threat. if not r.accepted and not p:IsMoving() and now-(r.nativeFleeAt or 0)>=30 then local threat=enemy if not threat then for _,attacker in pairs(p:GetAttackers()) do threat=attacker;break end end if threat then p:AttackStop();p:MoveFleeing(threat,8000) r.nativeFleeAt=now;r.nativeFleeUntil=now+8 audit(p,'retreat-path-request','Playerbot flee rejected; bounded native flee path requested') end end audit(p,'retreat-request', 'accepted='..tostring(r.accepted)..'; hp='..math.floor(hp)..'; dangerous='..tostring(overmatched)..'; attackers='..attackers..'; nearbySupport='..support) if not r.retreat then r.retreat={at=now,map=p:GetMapId(),x=p:GetX(),y=p:GetY()} end elseif avoid 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 resting then r.action='recovering readiness';r.actionAt=now p:AttackStop() if not p:IsCasting() then p:MoveStop() if now-(r.restAt or 0)>=20 then r.restAt=now ai:DoSpecificAction('eat',true);ai:DoSpecificAction('drink',true);ai:DoSpecificAction('sit',true) 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 if r.actionAt and (r.action~=previousAction or now-previousAt>30) then r.actionSince=now end end 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,2000,0) PrintInfo('[AzerothAI] Survival assistance enabled: danger memory, native retreat and nearby regrouping') CreateLuaEvent(function() AuthDBExecute("DELETE FROM azerothai_survival_audit WHERE at