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('
',' / ').replace('
',' / ') return re.sub(r']*)?>','',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