Add feature queries, dungeon information and event calendar UI
This commit is contained in:
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user