Add feature queries, dungeon information and event calendar UI

This commit is contained in:
shen
2026-10-15 02:28:01 -07:00
parent 9d9993b007
commit 4e2d39b612
17 changed files with 1248 additions and 11 deletions
+173
View File
@@ -0,0 +1,173 @@
// PORT: list calendar and opt-in reminders, backed by the same schedule as real server drops.
static const char c_szCalendarScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import chat
import game
import grp
import time
import ui
import wndMgr
import mt_feature_service
from mt_biolog import Action, Text, L
g_window=None
g_tick=None
NAMES={1:'月光宝箱掉落',2:'圣诞袜掉落'}
def Stamp(value):return time.strftime('%m-%d %H:%M',time.gmtime(value))
class CalendarWindow(ui.Window):
def __init__(self):
ui.Window.__init__(self);self.SetWindowName('EventCalendarWindow');self.AddFlag('float');self.AddFlag('movable')
self.data=None;self.pending=False;self.generation=0;self.offset=0;self.rowPage=0;self.pageSize=4
self.nextQuery=0;self.received=0;self.lastTick=app.GetTime();self.stale=True;self.watch=None;self.dimensions=None
self.heading=Text(self,18,18,'活动日历 · UTC',0xffffd68b)
self.closeButton=Action(self,0,6,60,'关闭',self.Hide)
self.refreshButton=Action(self,18,52,80,'刷新',self.Query)
self.remindButton=Action(self,110,52,154,'活动提醒:关',self.Remind)
self.status=Text(self,18,110,'正在读取服务器排期')
self.rows=[Text(self,24,0,'',0xffefdfc3) for i in range(4)]
self.dates=[Text(self,24,0,'',0xffb2aa9a) for i in range(4)]
self.buttons=[Action(self,0,0,90,'提醒',lambda i=i:self.Watch(i)) for i in range(4)]
self.prev=Action(self,18,0,80,'上一页',lambda:self.Page(-1));self.next=Action(self,0,0,80,'下一页',lambda:self.Page(1));self.pages=Text(self,120,0)
self.note=Text(self,18,0,'活动沿用原掉落条件和概率;不保证每次击杀掉落。',0xffb2aa9a)
self.Reflow();self.Hide()
def Open(self):
self.Reflow();self.SetCenterPosition();self.Show();self.SetTop();self.Query()
def Query(self):
if self.pending:return
self.pending=True;self.generation+=1;generation=self.generation
self.status.SetText(L('正在同步服务器排期…'));self.refreshButton.SetEnabled(False)
def received(data,error):
if generation!=self.generation:return
self.pending=False;self.refreshButton.SetEnabled(True);self.nextQuery=app.GetTime()+15
if error:
self.data=None;self.stale=True
if error=='unsupported':self.watch=None
self.status.SetText(L('服务器尚未发布有效排期' if error in ('unsupported','schedule_unavailable') else '排期同步失败,请刷新'))
else:
self.data=data;self.received=app.GetTime();self.stale=False
self.status.SetText(L('服务器排期 / UTC;频道和阵营范围见各条目'))
self.Notify()
self.Refresh()
if mt_feature_service.g_service:mt_feature_service.g_service.Request('calendar',1,self.offset,received)
else:received(None,'unsupported')
def Refresh(self):
rows=self.data['rows'][self.rowPage*self.pageSize:(self.rowPage+1)*self.pageSize] if self.data is not None and not self.stale else []
for i,(label,button) in enumerate(zip(self.rows,self.buttons)):
if i>=len(rows):label.Hide();self.dates[i].Hide();button.Hide();continue
event=rows[i];identifier,kind,start,end,channel,empire,active=event
now=self.data['time']+max(0,app.GetTime()-self.received)
state='已结束' if now>=end else '未开始' if now<start else '进行中' if active else '本排期范围不适用'
scope=('全频道' if not channel else '频道%d'%channel)+' / '+('全阵营' if not empire else '阵营%d'%empire)
label.SetText(L('%s · %s %s'%(NAMES[kind],state,scope)));label.Show();button.Show()
self.dates[i].SetText('%s ~ %s UTC'%(Stamp(start),Stamp(end)));self.dates[i].Show()
button.SetText('取消提醒' if self.watch is not None and self.watch['id']==identifier else '提醒')
total=self.data['total'] if self.data else 0;absolute=self.offset+self.rowPage*self.pageSize
self.pages.SetText('%d / %d'%(absolute//self.pageSize+1,max(1,(total+self.pageSize-1)//self.pageSize)))
self.prev.SetEnabled(not self.pending and absolute>0);self.next.SetEnabled(not self.pending and absolute+self.pageSize<total)
self.remindButton.SetText('活动提醒:'+('开' if self.watch else '关'))
self.remindButton.SetEnabled(self.watch is not None)
if self.data is not None and not self.data['rows']:self.status.SetText(L('服务器已发布空排期,当前没有计划活动。'))
def Watch(self,index):
if self.data is None or self.stale:return
rows=self.data['rows'][self.rowPage*self.pageSize:(self.rowPage+1)*self.pageSize]
if index>=len(rows):return
e=rows[index]
if self.watch is not None and self.watch['id']==e[0]:self.watch=None
else:
self.watch=dict(id=e[0],kind=e[1],start=e[2],end=e[3],began=bool(e[6]),offset=self.offset)
self.status.SetText(L('%s:%s — %s UTC'%(NAMES[e[1]],Stamp(e[2]),Stamp(e[3]))))
self.Refresh()
def Remind(self):
self.watch=None;self.Refresh()
def Notify(self):
if self.watch is None:return
e=next((r for r in self.data['rows'] if r[0]==self.watch['id']),None)
if e is None:
# A reload can move an event to another page. Cancel rather than announce unconfirmed times.
if self.offset==self.watch['offset']:self.watch=None
return
w=self.watch;w.update(start=e[2],end=e[3],offset=self.offset)
now=self.data['time']
if now>=e[3]:
if w['began']:chat.AppendChat(chat.CHAT_TYPE_INFO,L(NAMES[e[1]]+'的计划时段已结束。'))
self.watch=None
elif e[6] and not w['began']:
chat.AppendChat(chat.CHAT_TYPE_INFO,L(NAMES[e[1]]+'已开始,沿用原怪物掉落规则。'));w['began']=True
def Page(self,step):
if self.pending:return
total=self.data['total'] if self.data else 0;target=self.offset+self.rowPage*self.pageSize+step*self.pageSize
if not 0<=target<total:return
offset=(target//8)*8;self.rowPage=(target%8)//self.pageSize
if offset!=self.offset:self.offset=offset;self.data=None;self.Query()
else:self.Refresh()
def Reflow(self):
dims=(wndMgr.GetScreenWidth(),wndMgr.GetScreenHeight())
if dims==self.dimensions:return
self.dimensions=dims;width=min(720,max(500,dims[0]-16));height=min(450,max(300,dims[1]-16));self.SetSize(width,height)
self.closeButton.SetPosition(width-76,6)
count=4 if height>=380 else 2
if count!=self.pageSize:self.pageSize=count;self.rowPage=0
spacing=min(54,max(32,(height-210)//self.pageSize))
for i,(row,button) in enumerate(zip(self.rows,self.buttons)):row.SetPosition(24,134+i*spacing);self.dates[i].SetPosition(24,150+i*spacing);button.SetPosition(width-112,124+i*spacing)
self.prev.SetPosition(18,height-74);self.next.SetPosition(width-98,height-74);self.pages.SetPosition(120,height-56);self.note.SetPosition(18,height-18)
self.SetCenterPosition()
def OnUpdate(self):
now=app.GetTime();gap=now-self.lastTick;self.lastTick=now
if gap>5 or gap<0:self.stale=True;self.nextQuery=0
if (self.IsShow() or self.watch is not None) and not self.pending and now>=self.nextQuery and (self.data is not None or self.watch is not None or self.nextQuery==0):
if not self.IsShow() and self.watch:self.offset=self.watch['offset'];self.rowPage=0
self.Query()
if self.IsShow():self.Reflow();self.Refresh()
def OnRender(self):
x,y=self.GetGlobalPosition();grp.SetColor(0xf51b1916);grp.RenderBar(x,y,self.GetWidth(),self.GetHeight());grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,self.GetWidth(),self.GetHeight())
def Hide(self):ui.Window.Hide(self)
def OnPressEscapeKey(self):self.Hide();return True
def Destroy(self):
self.generation+=1;self.watch=None;self.data=None;self.Hide()
for b in self.buttons+[self.closeButton,self.refreshButton,self.remindButton,self.prev,self.next]:b.event=None
class ReminderTick(ui.Window):
def __init__(self):
ui.Window.__init__(self);self.AddFlag('not_pick');self.SetSize(0,0);self.Show()
def OnUpdate(self):
if g_window and not g_window.IsShow():g_window.OnUpdate()
def Toggle():
if g_window:
if g_window.IsShow():g_window.Hide()
else:g_window.Open()
_open=game.GameWindow.Open
_close=game.GameWindow.Close
def Open(self):
global g_window,g_tick
_open(self);g_window=CalendarWindow();g_tick=ReminderTick()
def Close(self):
global g_window,g_tick
if g_tick:g_tick.Hide();g_tick=None
if g_window:g_window.Destroy();g_window=None
_close(self)
game.GameWindow.Open=Open
game.GameWindow.Close=Close
)PY";
+135
View File
@@ -0,0 +1,135 @@
// PORT: AVA2-style categorized character details, sourced from this server's effective point values.
static const char c_szDetailsScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import game
import grp
import ui
import wndMgr
import player
import mt_feature_service
from mt_biolog import Action, Text, L
from mt_wiki import B, U
g_window=None
STAT_LAYOUT=[
('基础','level','等级',''),('基础','hp','当前生命',''),('基础','maxhp','最大生命',''),('基础','sp','当前魔力',''),('基础','maxsp','最大魔力',''),
('基础','attack','物理攻击等级',''),('基础','defense','物理防御等级',''),('基础','magic_attack','魔法攻击等级',''),('基础','magic_defense','魔法防御等级',''),
('攻击','monster','对怪物加成','%'),('攻击','stone','对矿石加成','%'),('攻击','boss','对首领加成','%'),('攻击','human','对半人加成','%'),
('攻击','animal','对动物加成','%'),('攻击','orc','对兽人加成','%'),('攻击','milgyo','对密教加成','%'),('攻击','undead','对不死加成','%'),('攻击','devil','对恶魔加成','%'),
('攻击','warrior','对战士加成','%'),('攻击','assassin','对忍者加成','%'),('攻击','sura','对修罗加成','%'),('攻击','shaman','对萨满加成','%'),
('攻击','critical','暴击概率','%'),('攻击','penetrate','穿透概率','%'),('攻击','poison','中毒概率','%'),('攻击','stun','眩晕概率','%'),('攻击','slow','减速概率','%'),
('攻击','skill_damage','技能伤害加成','%'),('攻击','normal_damage','普通攻击加成','%'),('攻击','melee_magic','近战及魔法攻击加成','%'),
('抗性','block','格挡概率','%'),('抗性','dodge','闪避概率','%'),('抗性','resist_human','半人抗性','%'),
('抗性','resist_warrior','战士抗性','%'),('抗性','resist_assassin','忍者抗性','%'),('抗性','resist_sura','修罗抗性','%'),('抗性','resist_shaman','萨满抗性','%'),
('抗性','sword','单手剑抗性','%'),('抗性','twohand','双手剑抗性','%'),('抗性','dagger','匕首抗性','%'),('抗性','bell','铃铛抗性','%'),('抗性','fan','扇子抗性','%'),('抗性','bow','弓箭抗性','%'),
('抗性','magic','魔法抗性','%'),('抗性','fire','火焰抗性','%'),('抗性','electric','雷电抗性','%'),('抗性','wind','风抗性','%'),('抗性','poison_reduce','中毒伤害减免','%'),
('抗性','skill_defense','技能伤害减免','%'),('抗性','normal_defense','普通攻击伤害减免','%'),('抗性','anti_critical','暴击抵抗','%'),('抗性','anti_penetrate','穿透抵抗','%'),
('速度恢复','attack_speed','攻击速度',''),('速度恢复','cast_speed','施法速度',''),('速度恢复','move_speed','移动速度',''),
('速度恢复','hp_regen','生命恢复加成','%'),('速度恢复','sp_regen','魔力恢复加成','%'),('速度恢复','steal_hp','吸取生命','%'),('速度恢复','steal_sp','吸取魔力','%'),
('速度恢复','exp_bonus','角色经验加成','%'),('速度恢复','gold_bonus','角色金币加成','%'),('速度恢复','item_bonus','角色掉落加成','%')]
GROUPS=('全部','基础','攻击','抗性','速度恢复')
class DetailsWindow(ui.Window):
def __init__(self):
ui.Window.__init__(self);self.SetWindowName('CharacterDetailsWindow');self.AddFlag('float');self.AddFlag('movable')
self.values=None;self.page=0;self.group=0;self.generation=0;self.pending=False;self.nextQuery=0;self.dimensions=None
self.heading=Text(self,18,18,'角色详细属性',0xffffd68b)
self.closeButton=Action(self,0,6,60,'关闭',self.Hide)
self.groupButton=Action(self,18,52,96,'全部',self.NextGroup)
self.inputBox=ui.SlotBar();self.inputBox.SetParent(self);self.inputBox.SetPosition(124,60);self.inputBox.SetSize(210,28);self.inputBox.Show()
self.search=ui.EditLine();self.search.SetParent(self.inputBox);self.search.SetPosition(6,5);self.search.SetSize(198,20);self.search.SetMax(60);self.search.SetReturnEvent(self.Search);self.search.SetEscapeEvent(self.Hide);self.search.Show()
self.searchButton=Action(self,344,52,64,'筛选',self.Search)
self.refreshButton=Action(self,418,52,64,'刷新',self.Query)
self.status=Text(self,18,106,'尚未同步角色属性')
self.rows=[Text(self,24,0,'',0xffefdfc3) for i in range(8)]
self.prev=Action(self,18,0,80,'上一页',lambda:self.Page(-1));self.next=Action(self,0,0,80,'下一页',lambda:self.Page(1));self.pages=Text(self,120,0)
self.note=Text(self,18,0,'研究对怪加成不计入首领;活动倍率另行显示。',0xffb2aa9a)
self.Reflow();self.Hide()
def Open(self):
self.Reflow();self.SetCenterPosition();self.Show();self.SetTop();self.Query()
def Query(self):
if self.pending:return
self.pending=True;self.generation+=1;generation=self.generation
self.status.SetText(L('正在同步服务器属性…'));self.refreshButton.SetEnabled(False)
def received(data,error):
if generation!=self.generation:return
self.pending=False;self.nextQuery=app.GetTime()+3;self.refreshButton.SetEnabled(True)
if error:
self.values=None;self.status.SetText(L('服务端尚未启用详细属性' if error=='unsupported' else '属性同步失败,请稍后刷新'))
else:
self.values=dict(data['rows']);self.status.SetText(L('服务器有效属性;不等同于对所有目标的最终伤害'))
self.Refresh()
if mt_feature_service.g_service:mt_feature_service.g_service.Request('stats',0,0,received)
else:received(None,'unsupported')
def Search(self):
self.page=0;self.Refresh()
def NextGroup(self):
self.group=(self.group+1)%len(GROUPS);self.page=0;self.groupButton.SetText(GROUPS[self.group]);self.Refresh()
def Page(self,step):
self.page+=step;self.Refresh()
def Refresh(self):
for r in self.rows:r.Hide()
query=player.FeatureLocaleToUTF8(self.search.GetText()).decode('utf-8').lower()
visible=[r for r in STAT_LAYOUT if (self.group==0 or r[0]==GROUPS[self.group]) and (not query or query in U(r[2]).lower())]
pages=max(1,(len(visible)+self.rowCount-1)//self.rowCount);self.page=max(0,min(self.page,pages-1))
for i,(_,key,label,unit) in enumerate(visible[self.page*self.rowCount:(self.page+1)*self.rowCount]):
value=(str(self.values[key])+unit) if self.values is not None and key in self.values else '—'
self.rows[i].SetText(L(B(U(label)+u': '+U(value))));self.rows[i].Show()
self.pages.SetText('%d / %d'%(self.page+1,pages));self.prev.SetEnabled(self.page>0);self.next.SetEnabled(self.page+1<pages)
def Reflow(self):
dims=(wndMgr.GetScreenWidth(),wndMgr.GetScreenHeight())
if dims==self.dimensions:return
self.dimensions=dims;width=min(600,max(500,dims[0]-16));height=min(480,max(300,dims[1]-16));self.SetSize(width,height)
self.rowCount=min(8,max(2,(height-204)//32));self.closeButton.SetPosition(width-76,6)
for i,r in enumerate(self.rows):r.SetPosition(24,136+32*i)
self.prev.SetPosition(18,height-56);self.next.SetPosition(width-98,height-56);self.pages.SetPosition(120,height-38);self.note.SetPosition(18,height-80)
self.SetCenterPosition();self.Refresh()
def OnUpdate(self):
self.Reflow()
if self.IsShow() and not self.pending and self.values is not None and app.GetTime()>=self.nextQuery:self.Query()
def OnRender(self):
x,y=self.GetGlobalPosition();grp.SetColor(0xf51b1916);grp.RenderBar(x,y,self.GetWidth(),self.GetHeight());grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,self.GetWidth(),self.GetHeight())
def Hide(self):
if hasattr(self,'search'):self.search.KillFocus()
if hasattr(self,'generation'):self.generation+=1;self.pending=False
ui.Window.Hide(self)
def OnPressEscapeKey(self):self.Hide();return True
def Destroy(self):
self.Hide();self.values=None;self.search.SetReturnEvent(None);self.search.SetEscapeEvent(None)
for b in (self.closeButton,self.groupButton,self.searchButton,self.refreshButton,self.prev,self.next):b.event=None
def Toggle():
if g_window:
if g_window.IsShow():g_window.Hide()
else:g_window.Open()
_open=game.GameWindow.Open
_close=game.GameWindow.Close
def Open(self):
global g_window
_open(self);g_window=DetailsWindow()
def Close(self):
global g_window
if g_window:g_window.Destroy();g_window=None
_close(self)
game.GameWindow.Open=Open
game.GameWindow.Close=Close
)PY";
+183
View File
@@ -0,0 +1,183 @@
// PORT: dungeon information from installed quests; original NPC admission remains authoritative.
static const char c_szDungeonScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import chat
import game
import grp
import ui
import wndMgr
import mt_feature_service
import mt_wiki
from mt_wiki import B
from mt_biolog import Action, Text, L
g_window=None
def Duration(seconds):
seconds=max(0,int(seconds))
return '%d天 %02d:%02d:%02d'%(seconds//86400,(seconds//3600)%24,(seconds//60)%60,seconds%60) if seconds>=86400 else '%02d:%02d:%02d'%(seconds//3600,(seconds//60)%60,seconds%60)
class DungeonWindow(ui.Window):
def __init__(self):
ui.Window.__init__(self);self.SetWindowName('DungeonInformationWindow');self.AddFlag('float');self.AddFlag('movable')
self.values=None;self.pending=False;self.generation=0;self.received=0;self.serverTime=0;self.nextQuery=0
self.lastTick=app.GetTime();self.reminder=None;self.tab=0;self.dimensions=None;self.stale=True
self.heading=Text(self,18,18,'副本信息',0xffffd68b)
self.closeButton=Action(self,0,6,60,'关闭',self.Hide)
self.lairButton=Action(self,18,52,150,'龙穴',lambda:self.Select(0))
self.weeklyButton=Action(self,180,52,150,'龙穴周任务',lambda:self.Select(1))
self.refreshButton=Action(self,342,52,80,'刷新',self.Query)
self.status=Text(self,18,108,'正在读取任务状态')
self.rows=[Text(self,24,142+32*i,'',0xffefdfc3) for i in range(6)]
self.remindButton=Action(self,18,0,146,'到期提醒:关',self.Remind)
self.entryButton=Action(self,180,0,146,'查看入口',self.Entry)
self.note=Text(self,18,0,'当前仅接入已核实的龙穴及相关周任务。',0xffb2aa9a)
self.Reflow();self.Hide()
def Open(self):
if mt_wiki.g_window:mt_wiki.g_window.Load()
self.Reflow();self.SetCenterPosition();self.Show();self.SetTop();self.Query()
def Select(self,tab):
self.tab=tab;self.RenderRows()
def Query(self):
if self.pending:return
self.pending=True;self.generation+=1;generation=self.generation
self.status.SetText(L('正在同步服务器任务…'));self.refreshButton.SetEnabled(False)
def received(data,error):
if generation!=self.generation:return
self.pending=False;self.refreshButton.SetEnabled(True);self.nextQuery=app.GetTime()+15
if error:
self.stale=True;self.values=None
if error=='unsupported':self.reminder=None
self.status.SetText(L('服务端未启用此副本查询' if error=='unsupported' else '状态未同步,请稍后刷新'))
else:
self.values=dict(data['rows']);self.received=app.GetTime();self.serverTime=data['time'];self.stale=False
self.status.SetText(L('已同步任务状态;入场仍由原 NPC 检查'))
self.CheckReminder()
self.RenderRows()
if mt_feature_service.g_service:mt_feature_service.g_service.Request('dungeons',0,0,received)
else:received(None,'unsupported')
def Now(self):
return self.serverTime+max(0,app.GetTime()-self.received)
def RenderRows(self):
lines=[];v=self.values
if v is not None and not self.stale:
now=self.Now()
if self.tab==0:
if now<=v['lair_join_end']:state='队伍加入窗口(需要口令)'
elif now<=v['lair_end']:state='场次占用中'
else:state='可向入口 NPC 申请新场次'
catalogue=mt_wiki.g_window.catalogue if mt_wiki.g_window else None
ticket=catalogue.FindItem(v['lair_ticket']) if catalogue else None
name=B(ticket['name']) if ticket else '门票'
lines=['状态:'+state,'新场次等待:'+Duration(v['lair_end']+1-now),
'加入窗口:'+Duration(v['lair_join_end']+1-now),
'%s:需要 %d,持有 %d'%(name,v['lair_tickets'],v['lair_owned']),
'场次为全服共享;不是个人通关冷却。','入场口令、门票及 NPC 锁由原任务校验。']
elif 'weekly_end' in v:
lines=['个人周任务等待:'+Duration(v['weekly_end']+1-now),
'当前任务:'+('收集中' if v.get('weekly_active') else '未在收集阶段'),
'收集剩余:'+(Duration(v['weekly_hunt_end']-now) if v.get('weekly_active') else '—'),
'周任务与龙穴入场计时独立。','接取时开始计时,完成后不会重置冷却。','资格、收集材料和奖励由原任务检查。']
else:lines=['服务端未安装或未提供周任务查询。']
for i,row in enumerate(self.rows):
if i<len(lines):row.SetText(L(lines[i]));row.Show()
else:row.Hide()
self.remindButton.SetText('到期提醒:'+('开' if self.reminder is not None and self.reminder[0]==self.tab else '关'))
self.remindButton.SetEnabled(v is not None and not self.stale and (self.tab==0 or 'weekly_end' in v))
self.entryButton.SetEnabled(v is not None and not self.stale and (self.tab==0 or 'weekly_npc' in v))
def Entry(self):
if self.values is None or self.stale:return
v=self.values
text='请前往流放者洞窟深处,与修罗之魂对话进入龙穴。' if self.tab==0 else '请前往流放者洞窟二层,与猛将之魂对话接取周任务。'
self.status.SetText(L(text))
def Remind(self):
if self.values is None or self.stale:return
if self.reminder is not None and self.reminder[0]==self.tab:self.reminder=None
else:
end=self.values.get('lair_end' if self.tab==0 else 'weekly_end',0)
if end<self.Now():return
self.reminder=(self.tab,end)
self.RenderRows()
def CheckReminder(self):
if self.reminder is None or self.values is None or self.stale:return
tab,old=self.reminder;key='lair_end' if tab==0 else 'weekly_end'
if key not in self.values:self.reminder=None;return
end=self.values[key]
# A later run/quest extends the authoritative deadline; never announce the old one.
self.reminder=(tab,end)
if self.Now()>end:
chat.AppendChat(chat.CHAT_TYPE_INFO,L('龙穴新场次等待已结束,请向入口申请。' if tab==0 else '龙穴周任务等待已结束,请向 NPC 确认资格。'))
self.reminder=None
def Reflow(self):
dims=(wndMgr.GetScreenWidth(),wndMgr.GetScreenHeight())
if dims==self.dimensions:return
self.dimensions=dims;width=min(660,max(500,dims[0]-16));height=min(450,max(300,dims[1]-16));self.SetSize(width,height)
self.closeButton.SetPosition(width-76,6)
spacing=min(32,max(16,(height-250)//5))
for i,row in enumerate(self.rows):row.SetPosition(24,132+i*spacing)
self.remindButton.SetPosition(18,height-74);self.entryButton.SetPosition(180,height-74);self.note.SetPosition(18,height-18)
self.SetCenterPosition()
def OnUpdate(self):
now=app.GetTime();gap=now-self.lastTick;self.lastTick=now
if gap>5 or gap<0:self.stale=True;self.nextQuery=0
active=self.IsShow() or self.reminder is not None
if active and not self.pending and now>=self.nextQuery and (self.values is not None or self.reminder is not None or self.stale and self.nextQuery==0):self.Query()
if self.IsShow():self.Reflow();self.RenderRows()
# Reminders only fire after a fresh server query confirms expiry.
if self.reminder is not None and not self.pending and self.values is not None and self.Now()>self.reminder[1] and now>=self.nextQuery:self.Query()
def OnRender(self):
x,y=self.GetGlobalPosition();grp.SetColor(0xf51b1916);grp.RenderBar(x,y,self.GetWidth(),self.GetHeight());grp.SetColor(0xff9b7b48);grp.RenderBox(x,y,self.GetWidth(),self.GetHeight())
def Hide(self):
ui.Window.Hide(self)
def OnPressEscapeKey(self):self.Hide();return True
def Destroy(self):
self.generation+=1;self.reminder=None;self.values=None;self.Hide()
for b in (self.closeButton,self.lairButton,self.weeklyButton,self.refreshButton,self.remindButton,self.entryButton):b.event=None
class ReminderTick(ui.Window):
def __init__(self):
ui.Window.__init__(self);self.AddFlag('not_pick');self.SetSize(0,0);self.Show()
def OnUpdate(self):
if g_window and not g_window.IsShow():g_window.OnUpdate()
def Toggle():
if g_window:
if g_window.IsShow():g_window.Hide()
else:g_window.Open()
_open=game.GameWindow.Open
_close=game.GameWindow.Close
g_tick=None
def Open(self):
global g_window,g_tick
_open(self);g_window=DungeonWindow();g_tick=ReminderTick()
def Close(self):
global g_window,g_tick
if g_tick:g_tick.Hide();g_tick=None
if g_window:g_window.Destroy();g_window=None
_close(self)
game.GameWindow.Open=Open
game.GameWindow.Close=Close
)PY";
@@ -0,0 +1,157 @@
// PORT read-only feature protocol. Requests are serialized, bounded and tied to this game session.
static const char c_szFeatureServiceScript[] = R"PY(
import app
import game
import net
import ui
from mt_wiki_data import Number
BITS={'mob':1,'chest':2,'stats':4,'sources':8,'dungeons':16,'calendar':32}
g_service=None
g_tick=None
_serial=0
def Signed(text):
value=int(text)
if len(text)>20 or not -(1<<63)<=value<(1<<63):raise ValueError('invalid stat value')
return value
class Service(object):
def __init__(self):
self.queue=[];self.pending=None;self.mask=None;self.nextSend=app.GetTime()+0.5
self.deadline=0;self.closed=False
def Request(self,kind,subject,offset,callback):
global _serial
if kind not in BITS or not 0<=subject<=4294967295 or not 0<=offset<=4096 or len(self.queue)>=8:
callback(None,'invalid');return 0
if (kind in ('stats','dungeons') and (subject or offset)) or (kind=='calendar' and subject!=1):
callback(None,'invalid');return 0
_serial=(_serial%2147483647)+1
self.queue.append(dict(id=_serial,kind=kind,subject=subject,offset=offset,callback=callback,retries=0))
return _serial
def Tick(self):
global _serial
now=app.GetTime()
if self.closed:return
if self.pending and now>self.deadline:
if self.pending['kind']=='cap':
self.mask=0;self.pending=None
else:self.Finish(None,'timeout')
self.nextSend=now+0.3
if self.pending or not self.queue or now<self.nextSend:return
if self.mask is None:
_serial=(_serial%2147483647)+1
self.pending=dict(id=_serial,kind='cap',subject=0,offset=0,callback=None,retries=0)
net.SendChatPacket('/mt_features 1 %d cap'%_serial)
else:
self.pending=self.queue.pop(0)
p=self.pending
if not self.mask&BITS[p['kind']]:
self.Finish(None,'unsupported');return
text='/mt_features 1 %d %s'%(p['id'],p['kind'])
if p['kind'] not in ('stats','dungeons'):text+=' %d %d'%(p['subject'],p['offset'])
net.SendChatPacket(text)
self.deadline=now+4.0;self.nextSend=now+0.3
def Finish(self,result,error=None):
pending=self.pending;self.pending=None
if pending and pending.get('callback') and not self.closed:pending['callback'](result,error)
def Receive(self,line):
if not line.startswith('MTF1 '):return False
p=self.pending
try:
w=line.split()
if len(w)<3 or len(line)>512:return True
request=Number(w[2],2147483647)
if not p or p['id']!=request:return True
op=w[1]
if op=='cap' and len(w)==6 and p['kind']=='cap':
if Number(w[3])!=1:raise ValueError('unsupported protocol')
self.mask=Number(w[4],4294967295);Number(w[5]);self.pending=None
elif op=='error' and len(w)==4:
if w[3]=='rate' and p['kind']!='cap' and p['retries']<2:
p['retries']+=1;self.queue.insert(0,p);self.pending=None;self.nextSend=app.GetTime()+0.4
else:self.Finish(None,w[3])
elif op=='begin' and len(w)==9 and p['kind']!='cap':
if 'result' in p or w[3]!=p['kind'] or Number(w[4])!=p['subject'] or Number(w[5])!=p['offset']:
raise ValueError('mismatched response')
total=Number(w[6],128 if p['kind'] in ('stats','dungeons') else 256 if p['kind']=='calendar' else 4096)
if total<p['offset']:raise ValueError('invalid page')
p['result']=dict(kind=w[3],subject=p['subject'],offset=p['offset'],total=total,mode=Number(w[7],11),
time=Number(w[8]),rows=[])
elif op in ('row','stat','event') and 'result' in p:
result=p['result'];rows=result['rows']
limit=result['total'] if p['kind'] in ('stats','dungeons') else min(8,result['total']-p['offset'])
if len(rows)>=limit or Number(w[3])!=p['offset']+len(rows):raise ValueError('invalid row order')
if op=='row' and len(w)==12 and p['kind'] not in ('stats','dungeons','calendar'):
values=[Number(v) for v in w[4:]]
if values[2] not in (0,1,2,3,4,5,10,11,20,21) or not values[0]:raise ValueError('invalid row')
rows.append(values)
elif op=='event' and len(w)==11 and p['kind']=='calendar':
values=[Number(v) for v in w[4:]]
if not values[0] or values[1] not in (1,2) or values[2]>=values[3] or values[4]>255 or values[5]>3 or values[6]>1:raise ValueError('invalid calendar event')
if any(r[0]==values[0] for r in rows):raise ValueError('duplicate event')
rows.append(values)
elif op=='stat' and len(w)==6 and p['kind'] in ('stats','dungeons'):
if not w[4].replace('_','').isalpha() or len(w[4])>32 or any(r[0]==w[4] for r in rows):raise ValueError('invalid stat')
rows.append((w[4],Signed(w[5])))
else:raise ValueError('invalid row format')
elif op=='end' and len(w)==4 and 'result' in p:
result=p['result'];expected=result['total'] if p['kind'] in ('stats','dungeons') else min(8,result['total']-p['offset'])
if Number(w[3])!=expected or len(result['rows'])!=expected:raise ValueError('incomplete reply')
if p['kind']=='dungeons':
values=dict(result['rows'])
required=('lair_end','lair_join_end','lair_phase','lair_ticket','lair_tickets','lair_boss','lair_owned','lair_alive','lair_npc','lair_map')
if any(k not in values or values[k]<0 for k in required):raise ValueError('incomplete dungeon snapshot')
if values['lair_phase']>2 or not values['lair_ticket'] or values['lair_join_end']>values['lair_end']:raise ValueError('invalid dungeon state')
weekly=('weekly_end','weekly_hunt_end','weekly_active','weekly_npc','weekly_map')
if any(k in values for k in weekly) and any(k not in values or values[k]<0 for k in weekly):raise ValueError('incomplete weekly state')
if values.get('weekly_active',0)>1:raise ValueError('invalid weekly state')
self.Finish(result)
else:raise ValueError('unknown response')
except (ValueError,TypeError,IndexError,OverflowError):
if p and self.pending is p:
if p['kind']=='cap':self.mask=0;self.pending=None
else:self.Finish(None,'bad_reply')
return True
def Close(self):
self.closed=True;self.pending=None;self.queue=[]
class TickWindow(ui.Window):
def __init__(self):
ui.Window.__init__(self);self.AddFlag('not_pick');self.SetSize(0,0);self.Show()
def OnUpdate(self):
if g_service:g_service.Tick()
_open=game.GameWindow.Open
_close=game.GameWindow.Close
_command=game.GameWindow.BINARY_ServerCommand_Run
def Open(self):
global g_service,g_tick
_open(self);g_service=Service();g_tick=TickWindow()
def Close(self):
global g_service,g_tick
if g_service:g_service.Close();g_service=None
if g_tick:g_tick.Hide();g_tick=None
_close(self)
def Command(self,line):
if line.startswith('MTF1 '):
if g_service:g_service.Receive(line)
return 1
return _command(self,line)
game.GameWindow.Open=Open
game.GameWindow.Close=Close
game.GameWindow.BINARY_ServerCommand_Run=Command
)PY";
+10
View File
@@ -993,7 +993,12 @@ namespace PythonBoot
#include "BiologScript.inc"
#include "FeatureBootstrapScript.inc"
#include "WikiDataScript.inc"
#include "FeatureServiceScript.inc"
#include "WikiScript.inc"
#include "DetailsScript.inc"
#include "DungeonScript.inc"
#include "CalendarScript.inc"
#include "WikiEntryScript.inc"
static void install_script_module(const char* c_szModule, const char* c_szScript)
{
@@ -1061,7 +1066,12 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
install_script_module("mt_features", c_szFeatureBootstrapScript);
install_script_module("mt_biolog", c_szBiologScript);
install_script_module("mt_wiki_data", c_szWikiDataScript);
install_script_module("mt_feature_service", c_szFeatureServiceScript);
install_script_module("mt_wiki", c_szWikiScript);
install_script_module("mt_details", c_szDetailsScript);
install_script_module("mt_dungeons", c_szDungeonScript);
install_script_module("mt_calendar", c_szCalendarScript);
install_script_module("mt_wiki_entries", c_szWikiEntryScript);
if (g_touch_input)
install_script_module("mt_mobile_hud", c_szMobileHUDScript);
}
+111
View File
@@ -0,0 +1,111 @@
// PORT query entry points: enemy target, and a pinned touch inventory tooltip.
static const char c_szWikiEntryScript[] = R"PY(# -*- coding: utf-8 -*-
import sys
import chr
import player
import uitarget
import uiToolTip
import uiInventory
import weakref
import wndMgr
from mt_biolog import Action
import mt_wiki
_pinned=None
def OpenMob(vid):
race=chr.GetVirtualNumber(vid)
wiki=mt_wiki.g_window
if not wiki:return
wiki.Open()
row=wiki.catalogue.mobs.get(race) if wiki.catalogue else None
if row:
wiki.Select('mobs',row);wiki.QueryRemote('mob',race)
_target=uitarget.TargetBoard.SetTargetVID
_enemy=uitarget.TargetBoard.SetEnemyVID
_hp=uitarget.TargetBoard.SetHP
def Target(self,vid):
_target(self,vid)
if hasattr(self,'wikiDropButton'):self.wikiDropButton.Hide()
def Enemy(self,vid):
_enemy(self,vid)
if not hasattr(self,'wikiDropButton'):
owner=weakref.proxy(self)
self.wikiDropButton=Action(self,0,0,72,'掉落',lambda:OpenMob(owner.GetTargetVID()))
PositionTarget(self)
self.wikiDropButton.Show()
def PositionTarget(self):
if getattr(self,'wikiDropButton',None):
x,y=self.GetGlobalPosition()
self.wikiDropButton.SetPosition(min(self.GetWidth()+4,max(0,wndMgr.GetScreenWidth()-x-76)),0)
def HP(self,value):
_hp(self,value);PositionTarget(self)
_clear=uiToolTip.ItemToolTip.ClearToolTip
_inventory=uiToolTip.ItemToolTip.SetInventoryItem
_out=uiInventory.InventoryWindow.OverOutItem
def Clear(self):
if getattr(self,'wikiItemButton',None):
self.wikiItemButton.event=None;self.wikiItemButton.Hide();self.wikiItemButton=None
if getattr(self,'featurePinned',False):self.SetFollow(True)
self.featurePinned=False
_clear(self)
def Inventory(self,slot,window_type=player.INVENTORY):
global _pinned
_inventory(self,slot,window_type)
mobile=sys.modules.get('mt_mobile_inventory')
if not mobile or not mobile.tap:return
vnum=player.GetItemIndex(window_type,slot)
if not vnum:return
owner=weakref.proxy(self)
def open_item():
if player.GetItemIndex(window_type,slot)!=vnum:return
owner.HideToolTip();mt_wiki.OpenItem(vnum)
wiki=mt_wiki.g_window
if wiki and wiki.catalogue:
row=wiki.catalogue.FindItem(vnum)
if row and row['kind'] in ('ITEM_GIFTBOX','ITEM_TREASURE_BOX'):wiki.QueryRemote('chest',vnum)
y=self.toolTipHeight
self.wikiItemButton=Action(self,8,y,max(80,self.GetWidth()-16),'百科 / 宝箱预览',open_item)
self.AppendSpace(52);self.SetFollow(False);self.featurePinned=True;_pinned=weakref.proxy(self)
x,y=self.GetGlobalPosition()
self.SetPosition(max(0,min(x,wndMgr.GetScreenWidth()-self.GetWidth())),max(0,min(y,wndMgr.GetScreenHeight()-self.GetHeight())))
def Out(self):
if self.tooltipItem and getattr(self.tooltipItem,'featurePinned',False):return
_out(self)
_mobile=sys.modules.get('mt_mobile_inventory')
if _mobile:
_drag=_mobile.SetDrag
def Drag(active):
if active and _pinned is not None:
try:_pinned.HideToolTip()
except ReferenceError:pass
_drag(active)
_mobile.SetDrag=Drag
uitarget.TargetBoard.SetTargetVID=Target
uitarget.TargetBoard.SetEnemyVID=Enemy
uitarget.TargetBoard.SetHP=HP
uiToolTip.ItemToolTip.ClearToolTip=Clear
uiToolTip.ItemToolTip.SetInventoryItem=Inventory
uiInventory.InventoryWindow.OverOutItem=Out
)PY";
+108 -9
View File
@@ -1,5 +1,5 @@
// PORT: AVA2-inspired encyclopedia, adapted to current server templates and touch controls.
// Gameplay/loot is never computed here. The schema explicitly advertises static data only.
// Static templates plus negotiated read-only server views; no gameplay mutation or loot RNG here.
static const char c_szWikiScript[] = R"PY(# -*- coding: utf-8 -*-
import app
import game
@@ -10,13 +10,22 @@ import player
import ui
import wndMgr
import weakref
import mt_feature_service
from mt_biolog import Action, Text, L
from mt_wiki_data import Catalogue
PATH='wiki/catalogue.txt'
g_window=None
g_entry=None
CAPABILITIES={'schema':1,'static_catalogue':True,'live_drops':False,'chest_preview':False,'model_preview':False}
def ServerCapabilities():
mask=mt_feature_service.g_service.mask if mt_feature_service.g_service else None
return {'schema':1,'static_catalogue':True,'model_preview':False,
'live_drops':None if mask is None else bool(mask&1),
'chest_preview':None if mask is None else bool(mask&2),
'attributes':None if mask is None else bool(mask&4),
'sources':None if mask is None else bool(mask&8),
'dungeons':None if mask is None else bool(mask&16),
'calendar':None if mask is None else bool(mask&32)}
def U(text):
@@ -46,7 +55,11 @@ class WikiWindow(ui.Window):
self.mode='items';self.category=0;self.page=0;self.detailPage=0
self.catalogue=None;self.error='';self.selected=None;self.results=[];self.links=[]
self.dimensions=None;self.lastQuery=None;self.history=[]
self.remote=None;self.remoteData=None;self.remoteError=None;self.remoteSerial=0
self.heading=Text(self,18,18,'游戏百科',0xffffd68b)
self.detailsButton=Action(self,130,6,120,'详细属性',self.OpenDetails)
self.dungeonButton=Action(self,258,6,96,'副本信息',self.OpenDungeons)
self.calendarButton=Action(self,362,6,80,'活动日历',self.OpenCalendar)
self.closeButton=Action(self,0,6,52,'关闭',self.Hide)
self.backButton=Action(self,0,6,72,'返回',self.Back)
self.searchBox=ui.SlotBar();self.searchBox.SetParent(self);self.searchBox.SetPosition(18,54)
@@ -72,6 +85,18 @@ class WikiWindow(ui.Window):
self.Reflow()
self.Hide()
def OpenCalendar(self):
import mt_calendar
mt_calendar.Toggle()
def OpenDungeons(self):
import mt_dungeons
mt_dungeons.Toggle()
def OpenDetails(self):
import mt_details
mt_details.Toggle()
def Load(self):
if self.catalogue:return True
try:
@@ -112,6 +137,13 @@ class WikiWindow(ui.Window):
self.page+=step;self.Refresh()
def DetailPage(self,step):
if self.remote and self.remoteData:
pages=max(1,(len(self.links)+self.rowCount-1)//self.rowCount)
kind,subject,offset=self.remote
if step>0 and self.detailPage+1>=pages and offset+len(self.remoteData['rows'])<self.remoteData['total']:
self.QueryRemote(kind,subject,offset+8);return
if step<0 and self.detailPage==0 and offset>0:
self.QueryRemote(kind,subject,max(0,offset-8));return
self.detailPage+=step;self.RefreshDetail()
def Refresh(self):
@@ -132,9 +164,12 @@ class WikiWindow(ui.Window):
def Select(self,mode,row,remember=True):
if remember and self.selected:self.history=(self.history+[self.selected])[-24:]
self.remote=None;self.remoteData=None;self.remoteSerial+=1
self.selected=(mode,row['id']);self.detailPage=0;self.RefreshDetail()
def Back(self):
if self.remote:
self.remote=None;self.remoteData=None;self.remoteSerial+=1;self.detailPage=0;self.RefreshDetail();return
if self.history:
self.selected=self.history.pop();self.detailPage=0;self.RefreshDetail()
@@ -142,6 +177,14 @@ class WikiWindow(ui.Window):
offset=self.detailPage*self.rowCount+index
if offset<len(self.links) and self.links[offset][1]:
vnum=self.links[offset][1]
if isinstance(vnum,tuple):
if vnum[0]=='query':self.QueryRemote(vnum[1],vnum[2],0)
elif vnum[0]=='mobs':
row=self.catalogue.mobs.get(vnum[1])
if row:self.Select('mobs',row)
else:self.QueryRemote('mob',vnum[1],0)
elif vnum[0]=='items':self.Open(vnum[1])
return
row=self.catalogue.FindItem(vnum)
if row:self.Select('items',row)
else:self.status.SetText(L(B('目标物品未收录:#%d'%vnum)))
@@ -149,7 +192,7 @@ class WikiWindow(ui.Window):
def RefreshDetail(self):
for b in self.detailButtons:b.Hide()
self.links=[];self.icon.Hide()
self.backButton.SetEnabled(bool(self.history))
self.backButton.SetEnabled(bool(self.history) or self.remote is not None)
if not self.selected or not self.catalogue:
self.dprev.SetEnabled(False);self.dnext.SetEnabled(False)
return
@@ -189,18 +232,74 @@ class WikiWindow(ui.Window):
else:
cost=self.catalogue.FindItem(offer['cost'])
lines.append((u'研究兑换:'+(cost['name'] if cost else u'#%d'%offer['cost'])+u' ×%d'%offer['amount'],offer['cost'] if cost else 0))
lines.append((u'怪物掉落来源:尚未接入',0))
if row['kind']=='ITEM_GIFTBOX':lines.append((u'宝箱奖励预览:尚未接入',0))
lines.append((u'查看获取来源',('query','sources',vnum)))
if row['kind'] in ('ITEM_GIFTBOX','ITEM_TREASURE_BOX'):lines.append((u'宝箱奖励预览',('query','chest',vnum)))
else:
lines=[(u'类型:'+U('矿石' if row['kind']=='STONE' else '怪物')+u' / '+U(RANK_NAMES.get(row['rank'],'普通')),0),(u'等级:%d 生命:%d'%(row['level'],row['hp']),0),
(u'基础攻击:%d—%d'%(row['attack_min'],row['attack_max']),0),
(u'基础防御:%d 经验:%d'%(row['defense'],row['exp']),0),(u'掉落列表:尚未接入',0)]
(u'基础防御:%d 经验:%d'%(row['defense'],row['exp']),0),(u'查看服务端掉落列表',('query','mob',vnum))]
if self.remote:lines=self.RemoteLines()
self.links=lines
pages=max(1,(len(lines)+self.rowCount-1)//self.rowCount)
self.detailPage=max(0,min(self.detailPage,pages-1))
for i,(label,link) in enumerate(lines[self.detailPage*self.rowCount:(self.detailPage+1)*self.rowCount]):
self.detailButtons[i].SetText(B(label,max(15,(self.split-40)//12)));self.detailButtons[i].SetEnabled(bool(link));self.detailButtons[i].label.SetPackedFontColor(0xffefdbac if link else 0xffc8bea8);self.detailButtons[i].Show()
self.dprev.SetEnabled(self.detailPage>0);self.dnext.SetEnabled(self.detailPage+1<pages)
before=bool(self.remoteData and self.remote and self.remote[2]>0)
after=bool(self.remoteData and self.remote and self.remote[2]+len(self.remoteData['rows'])<self.remoteData['total'])
self.dprev.SetEnabled(self.detailPage>0 or before);self.dnext.SetEnabled(self.detailPage+1<pages or after)
def QueryRemote(self,kind,subject,offset=0):
self.remote=(kind,subject,offset);self.remoteData=None;self.remoteError=None;self.detailPage=0;self.remoteSerial+=1
serial=self.remoteSerial
self.RefreshDetail()
def received(data,error):
if self.remoteSerial!=serial or self.remote!=(kind,subject,offset):return
self.remoteData=data;self.remoteError=error;self.RefreshDetail()
if mt_feature_service.g_service:mt_feature_service.g_service.Request(kind,subject,offset,received)
else:received(None,'unsupported')
def RemoteLines(self):
if self.remoteError:
text={'unsupported':'服务端尚未启用此查询','unsupported_chest':'此道具未接入通用宝箱预览',
'missing':'服务端不存在此目标','timeout':'查询超时,请返回后重试','bad_reply':'查询数据异常,请更新客户端',
'rate':'操作过快,请稍后重试','too_many':'来源过多,请按怪物分别查询'}.get(self.remoteError,'查询失败,请返回后重试')
return [(U(text),0)]
if self.remoteData is None:return [(U('正在向服务器查询…'),0)]
data=self.remoteData;kind,subject,offset=self.remote
lines=[]
if kind=='chest':
lines.append((U('各项独立判定,可能多项或无奖励' if data['mode']==11 else '按权重随机抽取一项'),0))
lines.append((U('仅预览;条件、钥匙与消耗仍由服务器校验'),0))
else:
lines.append((U('已加载常规掉落;任务/活动/矿石额外掉落未全覆盖'),0))
lines.append((U('列出候选及限制,不表示最终中奖率'),0))
for vnum,count,rule,low,high,weight,total,rare in data['rows']:
target=0
if kind=='chest' and vnum<=7:
label={1:U('金币'),2:U('经验'),3:U('召唤怪物'),4:U('减速效果'),5:U('生命损失'),6:U('中毒效果'),7:U('召唤怪物组')}.get(vnum,U('特殊效果'))
label+=u' %d'%count
elif kind=='sources' and rule<20:
row=self.catalogue.mobs.get(vnum);label=row['name'] if row else u'怪物 #%d'%vnum
target=('mobs',vnum);label+=u' ×%d'%count
else:
row=self.catalogue.FindItem(vnum);label=row['name'] if row else u'物品 #%d'%vnum
target=('items',vnum)
if kind!='sources':label+=u' ×%d'%count
lines.append((label,target))
if kind=='chest':
detail=(u'独立判定:%d/100'%min(weight,100)) if rule==11 else (u'权重:%d / %d'%(weight,total))
if rare:detail+=u' 可能附带随机属性'
elif rule>=20:detail=U('宝箱来源;点击可继续查看预览')
elif rule==0:detail=u'通用掉落;角色等级 %d—%d'%(low,high)
elif rule==2:detail=u'击杀组候选权重 %d/%d'%(weight,total)
elif rule==3:detail=u'等级组;角色至少 %d 级'%low
elif rule==4:detail=U('需掉宝手套或对应增益资格')
elif rule==5:detail=U('怪物模板附加掉落')
else:detail=U('怪物独立掉落组')
lines.append((detail,0))
if not data['rows']:lines.append((U('当前查询范围内无配置;不代表所有途径均不存在'),0))
lines.append((U('第 %d—%d 条,共 %d 条')%(offset+1 if data['total'] else 0,offset+len(data['rows']),data['total']),0))
return lines
def Reflow(self):
dims=(wndMgr.GetScreenWidth(),wndMgr.GetScreenHeight())
@@ -243,8 +342,8 @@ class WikiWindow(ui.Window):
def Destroy(self):
self.Hide();self.search.SetReturnEvent(None);self.search.SetEscapeEvent(None)
for b in self.listButtons+self.detailButtons+[self.closeButton,self.backButton,self.searchButton,self.itemTab,self.mobTab,self.categoryButton,self.prev,self.next,self.dprev,self.dnext]:b.event=None
self.catalogue=None
for b in self.listButtons+self.detailButtons+[self.detailsButton,self.dungeonButton,self.calendarButton,self.closeButton,self.backButton,self.searchButton,self.itemTab,self.mobTab,self.categoryButton,self.prev,self.next,self.dprev,self.dnext]:b.event=None
self.remoteSerial+=1;self.catalogue=None
class Entry(Action):
+4
View File
@@ -312,6 +312,10 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
if(MT_WIKI_PYTHON3)
add_test(NAME port.wiki_catalogue
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_catalogue.py)
add_test(NAME port.feature_timers
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_timers.py)
add_test(NAME port.feature_service
COMMAND ${MT_WIKI_PYTHON3} ${PROJECT_SOURCE_DIR}/tests/wiki/test_service.py)
endif()
# Offline locale data builder: retains 40250 proto layout and gameplay fields.
add_executable(locale_proto_patch ${PROJECT_SOURCE_DIR}/tests/port/locale_proto_patch.cpp)