Files
metin2-client/src/platform/ScriptLib/WikiScript.inc
T
shenleiandClaude Opus 5.5 84404cf1f1 Model preview: AVA2 render-target window on a Vulkan colour target
Port of AVA2's CRenderTarget / GrpRenderTargetTexture / renderTarget module.
The host gains an RGBA8 offscreen colour pass for D3D colour render targets;
mt_preview shows wearable items on every allowed race and monsters with
motions, reached by X+click in the inventory and shop and from the wiki.
Player races are created as TYPE_PC so their parts exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:48:07 +09:00

392 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PORT: AVA2-inspired encyclopedia, adapted to current server templates and touch controls.
// 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
import grp
import item
import pack
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
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):
return text if isinstance(text,unicode) else text.decode('utf-8')
def B(text, limit=0):
text=U(text)
if limit and len(text)>limit:text=text[:limit-1]+u'…'
return text.encode('utf-8')
KINDS=(('', '全部物品'),('ITEM_WEAPON','武器'),('ITEM_ARMOR','防具首饰'),('ITEM_MATERIAL','材料'),
('ITEM_QUEST','任务物品'),('ITEM_USE','消耗品'),('ITEM_GIFTBOX','宝箱'),('ITEM_METIN','宝石'))
MOBKINDS=(('', '全部怪物'),('MONSTER','怪物'),('STONE','矿石'))
TYPE_NAMES=dict(KINDS)
TYPE_NAMES.update({'ITEM_NONE':'特殊道具','ITEM_UNIQUE':'特殊装备','ITEM_TOOL':'工具','ITEM_FISH':'鱼类','ITEM_ROD':'鱼竿','ITEM_PICK':'采矿镐','ITEM_RESOURCE':'资源','ITEM_SKILLBOOK':'技能书','ITEM_SKILLFORGET':'技能忘却书','ITEM_COSTUME':'时装','ITEM_SPECIAL':'特殊道具'})
RANK_NAMES={'PAWN':'普通','S_PAWN':'弱小','KNIGHT':'精英','S_KNIGHT':'强力','BOSS':'首领','KING':'王者'}
class WikiWindow(ui.Window):
def __init__(self):
ui.Window.__init__(self)
self.SetWindowName('WikiWindow')
self.AddFlag('float')
self.AddFlag('movable')
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)
self.searchBox.SetSize(320,28);self.searchBox.Show()
self.search=ui.EditLine();self.search.SetParent(self.searchBox);self.search.SetPosition(8,6)
self.search.SetSize(302,20);self.search.SetMax(80);self.search.SetReturnEvent(self.Search)
self.search.SetEscapeEvent(self.Hide);self.search.Show()
self.searchButton=Action(self,346,50,72,'搜索',self.Search)
self.itemTab=Action(self,18,100,96,'物品',lambda:self.SetMode('items'))
self.mobTab=Action(self,122,100,96,'怪物',lambda:self.SetMode('mobs'))
self.categoryButton=Action(self,226,100,164,'全部物品',self.NextCategory)
self.status=Text(self,18,85,'输入名称或 ID 搜索',0xffc3b7a1)
self.title=Text(self,0,112,'选择左侧条目',0xffffd68b)
self.icon=ui.ExpandedImageBox();self.icon.SetParent(self);self.icon.AddFlag('not_pick')
self.listButtons=[Action(self,0,0,100,'',lambda i=i:self.SelectResult(i)) for i in range(8)]
self.detailButtons=[Action(self,0,0,100,'',lambda i=i:self.Follow(i)) for i in range(8)]
self.prev=Action(self,18,0,72,'上一页',lambda:self.Page(-1))
self.next=Action(self,0,0,72,'下一页',lambda:self.Page(1))
self.pageText=Text(self,110,0)
self.dprev=Action(self,0,0,72,'详情上页',lambda:self.DetailPage(-1))
self.dnext=Action(self,0,0,72,'详情下页',lambda:self.DetailPage(1))
self.revision=Text(self,430,66,'未加载资料',0xffaaa08b)
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:
if not pack.Exist(PATH):raise ValueError('missing catalogue')
self.catalogue=Catalogue(pack.Get(PATH))
self.revision.SetText(L(B(u'资料版本 '+self.catalogue.version[:12])))
self.error=''
return True
except (ValueError,TypeError,UnicodeError,RuntimeError,IOError):
self.error='百科资料缺失或版本不支持,请更新资源包。'
self.status.SetText(L(B(self.error,24)))
return False
def Open(self,vnum=0):
self.Reflow();self.SetCenterPosition();self.Show();self.SetTop()
if not self.Load():return
self.Search()
if vnum:
row=self.catalogue.FindItem(int(vnum))
if row:self.Select('items',row)
else:self.status.SetText(L(B('当前资料未收录此物品。')))
def Query(self):
value=self.search.GetText()
return player.FeatureLocaleToUTF8(value).decode('utf-8').strip()
def Search(self):
self.page=0;self.lastQuery=self.Query();self.Refresh()
def SetMode(self,mode):
self.mode=mode;self.category=0;self.page=0;self.Refresh()
def NextCategory(self):
choices=KINDS if self.mode=='items' else MOBKINDS
self.category=(self.category+1)%len(choices);self.page=0;self.Refresh()
def Page(self,step):
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):
for button in self.listButtons:button.Hide()
if not self.catalogue:return
choices=KINDS if self.mode=='items' else MOBKINDS
kind,label=choices[self.category]
self.categoryButton.SetText(label)
self.results,count,self.page,pages=self.catalogue.Search(self.mode,self.Query(),kind,self.page,self.rowCount)
for i,row in enumerate(self.results):
self.listButtons[i].SetText(B(row['name'],max(9,(self.split-130)//12))+' #%d'%row['id']);self.listButtons[i].Show()
self.status.SetText(L(B('%d 项;模板资料不保证可获取。'%count)))
self.pageText.SetText(L(B('%d / %d'%(self.page+1,pages))))
self.prev.SetEnabled(self.page>0);self.next.SetEnabled(self.page+1<pages)
def SelectResult(self,index):
if index<len(self.results):self.Select(self.mode,self.results[index])
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()
def Follow(self,index):
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])
elif vnum[0]=='preview':
import mt_preview
mt_preview.Preview(vnum[1],vnum[2])
return
row=self.catalogue.FindItem(vnum)
if row:self.Select('items',row)
else:self.status.SetText(L(B('目标物品未收录:#%d'%vnum)))
def RefreshDetail(self):
for b in self.detailButtons:b.Hide()
self.links=[];self.icon.Hide()
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
mode,vnum=self.selected
row=(self.catalogue.items if mode=='items' else self.catalogue.mobs)[vnum]
self.title.SetText(L(B(row['name'],max(8,(self.split-150)//12))+' #%d'%vnum))
lines=[]
if mode=='items':
lines.append((u'类型:'+U(TYPE_NAMES.get(row['kind'],'其他物品')),0))
lines.append((u'最低等级:%d 占格:%d'%(row['level'],row['size']),0))
if row['end']!=vnum:lines.append((u'模板 ID 范围:%d—%d'%(vnum,row['end']),0))
try:
path=item.FeatureGetIconPath(vnum)
if path:self.icon.LoadImage(path);self.icon.Show()
except (RuntimeError,ValueError):pass
target=row['refined'];recipe=self.catalogue.recipes.get(row['recipe'])
if target:
name=self.catalogue.FindItem(target)
lines.append((u'升级 → '+(name['name'] if name else u'#%d'%target),target if name else 0))
if recipe:
lines.append((u'基础配方:%d 金币 / %d%%'%(recipe['cost'],recipe['probability']),0))
for mat,count in recipe['materials']:
ingredient=self.catalogue.FindItem(mat)
lines.append((u'材料:'+(ingredient['name'] if ingredient else u'#%d'%mat)+u' ×%d'%count,mat if ingredient else 0))
lines.append((u'实际费用/概率以精炼窗口为准',0))
else:lines.append((u'升级配方尚未收录',0))
else:lines.append((u'没有配置下一阶升级',0))
import mt_biolog
for stage in sorted(mt_biolog.CATALOG):
bio=mt_biolog.CATALOG[stage]
if vnum in (bio['material'],bio['stone']):
lines.append((u'生物学家:%d 级研究'%bio['level'],0))
if vnum==bio['material']:lines.append((u'阶段所需:%d 个'%bio['count'],0))
offer=next((o for o in mt_biolog.SHOP if o['vnum']==vnum),None)
if offer:
if offer['gold']:lines.append((u'研究商店:%d 金币'%offer['gold'],0))
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))
if row['kind'] in ('ITEM_WEAPON','ITEM_ARMOR','ITEM_COSTUME'):
import mt_preview
if mt_preview.Previewable('item',vnum):lines.append((u'模型预览',('preview','item',vnum)))
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'模型预览',('preview','mob',vnum)),(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()
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())
if dims==self.dimensions:return
self.dimensions=dims
width=min(760,max(600,dims[0]-16));height=min(480,max(300,dims[1]-16))
self.SetSize(width,height);self.split=width//2
self.categoryButton.SetSize(self.split-234,48);self.categoryButton.label.SetPosition((self.split-234)//2,24)
self.rowCount=min(8,max(2,(height-216)//48))
self.closeButton.SetPosition(width-70,6);self.backButton.SetPosition(width-150,6)
self.title.SetPosition(self.split+48,122);self.icon.SetPosition(self.split+8,112)
for i,b in enumerate(self.listButtons):
b.SetPosition(18,154+i*48);b.SetSize(self.split-30,44);b.label.SetPosition((self.split-30)//2,22)
for i,b in enumerate(self.detailButtons):
b.SetPosition(self.split+8,154+i*48);b.SetSize(self.split-26,44);b.label.SetPosition((self.split-26)//2,22)
self.prev.SetPosition(18,height-54);self.next.SetPosition(self.split-90,height-54);self.pageText.SetPosition(112,height-30)
self.dprev.SetPosition(self.split+8,height-54);self.dnext.SetPosition(width-90,height-54)
self.SetCenterPosition();self.Refresh();self.RefreshDetail()
def OnUpdate(self):
if self.IsShow():self.Reflow()
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())
grp.RenderLine(x+self.split,y+108,0,self.GetHeight()-170)
def OnMouseWheel(self,delta):
x,y=self.GetGlobalPosition();mx,my=wndMgr.GetMousePosition()
if mx>=x+self.split:self.DetailPage(-1 if delta>0 else 1)
else:self.Page(-1 if delta>0 else 1)
return True
def Hide(self):
if hasattr(self,'search'):self.search.KillFocus()
ui.Window.Hide(self)
def OnPressEscapeKey(self):
self.Hide();return True
def Destroy(self):
self.Hide();self.search.SetReturnEvent(None);self.search.SetEscapeEvent(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):
def __init__(self,window):
Action.__init__(self,None,0,0,64,'百科',Toggle)
self.minimap=weakref.proxy(window.interface.wndMiniMap);self.SetWindowName('WikiEntry');self.OnUpdate()
def OnUpdate(self):
x,y=self.minimap.GetGlobalPosition();self.SetPosition(max(4,x-68),y+96)
def Toggle():
if g_window:
if g_window.IsShow():g_window.Hide()
else:g_window.Open()
def OpenItem(vnum):
if g_window:g_window.Open(vnum)
_open=game.GameWindow.Open
_close=game.GameWindow.Close
def Open(self):
global g_window,g_entry
_open(self);g_window=WikiWindow();g_entry=Entry(self)
def Close(self):
global g_window,g_entry
if g_entry:g_entry.Hide();g_entry.event=None;g_entry=None
if g_window:g_window.Destroy();g_window=None
_close(self)
game.GameWindow.Open=Open
game.GameWindow.Close=Close
)PY";