Initial import of HEROLL source and design documents
This commit is contained in:
@@ -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))
|
||||
Reference in New Issue
Block a user