Support persistent residents with normal resurrection and closer companions
Core tests / core (push) Successful in 2m54s
Core tests / core (push) Successful in 2m54s
This commit is contained in:
@@ -1,6 +1,13 @@
|
|||||||
-- Persistent hardcore retirement. The native random-bot manager fills vacancies
|
-- Persistent hardcore retirement. The native random-bot manager fills vacancies
|
||||||
-- from its factory-created, unused starting-level reserve; identities are never reset.
|
-- from its factory-created, unused starting-level reserve; identities are never reset.
|
||||||
-- Install only after deploy/playable/install-lifecycle.py has seeded the registry.
|
-- Install only after deploy/playable/install-lifecycle.py has seeded the registry.
|
||||||
|
-- The persisted mode is synchronized from dashboard settings by operations-agent.
|
||||||
|
AuthDBExecute("CREATE TABLE IF NOT EXISTS azerothai_realm_rules (id TINYINT PRIMARY KEY,hardcore TINYINT NOT NULL)")
|
||||||
|
AuthDBExecute("INSERT IGNORE INTO azerothai_realm_rules VALUES (1,1)")
|
||||||
|
function AzerothAIHardcoreEnabled()
|
||||||
|
local q=AuthDBQuery("SELECT hardcore FROM azerothai_realm_rules WHERE id=1")
|
||||||
|
return not q or q:GetUInt32(0)~=0
|
||||||
|
end
|
||||||
local residents = {}
|
local residents = {}
|
||||||
local ready = false
|
local ready = false
|
||||||
local vetoAt = {}
|
local vetoAt = {}
|
||||||
@@ -68,6 +75,7 @@ local function identity(player)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function recordDeath(player)
|
local function recordDeath(player)
|
||||||
|
if not AzerothAIHardcoreEnabled() then return end
|
||||||
local row,guid=identity(player)
|
local row,guid=identity(player)
|
||||||
if not row or row.state=="dead" or row.state=="blocked" then return end
|
if not row or row.state=="dead" or row.state=="blocked" then return end
|
||||||
row.state="dead"
|
row.state="dead"
|
||||||
@@ -77,6 +85,7 @@ local function recordDeath(player)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function enforce()
|
local function enforce()
|
||||||
|
local hardcore=AzerothAIHardcoreEnabled()
|
||||||
if not ready then reload() end
|
if not ready then reload() end
|
||||||
-- The dashboard may receive a verified death from an older transport backlog
|
-- The dashboard may receive a verified death from an older transport backlog
|
||||||
-- after this process seeded its registry. Import terminal states only; never
|
-- after this process seeded its registry. Import terminal states only; never
|
||||||
@@ -114,7 +123,7 @@ local function enforce()
|
|||||||
for _,player in pairs(players) do
|
for _,player in pairs(players) do
|
||||||
local guid=player:GetGUIDLow()
|
local guid=player:GetGUIDLow()
|
||||||
local row=residents[guid]
|
local row=residents[guid]
|
||||||
if row and row.state=="active" and row.account==player:GetAccountId() and row.name==player:GetName() and not player:IsDead() then
|
if row and row.state=="active" and row.account==player:GetAccountId() and row.name==player:GetName() and (not hardcore or not player:IsDead()) then
|
||||||
returning[#returning+1]={player=player,row=row,guid=guid}
|
returning[#returning+1]={player=player,row=row,guid=guid}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -203,4 +212,4 @@ end,1000,0)
|
|||||||
|
|
||||||
reload()
|
reload()
|
||||||
enforce()
|
enforce()
|
||||||
PrintInfo("[AzerothAI] Persistent hardcore retirement and reserve replacement enabled")
|
PrintInfo("[AzerothAI] Persistent residents and faction cap enabled; hardcore="..tostring(AzerothAIHardcoreEnabled()))
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ local function attempt(r)
|
|||||||
if not a or not b then return false,'Resident offline' end
|
if not a or not b then return false,'Resident offline' end
|
||||||
if a:GetGUIDLow()~=r.a.guid or a:GetAccountId()~=r.a.account or b:GetGUIDLow()~=r.b.guid or b:GetAccountId()~=r.b.account then return nil,'Identity changed' end
|
if a:GetGUIDLow()~=r.a.guid or a:GetAccountId()~=r.a.account or b:GetGUIDLow()~=r.b.guid or b:GetAccountId()~=r.b.account then return nil,'Identity changed' end
|
||||||
if not resident(a) or not resident(b) then return nil,'Resident retired or human-controlled' end
|
if not resident(a) or not resident(b) then return nil,'Resident retired or human-controlled' end
|
||||||
if a:GetTeam()~=b:GetTeam() or math.abs(a:GetLevel()-b:GetLevel())>10 then return nil,'Faction or level mismatch' end
|
if a:GetTeam()~=b:GetTeam() or math.abs(a:GetLevel()-b:GetLevel())>5 then return nil,'Faction or level mismatch' end
|
||||||
if not available(a) or not available(b) then return false,'Waiting for combat, cast, or taxi to finish' end
|
if not available(a) or not available(b) then return false,'Waiting for combat, cast, or taxi to finish' end
|
||||||
if a:GetMapId()~=b:GetMapId() or a:GetPhaseMask()~=b:GetPhaseMask() or a:GetDistance(b)>60 then return false,'Waiting for a nearby meeting within 60 yards' end
|
if a:GetMapId()~=b:GetMapId() or a:GetPhaseMask()~=b:GetPhaseMask() or a:GetDistance(b)>60 then return false,'Waiting for a nearby meeting within 60 yards' end
|
||||||
local g=a:GetGroup()
|
local g=a:GetGroup()
|
||||||
|
|||||||
@@ -51,9 +51,11 @@ local function dangerous(p,enemy)
|
|||||||
local rank=creature and creature:GetRank() or 0
|
local rank=creature and creature:GetRank() or 0
|
||||||
return enemy:GetLevel()>=p:GetLevel()+2 or rank==1 or rank==2 or rank==3
|
return enemy:GetLevel()>=p:GetLevel()+2 or rank==1 or rank==2 or rank==3
|
||||||
end
|
end
|
||||||
local function dangerousSelection(p)
|
local function dangerousSelection(p,r,now)
|
||||||
local target=p:GetSelection()
|
local target=p:GetSelection()
|
||||||
if not target or not dangerous(p,target) then return false end
|
if not target then return false end
|
||||||
|
local key=tostring(target:GetGUID())
|
||||||
|
if not dangerous(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
|
for _,enemy in pairs(p:GetUnfriendlyUnitsInRange(40)) do
|
||||||
if enemy:GetGUID()==target:GetGUID() then return true end
|
if enemy:GetGUID()==target:GetGUID() then return true end
|
||||||
end
|
end
|
||||||
@@ -124,9 +126,12 @@ local function sample()
|
|||||||
local attackers=0;for _ in pairs(p:GetAttackers()) do attackers=attackers+1 end
|
local attackers=0;for _ in pairs(p:GetAttackers()) do attackers=attackers+1 end
|
||||||
local enemy=p:GetVictim()
|
local enemy=p:GetVictim()
|
||||||
local overmatched=dangerous(p,enemy)
|
local overmatched=dangerous(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 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 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)
|
local avoid=not p:IsInCombat() and dangerousSelection(p,r,now)
|
||||||
if unsafe then r.recoverUntil=now+15 end
|
if unsafe then r.recoverUntil=now+15 end
|
||||||
if hp<85 or mana<35 then r.resting=true end
|
if hp<85 or mana<35 then r.resting=true end
|
||||||
if hp>=95 and mana>=65 then r.resting=false end
|
if hp>=95 and mana>=65 then r.resting=false end
|
||||||
@@ -155,13 +160,13 @@ local function sample()
|
|||||||
end
|
end
|
||||||
elseif enabled and now-enabledAt<180 and g and not p:IsInCombat() and not p:IsCasting() then
|
elseif enabled and now-enabledAt<180 and g and not p:IsInCombat() and not p:IsCasting() then
|
||||||
local leader=ai:GetGroupLeader()
|
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)>12 and p:GetDistance(leader)<200 and hp>=95 and mana>=65 and now-(r.followAt or 0)>=8 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)>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:SetMaster(leader);ownedMasters[id]=leader:GetGUIDLow()
|
||||||
ai:ChangeStrategy('-stay',1)
|
ai:ChangeStrategy('-stay',1)
|
||||||
r.followAt=now;r.action='regroup requested';r.actionAt=now;r.accepted=ai:DoSpecificAction('follow',true)
|
r.followAt=now;r.action='regroup requested';r.actionAt=now;r.accepted=ai:DoSpecificAction('follow',true)
|
||||||
elseif leader==p and hp>=80 then
|
elseif leader==p and hp>=95 then
|
||||||
for _,m in pairs(peers) do
|
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
|
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
|
r.waitSince=r.waitSince or now
|
||||||
if now-r.waitSince<20 then p:MoveStop();r.action='waiting for companion';r.actionAt=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
|
elseif now-r.waitSince>60 then r.waitSince=nil end
|
||||||
|
|||||||
+3
-2
@@ -54,7 +54,7 @@ export function companionScore(a:Bot,b:Bot){
|
|||||||
}
|
}
|
||||||
const fresh=(b:Bot)=>b.status==='online'&&b.autonomy?.managed&&b.world&&Date.now()/1000-b.world.at<90&&!b.world.inCombat&&!b.world.humanControlled&&!b.world.taxi&&!b.world.casting;
|
const fresh=(b:Bot)=>b.status==='online'&&b.autonomy?.managed&&b.world&&Date.now()/1000-b.world.at<90&&!b.world.inCombat&&!b.world.humanControlled&&!b.world.taxi&&!b.world.casting;
|
||||||
export function partyDecision(a:Bot,b:Bot){
|
export function partyDecision(a:Bot,b:Bot){
|
||||||
if(!fresh(a)||!fresh(b)||a.id===b.id||a.world!.team!==b.world!.team||Math.abs(a.level-b.level)>10||!a.world!.nearby.includes(b.name)||!b.world!.nearby.includes(a.name))return {accepted:false,reason:'No compatible nearby pair'};
|
if(!fresh(a)||!fresh(b)||a.id===b.id||a.world!.team!==b.world!.team||Math.abs(a.level-b.level)>5||!a.world!.nearby.includes(b.name)||!b.world!.nearby.includes(a.name))return {accepted:false,reason:'No compatible nearby pair'};
|
||||||
const danger=recentDanger(a)||recentDanger(b);
|
const danger=recentDanger(a)||recentDanger(b);
|
||||||
const willing=(x:Bot,y:Bot)=>affinity(x,y)>=0&&(danger||x.personality.sociability>=35||affinity(x,y)>=5)&&x.personality.activities.some(v=>v==='quest'||v==='explore');
|
const willing=(x:Bot,y:Bot)=>affinity(x,y)>=0&&(danger||x.personality.sociability>=35||affinity(x,y)>=5)&&x.personality.activities.some(v=>v==='quest'||v==='explore');
|
||||||
return {accepted:willing(a,b)&&willing(b,a),reason:willing(a,b)&&willing(b,a)?danger?'Seeking safety together after recent danger; compatible faction, levels, and non-hostile companions.':'Both accepted: compatible levels, shared adventure interests, and no hostile bond.':'One character prefers to adventure alone or does not trust the other.'};
|
return {accepted:willing(a,b)&&willing(b,a),reason:willing(a,b)&&willing(b,a)?danger?'Seeking safety together after recent danger; compatible faction, levels, and non-hostile companions.':'Both accepted: compatible levels, shared adventure interests, and no hostile bond.':'One character prefers to adventure alone or does not trust the other.'};
|
||||||
@@ -105,7 +105,8 @@ export async function communityTick(store:StateStore,db:D1Database){
|
|||||||
for(const a of bots){
|
for(const a of bots){
|
||||||
if(busy.has(a.id))continue;
|
if(busy.has(a.id))continue;
|
||||||
// Leave only a bot-only party whose peers have moved away, never a human party.
|
// Leave only a bot-only party whose peers have moved away, never a human party.
|
||||||
const far=a.world!.groupId&&a.world!.groupNames.length>1&&a.world!.groupNames.filter(n=>n!==a.name).every(n=>!a.world!.nearby.includes(n));
|
const recoveringPeer=a.world!.groupNames.some(name=>s.bots.some(peer=>peer.name===name&&peer.status!=='dead'&&peer.world&&Date.now()/1000-peer.world.at<90&&(peer.world.healthPct??100)<=0));
|
||||||
|
const far=!recoveringPeer&&a.world!.groupId&&a.world!.groupNames.length>1&&a.world!.groupNames.filter(n=>n!==a.name).every(n=>!a.world!.nearby.includes(n));
|
||||||
if(c.partiesEnabled&&far){
|
if(c.partiesEnabled&&far){
|
||||||
await db.prepare('INSERT OR IGNORE INTO community_clock VALUES (?,?)').bind('apart:'+a.id,now).run();
|
await db.prepare('INSERT OR IGNORE INTO community_clock VALUES (?,?)').bind('apart:'+a.id,now).run();
|
||||||
const apart=await db.prepare('SELECT at FROM community_clock WHERE id=?').bind('apart:'+a.id).first<{at:number}>();
|
const apart=await db.prepare('SELECT at FROM community_clock WHERE id=?').bind('apart:'+a.id).first<{at:number}>();
|
||||||
|
|||||||
@@ -36,9 +36,14 @@ and only when fresh and at their class starting level.
|
|||||||
5. Recreate only the world container with `docker compose up -d --no-deps ac-worldserver`.
|
5. Recreate only the world container with `docker compose up -d --no-deps ac-worldserver`.
|
||||||
6. Restart the ALE relay to load priority delivery for permanent-death events.
|
6. Restart the ALE relay to load priority delivery for permanent-death events.
|
||||||
|
|
||||||
The guard is enabled by installing its Lua file, independently of the dashboard
|
The installed guard reads `acore_auth.azerothai_realm_rules.hardcore`, synchronized
|
||||||
hardcore toggle. Remove the guard and recreate the world before intentionally
|
from the dashboard setting by the operations agent (normally within one minute).
|
||||||
switching back to non-hardcore play. Do not erase the registry to recycle deaths.
|
When disabled, future deaths use the native playerbot resurrection behavior:
|
||||||
|
identities, memories, gear, and party membership survive. Dead active residents
|
||||||
|
still occupy their faction slots, so a recoverable death cannot spawn a replacement
|
||||||
|
or bypass the 50-bot cap. Historical permanent retirements remain retired.
|
||||||
|
For an immediate mode switch, update the dashboard first and synchronize the
|
||||||
|
native rule before declaring the transition complete. Do not erase the registry.
|
||||||
|
|
||||||
## Verification and recovery
|
## Verification and recovery
|
||||||
|
|
||||||
|
|||||||
@@ -55,3 +55,20 @@ before any model call. Model instructions explicitly prioritize survival over
|
|||||||
quests, loot and social goals. Stuck recovery yields to the native survival hold.
|
quests, loot and social goals. Stuck recovery yields to the native survival hold.
|
||||||
Mortality improvement must be measured over subsequent real playtime; these rules
|
Mortality improvement must be measured over subsequent real playtime; these rules
|
||||||
do not make bots invulnerable or resurrect dead characters.
|
do not make bots invulnerable or resurrect dead characters.
|
||||||
|
|
||||||
|
|
||||||
|
## Persistent companions and failed-encounter avoidance
|
||||||
|
|
||||||
|
New party invitations now require a level gap of at most five. Followers regroup
|
||||||
|
at eight yards; healthy leaders pause when a nearby companion is injured or more
|
||||||
|
than fifteen yards away (the existing bounded wait still prevents permanent idle).
|
||||||
|
A recently observed dead party member in normal resurrection mode does not trigger
|
||||||
|
the runtime's separation-based party departure. Human and unmanaged groups remain
|
||||||
|
protected.
|
||||||
|
|
||||||
|
Overpowered enemies, extra attackers, and rapid health loss mark that selected
|
||||||
|
enemy for ten minutes of avoidance. The guard stops renewed attacks and asks the
|
||||||
|
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.
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ try:
|
|||||||
state=json.load(urllib.request.urlopen(base+'/api/state',timeout=10));days=max(3,min(30,int(state.get('opsRetentionDays',7))))
|
state=json.load(urllib.request.urlopen(base+'/api/state',timeout=10));days=max(3,min(30,int(state.get('opsRetentionDays',7))))
|
||||||
env=os.environ.copy();env['MYSQL_PWD']=cfg['DOCKER_DB_ROOT_PASSWORD']
|
env=os.environ.copy();env['MYSQL_PWD']=cfg['DOCKER_DB_ROOT_PASSWORD']
|
||||||
def sql(q):return subprocess.check_output(['docker','exec','-e','MYSQL_PWD','ac-database','mysql','-uroot','-N','-B','-e',q],env=env,text=True)
|
def sql(q):return subprocess.check_output(['docker','exec','-e','MYSQL_PWD','ac-database','mysql','-uroot','-N','-B','-e',q],env=env,text=True)
|
||||||
|
# Share the selected mode with the native guard; retain historical retirements.
|
||||||
|
sql("CREATE TABLE IF NOT EXISTS acore_auth.azerothai_realm_rules (id TINYINT PRIMARY KEY,hardcore TINYINT NOT NULL)")
|
||||||
|
hardcore=1 if state.get('settings',{}).get('hardcore',{}).get('enabled',True) else 0
|
||||||
|
sql(f"INSERT INTO acore_auth.azerothai_realm_rules VALUES (1,{hardcore}) ON DUPLICATE KEY UPDATE hardcore=VALUES(hardcore)")
|
||||||
# Reconcile delayed verified deaths by the full native identity. This never
|
# Reconcile delayed verified deaths by the full native identity. This never
|
||||||
# revives an identity, changes a protected resident, or counts a human as a bot.
|
# revives an identity, changes a protected resident, or counts a human as a bot.
|
||||||
if state.get('settings',{}).get('hardcore',{}).get('enabled'):
|
if state.get('settings',{}).get('hardcore',{}).get('enabled'):
|
||||||
|
|||||||
@@ -48,3 +48,9 @@ test('companion ranking prefers complementary native roles and shared active que
|
|||||||
const {companionScore}=require('../.test-build/community.js');const a=bot('A'),b=bot('B'),c=bot('C');a.world.combatRole='tank';b.world.combatRole='healer';c.world.combatRole='damage';assert.ok(companionScore(a,b)>companionScore(a,c));
|
const {companionScore}=require('../.test-build/community.js');const a=bot('A'),b=bot('B'),c=bot('C');a.world.combatRole='tank';b.world.combatRole='healer';c.world.combatRole='damage';assert.ok(companionScore(a,b)>companionScore(a,c));
|
||||||
b.world.combatRole='damage';a.world.quests=[1,2];c.world.quests=[1,2];assert.ok(companionScore(a,c)>companionScore(a,b));
|
b.world.combatRole='damage';a.world.quests=[1,2];c.world.quests=[1,2];assert.ok(companionScore(a,c)>companionScore(a,b));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('adventuring companions stay within five levels',()=>{
|
||||||
|
const a=bot('A'),b=bot('B');b.level=a.level+6;
|
||||||
|
assert.equal(partyDecision(a,b).accepted,false);
|
||||||
|
b.level=a.level+5;assert.equal(partyDecision(a,b).accepted,true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -92,3 +92,15 @@ test('unverified bridge death fails safe without erasing identity',async()=>{
|
|||||||
await applyAuthoritativeAleEvent(store,'player.snapshot',{roster:true,observedAt:100,characterGuids:[123]});
|
await applyAuthoritativeAleEvent(store,'player.snapshot',{roster:true,observedAt:100,characterGuids:[123]});
|
||||||
assert.equal(store.state.bots.filter(b=>b.status==='online').length,0);
|
assert.equal(store.state.bots.filter(b=>b.status==='online').length,0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('normal resurrection mode preserves memories and identity across death and later snapshot',async()=>{
|
||||||
|
const store=new Store();store.state.settings.hardcore.enabled=false;
|
||||||
|
const bot=store.state.bots.find(b=>b.id==='bot-6');
|
||||||
|
bot.memories.push({id:'keep',text:'My companion',importance:8,source:'observation',at:new Date().toISOString()});
|
||||||
|
const payload={name:bot.name,isBot:true,characterGuid:99,accountId:2,healthPct:0};
|
||||||
|
await applyAuthoritativeAleEvent(store,'player.killed-by-creature',payload);
|
||||||
|
assert.notEqual(store.state.bots.find(b=>b.id===bot.id).status,'dead');
|
||||||
|
await applyAuthoritativeAleEvent(store,'player.snapshot',{...payload,healthPct:100});
|
||||||
|
const revived=store.state.bots.find(b=>b.id===bot.id);
|
||||||
|
assert.equal(revived.death,undefined);assert.ok(revived.memories.some(m=>m.id==='keep'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ function player(guid,name,account,level,dead)
|
|||||||
players[name]=p; return p
|
players[name]=p; return p
|
||||||
end
|
end
|
||||||
function AuthDBQuery(sql)
|
function AuthDBQuery(sql)
|
||||||
|
if string.find(sql,"azerothai_realm_rules",1,true) then return {GetUInt32=function()return mode or 1 end} end
|
||||||
if string.find(sql,'SELECT id FROM account',1,true) then
|
if string.find(sql,'SELECT id FROM account',1,true) then
|
||||||
if string.find(sql,'id=99 ',1,true) then return nil end
|
if string.find(sql,'id=99 ',1,true) then return nil end
|
||||||
return {GetUInt32=function() return 2 end}
|
return {GetUInt32=function() return 2 end}
|
||||||
end
|
end
|
||||||
|
if string.find(sql,"SELECT guid,name,account_id FROM azerothai_lifecycle WHERE state='dead'",1,true) then return nil end
|
||||||
local q={i=1}
|
local q={i=1}
|
||||||
function q:GetUInt32(n) return rows[self.i][n+1] end
|
function q:GetUInt32(n) return rows[self.i][n+1] end
|
||||||
function q:GetString(n) return rows[self.i][n+1] end
|
function q:GetString(n) return rows[self.i][n+1] end
|
||||||
@@ -131,3 +133,9 @@ assert(entries==2,'Death Knights must be audited at level 55')
|
|||||||
events[8](8,nil,players.HordeDK);tick();assert(not players.HordeDK and deaths==1)
|
events[8](8,nil,players.HordeDK);tick();assert(not players.HordeDK and deaths==1)
|
||||||
""")
|
""")
|
||||||
print('PASS: retirement, protection, level-1 entry, replay safety, and 25/25 faction balancing')
|
print('PASS: retirement, protection, level-1 entry, replay safety, and 25/25 faction balancing')
|
||||||
|
|
||||||
|
normal=LuaRuntime();normal.execute(setup)
|
||||||
|
normal.execute("mode=0;players={};rows={};for i=1,25 do rows[#rows+1]={100+i,'Resident'..i,2,0,'active',2};player(100+i,'Resident'..i,2,5,i==1) end;rows[#rows+1]={200,'Reserve',2,0,'reserve',2};player(200,'Reserve',2,1,false)")
|
||||||
|
normal.execute((Path(__file__).parents[1]/'ale/azerothai_lifecycle.lua').read_text())
|
||||||
|
normal.execute("events[8](8,nil,players.Resident1);tick();assert(players.Resident1 and deaths==0);assert(not players.Reserve,'Recoverable death must retain its population slot');local n=0;for _ in pairs(players)do n=n+1 end;assert(n==25)")
|
||||||
|
print('Normal resurrection mode preserves identity and reserves the dead resident population slot')
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ l=new();l.execute("group();AzerothAISetSurvivalEnabled(false);step(0);assert(#ca
|
|||||||
print('Native survival: retreat cooldown, recovery, regroup range, human-party protection, and disabled grouping passed')
|
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=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.combat=true;A.hp=100;A.enemy={GetGUID=function()return 999 end,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.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.friendly=true;A.selected={GetGUID=function()return 999 end,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);local found=false;for _,q in pairs(auditRows)do if string.find(q,'6F75742D6F662D636F6D626174') then found=true end end;assert(found)")
|
l=new();l.execute("A.combat=true;A.hp=40;step(0);A.x=10;step(2);A.combat=false;step(2);local found=false;for _,q in pairs(auditRows)do if string.find(q,'6F75742D6F662D636F6D626174') then found=true end end;assert(found)")
|
||||||
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("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();B.distance=150;step(0);assert(B.master==A)")
|
||||||
@@ -59,3 +59,5 @@ l=new();l.execute("A.combat=true;A.hp=75;A.attackers={1,2};step(0);assert(calls[
|
|||||||
l=new();l.execute("A.combat=true;step(0);A.hp=75;step(2);assert(calls[1]=='flee')")
|
l=new();l.execute("A.combat=true;step(0);A.hp=75;step(2);assert(calls[1]=='flee')")
|
||||||
l=new();l.execute("A.hp=80;step(0);A.hp=100;local query=AuthDBQuery;function AuthDBQuery(sql)if string.find(sql,'survival_holds WHERE guid=1',1,true) then return {GetUInt32=function(_,i)return ({[0]=1,[2]=11,[3]=1,[4]=0,[5]=0,[6]=0,[7]=0})[i] end,GetString=function()return 'A' end} end;return query(sql)end;step(30);assert(A.strategies.rpg==true);assert(not AzerothAISurvivalOverride(A))")
|
l=new();l.execute("A.hp=80;step(0);A.hp=100;local query=AuthDBQuery;function AuthDBQuery(sql)if string.find(sql,'survival_holds WHERE guid=1',1,true) then return {GetUInt32=function(_,i)return ({[0]=1,[2]=11,[3]=1,[4]=0,[5]=0,[6]=0,[7]=0})[i] end,GetString=function()return 'A' end} end;return query(sql)end;step(30);assert(A.strategies.rpg==true);assert(not AzerothAISurvivalOverride(A))")
|
||||||
print('Readiness holds, mana recovery, outnumbered/rapid-loss retreat, and original-strategy restoration passed')
|
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')
|
||||||
|
|||||||
Reference in New Issue
Block a user