Files
shenleiandClaude Opus 5.5 ffe097ec57 Add world boss window (AVA2 uitrack world-boss tab, read-only)
mt_boss lists the server's timed boss regens from the MTF1 "bosses"
query: alive/max, countdown, map-local position and period, paged by 8,
with per-boss spawn reminders that only fire on a fresh alive snapshot.
Teleport is left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:36:04 +09:00

118 lines
6.3 KiB
Python

import sys
import types
import unittest
from pathlib import Path
ROOT=Path(__file__).resolve().parents[2]
def script(name):return (ROOT/'src/platform/ScriptLib'/name).read_text().split('R"PY(',1)[1].rsplit(')PY";',1)[0]
class Widget(object):
def __init__(self,*a,**k):self.shown=False;self.text='';self.pos=(0,0);self.size=(0,0)
def __getattr__(self,name):return lambda *a,**k:None
def Show(self):self.shown=True
def Hide(self):self.shown=False
def IsShow(self):return self.shown
def SetText(self,text):self.text=text
def SetPosition(self,x,y):self.pos=(x,y)
def GetGlobalPosition(self):return self.pos
def SetSize(self,w,h):self.size=(w,h)
def GetWidth(self):return self.size[0]
def GetHeight(self):return self.size[1]
class Action(Widget):
def __init__(self,parent,x,y,width,text,event):
Widget.__init__(self);self.pos=(x,y);self.event=event;self.enabled=True;self.text=text;self.shown=True
def SetEnabled(self,enabled):self.enabled=enabled
class Service(object):
def __init__(self):self.requests=[]
def Request(self,kind,subject,offset,callback):self.requests.append((kind,subject,offset,callback));return len(self.requests)
def Reply(self,rows,time=1700000000,error=None):
callback=self.requests[-1][3]
callback(None if error else dict(kind='bosses',subject=0,offset=0,total=len(rows),mode=0,time=time,rows=rows),error)
TRENT=[2307,68,51200,-300,1,0,1,1700000100,9000]
SKIPIA=[1192,72,0,0,1,1,1,1700028800,28800]
DESERT=[2191,999,10000,20000,99,0,1,0,1800]
class BossTests(unittest.TestCase):
def setUp(self):
self.time=100.;self.chat=[]
mods={n:types.ModuleType(n) for n in ('app','chat','game','grp','nonplayer','ui','wndMgr','localeInfo','mt_feature_service','mt_biolog')}
mods['app'].GetTime=lambda:self.time
mods['chat'].CHAT_TYPE_INFO=1;mods['chat'].AppendChat=lambda t,m:self.chat.append(m)
mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None))
grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None
mods['nonplayer'].GetMonsterName=lambda vnum:{2307:'TreeKing',1192:'Skipia',2191:'Turtle'}.get(vnum,'')
mods['ui'].Window=Widget
mods['wndMgr'].GetScreenWidth=lambda:1280;mods['wndMgr'].GetScreenHeight=lambda:800
mods['localeInfo'].MINIMAP_ZONE_NAME_DICT={'metin2_map_trent02':'RedWood'}
self.service=Service();mods['mt_feature_service'].g_service=self.service
mods['mt_biolog'].Action=Action;mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:Widget();mods['mt_biolog'].L=lambda t:t
self.previous={n:sys.modules.get(n) for n in mods}
sys.modules.update(mods)
self.ns={'__name__':'mt_boss'};exec(script('BossScript.inc'),self.ns)
self.game=sys.modules['game'];self.gw=self.game.GameWindow()
self.gw.interface=types.SimpleNamespace(wndMiniMap=Widget())
def tearDown(self):
for name,value in self.previous.items():
if value is None:sys.modules.pop(name,None)
else:sys.modules[name]=value
def open(self):
self.game.GameWindow.Open(self.gw);w=self.ns['g_window'];w.Open();return w
def test_window_lists_server_rows(self):
w=self.open();self.assertEqual(self.ns['g_entry'].pos,(4,356))
self.assertEqual(self.service.requests[-1][:3],('bosses',0,0));self.assertFalse(w.refreshButton.enabled)
self.service.Reply([TRENT,SKIPIA,DESERT]);self.assertTrue(w.refreshButton.enabled)
self.assertEqual(w.lines[0].text,'[CH1] TreeKing RedWood')
self.assertEqual(w.details[0].text,'刷新倒计时 00:01:40 · 坐标 (512, -3) · 周期 02:30:00')
self.assertIn('存活 1/1',w.details[1].text);self.assertIn('随机位置',w.details[1].text)
self.assertEqual(w.lines[2].text,'[共享] Turtle 地图999');self.assertIn('不再刷新',w.details[2].text)
self.assertFalse(w.remindButtons[1].enabled) # alive
self.assertFalse(w.remindButtons[2].enabled) # no timer
self.assertFalse(w.lines[3].shown);self.assertIn('共 3 个',w.status.text)
self.time+=40;w.Render();self.assertIn('00:01:00',w.details[0].text) # counts down from the server time
def test_pages(self):
rows=[[2000+i,68,0,0,1,0,1,1700000100,600] for i in range(11)]
w=self.open();self.service.Reply(rows)
self.assertFalse(w.prevButton.enabled);self.assertTrue(w.nextButton.enabled)
w.nextButton.event();self.assertTrue(w.lines[2].shown);self.assertFalse(w.lines[3].shown);self.assertIn('第 2/2 页',w.status.text)
self.assertFalse(w.nextButton.enabled)
def test_reminder_fires_only_on_confirmed_spawn(self):
w=self.open();self.service.Reply([TRENT,SKIPIA]);w.remindButtons[0].event()
self.assertIn('开',w.remindButtons[0].text);w.Hide()
tick=self.ns['g_tick'];count=len(self.service.requests)
self.time+=50;tick.OnUpdate();self.assertEqual(len(self.service.requests),count) # not due yet
self.time+=60;tick.OnUpdate();self.assertEqual(len(self.service.requests),count+1)
self.service.Reply([TRENT,SKIPIA],time=1700000110);self.assertEqual(self.chat,[]) # timer passed, not spawned
self.assertIn(('2307',),[(str(k[0]),) for k in self.ns['g_tracker'].reminders])
spawned=list(TRENT);spawned[5]=1;spawned[7]=1700009110
self.time+=40;tick.OnUpdate();self.service.Reply([spawned,SKIPIA],time=1700000150)
self.assertEqual(self.chat,['TreeKing 已在 RedWood 刷新']);self.assertEqual(self.ns['g_tracker'].reminders,set())
def test_errors_and_close(self):
w=self.open();self.service.Reply([],error='unsupported');self.assertIn('未启用',w.status.text)
w.Query();self.service.Reply([]);self.assertIn('没有定时刷新',w.status.text)
w.Query();self.service.Reply([TRENT]);w.remindButtons[0].event()
self.game.GameWindow.Close(self.gw)
self.assertIsNone(self.ns['g_window']);self.assertIsNone(self.ns['g_entry'])
self.assertEqual(self.ns['g_tracker'].reminders,set());self.assertIsNone(self.ns['g_tracker'].rows)
def test_maps_match_server_index(self):
maps=self.ns['MAPS']
self.assertEqual((maps[68],maps[72],maps[302]),('metin2_map_trent02','metin2_map_skipia_dungeon_01','metin2_map_dawnmistwood'))
if __name__=='__main__':unittest.main()