// PORT: battle pass (战令), from AVA2's uibattlepassex.py. The server runs monthly seasons of up to ten missions // counted from real events (kills, fishing, refines, levels, NPC trades, play time); each mission has a reward and // a final reward follows once all are done. This window asks with /mt_bp, shows progress live while open and // claims rewards. Entry points: F6 and a "战令" button by the minimap. Protocol: metin2-server // docs/FEATURE-BATTLEPASS.md. static const char c_szBattlePassScript[] = R"PY(# -*- coding: utf-8 -*- import app import chat import game import grp import item import net import nonplayer import player import ui import weakref from mt_biolog import Action, Text, L g_window=None g_entry=None SEND_GAP=.4 TIMEOUT=3. PAGE=5 ROW=56 WIDTH=620 MAX_SLOT=10 # type: (name without sub, name with a sub vnum: kind of the vnum) TYPES={1:('击杀怪物','击杀 ','mob'),2:('击杀 Boss','击杀 Boss ','mob'),3:('摧毁陨石','摧毁 ','mob'),4:('累计在线',None,None), 5:('钓鱼','钓到 ','item'),6:('成功强化装备','强化出 ','item'),7:('提升等级',None,None),8:('在 NPC 商店消费金币',None,None), 9:(None,'向 NPC 出售 ','item')} ERRORS={'loading':'数据加载中,请稍后再试','slot':'没有这个任务','claimed':'奖励已经领取过了','incomplete':'任务尚未完成', 'space':'背包空间不足,请整理后再领取','config':'奖励配置有误,请联系管理员','db':'服务器繁忙,请稍后再试', 'rate':'操作过快,请稍后再试','invalid':'参数无效','ticket':'请使用当月战令票激活','month':'这张战令票不属于当前月份', 'activated':'本月战令已经激活过了','busy':'请先结束交易、商店、仓库或传送'} def Number(text,low=0,high=10**12): if not text.isdigit() or len(text)>13:raise ValueError(text) value=int(text) if not low<=value<=high:raise ValueError(text) return value def Rewards(words): if len(words)%2:raise ValueError(words) return [(Number(words[i],1,2**32-1),Number(words[i+1],1,10000)) for i in range(0,len(words),2)] def Grouped(value): text=str(int(value));parts=[] while len(text)>3:parts.insert(0,text[-3:]);text=text[:-3] return ','.join([text]+parts) def Amount(kind,value,target): # Play time is counted in seconds and shown in hours, or in minutes for a target under an hour. if kind==4: unit=3600. if target>=3600 else 60. amount=value/unit return ('%d'%amount) if amount==int(amount) else ('%.1f'%amount) return Grouped(value) def Unit(kind,target): if kind!=4:return '' return ' 小时' if target>=3600 else ' 分钟' def SeasonName(season): year,month=divmod(season,100) if 2000<=year<=2999 and 1<=month<=12:return '%d 年 %d 月'%(year,month) return '第 %d 期'%season def Left(seconds): seconds=int(seconds) if seconds<=0:return '已结束' days,rest=divmod(seconds,86400);hours,rest=divmod(rest,3600) if days:return '剩余 %d 天 %d 小时'%(days,hours) return '剩余 %d 小时 %d 分'%(hours,max(1,rest//60)) class Pass(object): # Protocol state only. send(text) posts a chat command, now() is seconds. def __init__(self,send,now,listener=None): self.send=send;self.now=now;self.listener=listener self.waiting=None;self.nextSend=0;self.unsupported=False self.Clear() def Clear(self): self.season=None;self.missions={};self.final=None;self.reading=None;self.none=False;self.error=None;self.claiming=None;self.access=None;self.month=None def Ready(self): return self.waiting is None or self.now()>=self.waiting def Post(self,text,expect=True): now=self.now() if now=self.waiting: self.waiting=None;self.claiming=None if self.season is None:self.unsupported=True self.Notify('timeout') def SecondsLeft(self): if not self.season:return 0 _,_,end,now,at=self.season return end-(now+(self.now()-at)) def Notify(self,kind,value=None): if self.listener:self.listener(kind,value) def Receive(self,line): if not line.startswith('MTBP1 '):return False words=line.split() try:self.Parse(words[1],words[2:]) except (ValueError,IndexError):self.reading=None return True def Parse(self,kind,args): if kind=='season' and len(args)==5: seasonId,start,end,now=[Number(w,0,2**32-1) for w in args[:4]];count=Number(args[4],1,MAX_SLOT) self.reading=dict(season=(seasonId,start,end,now,self.now()),count=count,missions={},final=None,access=None,month=None) elif kind=='access' and len(args) in (1,2) and self.reading is not None: self.reading['access']=Number(args[0],0,2) if len(args)==2:self.reading['month']=Number(args[1],1,12) elif kind=='mission' and self.reading is not None and len(args)>=6: slot=Number(args[0],1,MAX_SLOT);kind=Number(args[1],1,9) self.reading['missions'][slot]=dict(slot=slot,type=kind,sub=Number(args[2],0,2**32-1),target=Number(args[3],1), value=Number(args[4]),claimed=Number(args[5],0,1)==1,rewards=Rewards(args[6:])) elif kind=='final' and self.reading is not None and len(args)>=4: self.reading['final']=dict(claimed=Number(args[0],0,1)==1,all=Number(args[1],0,1)==1,rewards=Rewards(args[2:])) elif kind=='end' and not args and self.reading is not None: r=self.reading;self.reading=None if len(r['missions'])!=r['count'] or r['final'] is None:return self.season=r['season'];self.missions=r['missions'];self.final=r['final'];self.access=r['access'];self.month=r['month'];self.none=False self.Answered('info') elif kind=='none' and not args: self.Clear();self.none=True;self.Answered('none') elif kind=='progress' and len(args)==2: slot=Number(args[0],1,MAX_SLOT);m=self.missions.get(slot) if m:m['value']=min(m['target'],Number(args[1]));self.Notify('progress',slot) elif kind=='done' and len(args)==1: slot=Number(args[0],1,MAX_SLOT);m=self.missions.get(slot) if m:m['value']=m['target'] self.Notify('done',slot) elif kind=='claimed' and len(args)==1: slot=Number(args[0],0,MAX_SLOT) if slot: if slot in self.missions:self.missions[slot]['claimed']=True elif self.final:self.final['claimed']=True self.claiming=None;self.Notify('claimed',slot) # a full info follows elif kind=='error' and len(args)==1 and args[0] in ERRORS: self.error=args[0];self.claiming=None;self.Answered('error') def Answered(self,kind): self.waiting=None;self.unsupported=False;self.Notify(kind) def AllDone(self): return bool(self.missions) and all(m['value']>=m['target'] for m in self.missions.values()) def ItemName(vnum): try: item.SelectItem(vnum);name=item.GetItemName() except Exception:name='' return name if name else L('物品 %d'%vnum) def MobName(vnum): try:name=nonplayer.GetMonsterName(vnum) except Exception:name='' return name if name else L('怪物 %d'%vnum) def MissionName(m): # Locale text: the utf-8 parts go through L, game names are already in the locale's code page. plain,prefix,kind=TYPES.get(m['type'],('任务',None,None)) if m['sub'] and prefix:return L(prefix)+(MobName(m['sub']) if kind=='mob' else ItemName(m['sub'])) return L(plain or '任务') def RewardText(rewards): return L(',').join([ItemName(vnum)+(' x%d'%count) for vnum,count in rewards]) class BattlePassWindow(ui.Window): def __init__(self,state): ui.Window.__init__(self);self.SetWindowName('BattlePassWindow');self.AddFlag('float');self.AddFlag('movable') self.state=state;self.page=0 self.SetSize(WIDTH,74+PAGE*ROW+ROW+20+56) self.heading=Text(self,18,18,'战令',0xffffd68b) self.season=Text(self,80,18,'',0xffc3b7a1) self.closeButton=Action(self,WIDTH-76,6,64,'关闭',self.Hide) self.rows=[] for i in range(PAGE): y=64+i*ROW self.rows.append(dict(name=Text(self,18,y+4,''),value=Text(self,330,y+4,''), reward=Text(self,18,y+26,'',0xffb3aa98),button=Action(self,WIDTH-118,y,100,'领取',lambda i=i:self.ClaimRow(i)))) y=64+PAGE*ROW+8 self.finalName=Text(self,18,y+4,'最终奖励',0xffffd68b) self.finalState=Text(self,330,y+4,'') self.finalReward=Text(self,18,y+26,'',0xffb3aa98) self.finalButton=Action(self,WIDTH-118,y,100,'领取',lambda:self.state.Claim(0) and self.Refresh()) self.activateButton=Action(self,WIDTH-218,y,200,'使用当月战令票激活',self.AskActivate) self.activateButton.Hide();self.confirm=None;self.pendingTicket=None y=self.GetHeight()-56 self.prev=Action(self,18,y,80,'上一页',lambda:self.Turn(-1)) self.pageText=Text(self,140,y+24,'');self.pageText.SetHorizontalAlignCenter() self.next=Action(self,182,y,80,'下一页',lambda:self.Turn(1)) self.note=Text(self,280,y+16,'',0xffc3b7a1) self.second=-1 self.Hide() def Open(self): self.state.error=None;self.state.Query();self.Refresh();self.SetCenterPosition();ui.Window.Show(self);self.SetTop() def Toggle(self): if self.IsShow():self.Hide() else:self.Open() def Hide(self): state=self.__dict__.get('state') # ui.Window.__init__ hides the window before state is set if self.IsShow() and state and state.season:state.Close() self.pendingTicket=None self.CloseConfirm() ui.Window.Hide(self) def CloseConfirm(self): confirm=self.__dict__.get('confirm') if confirm: confirm.Close();confirm.SetAcceptEvent(None);confirm.SetCancelEvent(None);self.confirm=None def OfferTicket(self,cell,vnum): self.pendingTicket=(cell,vnum) self.Open() def AskTicket(self,cell,vnum): if self.state.access!=0 or player.GetItemIndex(cell)!=vnum:return item.SelectItem(vnum) if item.GetValue(0)!=self.state.month:self.state.error='month';self.Refresh();return import uiCommon self.CloseConfirm();self.confirm=uiCommon.QuestionDialog() self.confirm.SetText(L('消耗战令票并激活本月战令?')) self.confirm.SetAcceptEvent(lambda:self.ConfirmActivate(cell,vnum)) self.confirm.SetCancelEvent(self.CloseConfirm);self.confirm.Open() def AskActivate(self): if self.state.access!=0:return for cell in range(player.INVENTORY_PAGE_SIZE*player.INVENTORY_PAGE_COUNT): vnum=player.GetItemIndex(cell) if not 25150<=vnum<=25161:continue item.SelectItem(vnum) # Display uses server time, not the phone's local month/time zone. if item.GetValue(0)!=self.state.month:continue self.AskTicket(cell,vnum);return self.state.error='ticket';self.Refresh() def ConfirmActivate(self,cell,vnum): self.CloseConfirm();self.state.Activate(cell,vnum);self.Refresh() def Slots(self): return sorted(self.state.missions) def Pages(self): return max(1,(len(self.Slots())+PAGE-1)//PAGE) def Turn(self,delta): self.page=max(0,min(self.Pages()-1,self.page+delta));self.Refresh() def ClaimRow(self,i): slots=self.Slots()[self.page*PAGE:] if i=len(slots): for w in widgets:w.Hide() continue for w in widgets:w.Show() m=s.missions[slots[i]];done=m['value']>=m['target'] row['name'].SetText(MissionName(m)) row['value'].SetText(Amount(m['type'],m['value'],m['target'])+' / '+Amount(m['type'],m['target'],m['target'])+L(Unit(m['type'],m['target']))) row['value'].SetPackedFontColor(0xff9fe08b if done else 0xffe8dec8) row['reward'].SetText(L('奖励:')+RewardText(m['rewards']) if m['rewards'] else L('无奖励')) if m['claimed']:row['button'].SetText('已领取') elif s.claiming==m['slot']:row['button'].SetText('领取中') else:row['button'].SetText('领取' if done else '未完成') row['button'].SetEnabled(s.access in (None,1) and done and not m['claimed'] and bool(m['rewards']) and s.claiming is None) f=s.final for w in (self.finalName,self.finalState,self.finalReward,self.finalButton): if f:w.Show() else:w.Hide() if f: ready=s.AllDone() self.finalState.SetText(L('已领取' if f['claimed'] else '全部任务完成' if ready else '完成全部任务后可领取')) self.finalReward.SetText(L('奖励:')+RewardText(f['rewards'])) self.finalButton.SetText('已领取' if f['claimed'] else '领取中' if s.claiming==0 else '领取') self.finalButton.SetEnabled(s.access in (None,1) and ready and not f['claimed'] and s.claiming is None) if s.access==0: self.finalButton.Hide();self.finalState.Hide();self.finalReward.Hide();self.activateButton.Show() self.activateButton.SetEnabled(s.waiting is None) else:self.activateButton.Hide() many=self.Pages()>1 for w in (self.prev,self.next,self.pageText): if many and s.season:w.Show() else:w.Hide() self.prev.SetEnabled(self.page>0);self.next.SetEnabled(self.page