Initial import of HEROLL source and design documents

This commit is contained in:
toshiocn
2026-10-26 16:05:39 +08:00
commit fdb55672c1
1846 changed files with 290692 additions and 0 deletions
@@ -0,0 +1,264 @@
from data_source import *
from module_content import CONTENT
from collections import Counter, defaultdict
import shutil
audit=OUT/'99-审计与证据'
before=snapshot()
baseline=audit/'旧目录生成前快照.json'
if baseline.exists():
assert before==json.loads(baseline.read_text(encoding='utf-8')), '旧目录已与首次快照不一致,停止生成'
else: write(baseline,json.dumps(before,ensure_ascii=False,indent=2))
modules,tables,local,oldconstants=load()
assert {m['id'] for m in modules}==set(CONTENT)
table_notes=constants(OLD/'99-审计与证据/工具/organize_module_docs.py')['NOTES']
tn={line.split('|')[0]:line.split('|')[1:] for line in table_notes.strip().splitlines()}
fieldzh=oldconstants['FIELD_ZH']
def clean(s):
s=s.replace('<br>',' / ').replace('<br/>',' / ')
return re.sub(r'</?(?:b|i|color|size)(?:=[^>]*)?>','',s)
def loc(key): return clean(local.get(key,{}).get('zh',''))
def translated(r):
texts={h:loc(v) for h,v in r.items() if v and loc(v)}
names=[v for h,v in texts.items() if re.search('title',h,re.I)]
effects=[v for h,v in texts.items() if re.search('desc|challengeText',h,re.I)]
return (names[0] if names else r['StringId']), ' / '.join(effects), texts
# 重要参数用紧凑业务表展示,完整配置与中文文本目录同目录保留。
HIGHLIGHTS={
'01': [('newbieguide',['clearStage','rewardID','rewardCount'])],
'02': [('lobbyticket',['EAsset','ELobbyAsset','defaultValue','boostValue'])],
'03': [('equipmentdata',['grade','starCount','maxUpgradeLevel','materialLevel','materialCount','sameGradeAnyEquipmentCount']),('equipment',['slotNum','isSpecial','isMaterial','baseStatus'])],
'04': [('bonusstatus',['EMonoStatus','isPercent','upgradeValue','maxLevel']),('slotupgrade',['slotNum','level','needItemID','needCount','needGold'])],
'05': [('relicdata',['grade','starCount','materialLevel','materialCount','sameGradeAnyRelicCount']),('relic',['slotNum','baseStatus','statusValue','isPercent'])],
'06': [('pet',['grade','baseCoolTime','plusCoolTime','unlockPrice']),('petskill',['petId','skillUnlockLevel','EPetSkill','skillValue'])],
'07': [('mount',['grade','unlockPiece','unlockDia','refundItemId','refundItemCount']),('mountawaken',['grade','level','piece','isOwnEffect','baseStatus0','value0'])],
'08': [('destinystat',['isEnabled','EDestinyStatGrade','EMonoStatus','addValue','normalWeight','rareWeight'])],
'09': [('collection',['equipmentIds','equipmentLevels','equipmentGrades','EMonoStatus','value'])],
'10': [('skin',['EEquipmentSlot','additionalStatus','additionalValue','maxSetCount','setAdditionalValueList'])],
'11': [('outgametileresearch',['totalUpgradeCount','researchLevel','weaponPlusLevel','EMonoWeapon']),('outgametileafkconfig',['afkTime','maxPackCount','startAddPack','needMileage','mileageRewardId','mileageRewardCount'])],
'12': [('afkconfig',['checkTime','maxAfkTime','quickAfkTime','dailyMaxQuickAfkCount','quickAfkEnergy','dailyMaxQuickAfkAdCount']),('afkreward',['lastClearStage','goldPerHour','amountPerCheckTime','maxAmount','dropPercent'])],
'13': [('gachatype',['type','gachaCount','isFree','isAD','needItemID','needCount']),('gachaceildata',['type','ceilCount','isResetBeforeCeil','ceilRewardString'])],
'14': [('pigbank',['startValue','endValue','winUpValue','loseUpValue','purchaseAvailableMinutes','cooldownMinutes']),('relaypackagedata',['relayPackageId','order','isFree','freeItemId','freeItemCount'])],
'15': [('achievement',['conditionType','goalCount','isPerGameReset','rewardType','rewardId','rewardValue'])],
'16': [('comebackconfig',['comebackTime','buffTime','rewardBuffPercent','rewardBuffCount','playSpeedBuffCount']),('newbiebuffconfig',['buffTime','additionalSpeed','rewardBuffPercent','playSpeedBuffCount','rewardBuffCount'])],
'17': [('passdata',['dailyQuestCount','weeklyQuestCount','challengeQuestCount','playMaxXP','primiumPlayMaxXP','loopClearXP'])],
'18': [('starchallenge',['challengeText','value']),('starchallengereward',['starCount','rewardID1','value1','rewardID2','value2'])],
'19': [('idledungeonconfig',['ticketChargeTime','maxTicket','startTicket','maxInvSize']),('dayoftheweekdungeon',['day','difficulty','itemId','itemCount'])],
'20': [('infiniteconfig',['playAssetId','playAssetCount','excludeReward','maxGold','shopIncrease'])],
'21': [('raidconfig',['startTicketCount','maxTicketCount','ticketChargeTime','playAssetId','playAssetCount']),('raidseasonpetlist',['season','deckIndex','petId','trialLevel'])],
'22': [('potionbattleconfig',['targetSeason','searchMmrRange','searchRange','maxTicketCount','ticketChargeTime']),('rankbasedata',['gameMode','followingPassId','resetSeasonAdjustTier'])],
'23': [('rankingrewarddata',['rankingDataKey','minRank','maxRank','rewardId','rewardCount'])],
'24': [('superballschedule',['startReward','RewardPercent','countingTime','superBallStartDay','superBallEndDay'])],
'25': [('plinkolevel',['maxBallCount','chargeSecond','adKey','level']),('plinkoad',['maxADPerDay','buffSecond','values'])],
'26': [('deoksun',['likePoint','goldUP','diaUP','ingameDiscount']),('deokjashop',['isEnabled','probabilityWeight','baseAdditionalValue','additionalValue','maxLevel'])]
}
fieldzh.update({'dailyQuestCount':'每日任务','weeklyQuestCount':'每周任务','challengeQuestCount':'挑战任务','maxTicket':'门票上限','startTicket':'初始门票','maxInvSize':'未装备物容量','materialCount':'指定材料数','sameGradeAnyEquipmentCount':'任意同品质材料数','starCount':'星数','grade':'品质','level':'阶段/等级','playMaxXP':'普通每日游玩经验上限','primiumPlayMaxXP':'高级每日游玩经验上限','loopClearXP':'每层通关经验','isSpecial':'特殊装备','baseStatus':'主属性','startReward':'初始奖池','RewardPercent':'前三名比例','countingTime':'统计分钟数','challengeText':'挑战描述','goldUP':'金币收益参数','diaUP':'钻石概率参数','value1':'奖励1数量','value2':'奖励2数量','maxBallCount':'球容量','chargeSecond':'恢复秒数','buffSecond':'增益秒数','maxADPerDay':'每日广告次数'})
rules_index=[];manifest=[];coverage=[];localized_count=0
shared=OUT/'00-共享资源与配置'
for t in ['gameconfig','item','newtile','weaponstatus','skilleffect']:
savecsv(shared/(t+'.csv'),tables[t]['rows'],tables[t]['headers'])
manifest.append(dict(来源=SOURCE.joinpath(t+'.csv').relative_to(ROOT).as_posix(),SHA256=sha(SOURCE/(t+'.csv')),用途='共享原作配置'))
GUIDE='''# 局外原作开发文档编写规范
## 1. 采用的文档结构
参考局内v0.2.8“玩法总纲 → 分模块规则 → 权威CSV → 规则状态与误写项”的顺序。正文先回答玩家做什么、进度如何变化,再给公式、数值和配置关系。不能用字段目录代替玩法说明。
每个模块的《设计与规则》包含:功能定位、主要状态、逐项规则、操作流程、关键数值、中文内容、开发权威表、验收样例和待补充边界。Markdown和CSV放在同一模块文件夹。
## 2. 规则状态
| 状态 | 处理方式 |
| --- | --- |
| 原作配置确认 | 字段、数组、类型和原注释直接支持的值,按原值使用 |
| 原作文本确认 | 原作中文名称、效果或条件文本;占位符未映射时保留,不当作执行公式 |
| 原作部分确认 | 功能主体或配置关系明确,仍有执行顺序或参数含义缺口 |
| 条件推导 | 写明公式与前提,只用于检查,不充当确认的完整运行规则 |
| 开发映射 | 为理解和实现而拆分的概念状态,不宣称是原作字段名或存档格式 |
| 未知 | 写出具体缺口,不填写新的设计默认值 |
状态对应证据精度,而非功能是否收录。原作配置、原作文本和条件推导分列;文字与数值不一致时保留两者并标明,不能无依据覆盖。
## 3. 使用约定
- 原作版本为HEROLL 2.0.1。只借用v0.2.8文档组织方法,不借用其题材包装、裁剪清单或默认数值。
- 中文名称使用原作本地化文本,保留原字符。没有中文文本时显示StringId,不创造名称。
- 正文列出的操作流程是功能阅读顺序;精确扣除和结算时序未确认的地方不冻结成新玩法。
- 执行规则表、配置表和中文对照表职责分开。中文对照列仅帮助读懂数据,不改写原字段。
- 详细数值以同模块CSV为权威;正文大表注明节选范围。状态和待确认项写入规则表,不靠口头补充。
'''
write(OUT/'文档规范与阅读说明.md',GUIDE)
def section_body(m):
global localized_count
c=CONTENT[m['id']]; d=OUT/m['folder']; rule_rows=[];checks=[]
body=f"# {m['name']}:原作设计与规则\n\n版本:HEROLL 2.0.1 原作还原文档 v1.0。\n\n## 1. 功能定位\n\n{c['intro']}\n\n## 2. 主要状态\n\n下面是开发阅读所需的概念状态,属于开发映射,不要求与原作内部字段同名。\n\n"
states=[]
for x in c['states'].split(';'):
a,b=x.split(':',1);states.append(dict(状态=a,用途=b,规则状态='开发映射'))
body+=md(['状态','用途'],[(r['状态'],r['用途']) for r in states])
savecsv(d/'状态字段.csv',states)
body+='\n## 3. 功能规则\n\n'
for n,(title,text) in enumerate(c['rules'],1):
body+=f'### 3.{n} {title}\n\n{text}\n\n'
rule_rows.append(dict(规则ID=f"O{m['id']}-{n:02}",规则项目=title,执行说明=text,规则状态='原作部分确认',来源=';'.join(m['tables']),边界=c['boundary']))
body+='## 4. 操作流程\n\n以下顺序用于拆解功能;涉及未确认的结算时机,以本章末尾具体缺口为准。\n\n'
body+='\n'.join(f'{n}. {text}' for n,text in enumerate(c['flow'],1))+'\n\n'
body+='## 5. 关键数值与计算\n\n'
facts=[]
for t,key,desc,fields in m['facts']:
matches=[r for r in tables[t]['rows'] if r['StringId']==key];assert len(matches)==1,(t,key)
r=matches[0];v=';'.join(f'{h}={r[h]}' for h in fields.split())
facts.append((desc,v,'原作配置确认'))
rule_rows.append(dict(规则ID=f"O{m['id']}-N{len(facts):02}",规则项目=desc,执行说明=v,规则状态='原作配置确认',来源=f'{t}.csv/StringId={key}',边界='原字段值;复合公式以正文边界为准'))
checks.append(dict(用例ID=f"T{m['id']}-{len(checks)+1:02}",输入=f'读取{t}中StringId={key}',检查动作='查询本条配置并核对界面/配置输出',预期=v,验收范围='配置数值与显示;不覆盖未知执行分支'))
if facts: body+=md(['项目','数值','状态'],facts)+'\n'
else: body+='关键数值见下列表格。\n\n'
if m['id']=='12':
body+='### 5.A 时间换算与倍数检查\n\n1440÷60=24小时;360÷60=6小时。若所有倍数按给定权重参与,则`E[m]=(1×65+1.2×25+1.5×10)/100=1.1`。时间换算是算术确认,期望倍数仅是条件推导。\n\n'
if m['id']=='11': body+='### 5.A 挂机容量换算\n\n`30分钟/包 × 30包 = 900分钟 = 15小时`。此式只换算地块包容量,不能替代巡逻挂机的24小时上限。\n\n'
if m['id']=='13':
body+='### 5.A 权重检查公式\n\n对指定池、并假定该池所有记录均按权重参与时,`P(i)=weight(i)/Σweight(pool)`。保底和动态筛选不在此式内,见[条件概率检查](./抽卡条件概率.csv)。\n\n'
for t,cols in HIGHLIGHTS[m['id']]:
rows=tables[t]['rows'];assert set(cols)<=set(tables[t]['headers']),t
chosen=rows if len(rows)<=40 else rows[:5]+rows[-3:]
tag='全表' if len(rows)<=40 else f'节选前5条与末3条,完整{len(rows)}条见CSV'
body+=f"### {tn[t][0]}({tag})\n\n"
outrows=[]
for r in chosen:
name,desc,tx=translated(r)
outrows.append((r['Id'],name,*[loc(r[h]) if h=='challengeText' and loc(r[h]) else r[h] for h in cols]))
body+=md(['ID','名称/键']+[fieldzh.get(h,h) for h in cols],outrows)+'\n'
body+='## 6. 原作中文内容\n\n中文名称和效果使用原作文本;{0}、{B}等占位符未确认绑定时保留,文字不能代替参数公式。\n\n'
visible=[];references=[]
for t in m['tables']:
for r in tables[t]['rows']:
name,effect,texts=translated(r)
for h,text in texts.items():
references.append(dict(表=t,Id=r['Id'],StringId=r['StringId'],源字段=h,文本键=r[h],原作中文=text,来源='原作本地化/zh-cn'))
if name!=r['StringId'] or effect:
visible.append((t,r['Id'],name,effect))
if visible:
shown=visible if len(visible)<=30 else visible[:20]
body+=md(['来源表','ID','名称','效果/条件文本'],shown)+'\n'
if len(visible)>30: body+=f'本节展示前20条中文内容;完整{len(visible)}条有名称或效果的记录见对应中文目录及[中文文本对照](./中文文本对照.csv)。\n\n'
else: body+='本模块主要由数值和键值组成,原作文本对照见同目录CSV。\n\n'
savecsv(d/'中文文本对照.csv',references,['表','Id','StringId','源字段','文本键','原作中文','来源'])
localized_count+=len(references)
body+='## 7. 开发权威表\n\n'
global_config=[r for r in tables['gameconfig']['rows'] if re.search(oldconstants['GROUTES'][m['id']],r['StringId'],re.I)]
savecsv(d/'关联全局配置.csv',global_config,tables['gameconfig']['headers'])
rows=[]
for t in m['tables']:
desc=tn[t][1];records=tables[t]['rows']
savecsv(d/(t+'.csv'),records,tables[t]['headers'])
catalog=[]
for r in records:
name,effect,_=translated(r)
catalog.append(dict(原作中文名称=name,原作效果文本=effect,**r))
savecsv(d/(t+'-中文目录.csv'),catalog,['原作中文名称','原作效果文本']+tables[t]['headers'])
rows.append((tn[t][0],f'[{t}.csv](./{t}.csv)',len(records),f'[中文目录](./{t}-中文目录.csv)'))
manifest.append(dict(来源=(SOURCE/(t+'.csv')).relative_to(ROOT).as_posix(),SHA256=sha(SOURCE/(t+'.csv')),用途=m['folder']))
coverage.append(dict(模块=m['folder'],子功能=tn[t][0],表=t,行数=len(records),规则文档=m['folder']+'/设计与规则.md',数值表=m['folder']+'/'+t+'.csv',数值用途=desc))
body+=md(['配置用途','权威原字段表','记录数','开发阅读表'],rows)+'\n'
body+='[执行规则](./执行规则.csv) · [状态字段](./状态字段.csv) · [验收样例](./验收样例.csv) · [字段字典](./字段字典.csv) · [关联全局配置](./关联全局配置.csv)\n\n'
if global_config:
body+='全局键按模块名称关联收录;其具体含义优先以正文已确认的规则为准。\n\n'+md(['配置键','原值'],[(r['StringId'],r['Values']) for r in global_config])+'\n'
dictionary=[]
for t in m['tables']:
for i,h in enumerate(tables[t]['headers']):
dictionary.append(dict(表=t,字段=h,含义=fieldzh.get(h,'按本模块功能规则与原注释解释'),原类型=tables[t]['types'][i],原注释=tables[t]['comments'][i] if i<len(tables[t]['comments']) else ''))
savecsv(d/'字段字典.csv',dictionary)
body+='## 8. 开发验收与禁止误写\n\n'
if checks: body+=md(['输入','检查内容','预期'],[(r['输入'],r['检查动作'],r['预期']) for r in checks])+'\n'
body+='验收样例覆盖已经确认的值;流程中的未知分支不能以本表通过代替规则确认。数值表内的空值、0和-1分别保留;升级数组不默认使用玩家等级作为索引。\n\n'
body+=f"## 9. 当前待补充边界\n\n{c['boundary']}\n\n[返回总纲](../局外玩法设计文档_HEROLL_原作_v1.0.md) · [阅读规范](../文档规范与阅读说明.md)\n"
savecsv(d/'执行规则.csv',rule_rows)
if not checks:
t=m['tables'][0];r=tables[t]['rows'][0]
checks=[dict(用例ID=f"T{m['id']}-01",输入=f'{t}/Id={r["Id"]}',检查动作='按主键读取配置',预期=json.dumps(r,ensure_ascii=False),验收范围='配置读取')]
savecsv(d/'验收样例.csv',checks)
write(d/'设计与规则.md',body)
write(d/'README.md',f"# {m['folder']}\n\n{c['intro']}\n\n[设计与规则](./设计与规则.md)是本模块正文。数值和规则CSV与正文同目录,原作中文目录用于开发阅读。\n\n"+md(['配置用途','权威原字段表','记录数','开发阅读表'],rows))
rules_index.extend(dict(模块=m['folder'],**r) for r in rule_rows)
return body
# 条件概率作为明确标记的算术附表。
pool=defaultdict(list)
for r in tables['gacha']['rows']:pool[r['type']].append(r)
prob=[]
for key,rows in pool.items():
total=sum(float(r['prob']) for r in rows)
for r in rows:
prob.append(dict(池=key,Id=r['Id'],权重=r['prob'],池总权重=total,条件概率=float(r['prob'])/total if total else '',状态='条件推导',前提='同池所有行按权重参与,未计保底和动态筛选'))
savecsv(OUT/'13-抽卡与保底/抽卡条件概率.csv',prob)
overview='''# 局外玩法设计文档
版本:HEROLL 2.0.1 原作还原 v1.0(开发阅读版)
本文描述原作局外的资源、长期养成、任务、商业化入口和模式外围规则。组织方式参照局内v0.2.8:先定义玩家玩法和状态,再列操作规则、数值示例、权威配置和待确认边界。开发阅读以本总纲或对应模块《设计与规则》为入口。
## 0. 文档状态与采用原则
### 0.1 状态
原作配置确认用于表值及原注释,原作文本确认用于中文名称与效果,原作部分确认用于尚有执行细节缺口的规则。开发映射只解释状态拆分,条件推导只用于带前提的计算检查。缺少证据的精确规则标未知,不填写新设计默认值。
### 0.2 采用顺序
1. 已确认的原作执行证据用于确定实际流程。
2. 原作配置用于确定档位、消耗、奖励和参数。
3. 原作中文文本用于解释名称与玩家可见效果,不能独立确定内部计算。
4. 条件推导与开发状态拆分始终保留各自标识。
### 0.3 模块采用矩阵
'''
overview+=md(['模块','本版执行阅读结论','文档'],[(m['name'],CONTENT[m['id']]['intro'],f"[{m['folder']}](./{m['folder']}/设计与规则.md)") for m in modules])
overview+='\n### 0.4 整体玩法关系\n\n主线推进开放局外入口;资源通过奖励、巡逻、活动等来源获得;装备、遗物、宠物、坐骑、命运和地块提供长期成长;任务、通行证与活动为成长提供目标和收益。不同模式拥有自己的门票、奖励、赛季和配置,不能只用一套全局等级或计数器替代。\n\n'
overview+='[文档规范](./文档规范与阅读说明.md) · [规则完整度报告](./规则完整度报告.md) · [开发权威表索引](./开发权威表索引.csv)\n\n'
bodies=[]
for m in modules:
body=section_body(m)
# 总纲内展开全部模块正文,保留独立模块文件;将相对资源链接改到模块目录。
expanded=body.split('\n',1)[1]
expanded=re.sub(r'^### (?:\d+\.[\dA-Z]+ )?(.+)$',r'**\1**',expanded,flags=re.M)
expanded=re.sub(r'^## (\d+)\. (.+)$',lambda x:f"### {int(m['id'])}.{x[1]} {x[2]}",expanded,flags=re.M)
expanded=re.sub(r'\]\(\./([^)]+)\)',lambda x:f"](./{m['folder']}/{x[1]})",expanded)
expanded=expanded.replace('](../','](./')
bodies.append(f"\n## {int(m['id'])}. {m['name']}\n"+expanded)
write(OUT/'局外玩法设计文档_HEROLL_原作_v1.0.md',overview+''.join(bodies))
write(OUT/'README.md','# HEROLL 原作局外开发文档 v1.0\n\n先读[局外玩法设计文档](./局外玩法设计文档_HEROLL_原作_v1.0.md),或直接进入下列模块。正文采用局内v0.2.8的规则叙述、数值表、状态区分与权威CSV结构。\n\n'+md(['模块','模块正文'],[(m['folder'],f"[设计与规则](./{m['folder']}/设计与规则.md)") for m in modules])+'\n[文档规范](./文档规范与阅读说明.md) · [规则完整度报告](./规则完整度报告.md) · [权威表索引](./开发权威表索引.csv)\n')
savecsv(OUT/'开发权威表索引.csv',coverage)
savecsv(audit/'规则总索引.csv',rules_index)
savecsv(audit/'源文件清单.csv',manifest+[dict(来源=LOC.relative_to(ROOT).as_posix(),SHA256=sha(LOC),用途='原作中文文本')])
boundary=[dict(模块=m['folder'],状态='原作部分确认',可直接使用='原作配置、中文内容和已注明单位的参数',尚需补充=CONTENT[m['id']]['boundary']) for m in modules]
savecsv(audit/'待确认边界.csv',boundary)
write(OUT/'规则完整度报告.md','# 原作局外规则完整度报告\n\n本版完成26模块的开发阅读文档、原字段配置、中文内容和规则索引。文档组织不等于所有公式已经恢复,以下逐模块列出精确缺口。\n\n'+md(['模块','可直接使用','待确认边界'],[(x['模块'],x['可直接使用'],x['尚需补充']) for x in boundary])+'\n数据总量:132张模块配置表、11742条原记录。所有原行和空值保留。\n')
write(audit/'scope.md','# 范围\n\n原作HEROLL 2.0.1局外功能。以现有配置及原作中文文本生成开发文档;仅在新目录写入。旧局外目录及v0.2.8参考文档不作修改。\n')
write(audit/'timeline.md','# 生成记录\n\n2026-09-06:读取v0.2.8总纲及规则闭合报告,确定开发文档结构。\n2026-09-06:快照旧目录,以只读AST读取原模块划分,不运行旧生成器。\n2026-09-06:提取原作中文文本,重写26模块玩法、状态、规则、数值及样例。\n2026-09-06:核对配置逐单元格一致性、文档链接和旧目录保护结果。\n')
write(audit/'来源与复现.md','# 来源与复现\n\nEvidence:源文件清单提供每张原作表与中文文本资源的SHA256。\n\nFinding:规则总索引按原作配置确认、原作部分确认分类;中文目录直接关联原字段。\n\nPath:原配置与本地化文本 → 业务正文 → 同目录权威CSV → 验收查询。\n\n从项目根目录执行 `HEROLL_OUTGAME_DEV_v1.0/99-审计与证据/工具/build_dev_docs.py` 可重建本目录;使用Python3标准库,不运行旧目录生成器。\n')
# 独立读取写出的配置,检查源值及中文目录原字段一致。
cells=0
for m in modules:
for t in m['tables']:
for suffix in ['', '-中文目录']:
with (OUT/m['folder']/(t+suffix+'.csv')).open(encoding='utf-8-sig',newline='') as f: rows=list(csv.DictReader(f))
recovered=[{h:r[h] for h in tables[t]['headers']} for r in rows]
assert recovered==tables[t]['rows'],(m['folder'],t,suffix)
cells+=len(tables[t]['headers'])*len(tables[t]['rows'])
for p in OUT.rglob('*.md'):
for target in re.findall(r'\]\(([^)]+)\)',p.read_text(encoding='utf-8')):
if target.startswith(('https:','http:','#')):continue
assert (p.parent/target.split('#')[0]).exists(),(p,target)
after=snapshot();assert after==before, '旧目录发生变化'
result=dict(模块数=len(modules),模块配置表数=len(coverage),模块原记录=sum(x['行数'] for x in coverage),原作中文对照项=localized_count,校验原字段单元格数=cells,旧目录文件数=len(before),旧目录内容大小修改时间='全部保持不变',配置与中文目录原字段='逐单元格一致',文档相对链接='全部有效',完整规则状态='已整理开发文档;具体执行缺口见规则完整度报告')
write(audit/'校验结果.json',json.dumps(result,ensure_ascii=False,indent=2))
files=[dict(文件=p.relative_to(OUT).as_posix(),大小=p.stat().st_size,SHA256=sha(p)) for p in sorted(OUT.rglob('*')) if p.is_file() and p.name!='文件索引.csv']
savecsv(OUT/'文件索引.csv',files)
print(json.dumps(result,ensure_ascii=False,indent=2))
@@ -0,0 +1,61 @@
from pathlib import Path
import ast, csv, io, hashlib, json, re
ROOT=Path(__file__).resolve().parents[3]
OUT=ROOT/'HEROLL_OUTGAME_DEV_v1.0'
OLD=ROOT/'HEROLL_OUTGAME_2.0.1'
CORE=ROOT/'HEROLL_APK_REVERSE_ONLY/01_core_reverse_archive'
SOURCE=CORE/'resource-tables/extracted/csv'
LOC=CORE/'unity/assets/bin/Data/65724e71f83a175409c383f97a5a8ed9'
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest()
def snapshot():
return {p.relative_to(OLD).as_posix():dict(size=p.stat().st_size,mtime_ns=p.stat().st_mtime_ns,sha256=sha(p)) for p in OLD.rglob('*') if p.is_file()}
def constants(path):
result={}
for node in ast.parse(path.read_text(encoding='utf-8')).body:
if isinstance(node,ast.Assign) and len(node.targets)==1 and isinstance(node.targets[0],ast.Name):
try: result[node.targets[0].id]=ast.literal_eval(node.value)
except (ValueError,TypeError): pass
return result
def write(p,s):
assert p.resolve().is_relative_to(OUT.resolve()),p
p.parent.mkdir(parents=True,exist_ok=True); p.write_text(s,encoding='utf-8')
def savecsv(p,rows,fields=None):
assert p.resolve().is_relative_to(OUT.resolve()),p
p.parent.mkdir(parents=True,exist_ok=True)
if fields is None: fields=list(rows[0]) if rows else ['说明']
with p.open('w',encoding='utf-8-sig',newline='') as f:
w=csv.DictWriter(f,fieldnames=fields);w.writeheader();w.writerows(rows)
def md(headers,rows):
def esc(x): return str(x).replace('|','\\|').replace('\r','').replace('\n','<br>').replace('<sprite','&lt;sprite')
return '| '+' | '.join(headers)+' |\n| '+' | '.join(['---']*len(headers))+' |\n'+''.join('| '+' | '.join(esc(v) for v in row)+' |\n' for row in rows)
def load():
c=constants(OLD/'99-审计与证据/工具/build_outgame.py')
modules=[]
for line in c['SPECS'].strip().splitlines():
k,name,tables,role,flow,unknown,terms=line.split('|')
modules.append(dict(id=k,name=name,folder=k+'-'+name,tables=tables.split(),role=role,flow=flow,unknown=c['DETAILS'].get(k,unknown),facts=c['FACTS'][k]))
tables={}
for p in sorted(SOURCE.glob('*.csv')):
with p.open(encoding='utf-8-sig',newline='') as f: raw=list(csv.reader(f))
h=next(i for i,r in enumerate(raw) if 'Id' in r and 'StringId' in r)
assert all(len(r)==len(raw[h]) for r in raw[h+2:] if any(r)),p
tables[p.stem]=dict(headers=raw[h],types=raw[h+1],comments=raw[h-1],rows=[dict(zip(raw[h],r)) for r in raw[h+2:] if any(r)])
text=LOC.read_bytes().decode('utf-8',errors='ignore').replace('\r\n','\n')
marker=',ko,en,ja,zh-tw,it,fr,th,tl,zh-cn,es,de,id\n'
start=text.index(marker)+len(marker); local={}
for row in csv.reader(io.StringIO(text[start:])):
if len(row)!=13: continue
key=row[0].strip('\x00 ')
if key and re.fullmatch(r'[A-Za-z0-9_. -]+',key):
local[key]={'zh':row[9],'en':row[2],'tw':row[4]}
return modules,tables,local,c
if __name__=='__main__':
modules,tables,local,c=load()
print('localization_entries',len(local))
for name in ['equipment','relic','pet','mount','starchallenge','item']:
print(name)
for r in tables[name]['rows'][:4]:
print(r['StringId'],[(k,local[v]['zh']) for k,v in r.items() if v in local and local[v]['zh']][:4])
@@ -0,0 +1,159 @@
"""开发阅读正文。字段名用于定位配置,叙述围绕玩家功能及状态。"""
CONTENT={
'01':dict(
intro='大厅是局外功能的统一入口。玩家通过主线进度和账号开放日序逐步接触不同玩法;引导负责把玩家带到指定界面,并在目标阶段提供奖励。入口是否开放、入口排序和引导是否完成是不同状态。',
states='已通关阶段:用于检查功能进度条件;账号开放日序:用于按日开放的内容;功能开放键:记录内容入口资格;引导步骤:区分当前步骤与已完成步骤',
rules=[('功能开放','模式选择、巡逻、每日副本、讨伐等分别读取自己的开放条件,不能以“某个局外功能已开放”替代所有模块的判断。阶段值保留原索引,不擅自全部加1。'),('新手引导','引导步骤可以清理已有界面、等待前后动画、设置遮罩以及指定触摸推进方式。进入目标界面与领取引导奖励分开描述;奖励读取该条引导的道具ID和数量。'),('入口显示','模式展示顺序由入口排序表决定。社区入口按语言使用对应配置;通知标题正文与玩法奖励无关,不能当成邮件奖励内容。')],
flow=['选择要展示的功能入口,读取其开放条件。','结合账号进度显示入口及当前引导目标。','进入目标界面后交给相应模块处理操作。','引导完成状态和模式开放状态分别更新;具体完成事件以引导配置为准。'],
boundary='账号阶段索引、按日开放的首日编号以及引导结束事件尚需进一步核对;不在本版填写通用默认条件。'),
'02':dict(
intro='局外资源承担长期养成、入场和购买等用途。金币、钻石、体力、碎片和票券不能仅按显示图标合并成一种余额;奖励除了普通道具,还可能指向宠物、装备或其他内容类型。',
states='资源ID及数量:同一种资源的库存;奖励类型与引用:确定奖励应进入哪种内容;道具组选择状态:确定礼包候选或选择结果;大厅资源映射:记录带入局内的资源及数量',
rules=[('资源身份','使用数值ID或字符串键识别资源,中文名称只用于显示。解析奖励时同时保留奖励类型与奖励ID,宠物ID不能直接按普通道具ID解释。'),('体力','基础体力上限为40,主线入场体力需求配置为5,VIP额外上限配置为20。恢复配置原数组为600、1;其计时单位与两项含义没有调用证据时不直接写成运行公式。'),('道具组与选择','一个道具组可包含多项道具、数量和权重。对应数组按位置关联,数量为0、空值与权重0不是同一含义。道具是否随机使用、选择数量与后续选择组按道具配置分别读取。'),('带入局内','大厅票券存在普通值与增强值两档。它把局外资产映射到局内资产,而不是把所有局外库存复制进对局。')],
flow=['从奖励来源得到奖励类型、内容ID和数量。','依据类型找到目标道具或养成对象;若为道具组,继续解析组内内容。','使用资源时读取对应功能的消耗项;库存数据按资源ID区分。','进入对局时只转换大厅映射表指定的资产。'],
boundary='体力恢复数组的单位及索引、道具组抽取是否有放回仍待确认。'),
'03':dict(
intro='永久装备是局外长期培养的装备。玩家获得装备后配置到对应部位,通过强化提高数值,通过合成推进品质和星级。装备身份、品质星级阶段、强化等级必须分开记录。骑士团长矛、烈火之剑等名称来自原作中文文本。',
states='装备身份ID:决定部位、类型与技能;品质星级阶段:决定品质、星数和强化上限;强化等级:用于逐级成长;装备槽位:记录各部位当前装备;材料库存:按材料ID计数',
rules=[('身份和品质','目录共有31条装备身份与4条合成材料。槽位0为武器、1为头部、2为盾牌、3为衣服,主属性分别为攻击、命中、防御、生命。装备目录还定义类型、特殊标记和技能组。品质星级由另一张表给出,不能把名称中的装备种类视为品质,也不能用强化等级代替品质阶段。'),('强化','普通0星的强化上限为5,稀有0星为7,史诗0星为10;后续各阶段读取完整上限表,永恒3星档为135。每一级的材料和金币采用逐行配置,不拟合成统一等差价格。'),('合成','合成材料包含“指定材料阶段与数量”和“同品质任意装备数量”两类要求。原注释对任意同品质材料注明带星级者不能用作材料。空材料字段不能推导为免费合成。'),('基础属性与技能','普通装备和特殊装备分别拥有基础值、强化增量。技能是按档保存的数组,必须保留技能、主参数、额外参数的对应位置;当前资料不足以把数组位置自动映射为每个星级。')],
flow=['选择一件装备,读取其身份、品质星级阶段和强化等级。','强化时找到适用的逐级消耗行及该品质的强化上限。','合成时读取材料条件,分别展示指定阶段材料与任意同品质材料。','结果界面分别展示品质星级变化、强化等级及属性变化;属性组合公式未确认的部分不使用临时倍率填充。'],
boundary='合成材料是否包含目标本体、合成后强化等级继承及基础属性组合公式尚未恢复。'),
'04':dict(
intro='槽位强化培养的是装备槽,而不是某一件装备。永久属性则按攻击、生命、命中等属性独立成长。两套进度与装备本体强化分开管理。',
states='各槽位等级:按槽位编号独立记录;各永久属性等级:按属性类型独立记录;升级资源:材料与金币余额',
rules=[('槽位升级','槽位表中的等级行描述从该等级升到下一等级所需的资源。槽0处于等级0时,需要道具119×2和金币1000;处于等级1时,需要道具119×3和金币2000。'),('升级效果','普通升级值与特殊效果键、特殊效果数值单列。展示槽位增益时不能将特殊值直接累加进普通升级值。'),('永久属性','攻击永久属性的每级增量配置为1、上限1000,百分比标记为True;命中等属性应读取自己的增量。百分比标记不意味着所有数值都用同一种内部缩放。')],
flow=['选择槽位或永久属性,读取该对象自己的等级。','读取下一步消耗、等级上限及效果。','完成升级后更新对应对象等级,并重新显示该对象的下一步消耗。'],
boundary='槽位效果作用于基础值还是最终值,以及属性百分比的内部缩放尚待确认。'),
'05':dict(
intro='遗物是一套独立于永久装备的长期配置。剪刀、冰冷的书、战舰号角等遗物分别具有装备槽、属性或技能,通过品质星级合成推进成长。',
states='遗物身份ID:决定属性与技能;品质星级阶段:决定当前合成条件;遗物槽位:记录配置对象;遗物材料:区分材料身份与阶段',
rules=[('遗物效果','遗物可同时有基础属性和技能参数。属性数值是否按百分比解释读取本条标记,不把遗物描述中的风味文本当作技能规则。'),('品质合成','普通、稀有、史诗的材料数量字段为2。传说及以上存在任意同品质材料条件,必须按各行分别显示需求。材料字段的数量不直接证明总计消耗几件遗物。'),('装备与材料','遗物材料标记与遗物装备槽是独立字段;品质阶段和星数与永久装备也不能共用同一数据行。')],
flow=['查看遗物身份与当前品质星级。','按遗物槽位配置使用对象。','准备合成时读取遗物合成表,展示该阶段材料要求。','结果按遗物属性和技能配置重新展示。'],
boundary='材料是否含目标本体、遗物技能叠加及合成后的继承规则尚待确认。'),
'06':dict(
intro='宠物提供对局中的协助能力,并通过碎片和金币长期成长。冰冻蛙可以冰冻敌人,黑暗喵可处刑低生命敌人;具体效果模板和数值应一起读取,不能只显示宠物基础属性。限时成长活动是宠物系统中的独立进度。',
states='宠物拥有状态:区分未解锁和已拥有;宠物等级:对应养成消耗与技能;碎片金币余额:支付养成投入;已开放技能:按宠物和等级区分;活动经验等级:独立于常规宠物等级',
rules=[('解锁和重复获得','宠物有解锁价格及重复转化字段。是否拥有即转化要读取该宠物标记;冰冻蛙的该标记为False,不能对所有宠物统一执行获得即转钻。'),('碎片成长','冰冻蛙碎片数组有20项,而金币数组有19项;原注释指出碎片数组第一项不使用。因此两数组不能直接按相同索引拼成升级表,也不能仅用最长数组长度确定最高等级。'),('技能开放','技能按宠物ID和开放等级配置。冷却、伤害和额外效果各自有基础值及增量;原作中文模板中的{B}等占位符暂保留,不擅自替换成某个字段。'),('成长活动','活动指定目标宠物、消费道具、事件池和时间。事件给出选择概率、奖励与经验,活动等级表负责经验需求及奖励,与常规碎片升级分开。')],
flow=['打开宠物信息,显示拥有状态、等级、技能与成长消耗。','常规升级读取该宠物的碎片与金币数组,并按已确认的索引规则定位。','技能界面按等级条件展示相应技能条目。','成长活动单独读取事件、经验和奖励,不写入普通宠物碎片等级。'],
boundary='碎片与金币的精确级别索引、效果模板占位符绑定、满级转化及技能成长公式尚待确认。'),
'07':dict(
intro='坐骑提供基础属性和被动能力,通过升级与觉醒形成两条成长。斯雷普、灵魂斯雷普、赤红飞龙等坐骑分别具有品质、解锁配置和技能组。拥有加成与出战效果必须区分。',
states='坐骑拥有状态:记录解锁;坐骑等级:对应升级材料与品质收益;觉醒阶段:对应碎片和技能等级;持有效果状态:区分拥有加成;使用坐骑:决定本次配置的坐骑',
rules=[('解锁','斯雷普的碎片解锁字段为50、钻石字段为500。部分坐骑还有商品键;这些字段可支持成本展示,但不足以证明所有解锁方式同时支付。'),('升级','升级表exp的原注释为坐骑粉末。等级1、2、3行分别配置300、350、400,不把这一字段当作角色经验。品质不同的收益分别读取。'),('觉醒','稀有觉醒0档消耗碎片50,拥有加成标记True,生命数值0.02、被动技能1等级为1。后续档按品质和阶段读取,不能按该档直接外推。'),('技能触发','坐骑技能分组包含事件、目标、概率、持续时间和叠加上限。主技能、持有效果与技能触发必须分开显示;数值单位以对应技能和字段为准。')],
flow=['选择坐骑并显示解锁、等级与觉醒进度。','升级读取等级行及坐骑品质列。','觉醒读取品质与阶段对应的碎片、属性和技能等级。','显示效果时把持有效果与使用效果分组。'],
boundary='解锁方式的组合关系、升级觉醒的属性合并公式和效果应用顺序尚待确认。'),
'08':dict(
intro='命运系统从词条候选中提供属性和特殊效果。词条具有品质、启用标记以及两套权重;它不是装备品质随机系统的另一种名称。',
states='命运栏位开放状态:对应开放阶段数组;当前词条:保存属性或特殊效果;抽取类型:决定使用哪套权重;召唤资源:保存消耗',
rules=[('候选资格','命运词条的启用开关和权重都需保留。S档命中词条虽然存在,启用为False且两套权重为0,不能因为表中有记录就纳入正常候选。'),('效果数值','S档攻击词条的百分比标记为True、数值20,普通/稀有权重分别为2和1。生命、防御及特殊效果应读取各自条目,不共用攻击值。'),('召唤配置','开放阶段数组为5、5、5、7、7;召唤消耗数组为40、40、10000、10000。数组项对应哪种栏位或资源,当前不做未经确认的绑定。')],
flow=['显示已开放的命运栏位及已有词条。','选择召唤类型后读取对应成本与候选权重组。','展示结果时区分普通属性、百分比标记和特殊效果。'],
boundary='栏位与消耗数组映射、两套权重对应操作以及锁定替换规则尚待确认。'),
'09':dict(
intro='收藏以装备组合为目标。每条收藏要求特定装备达到指定等级与品质,完成后对应一项永久属性收益。开发需要把“拥有装备”“达到组合条件”“获得收藏收益”作为不同判断。',
states='收藏条目状态:区分未满足与满足;装备组合进度:逐项核对身份等级品质;收藏属性:记录目标属性及收益',
rules=[('组合条件','收藏001要求装备103、111、118,其等级要求均为1、品质要求均为1。三组数组一一对应;不能把所有装备要求的最高等级应用到整个组合。'),('收藏收益','收藏001的攻击收益配置为0.4,收藏002为2.0。该表没有统一百分比开关,不能仅凭数值大小把它们转换成40%或2%。'),('开放','收藏功能开放阶段配置为6;每条收藏还保留自身启用与展示顺序。')],
flow=['读取收藏条目,将三组条件按位置组成要求列表。','逐项展示玩家装备与条件的差异。','显示该收藏对应的属性收益及满足状态。'],
boundary='是否要求同时持有、是否消耗收藏装备、领取方式与收益单位尚待确认。'),
'10':dict(
intro='外观包括皮肤、头像、头像框和称号。头像类主要改变展示;皮肤同时配置单件属性和套装协同,因此不能把所有外观统一做成纯展示数据。',
states='已解锁外观集合:按外观ID记录;当前各槽皮肤:对应外观部位;当前头像头像框称号:分别选择;套装组成:用于查看协同档',
rules=[('皮肤部位','皮肤按外观槽位配置,对应动画与资源。皮肤所附属性和原装备身份分开保存。'),('套装协同','南瓜剑皮肤附加攻击数值5,套装最大件数4;原注释的1至4件协同数组为0、50、100、150。数值缩放及是否持有即生效尚未确认,不在本版补成百分比公式。'),('头像与称号','头像和头像框按开放键判断资格并按顺序展示;称号另有颜色字段。文字键是展示引用,不作为拥有凭证。')],
flow=['列出已开放或可查看的外观条目。','选择部位皮肤或头像类外观。','皮肤详情分别展示单件属性与套装各档收益。'],
boundary='持有与穿戴的加成条件、套装收益单位及称号获得条件尚待确认。'),
'11':dict(
intro='局外地块系统让玩家收集和培养可影响局内棋盘的地块。它同时包含地块等级、技能、研究及地块包挂机四套关联进度,不能用一个总等级代替。',
states='地块身份品质等级:决定成长及技能;配置地块集合:用于局内牌组;总升级次数与研究阶段:连接研究成长;研究节点:记录选择或升级进度;挂机包和里程:独立累计',
rules=[('地块对应关系','地块通过字符串键关联局内事件,例如GoodEventSmithy。稀有铁匠地块默认配置标记True,技能开放等级8;史诗与传说则配置多项等级和技能,须按数组保留。'),('研究推进','总升级次数24对应研究等级1,配置鞋类武器额外等级1;48对应研究等级2及戒指配置。这里是研究阶段查表,不是每次升级都增加武器1级。'),('研究节点','研究0的攻击节点0数值为0.3、消耗为8;节点2消耗已变为10。应按研究等级和节点索引查询,不沿用首节点价格。'),('地块包挂机','每30分钟形成1包,最多累计30包,开放时配置初始10包;容量折算为900分钟,即15小时。包权重为90、10、0,里程需求37、奖励3005×1。里程奖励与自然包候选分开结算。')],
flow=['查看地块身份品质、当前成长和已配置状态。','成长后依据总升级次数查询研究阶段。','研究节点按阶段与节点读取属性和消耗。','挂机包界面显示累计容量和里程奖励,局内牌组使用地块到事件的关联键。'],
boundary='地块装备容量、升级索引、研究经验累计公式和里程重置处理尚待确认。'),
'12':dict(
intro='巡逻把离线累计时间转成长期资源。奖励随已通关进度变化;快速巡逻按固定时长给收益并受每日次数和体力限制。该计时与地块包挂机完全独立。',
states='巡逻累计时长:受24小时容量限制;已通关阶段:决定收益档;快速巡逻次数:独立每日计数;广告巡逻次数:独立每日计数',
rules=[('普通巡逻','巡逻每6分钟为一次检查周期,累计上限1440分钟。金币使用每小时产率,其他奖励还配置每周期数量、上限与掉落参数;不能直接按同一种倍率生成所有奖励。'),('快速巡逻','一次对应360分钟收益,消耗体力15,日最大次数3。广告快速巡逻日次数1单独保存,不能直接把3与1相加后当作所有操作共用次数。'),('倍数','倍数候选为1、1.2、1.5,对应权重65、25、10。若按全部候选线性加权,期望倍数为1.1;这是条件推导,不证明每类奖励都应用该倍率。')],
flow=['计算可展示的累计巡逻时间并读取当前通关收益档。','普通领取展示本次金币及各道具奖励。','快速巡逻读取360分钟收益,并显示体力与当日次数。','广告巡逻独立显示当日使用次数。'],
boundary='不足检查周期的舍入、领取后计时重置和倍数适用奖励范围尚待确认。'),
'13':dict(
intro='抽卡由三个部分组成:玩家点击的抽卡按钮、按钮指向的奖励池、该池的保底规则。它们必须按池类型关联,不能按按钮名称或显示文字推断价格和保底。',
states='抽卡池类型:决定奖励候选;按钮档:决定抽数和消费;池保底计数:按池区分;广告周期使用状态:独立于付费次数;替代道具余额:用于展示抵扣',
rules=[('按钮与消费','shopGacha单抽配置钻石30、十连300。替代道具为204,抵扣字段30表示单道具可替代的资源量,不能显示成每抽消耗30张券。另有消耗资源110×35的按钮及每日广告按钮。'),('奖励池','奖励行同时记录类型、品质、ID、数量和权重。相同池类型才属于同一候选组;奖励ID为-1时表示指定类型全池,仍须保留奖励类型。'),('保底','高级装备池20次保底、提前命中重置标记False;增强装备池50次、重置标记False;坐骑池400次、重置标记True。坐骑记录名字虽带50,真实次数以400为准。'),('概率表达','配置权重先按池分组再讨论归一化,不能将某行权重100直接写成100%掉落。十连如何逐次推进保底及命中保底的优先级仍需要原作执行证据。')],
flow=['由按钮找到池类型、抽数、成本和免费广告标记。','读取同池候选与保底规则,展示价格及保底目标。','抽取结果保留奖励类型、ID、数量,交给对应内容模块展示。','更新计数的精确时点及十连分支待确认,不把整次十连当作一次保底进度。'],
boundary='十连逐抽结算、券抵扣不足时的组合支付、跨池计数以及保底命中顺序尚待确认。'),
'14':dict(
intro='局外商店包括常驻商品、每日随机商店、接力礼包、分组兑换、购买积分和存钱罐。它们共用资源与商品目录,但消费、限购、刷新和奖励持续方式不同。',
states='商品标识与可见状态:关联具体商品或触发入口;购买次数与周期:按商品分别记录;每日商店槽位:记录当前候选;接力档位:记录礼包顺序;存钱罐累计值:记录胜败增加',
rules=[('商品购买','每件商品分别读取消费类型、价格、限购数和重置周期。商品价格字段与美元参考字段分开,不将两者相乘或当作自动换算。一次性内容和每日发放内容也分开。'),('每日商店','候选有各自权重和售价。每日重掷次数配置为3;没有证据支持直接套用局内商店3、5、7的刷新价格链。'),('接力与兑换','接力礼包按礼包组和顺序组织,免费档与商品档分开。兑换同时有投入道具、数量、产出及兑换次数周期,不能只显示奖励而忽略投入。'),('存钱罐','档1从60累计到120,胜利增加20、失败增加10;购买窗口1440分钟、冷却4320分钟。各档数值不同,应切换到对应档行。')],
flow=['按商品类别选择对应目录,读取开放或触发条件。','展示价格、内容、次数与周期;每日发放额外展示持续天数。','接力商品按当前档定位,兑换商品显示投入产出。','存钱罐按当前档展示进度、购买时限与冷却。'],
boundary='触发条件优先级、每日候选筛选和刷新操作的精确消耗尚待确认。'),
'15':dict(
intro='任务描述玩家要完成的目标,任务组决定目标数量和奖励;成就则可通过前置链逐步开放。一个条件模板可以被活动或通行证复用,不应重复定义事件语义。',
states='任务实例与所属组:区分同模板的多个目标;当前计数:保存目标进度;开放状态:决定是否展示与是否计数;领奖状态:区分完成与已领取;成就前置状态:连接前置链',
rules=[('任务条件','任务定义表给事件类型、条件键及附加参数。任务组引用该定义后再设置目标次数、领取类型及奖励;“目标完成”与“领取奖励”是两项状态。'),('锁定时计数','任务组有锁定时是否允许累计的配置,不能统一让所有未开放任务累积进度,也不能统一清空。'),('成就前置','前置成就未完成时,原注释说明后续成就不会显示。普通装备收集成就目标4,配置每局开始重置进度,奖励道具52000×1;该重置只适用于带此标记的条目。')],
flow=['通过任务组或成就ID定位目标。','连接事件定义,读取目标和锁定累计标记。','事件达到目标后显示完成状态及可用奖励。','领取类型和前置链分别处理,避免把UI显示状态当作进度。'],
boundary='事件计数的精确时机、隐藏成就显示条件和各领取类型枚举语义尚待确认。'),
'16':dict(
intro='签到、新人和回归是三类不同福利。签到按日序提供奖励,新人和回归提供有限时长或次数的增益。签到漏天补领也由专门字段配置。',
states='签到类型与日序:定位奖励;签到领取记录:区分各日奖励;新人加成剩余时间次数:区分两种限制;回归资格及增益:单独记录',
rules=[('签到','签到类型和日序共同决定奖励。补签方式及价格独立记录,不等于错过的天数可以一律免费补领。'),('新人','新人增益4320分钟,即3天;额外加速50、奖励加成20。加速次数字段为-1、奖励次数1;-1原值保留,不自行解释为无限。'),('回归','距上次登录20160分钟,即14天,是回归门槛配置;增益持续4320分钟,即3天,奖励加成100,奖励与加速均配置3次,并关联回归任务。')],
flow=['判断是签到、新人还是回归入口,分别读取其配置。','签到按类型和日序显示奖励与补签方式。','新人和回归同时显示持续时间与剩余次数。','回归任务交由任务模块读取,不与普通签到进度合并。'],
boundary='日切边界、漏签可补日期以及-1次数字段的运行含义尚待确认。'),
'17':dict(
intro='通行证通过任务和游玩经验推动等级,并按普通、高级和高级Plus三条轨道发放配置奖励。不同通行证可以有不同经验来源和赛季规则。',
states='通行证ID与赛季:区分进度域;当前经验等级:推进奖励档;奖励轨道资格:区分普通高级Plus;各档领取状态:避免混用轨道;日任务及游玩经验:分别累计',
rules=[('普通通行证任务','normalPass每日任务6、每周任务9、挑战任务3;每日重掷次数3。其他通行证有独立行,不自动沿用这组数值。'),('游玩经验','normalPass每层通关经验10,普通每日游玩上限200,高级400。每日固定经验为0;固定经验、任务经验和游玩经验必须分开读配置。'),('等级奖励','奖励按通行证ID、赛季与等级定位,经验需求和重复配置单列;普通、高级、高级Plus各有自己的奖励ID、数量与键。'),('赛季结束','赛季表分别给出购买阻止、游玩结束与赛季结束时间,不把所有入口统一在同一个时刻关闭。')],
flow=['定位当前通行证及赛季,显示经验等级与奖励轨道。','根据任务或游玩来源读取经验,游玩经验显示本日上限。','达到奖励档后按轨道展示奖励及领取状态。','活动关联商品、任务与排行榜通过赛季引用定位。'],
boundary='经验上限重置时点、重复等级、补领奖励和跨赛季进度处理尚待确认。'),
'18':dict(
intro='主线入口负责选关、进度展示和通关奖励。星级挑战则在完成关卡之外增加目标,按累计星数提供额外奖励;关卡中的战斗细节不在此重复定义。',
states='当前及已通关阶段:决定选关进度;模式:区分普通等玩法;各挑战完成状态:累计星数;星数奖励领取档:独立于通关奖励',
rules=[('主线关卡','关卡以模式和阶段识别,不能只用一个数字混合所有模式。关卡目录保留Boss与场景等入口信息。'),('通关与层数奖励','关卡奖励可能按模式、阶段、层数、进度和是否通关区分;层数奖励是另一张表。两种来源不能因奖励道具相同就合并成一次领取。'),('星级条件','原作中文文本确认挑战0为游戏通关、挑战1为以指定百分比以上生命值通关、挑战2为指定秒数内击杀首领;对应参数0、70、5。因此后两项可显示为70%生命、5秒击杀首领,边界是否包含仍需验证。'),('星数收益','累计1星档奖励钻石50,奖励2与3的ID为-1,不能把该占位ID作为实际道具发放。')],
flow=['选关时读取模式与阶段,显示已开放进度。','完成局内玩法后,分别更新通关状态与星级条件。','按星数定位奖励档,按关卡结算条件定位通关奖励。'],
boundary='生命百分比和秒数条件的精确比较边界、星级能否跨次累计及奖励重复领取条件尚待确认。'),
'19':dict(
intro='每日副本与探索副本是两个独立玩法。每日副本按星期和难度给奖励;探索副本有独立门票、背包、装备成长及分解体系,不与巡逻挂机共用进度。',
states='每日副本星期难度:选择奖励档;探索门票和恢复进度:控制入场;探索章节:推进地图;探索背包装备:独立于永久装备',
rules=[('每日副本','奖励按星期和难度查询,推荐阶段来自难度配置,战斗属性按难度和层数区分。不能把星期枚举直接假定为从星期一开始的1至7。'),('探索入场','探索门票初始6、上限12,恢复间隔14400秒,即4小时。背包容量30,原注释明确不包含已装备物。'),('探索装备','探索装备按等级和类型读取各品质属性;分解表分别保存每品质的回收ID及数量。永久装备的强化成本与合成规则不适用于这套装备。'),('掉落','掉落权重按当前已掉落数量分档,同时有失败及多个品质数组;它说明候选权重结构,不足以确定完整的掉落保护顺序。')],
flow=['选择每日副本或探索副本,进入各自的进度域。','每日副本按星期难度显示奖励,探索副本显示门票和章节。','探索获得的装备进入专用背包,属性与分解按探索表读取。'],
boundary='星期枚举、满票后的恢复处理、探索掉落权重算法和分解操作约束尚待确认。'),
'20':dict(
intro='无限模式使用独立难度、层数和赛季Boss。它有入场消耗、奖励排除项与金币上限,不能直接复用普通主线的所有收益规则。',
states='无限难度:选择数值列;当前层数:选择敌方与价格参数;赛季:定位Boss;当前收益:对应金币上限和排除项',
rules=[('入场','入场资源ID109,数量5。奖励排除ID为101,即钻石;不把该排除项当作额外消耗。'),('奖励上限','金币上限数组为100000、150000,原注释指难度分别对应的最大金币量。难度枚举映射未确认前保留两个档值。'),('商店涨价','每层涨价参数为1、2,单位为百分数;最终价格的小数舍去由原注释确认。逐层复利还是基于初始价格累计仍未确认,不能只凭两项数值写完整公式。')],
flow=['选择难度与赛季配置,显示入场资源。','按层数查询对应怪物组及属性。','收益显示分别处理排除项和金币上限,商店价格使用本模式规则。'],
boundary='难度数组映射、完整价格公式和金币上限的累计范围尚待确认。'),
'21':dict(
intro='讨伐以赛季预设阵容和Boss击杀分数为核心。入场门票、普通分数段奖励、最高分奖励是三套独立配置;试用宠物等级不改变玩家普通宠物养成等级。',
states='讨伐赛季:选择预设与奖励;门票及恢复进度:控制参加次数;击杀数与本次得分:推进对局;历史最高分:对应独立目标奖励;试用阵容:记录宠物及等级',
rules=[('门票','初始2张、上限10张,每43200秒恢复,即12小时;每次配置消耗道具155×1。'),('预设','赛季宠物表按阵容编号提供宠物ID和试用等级;赛季预设还可覆盖强化项。这些是赛季内容,不应覆盖玩家永久宠物记录。'),('奖励','普通奖励按档位、赛季、分数下界与上界定位;最高分奖励另按最高分门槛。两套奖励同时保留,不合成同一张“按分数发奖”表。'),('计分','Boss击杀数量对应击杀分数与战斗属性档,按表查询,不将首个Boss分数用于全部击杀。')],
flow=['确定赛季,展示门票与预设阵容。','进入玩法时使用预设及试用等级。','玩法结果分别关联普通分数段和最高分门槛。'],
boundary='分数端点、门票实际消耗时点和最高分奖励重复规则尚待确认。'),
'22':dict(
intro='药水竞技把药水养成、重量配置、竞技门票和段位评分结合起来。药水的重量、冷却、作用值是不同维度;不能用药水等级直接替代整套配装条件。',
states='药水等级与组合:决定重量及效果;门票与恢复进度:控制参加次数;当前MMR及段位:对应竞技进度;当前赛季:关联通行证;竞技加成:按条件生效',
rules=[('药水养成','每种药水分别有基础重量及增量、基础冷却及增量、主值和额外值以及碎片成本。技能文字保留原作模板,升级公式不套用宠物公式。'),('门票','setting2适用赛季列表为4至19,门票上限10,恢复3600秒,即1小时;setting1是赛季3的独立配置,不按ID大小选择配置。'),('段位','段位表给出MMR上下界和步长,另有推荐战力、通行证经验。竞技赛季跟随potionBattlePass,赛季调整字段-5是段位调整配置,不能解释成直接减5MMR。'),('积分修正','胜败基础分、战力差修正、额外修正和最终修正分别保存,顺序不明时不生成最终积分算式。')],
flow=['显示药水等级、组合重量和效果。','按适用赛季选择竞技配置,显示门票与当前段位。','结算后根据MMR映射段位;积分变化公式只使用已确认部分。'],
boundary='重量组合约束、完整MMR公式与全局参数和专表的优先关系尚待确认。'),
'23':dict(
intro='排行榜连接玩法成绩和名次奖励。不同榜单可以按赛季或自动周期运行;奖励应按榜单键和名次区间读取,不按界面标题匹配。',
states='榜单键和赛季:确定排行榜实例;当前名次:用于奖励区间匹配;玩法引用:确定成绩来源;奖励档:定位内容',
rules=[('榜单关联','引用表以排行键、引用类型和来源键连接玩法或活动。名字相近的活动不自动共用同一榜。'),('名次奖励','奖励按榜单键和最小最大名次配置;ID与数量为并列数组,必须按位置配对。2025万圣节活动第1名配置钻石100和金币10000。'),('周期','是否赛季排行与是否自动赛季是独立标记;不同环境排期字段完整保留,开发展示以明确选定的排期域为准。')],
flow=['由玩法或活动找到对应榜单键。','显示当前榜单赛季和名次。','按本榜名次区间展示奖励预览。'],
boundary='同分排序、名次端点及周期结算时刻尚待确认。'),
'24':dict(
intro='赛季活动组合签到、任务、转盘、兑换和小游戏。活动开放期与兑换期不一定同时结束;超级球是具有独立奖池和统计阶段的活动,不能当作普通转盘处理。',
states='活动及赛季:选择排期;活动阶段:区分参与与兑换;活动资源:保存消耗奖励;超级球奖池参数:区分初始奖池与增加值;统计阶段:独立时间段',
rules=[('活动生命周期','活动表分别记录活动结束与兑换结束,并通过键关联商品、兑换、小游戏与签到。活动入口展示应区分“玩法可参与”和“资源可兑换”。'),('活动任务','任务可关联通行证经验,同时给道具奖励;经验与道具分开处理,不能把经验数作为道具数量。'),('超级球','赛季1初始奖池10000,每参与者增加奖励范围1至10,前三名分配参数30、30、40。开始结束时间原注释为UTC零点,统计30分钟。参与人数如何转成奖池仍不凭字段名补造公式。')],
flow=['由活动键确定开放、参与、兑换等阶段。','进入子功能时使用该活动引用的任务、转盘、商店或小游戏。','超级球单独显示奖池参数与统计阶段。'],
boundary='活动边界的包含规则、超级球人数增量计算及多活动重叠选择尚待确认。'),
'25':dict(
intro='活动小游戏包含泡泡模板、拆卡包、抓娃娃、骰子投注、骰子机器、拼形、弹珠和刮奖等。这里统一组织入口与奖励,但每种玩法保留自己的规则参数,不建立一个通用概率替代所有小游戏。',
states='小游戏包及活动键:定位玩法;游玩资源:对应单次消耗;玩法模板或档位:选择布局和奖励;弹珠容量恢复:专用状态;广告增益次数时间:专用状态',
rules=[('入口与资源','小游戏包同时配置初始资源、游玩资源、单次消耗与兑换资源。它们可能不是同一种道具,不能只记录一个“活动币”。'),('各类奖励池','拆卡包按包档给成本与奖励概率;抓娃娃还有胶囊档、品质、尺寸参数;刮奖有奖励列表和概率。概率字段相同名称不代表同一归一化范围。'),('弹珠','初始容量15,恢复900秒,即15分钟;该行等级原值-1。广告金钉每日最多3次,持续180秒;效果数组150、200、250、300、330保留原档,不自动当百分比。'),('布局模板','泡泡和拼形表描述行列布局,不是奖励表。布局配置与活动资源、排行引用分别维护。')],
flow=['根据小游戏包读取入口资源及对应玩法。','加载该玩法的模板、档位或候选奖励。','展示本次收益及活动资源变化,弹珠恢复和广告增益单独显示。'],
boundary='各小游戏的胜负判定、碰撞到奖励的映射以及奖励权重处理尚待补充。'),
'26':dict(
intro='NPC系统通过好感档改变对话、服饰与收益参数,并包含独立签到轮次和特殊养成商店。好感档与特殊商店的属性升级不能合并成一条等级。',
states='NPC好感档:决定对话服饰收益;签到组轮次日序:决定当日奖励;特殊属性等级:按商店条目记录',
rules=[('好感收益','好感1配置金币收益参数1.1、钻石获得概率参数5及槽位品质数组。好感2对应1.2和10;对话性格与服饰也随档不同。金币收益与钻石概率不能使用同一个计算字段。'),('NPC签到','签到以组、轮次、开始日和日序定位奖励,与全局活动签到类型分开。'),('特殊属性商店','攻击升级条目权重30、基础增加1、增长1,等级上限字段-1。原表的两个金币字段注明不使用,因此本版不以它们写升级价格公式。')],
flow=['显示当前好感档与该档对话、服饰及收益。','签到按NPC签到轮次定位奖励。','特殊商店按升级项展示属性成长,与好感进度分开。'],
boundary='好感获得方式、收益缩放、特殊商店实际价格和-1上限的语义尚待确认。')
}