From c0dfa441a5fda84b3e9e4c584d4bfe1b6291e3dd Mon Sep 17 00:00:00 2001
From: JA <1323160571@qq.com>
Date: Mon, 8 Jun 2026 10:49:52 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=87=E5=AD=97=E8=84=9A=E6=9C=AC=E8=87=AA?=
=?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../memories/data_source_mapping.json | 108 +++
wenzijiaoben/memories/doc_format_spec.json | 52 ++
.../memories/icon_resource_mapping.json | 52 ++
wenzijiaoben/memories/project_config.json | 33 +
wenzijiaoben/memories/system_prompts.json | 104 +++
.../__pycache__/generate_doc.cpython-311.pyc | Bin 0 -> 27388 bytes
wenzijiaoben/py/generate_doc.py | 568 +++++++++++++++
wenzijiaoben/py/universal_tool.py | 682 ++++++++++++++++++
wenzijiaoben/py/xlsx_to_csv.py | 104 +++
wenzijiaoben/使用请看.txt | 1 +
10 files changed, 1704 insertions(+)
create mode 100644 wenzijiaoben/memories/data_source_mapping.json
create mode 100644 wenzijiaoben/memories/doc_format_spec.json
create mode 100644 wenzijiaoben/memories/icon_resource_mapping.json
create mode 100644 wenzijiaoben/memories/project_config.json
create mode 100644 wenzijiaoben/memories/system_prompts.json
create mode 100644 wenzijiaoben/py/__pycache__/generate_doc.cpython-311.pyc
create mode 100644 wenzijiaoben/py/generate_doc.py
create mode 100644 wenzijiaoben/py/universal_tool.py
create mode 100644 wenzijiaoben/py/xlsx_to_csv.py
create mode 100644 wenzijiaoben/使用请看.txt
diff --git a/wenzijiaoben/memories/data_source_mapping.json b/wenzijiaoben/memories/data_source_mapping.json
new file mode 100644
index 0000000..62193be
--- /dev/null
+++ b/wenzijiaoben/memories/data_source_mapping.json
@@ -0,0 +1,108 @@
+{
+ "description": "CSV数据源到文档章节的映射关系",
+ "csv_base_dir": "d:\\p-L12\\work\\cvs",
+ "mappings": {
+ "system_prompts": {
+ "source": "GameTextConst.cs (代码引用分析)",
+ "section": "一、系统提示",
+ "field": "常量值文本",
+ "filter": "仅保留在代码中被实际引用的常量(排除仅在注释中出现的)",
+ "dedup": "按文本值去重"
+ },
+ "quest_texts": {
+ "source": "quest.csv",
+ "section": "二、任务文本",
+ "field": "desc (第4列, index=3)",
+ "filter": "category == 1 或 category == 2",
+ "dedup": "按desc文本去重"
+ },
+ "achievement_texts": {
+ "source": "quest.csv",
+ "section": "三、成就文本",
+ "field": "desc (第4列, index=3)",
+ "filter": "category == 9",
+ "dedup": "按desc文本去重"
+ },
+ "player_names": {
+ "source": "原始文档固定数据 (100对姓名)",
+ "section": "四、玩家随机名称文本",
+ "format": "表格: 姓 | 名"
+ },
+ "guide_texts": {
+ "source": "guide.csv",
+ "section": "五、新手指引文本",
+ "field": "desc (第8列, index=7)",
+ "filter": "无",
+ "dedup": "按desc文本去重"
+ },
+ "props": {
+ "source": "prop.csv",
+ "section": "六、道具说明",
+ "fields": {
+ "name": "第3列 (index=2)",
+ "tips": "第7列 (index=6)",
+ "icon": "第8列 (index=7)"
+ },
+ "icon_dir": "atlas_product_icons",
+ "icon_naming": "icon_product_XX.png (XX为补零两位数, 如 icon_product_01)"
+ },
+ "skills": {
+ "source": "skill.csv",
+ "section": "七、技能",
+ "fields": {
+ "name": "第2列 (index=1)",
+ "icon": "第7列 (index=6)",
+ "desc": "第22列 (index=21)"
+ },
+ "icon_dir": "atlas_skill",
+ "icon_naming": "skillX.png (X为数字, 不补零, 如 skill1, skill10)"
+ },
+ "runes": {
+ "source": "rune.csv",
+ "section": "八、灵石",
+ "fields": {
+ "name": "第2列 (index=1)",
+ "icon": "第3列 (index=2)",
+ "desc": "第9列 (index=8)"
+ },
+ "icon_dir": "atlas_rune_icons",
+ "icon_naming": "rune_icon_X.png (X为数字, 不补零)"
+ },
+ "talents": {
+ "source": "playerlevel.csv",
+ "section": "九、天赋",
+ "fields": {
+ "name": "第14列 (index=13)",
+ "desc": "第15列 (index=14)",
+ "icon": "第16列 (index=15)"
+ },
+ "icon_dir": "atlas_talent_icons",
+ "icon_naming": "icon_talent_X.png (X为数字, 不补零)",
+ "dedup": "按name去重"
+ },
+ "equipment": {
+ "source": "equip.csv",
+ "section": "十、装备",
+ "fields": {
+ "name": "第2列 (index=1)",
+ "icon": "第6列 (index=5)"
+ },
+ "icon_dir": "atlas_equip_icons",
+ "icon_naming": "equip_icon_X.png (X为数字, 不补零)",
+ "dedup": "按name去重",
+ "note": "equip.csv无desc字段"
+ },
+ "level_names": {
+ "source": "fight_sample.csv",
+ "section": "十一、关卡名称",
+ "field": "name (第3列, index=2)",
+ "filter": "无",
+ "dedup": "按name去重"
+ }
+ },
+ "csv_format": {
+ "header_rows": 3,
+ "header_structure": "第1行:中文头, 第2行:类型定义, 第3行:英文字段名",
+ "encoding_note": "部分文件为GBK编码,需自动检测"
+ }
+}
diff --git a/wenzijiaoben/memories/doc_format_spec.json b/wenzijiaoben/memories/doc_format_spec.json
new file mode 100644
index 0000000..185edd1
--- /dev/null
+++ b/wenzijiaoben/memories/doc_format_spec.json
@@ -0,0 +1,52 @@
+{
+ "document_title": "《不朽封仙》文字脚本",
+ "sections": [
+ {"number": "一", "title": "系统提示", "type": "numbered_list"},
+ {"number": "二", "title": "任务文本", "type": "numbered_list"},
+ {"number": "三", "title": "成就文本", "type": "numbered_list"},
+ {"number": "四", "title": "玩家随机名称文本", "type": "table", "columns": ["姓", "名"]},
+ {"number": "五", "title": "新手指引文本", "type": "numbered_list"},
+ {"number": "六", "title": "道具说明", "type": "table", "columns": ["道具名称", "图标", "道具描述"]},
+ {"number": "七", "title": "技能", "type": "table", "columns": ["技能名称", "技能图标", "技能描述"]},
+ {"number": "八", "title": "灵石", "type": "table", "columns": ["灵石名称", "灵石图标", "灵石介绍"]},
+ {"number": "九", "title": "天赋", "type": "table", "columns": ["天赋名称", "天赋图标", "天赋介绍"]},
+ {"number": "十", "title": "装备", "type": "table", "columns": ["装备名称", "装备图标"]},
+ {"number": "十一", "title": "关卡名称", "type": "numbered_list"}
+ ],
+ "format_rules": {
+ "title": {
+ "font": "宋体",
+ "font_en": "SimSun",
+ "size": "二号 (22pt)",
+ "bold": true,
+ "underline": false,
+ "color": "黑色 (无蓝色)",
+ "alignment": "居中"
+ },
+ "section_heading": {
+ "font": "仿宋",
+ "font_en": "FangSong",
+ "size": "四号 (14pt)",
+ "bold": true,
+ "numbering": "中文数字 (一、二、三...十一)"
+ },
+ "text_content": {
+ "font": "仿宋",
+ "font_en": "FangSong",
+ "size": "四号 (14pt)",
+ "bold": false,
+ "numbering": "阿拉伯数字 (1. 2. 3. ...)"
+ },
+ "table_images": {
+ "height": "3cm",
+ "aspect_ratio": "锁定纵横比",
+ "missing_image": "留空"
+ }
+ },
+ "dedup_rules": {
+ "general": "所有文本和表格内容不得出现重复项",
+ "system_prompts": "GameTextConst中相同文本值的常量只保留第一个",
+ "talents": "天赋按name去重(同一天赋在不同等级重复出现)",
+ "equipment": "装备按name去重(同一装备不同品质重复出现)"
+ }
+}
diff --git a/wenzijiaoben/memories/icon_resource_mapping.json b/wenzijiaoben/memories/icon_resource_mapping.json
new file mode 100644
index 0000000..3640e3e
--- /dev/null
+++ b/wenzijiaoben/memories/icon_resource_mapping.json
@@ -0,0 +1,52 @@
+{
+ "description": "图标资源名称到图片文件的映射规则与目录结构",
+ "base_dir": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Resources\\Resources_moved\\gui\\atlas_ui",
+ "directories": {
+ "atlas_product_icons": {
+ "section": "六、道具说明",
+ "csv_field": "prop.csv icon (index=7)",
+ "naming_pattern": "icon_product_XX.png",
+ "naming_note": "XX为补零两位数 (01-63), 如 icon_product_1 -> icon_product_01.png",
+ "file_count": 63,
+ "missing_icons": []
+ },
+ "atlas_skill": {
+ "section": "七、技能",
+ "csv_field": "skill.csv icon (index=6)",
+ "naming_pattern": "skillX.png",
+ "naming_note": "X为数字不补零 (1-99), 如 skill1, skill10, skill99",
+ "file_count": 99,
+ "missing_icons": []
+ },
+ "atlas_rune_icons": {
+ "section": "八、灵石",
+ "csv_field": "rune.csv icon (index=2)",
+ "naming_pattern": "rune_icon_X.png",
+ "naming_note": "X为数字不补零 (1-18)",
+ "file_count": 12,
+ "missing_icons": ["rune_icon_13", "rune_icon_14", "rune_icon_15", "rune_icon_16", "rune_icon_17", "rune_icon_18"]
+ },
+ "atlas_talent_icons": {
+ "section": "九、天赋",
+ "csv_field": "playerlevel.csv icon (index=15)",
+ "naming_pattern": "icon_talent_X.png",
+ "naming_note": "X为数字不补零 (1-6)",
+ "file_count": 6,
+ "missing_icons": []
+ },
+ "atlas_equip_icons": {
+ "section": "十、装备",
+ "csv_field": "equip.csv icon (index=5)",
+ "naming_pattern": "equip_icon_X.png",
+ "naming_note": "X为数字不补零 (1-6)",
+ "file_count": 6,
+ "missing_icons": []
+ }
+ },
+ "image_lookup_strategy": "先直接匹配 icon_name.png,若失败则对末尾数字补零至2位或3位重试",
+ "image_insert_rules": {
+ "height": "3cm",
+ "aspect_ratio": "锁定纵横比",
+ "missing_image_action": "留空单元格"
+ }
+}
diff --git a/wenzijiaoben/memories/project_config.json b/wenzijiaoben/memories/project_config.json
new file mode 100644
index 0000000..ffe1ee8
--- /dev/null
+++ b/wenzijiaoben/memories/project_config.json
@@ -0,0 +1,33 @@
+{
+ "project_name": "P-L12《不朽封仙》",
+ "workspace_root": "d:\\p-L12",
+ "paths": {
+ "config_dir": "d:\\p-L12\\P-L12_config",
+ "game_dir": "d:\\p-L12\\P-L12_game",
+ "csv_dir": "d:\\p-L12\\work\\cvs",
+ "scripts_dir": "d:\\p-L12\\work\\py",
+ "memories_dir": "d:\\p-L12\\work\\memories",
+ "output_doc": "d:\\p-L12\\work\\《不朽封仙》游戏文字脚本.docx"
+ },
+ "scripts": {
+ "xlsx_to_csv": "d:\\p-L12\\work\\py\\xlsx_to_csv.py",
+ "generate_doc": "d:\\p-L12\\work\\py\\generate_doc.py"
+ },
+ "dependencies": {
+ "python_packages": ["openpyxl", "python-docx"],
+ "install_commands": [
+ "pip install openpyxl",
+ "pip install python-docx"
+ ]
+ },
+ "csv_encoding": {
+ "note": "部分CSV文件使用GBK/GB2312编码,非UTF-8",
+ "read_strategy": "按优先级尝试 utf-8-sig -> gbk -> gb2312 -> utf-8",
+ "write_encoding": "xlsx_to_csv.py 输出使用 UTF-8 with BOM (utf-8-sig)"
+ },
+ "game_code_paths": {
+ "game_text_const": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts\\mgame\\error\\GameTextConst.cs",
+ "scripts_root": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts"
+ },
+ "icon_resource_base": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Resources\\Resources_moved\\gui\\atlas_ui"
+}
diff --git a/wenzijiaoben/memories/system_prompts.json b/wenzijiaoben/memories/system_prompts.json
new file mode 100644
index 0000000..179d32d
--- /dev/null
+++ b/wenzijiaoben/memories/system_prompts.json
@@ -0,0 +1,104 @@
+{
+ "description": "GameTextConst.cs 中被代码实际引用的系统提示常量",
+ "source_file": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts\\mgame\\error\\GameTextConst.cs",
+ "total_referenced": 85,
+ "total_unique_text": 83,
+ "note": "85个被引用常量中,有2对文本值重复('{0}不足。' 和 '保存失败。'),去重后为83条",
+ "referenced_constants": {
+ "ErrorAlreadyClaimed": "已领取。",
+ "ErrorApplySuccess": "申请成功。",
+ "ErrorBagFull": "背包已满。",
+ "ErrorBreakthroughSuccess": "突破成功。",
+ "ErrorBuyFailed": "购买失败。",
+ "ErrorBuySuccess": "购买成功。",
+ "ErrorChallengeTimesNotEnough": "今日挑战次数不足。",
+ "ErrorCollectionUnlock": "请在关卡中拾取藏品解锁。",
+ "ErrorConditionNotMet": "条件不足。",
+ "ErrorCopySuccess": "复制成功。",
+ "ErrorCreateGuildSuccess": "创建公会成功。",
+ "ErrorCurrencyNotEnough": "货币不足。",
+ "ErrorDiamondNotEnough": "星棱幻晶不足。",
+ "ErrorEnergyNotEnough": "体力不足。",
+ "ErrorEquipBreakthroughSuccess": "装备升品成功。",
+ "ErrorEquipStrengthSuccess": "装备强化成功。",
+ "ErrorFriendAddOrNotExist": "玩家已添加或不存在。",
+ "ErrorFriendApplyRefuse": "已拒绝成为好友。",
+ "ErrorFriendApplySuccess": "已同意成为好友。",
+ "ErrorFusionRuneCountMax": "合成铭文所需数量已达上限。",
+ "ErrorFusionRuneCountNotEnough": "合成铭文所需数量不足。",
+ "ErrorFusionRuneNotFusion": "该铭文不能合成。",
+ "ErrorFusionRuneRarityNotSame": "合成铭文必须是相同稀有度。",
+ "ErrorGuildNameEmpty": "公会名称不能为空。",
+ "ErrorGuildNameTooLong": "公会名称不能超过6个字符。",
+ "ErrorGuildSetSuccess": "设置公会成功。",
+ "ErrorHeroLevelUnlock": "修士{0}级解锁。",
+ "ErrorHeroNotUnlock": "该伙伴尚未获得,需前往旅人驿站招募哦。",
+ "ErrorHeroStarUpSuccess": "升星成功。",
+ "ErrorInvalidGuideId": "无效的引导。",
+ "ErrorItemDailyUseTimesReached": "该道具今日使用次数已达上限。",
+ "ErrorItemNotEnough": "道具不足。",
+ "ErrorJoinSuccess": "加入成功。",
+ "ErrorLearnSuccess": "学习成功。",
+ "ErrorLevelNotEnough": "等级不足。",
+ "ErrorLevelNotExist": "关卡不存在。",
+ "ErrorMaterialNotEnough": "材料不足。",
+ "ErrorNameNotAvailable": "含有敏感字符,无法使用该名字。",
+ "ErrorNameOnlyChinese": "昵称只能包含中文。",
+ "ErrorNoFreePos": "人数已达上限。",
+ "ErrorNoHeroCarry": "无上阵伙伴。",
+ "ErrorNoHeroInTeam": "至少上阵一名修士。",
+ "ErrorNoPackagePurchased": "未购买礼包。",
+ "ErrorNoReward": "暂无奖励。",
+ "ErrorNotMeetClaimCondition": "暂未达到领取条件。",
+ "ErrorNotNewApply": "暂无新的好友申请。",
+ "ErrorNotPassBigBox": "请通关当前所有关卡后领取。",
+ "ErrorNotPassBigBoxStar": "满星通过前置关卡后解锁。",
+ "ErrorNotSettlementTime": "未到结算时间。",
+ "ErrorNotUnlock": "未解锁。",
+ "ErrorPassNotOpen": "关卡未开放。",
+ "ErrorPassNotUnlock": "通关后解锁。",
+ "ErrorPleaseInputName": "请输入昵称。",
+ "ErrorPreyFailed": "祈愿失败。",
+ "ErrorPushBoxLock": "通过{0}后解锁。",
+ "ErrorQuickSweepTimesNotEnough": "快速游历次数不足。",
+ "ErrorQuitGuildSuccess": "退出公会成功。",
+ "ErrorRefreshTimesNotEnough": "刷新次数不足。",
+ "ErrorRetreatNotNow": "现在还不能撤离。",
+ "ErrorRewardAlreadyClaimed": "您已领奖。",
+ "ErrorRunePutOnSuccess": "镶嵌成功。",
+ "ErrorRuneSlotFull": "铭文槽已满,请先卸下。",
+ "ErrorSaveHostage": "解救{0}个人质后解锁。",
+ "ErrorSaveSuccess": "保存成功。",
+ "ErrorScoreNotEnough": "积分不足。",
+ "ErrorSendGiftSuccess": "赠送成功",
+ "ErrorSetSuccess": "修改成功。",
+ "ErrorSomethingNotEnough": "{0}不足。",
+ "ErrorTakesuccess": "领取成功。",
+ "ErrorTargetNotEnough": "{0}不足。",
+ "ErrorTaskFailed": "领取失败。",
+ "ErrorTaskNotFinish": "任务未完成。",
+ "ErrorTaskRewardNotClaimed": "任务奖励未领取。",
+ "ErrorTimesNotEnough": "次数不足。",
+ "ErrorTitleNotUnlock": "称号未解锁。",
+ "ErrorUpLevelFirst": "请先升级。",
+ "ErrorUpLevelSuccess": "升级成功。",
+ "ErrorUpStage1First": "请先升品。",
+ "ErrorUpStageFirst": "请先升阶。",
+ "ErrorUpStageSuccess": "升阶成功。",
+ "ErrorUpStrengthFirst": "请先强化。",
+ "ErrorVipLevelNotEnough": "权益等级不足。",
+ "TipChallengeTimesConfirm": "\u3000是否消耗{0}进行挑战。\n\u3000今日剩余({1}/{2})次。",
+ "TipFusionSuccess": "合成成功。"
+ },
+ "duplicates_removed": [
+ {"kept": "ErrorTargetNotEnough", "removed": "ErrorSomethingNotEnough", "text": "{0}不足。"},
+ {"kept": "ErrorBuyFailed", "removed": "ErrorSaveFailed", "text": "保存失败。"}
+ ],
+ "unreferenced_constants": [
+ "ErrorUseFailed", "ErrorStrengthLimit", "ErrorRecruitTimesNotEnough",
+ "ErrorRecruitTicketNotEnough", "ErrorRecruitDiamondNotEnough",
+ "ErrorUpStarFirst", "ErrorUpStrengthFirst", "ErrorTujianProgressNotEnough",
+ "ErrorHeroMaxCountReached", "ErrorNoHeroToCombine", "ErrorStrengthSuccess",
+ "ErrorSaveFailed", "ErrorInvalidId", "ErrorReceiveConditionNotMet"
+ ]
+}
diff --git a/wenzijiaoben/py/__pycache__/generate_doc.cpython-311.pyc b/wenzijiaoben/py/__pycache__/generate_doc.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b73db0bd60481600cbaa6eb54b70042c7b707b23
GIT binary patch
literal 27388
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
literal 0
HcmV?d00001
diff --git a/wenzijiaoben/py/generate_doc.py b/wenzijiaoben/py/generate_doc.py
new file mode 100644
index 0000000..960fbfe
--- /dev/null
+++ b/wenzijiaoben/py/generate_doc.py
@@ -0,0 +1,568 @@
+# -*- coding: utf-8 -*-
+"""
+生成《不朽封仙》游戏文字脚本文档
+"""
+
+import csv
+import os
+import glob
+from docx import Document
+from docx.shared import Pt, Cm, Inches
+from docx.enum.text import WD_ALIGN_PARAGRAPH
+from docx.enum.table import WD_TABLE_ALIGNMENT
+from docx.oxml.ns import qn
+
+# ============ 路径配置 ============
+CSV_DIR = r"d:\p-L12\work\cvs"
+GAME_DIR = r"d:\p-L12\P-L12_game\unity\Assets\Resources\Resources_moved\gui\atlas_ui"
+ICON_DIRS = {
+ "product": os.path.join(GAME_DIR, "atlas_product_icons"),
+ "skill": os.path.join(GAME_DIR, "atlas_skill"),
+ "rune": os.path.join(GAME_DIR, "atlas_rune_icons"),
+ "talent": os.path.join(GAME_DIR, "atlas_talent_icons"),
+ "equip": os.path.join(GAME_DIR, "atlas_equip_icons"),
+}
+OUTPUT_PATH = r"d:\p-L12\work\《不朽封仙》游戏文字脚本.docx"
+
+# ============ 辅助函数 ============
+
+def read_csv(filename):
+ """读取CSV文件,跳过前3行(中文头、类型头、英文头),返回数据行"""
+ path = os.path.join(CSV_DIR, filename)
+ # 尝试多种编码
+ for encoding in ["utf-8-sig", "gbk", "gb2312", "utf-8"]:
+ try:
+ with open(path, "r", encoding=encoding) as f:
+ reader = csv.reader(f)
+ rows = list(reader)
+ return rows[3:] # skip header rows
+ except (UnicodeDecodeError, UnicodeError):
+ continue
+ raise ValueError(f"无法读取文件 {filename},尝试了所有编码")
+
+def find_image(icon_name, icon_dir_key):
+ """根据icon名称查找图片文件路径,返回完整路径或None"""
+ icon_dir = ICON_DIRS.get(icon_dir_key)
+ if not icon_dir or not os.path.isdir(icon_dir):
+ return None
+ # 尝试直接匹配
+ for ext in [".png", ".jpg"]:
+ p = os.path.join(icon_dir, icon_name + ext)
+ if os.path.isfile(p):
+ return p
+ # 尝试补零匹配(如 icon_product_1 -> icon_product_01)
+ import re
+ m = re.match(r"^(.+?)(\d+)$", icon_name)
+ if m:
+ prefix, num = m.group(1), m.group(2)
+ for pad in [2, 3]:
+ padded = prefix + num.zfill(pad)
+ for ext in [".png", ".jpg"]:
+ p = os.path.join(icon_dir, padded + ext)
+ if os.path.isfile(p):
+ return p
+ return None
+
+def add_image_to_cell(cell, image_path, height_cm=3):
+ """在表格单元格中插入图片,锁定纵横比,设置高度"""
+ paragraph = cell.paragraphs[0]
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ run = paragraph.add_run()
+ run.add_picture(image_path, height=Cm(height_cm))
+
+def set_cell_text(cell, text):
+ """设置单元格文本"""
+ cell.text = ""
+ paragraph = cell.paragraphs[0]
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ run = paragraph.add_run(str(text))
+ run.font.size = Pt(10)
+
+def add_table_with_icons(doc, headers, rows_data, icon_dir_key):
+ """添加带图标的表格"""
+ table = doc.add_table(rows=1, cols=len(headers), style="Table Grid")
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
+ # 设置表头
+ for i, header in enumerate(headers):
+ cell = table.rows[0].cells[i]
+ cell.text = header
+ for paragraph in cell.paragraphs:
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ for run in paragraph.runs:
+ run.font.bold = True
+ run.font.size = Pt(10)
+ # 添加数据行
+ for row_data in rows_data:
+ row = table.add_row()
+ name, icon_name, desc = row_data[0], row_data[1], row_data[2] if len(row_data) > 2 else ""
+ set_cell_text(row.cells[0], name)
+ # 图标列
+ img_path = find_image(icon_name, icon_dir_key) if icon_name else None
+ if img_path:
+ add_image_to_cell(row.cells[1], img_path)
+ else:
+ set_cell_text(row.cells[1], "")
+ # 描述列
+ if desc:
+ set_cell_text(row.cells[2], desc)
+
+def add_table_equip(doc, headers, rows_data, icon_dir_key):
+ """添加装备表格(只有名称和图标两列)"""
+ table = doc.add_table(rows=1, cols=len(headers), style="Table Grid")
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
+ for i, header in enumerate(headers):
+ cell = table.rows[0].cells[i]
+ cell.text = header
+ for paragraph in cell.paragraphs:
+ paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ for run in paragraph.runs:
+ run.font.bold = True
+ run.font.size = Pt(10)
+ for row_data in rows_data:
+ row = table.add_row()
+ name, icon_name = row_data[0], row_data[1]
+ set_cell_text(row.cells[0], name)
+ img_path = find_image(icon_name, icon_dir_key) if icon_name else None
+ if img_path:
+ add_image_to_cell(row.cells[1], img_path)
+ else:
+ set_cell_text(row.cells[1], "")
+
+
+# ============ 数据准备 ============
+
+def get_system_prompts():
+ """获取被引用的GameTextConst系统提示"""
+ # 所有GameTextConst常量及其文本值
+ all_consts = {
+ "ErrorTaskFailed": "领取失败。",
+ "ErrorBuyFailed": "购买失败。",
+ "ErrorUseFailed": "使用失败。",
+ "ErrorTakesuccess": "领取成功。",
+ "ErrorStrengthLimit": "强化已达上限。",
+ "ErrorChallengeTimesNotEnough": "今日挑战次数不足。",
+ "ErrorRecruitTimesNotEnough": "今日可招募次数不足。",
+ "ErrorRecruitTicketNotEnough": "召唤道具与月灵玉璧不足。",
+ "ErrorRecruitDiamondNotEnough": "星棱幻晶不足,无法进行招募。",
+ "ErrorUpStageFirst": "请先升阶。",
+ "ErrorUpStage1First": "请先升品。",
+ "ErrorUpLevelFirst": "请先升级。",
+ "ErrorUpStarFirst": "请先升星。",
+ "ErrorUpStrengthFirst": "请先强化。",
+ "ErrorLevelNotExist": "关卡不存在。",
+ "ErrorInvalidGuideId": "无效的引导。",
+ "ErrorItemNotEnough": "道具不足。",
+ "ErrorRewardAlreadyClaimed": "您已领奖。",
+ "ErrorNotSettlementTime": "未到结算时间。",
+ "ErrorNoReward": "暂无奖励。",
+ "ErrorHeroStarUpSuccess": "升星成功。",
+ "ErrorTujianProgressNotEnough": "图鉴进度未达到领取条件。",
+ "ErrorUpLevelSuccess": "升级成功。",
+ "ErrorUpStageSuccess": "升阶成功。",
+ "ErrorGuildSetSuccess": "设置公会成功。",
+ "ErrorNoPackagePurchased": "未购买礼包。",
+ "ErrorNotMeetClaimCondition": "暂未达到领取条件。",
+ "ErrorNameNotAvailable": "含有敏感字符,无法使用该名字。",
+ "ErrorNameOnlyChinese": "昵称只能包含中文。",
+ "ErrorPleaseInputName": "请输入昵称。",
+ "ErrorSetSuccess": "修改成功。",
+ "ErrorCurrencyNotEnough": "货币不足。",
+ "ErrorHeroMaxCountReached": "伙伴达到最大人数。",
+ "ErrorNoHeroToCombine": "没有可合成的伙伴。",
+ "ErrorJoinSuccess": "加入成功。",
+ "ErrorApplySuccess": "申请成功。",
+ "ErrorEnergyNotEnough": "体力不足。",
+ "ErrorBuySuccess": "购买成功。",
+ "ErrorQuitGuildSuccess": "退出公会成功。",
+ "ErrorGuildNameEmpty": "公会名称不能为空。",
+ "ErrorGuildNameTooLong": "公会名称不能超过6个字符。",
+ "ErrorCreateGuildSuccess": "创建公会成功。",
+ "ErrorMaterialNotEnough": "材料不足。",
+ "ErrorEquipStrengthSuccess": "装备强化成功。",
+ "ErrorEquipBreakthroughSuccess": "装备升品成功。",
+ "ErrorStrengthSuccess": "强化成功。",
+ "ErrorDiamondNotEnough": "星棱幻晶不足。",
+ "ErrorQuickSweepTimesNotEnough": "快速游历次数不足。",
+ "ErrorCopySuccess": "复制成功。",
+ "ErrorReceiveConditionNotMet": "未达成领取条件。",
+ "ErrorAlreadyClaimed": "已领取。",
+ "ErrorItemDailyUseTimesReached": "该道具今日使用次数已达上限。",
+ "ErrorConditionNotMet": "条件不足。",
+ "ErrorVipLevelNotEnough": "权益等级不足。",
+ "ErrorLevelNotEnough": "等级不足。",
+ "ErrorScoreNotEnough": "积分不足。",
+ "ErrorPassNotOpen": "关卡未开放。",
+ "ErrorPreyFailed": "祈愿失败。",
+ "ErrorInvalidId": "无效的ID。",
+ "ErrorSaveSuccess": "保存成功。",
+ "ErrorSaveFailed": "保存失败。",
+ "ErrorNoHeroCarry": "无上阵伙伴。",
+ "ErrorNoHeroInTeam": "至少上阵一名修士。",
+ "ErrorNoFreePos": "人数已达上限。",
+ "ErrorRefreshTimesNotEnough": "刷新次数不足。",
+ "ErrorTimesNotEnough": "次数不足。",
+ "ErrorHeroLevelUnlock": "修士{0}级解锁。",
+ "TipChallengeTimesConfirm": "\u3000是否消耗{0}进行挑战。\n\u3000今日剩余({1}/{2})次。",
+ "ErrorFusionRuneCountNotEnough": "合成铭文所需数量不足。",
+ "TipFusionSuccess": "合成成功。",
+ "ErrorFusionRuneRarityNotSame": "合成铭文必须是相同稀有度。",
+ "ErrorFusionRuneCountMax": "合成铭文所需数量已达上限。",
+ "ErrorFusionRuneNotFusion": "该铭文不能合成。",
+ "ErrorRunePutOnSuccess": "镶嵌成功。",
+ "ErrorTitleNotUnlock": "称号未解锁。",
+ "ErrorHeroNotUnlock": "该伙伴尚未获得,需前往旅人驿站招募哦。",
+ "ErrorRuneSlotFull": "铭文槽已满,请先卸下。",
+ "ErrorPushBoxLock": "通过{0}后解锁。",
+ "ErrorFriendApplySuccess": "已同意成为好友。",
+ "ErrorFriendApplyRefuse": "已拒绝成为好友。",
+ "ErrorFriendAddOrNotExist": "玩家已添加或不存在。",
+ "ErrorBagFull": "背包已满。",
+ "ErrorTargetNotEnough": "{0}不足。",
+ "ErrorPassNotUnlock": "通关后解锁。",
+ "ErrorSaveHostage": "解救{0}个人质后解锁。",
+ "ErrorSomethingNotEnough": "{0}不足。",
+ "ErrorRetreatNotNow": "现在还不能撤离。",
+ "ErrorNotUnlock": "未解锁。",
+ "ErrorSendGiftSuccess": "赠送成功",
+ "ErrorCollectionUnlock": "请在关卡中拾取藏品解锁。",
+ "ErrorTaskNotFinish": "任务未完成。",
+ "ErrorTaskRewardNotClaimed": "任务奖励未领取。",
+ "ErrorNotNewApply": "暂无新的好友申请。",
+ "ErrorNotPassBigBox": "请通关当前所有关卡后领取。",
+ "ErrorNotPassBigBoxStar": "满星通过前置关卡后解锁。",
+ "ErrorBreakthroughSuccess": "突破成功。",
+ "ErrorLearnSuccess": "学习成功。",
+ }
+ # 被引用的常量(排除只在注释中引用的)
+ referenced = {
+ "ErrorAlreadyClaimed", "ErrorApplySuccess", "ErrorBagFull",
+ "ErrorBreakthroughSuccess", "ErrorBuyFailed", "ErrorBuySuccess",
+ "ErrorChallengeTimesNotEnough", "ErrorCollectionUnlock",
+ "ErrorConditionNotMet", "ErrorCopySuccess", "ErrorCreateGuildSuccess",
+ "ErrorCurrencyNotEnough", "ErrorDiamondNotEnough", "ErrorEnergyNotEnough",
+ "ErrorEquipBreakthroughSuccess", "ErrorEquipStrengthSuccess",
+ "ErrorFriendAddOrNotExist", "ErrorFriendApplyRefuse",
+ "ErrorFriendApplySuccess", "ErrorFusionRuneCountMax",
+ "ErrorFusionRuneCountNotEnough", "ErrorFusionRuneNotFusion",
+ "ErrorFusionRuneRarityNotSame", "ErrorGuildNameEmpty",
+ "ErrorGuildNameTooLong", "ErrorGuildSetSuccess", "ErrorHeroLevelUnlock",
+ "ErrorHeroNotUnlock", "ErrorHeroStarUpSuccess", "ErrorInvalidGuideId",
+ "ErrorItemDailyUseTimesReached", "ErrorItemNotEnough",
+ "ErrorJoinSuccess", "ErrorLearnSuccess", "ErrorLevelNotEnough",
+ "ErrorLevelNotExist", "ErrorMaterialNotEnough", "ErrorNameNotAvailable",
+ "ErrorNameOnlyChinese", "ErrorNoFreePos", "ErrorNoHeroCarry",
+ "ErrorNoHeroInTeam", "ErrorNoPackagePurchased", "ErrorNoReward",
+ "ErrorNotMeetClaimCondition", "ErrorNotNewApply", "ErrorNotPassBigBox",
+ "ErrorNotPassBigBoxStar", "ErrorNotSettlementTime", "ErrorNotUnlock",
+ "ErrorPassNotOpen", "ErrorPassNotUnlock", "ErrorPleaseInputName",
+ "ErrorPreyFailed", "ErrorPushBoxLock", "ErrorQuickSweepTimesNotEnough",
+ "ErrorQuitGuildSuccess", "ErrorRefreshTimesNotEnough",
+ "ErrorRetreatNotNow", "ErrorRewardAlreadyClaimed",
+ "ErrorRunePutOnSuccess", "ErrorRuneSlotFull", "ErrorSaveHostage",
+ "ErrorSaveSuccess", "ErrorScoreNotEnough", "ErrorSendGiftSuccess",
+ "ErrorSetSuccess", "ErrorSomethingNotEnough", "ErrorTakesuccess",
+ "ErrorTargetNotEnough", "ErrorTaskFailed", "ErrorTaskNotFinish",
+ "ErrorTaskRewardNotClaimed", "ErrorTimesNotEnough",
+ "ErrorTitleNotUnlock", "ErrorUpLevelFirst", "ErrorUpLevelSuccess",
+ "ErrorUpStage1First", "ErrorUpStageFirst", "ErrorUpStageSuccess",
+ "ErrorUpStrengthFirst", "ErrorVipLevelNotEnough",
+ "TipChallengeTimesConfirm", "TipFusionSuccess",
+ }
+ # 只保留被引用的,去重文本值
+ seen_texts = set()
+ result = []
+ for name, text in all_consts.items():
+ if name in referenced and text not in seen_texts:
+ seen_texts.add(text)
+ result.append(text)
+ return result
+
+def get_quest_texts():
+ """获取任务文本(category 1和2的desc字段,去重)"""
+ rows = read_csv("quest.csv")
+ seen = set()
+ result = []
+ for row in rows:
+ if len(row) > 3:
+ category = row[1].strip()
+ desc = row[3].strip()
+ if category in ("1", "2") and desc and desc not in seen:
+ seen.add(desc)
+ result.append(desc)
+ return result
+
+def get_achievement_texts():
+ """获取成就文本(category 9的desc字段,去重)"""
+ rows = read_csv("quest.csv")
+ seen = set()
+ result = []
+ for row in rows:
+ if len(row) > 3:
+ category = row[1].strip()
+ desc = row[3].strip()
+ if category == "9" and desc and desc not in seen:
+ seen.add(desc)
+ result.append(desc)
+ return result
+
+def get_guide_texts():
+ """获取新手指引文本(guide.csv的desc字段,去重)"""
+ rows = read_csv("guide.csv")
+ seen = set()
+ result = []
+ for row in rows:
+ if len(row) > 7:
+ desc = row[7].strip()
+ if desc and desc not in seen:
+ seen.add(desc)
+ result.append(desc)
+ return result
+
+def get_prop_data():
+ """获取道具数据 [(name, icon, tips), ...]"""
+ rows = read_csv("prop.csv")
+ result = []
+ for row in rows:
+ if len(row) > 7:
+ name = row[2].strip()
+ tips = row[6].strip()
+ icon = row[7].strip()
+ if name:
+ result.append((name, icon, tips))
+ return result
+
+def get_skill_data():
+ """获取技能数据 [(name, icon, desc), ...]"""
+ rows = read_csv("skill.csv")
+ result = []
+ for row in rows:
+ if len(row) > 21:
+ name = row[1].strip()
+ icon = row[6].strip()
+ desc = row[21].strip()
+ if name:
+ result.append((name, icon, desc))
+ return result
+
+def get_rune_data():
+ """获取灵石数据 [(name, icon, desc), ...]"""
+ rows = read_csv("rune.csv")
+ result = []
+ for row in rows:
+ if len(row) > 8:
+ name = row[1].strip()
+ icon = row[2].strip()
+ desc = row[8].strip()
+ if name:
+ result.append((name, icon, desc))
+ return result
+
+def get_talent_data():
+ """获取天赋数据 [(name, icon, desc), ...] 去重"""
+ rows = read_csv("playerlevel.csv")
+ seen = set()
+ result = []
+ for row in rows:
+ if len(row) > 15:
+ name = row[13].strip()
+ desc = row[14].strip()
+ icon = row[15].strip()
+ if name and name not in seen:
+ seen.add(name)
+ result.append((name, icon, desc))
+ return result
+
+def get_equip_data():
+ """获取装备数据 [(name, icon), ...] 去重"""
+ rows = read_csv("equip.csv")
+ seen = set()
+ result = []
+ for row in rows:
+ if len(row) > 5:
+ name = row[1].strip()
+ icon = row[5].strip()
+ if name and name not in seen:
+ seen.add(name)
+ result.append((name, icon))
+ return result
+
+def get_level_names():
+ """获取关卡名称(fight_sample.csv的name字段,去重)"""
+ rows = read_csv("fight_sample.csv")
+ seen = set()
+ result = []
+ for row in rows:
+ if len(row) > 2:
+ name = row[2].strip()
+ if name and name not in seen:
+ seen.add(name)
+ result.append(name)
+ return result
+
+# 玩家随机名称文本(保持不变)
+PLAYER_NAMES = [
+ ("李", "子轩"), ("王", "梓涵"), ("张", "浩然"), ("刘", "欣怡"),
+ ("陈", "宇轩"), ("杨", "晨曦"), ("赵", "俊杰"), ("黄", "雅静"),
+ ("周", "天佑"), ("吴", "梦琪"), ("徐", "博文"), ("孙", "思彤"),
+ ("胡", "睿智"), ("朱", "静怡"), ("高", "昊天"), ("林", "雨桐"),
+ ("何", "泽宇"), ("郭", "婉清"), ("马", "俊熙"), ("罗", "雪柔"),
+ ("梁", "子豪"), ("宋", "依琳"), ("郑", "宇航"), ("谢", "诗涵"),
+ ("韩", "志强"), ("唐", "梦瑶"), ("冯", "文博"), ("于", "雅楠"),
+ ("董", "天宇"), ("萧", "欣妍"), ("程", "子睿"), ("曹", "静雯"),
+ ("袁", "俊豪"), ("邓", "雨欣"), ("许", "浩宇"), ("傅", "语嫣"),
+ ("沈", "子涵"), ("曾", "思雨"), ("彭", "博涛"), ("吕", "雅婷"),
+ ("苏", "天翔"), ("卢", "梦洁"), ("蒋", "文轩"), ("蔡", "雪梅"),
+ ("贾", "志远"), ("丁", "依娜"), ("魏", "子安"), ("薛", "静雅"),
+ ("叶", "俊峰"), ("阎", "雨婷"), ("余", "浩轩"), ("潘", "诗琪"),
+ ("杜", "子恒"), ("戴", "思雅"), ("夏", "博超"), ("钟", "雅琪"),
+ ("汪", "天瑞"), ("田", "梦菲"), ("任", "文昊"), ("姜", "雪莲"),
+ ("范", "志鹏"), ("方", "依婷"), ("石", "子健"), ("姚", "静淑"),
+ ("谭", "俊凯"), ("廖", "雨菲"), ("邹", "浩南"), ("熊", "诗瑶"),
+ ("金", "子宁"), ("陆", "思琪"), ("郝", "博远"), ("孔", "雅静"),
+ ("白", "天浩"), ("崔", "梦婷"), ("康", "文杰"), ("毛", "雪琪"),
+ ("邱", "志伟"), ("秦", "依静"), ("江", "子文"), ("史", "静怡"),
+ ("顾", "俊杰"), ("侯", "雨萱"), ("邵", "浩天"), ("孟", "诗涵"),
+ ("龙", "子辰"), ("万", "思妍"), ("段", "博文"), ("雷", "雅欣"),
+ ("钱", "天佑"), ("汤", "梦瑶"), ("尹", "文豪"), ("黎", "雪莹"),
+ ("易", "志强"), ("常", "依琳"), ("武", "子轩"), ("乔", "静雯"),
+ ("贺", "俊熙"), ("赖", "雨桐"), ("龚", "浩宇"), ("文", "诗琪"),
+]
+
+
+# ============ 文档生成 ============
+
+# 中文编号
+CN_NUMBERS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一"]
+
+def add_title(doc, text):
+ """添加文档标题:宋体、二号(22pt)、加粗、居中、无下划线"""
+ para = doc.add_paragraph()
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ run = para.add_run(text)
+ run.font.name = "宋体"
+ run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
+ run.font.size = Pt(22)
+ run.font.bold = True
+ run.font.underline = False
+ run.font.color.rgb = None # 确保黑色,无蓝色
+
+def add_section_heading(doc, cn_num, title_text):
+ """添加小标题:仿宋、四号(14pt)、加粗、中文编号"""
+ para = doc.add_paragraph()
+ run = para.add_run(f"{cn_num}、{title_text}")
+ run.font.name = "仿宋"
+ run._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
+ run.font.size = Pt(14)
+ run.font.bold = True
+
+def add_numbered_paragraph(doc, index, text):
+ """添加编号段落:仿宋、四号(14pt)、不加粗、阿拉伯数字编号"""
+ para = doc.add_paragraph()
+ run = para.add_run(f"{index}.{text}")
+ run.font.name = "仿宋"
+ run._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
+ run.font.size = Pt(14)
+ run.font.bold = False
+
+def add_numbered_items(doc, items):
+ """批量添加编号段落"""
+ for i, text in enumerate(items, 1):
+ add_numbered_paragraph(doc, i, text)
+
+def add_name_table(doc, names):
+ """添加玩家随机名称表格:姓/名两列"""
+ table = doc.add_table(rows=1, cols=2, style="Table Grid")
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
+ # 表头
+ for i, header in enumerate(["姓", "名"]):
+ cell = table.rows[0].cells[i]
+ cell.text = ""
+ para = cell.paragraphs[0]
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ run = para.add_run(header)
+ run.font.name = "仿宋"
+ run._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
+ run.font.size = Pt(14)
+ run.font.bold = True
+ # 数据行
+ for surname, given_name in names:
+ row = table.add_row()
+ for j, text in enumerate([surname, given_name]):
+ cell = row.cells[j]
+ cell.text = ""
+ para = cell.paragraphs[0]
+ para.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ run = para.add_run(text)
+ run.font.name = "仿宋"
+ run._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
+ run.font.size = Pt(14)
+ run.font.bold = False
+
+def build_document():
+ doc = Document()
+
+ # 设置默认字体
+ style = doc.styles["Normal"]
+ style.font.name = "仿宋"
+ style.font.size = Pt(14)
+ style._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
+
+ # 标题:宋体、二号、加粗、无蓝色下划线
+ add_title(doc, "《不朽封仙》文字脚本")
+
+ # ======= 一、系统提示 =======
+ add_section_heading(doc, CN_NUMBERS[0], "系统提示")
+ add_numbered_items(doc, get_system_prompts())
+
+ # ======= 二、任务文本 =======
+ add_section_heading(doc, CN_NUMBERS[1], "任务文本")
+ add_numbered_items(doc, get_quest_texts())
+
+ # ======= 三、成就文本 =======
+ add_section_heading(doc, CN_NUMBERS[2], "成就文本")
+ add_numbered_items(doc, get_achievement_texts())
+
+ # ======= 四、玩家随机名称文本 =======
+ add_section_heading(doc, CN_NUMBERS[3], "玩家随机名称文本")
+ add_name_table(doc, PLAYER_NAMES)
+
+ # ======= 五、新手指引文本 =======
+ add_section_heading(doc, CN_NUMBERS[4], "新手指引文本")
+ add_numbered_items(doc, get_guide_texts())
+
+ # ======= 六、道具说明 =======
+ add_section_heading(doc, CN_NUMBERS[5], "道具说明")
+ prop_data = get_prop_data()
+ add_table_with_icons(doc, ["道具名称", "图标", "道具描述"], prop_data, "product")
+
+ # ======= 七、技能 =======
+ add_section_heading(doc, CN_NUMBERS[6], "技能")
+ skill_data = get_skill_data()
+ add_table_with_icons(doc, ["技能名称", "技能图标", "技能描述"], skill_data, "skill")
+
+ # ======= 八、灵石 =======
+ add_section_heading(doc, CN_NUMBERS[7], "灵石")
+ rune_data = get_rune_data()
+ add_table_with_icons(doc, ["灵石名称", "灵石图标", "灵石介绍"], rune_data, "rune")
+
+ # ======= 九、天赋 =======
+ add_section_heading(doc, CN_NUMBERS[8], "天赋")
+ talent_data = get_talent_data()
+ add_table_with_icons(doc, ["天赋名称", "天赋图标", "天赋介绍"], talent_data, "talent")
+
+ # ======= 十、装备 =======
+ add_section_heading(doc, CN_NUMBERS[9], "装备")
+ equip_data = get_equip_data()
+ add_table_equip(doc, ["装备名称", "装备图标"], equip_data, "equip")
+
+ # ======= 十一、关卡名称 =======
+ add_section_heading(doc, CN_NUMBERS[10], "关卡名称")
+ add_numbered_items(doc, get_level_names())
+
+ # 保存文档
+ doc.save(OUTPUT_PATH)
+ print(f"文档已生成: {OUTPUT_PATH}")
+
+
+if __name__ == "__main__":
+ build_document()
diff --git a/wenzijiaoben/py/universal_tool.py b/wenzijiaoben/py/universal_tool.py
new file mode 100644
index 0000000..94d9609
--- /dev/null
+++ b/wenzijiaoben/py/universal_tool.py
@@ -0,0 +1,682 @@
+# -*- coding: utf-8 -*-
+"""
+通用游戏文字脚本文档生成工具
+
+功能:自动扫描项目目录,识别标准结构,生成游戏文字脚本Word文档。
+无需手动配置路径,只需提供项目根目录即可自动完成全部流程。
+
+标准目录结构约定:
+ /
+ ├── *_config/ ← 配置表目录 (xlsx/csv)
+ ├── *_game/ ← 游戏客户端目录
+ │ └── unity/Assets/Resources/.../atlas_ui/ ← 图标资源
+ │ └── unity/Assets/Scripts/.../*Const*.cs ← 系统提示常量
+ └── work/ ← 工作目录 (CSV缓存、输出等)
+
+用法:
+ python universal_tool.py [项目路径] [--output 输出路径]
+
+依赖:pip install openpyxl python-docx
+"""
+
+import csv
+import os
+import re
+import sys
+import glob
+import argparse
+from docx import Document
+from docx.shared import Pt, Cm
+from docx.enum.text import WD_ALIGN_PARAGRAPH
+from docx.enum.table import WD_TABLE_ALIGNMENT
+from docx.oxml.ns import qn
+
+# ============ 常量 ============
+CN_NUMBERS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一"]
+
+# 11章节数据源定义(字段名取自CSV英文头行第3行)
+SECTION_DEFS = {
+ "quest_texts": {"csv": "quest", "field": "desc", "filter": {"category": ["1", "2"]}},
+ "achievements": {"csv": "quest", "field": "desc", "filter": {"category": ["9"]}},
+ "guide_texts": {"csv": "guide", "field": "desc"},
+ "props": {"csv": "prop", "fields": ["name", "icon", "tips"], "icon_key": "atlas_product_icons"},
+ "skills": {"csv": "skill", "fields": ["name", "icon", "desc"], "icon_key": "atlas_skill"},
+ "runes": {"csv": "rune", "fields": ["name", "icon", "desc"], "icon_key": "atlas_rune_icons"},
+ "talents": {"csv": "playerlevel", "fields": ["name", "icon", "desc"], "icon_key": "atlas_talent_icons", "dedup": "name"},
+ "equips": {"csv": "equip", "fields": ["name", "icon"], "icon_key": "atlas_equip_icons", "dedup": "name"},
+ "level_names": {"csv": "fight_sample", "field": "name"},
+}
+
+
+# ================================================================
+# 一、项目结构自动扫描
+# ================================================================
+
+class ProjectScanner:
+ """自动扫描并识别项目目录结构"""
+
+ def __init__(self, project_root):
+ self.root = os.path.abspath(project_root)
+ self.config_dir = None
+ self.game_dir = None
+ self.csv_dir = None
+ self.work_dir = None
+ self.resources_dir = None
+ self.icon_dirs = {}
+ self.scripts_dir = None
+ self.game_text_const = None
+ self.project_name = ""
+ self._scan()
+
+ def _scan(self):
+ # 1. 查找 _config / _game / work 目录
+ for entry in os.listdir(self.root):
+ p = os.path.join(self.root, entry)
+ if not os.path.isdir(p):
+ continue
+ if entry.endswith("_config"):
+ self.config_dir = p
+ elif entry.endswith("_game"):
+ self.game_dir = p
+ elif entry == "work":
+ self.work_dir = p
+
+ # 2. 项目名
+ if self.config_dir:
+ base = os.path.basename(self.config_dir)
+ self.project_name = base.replace("_config", "")
+
+ # 3. CSV目录: work/cvs 优先,其次 config_dir 本身
+ if self.work_dir:
+ cvs = os.path.join(self.work_dir, "cvs")
+ if os.path.isdir(cvs) and os.listdir(cvs):
+ self.csv_dir = cvs
+ if not self.csv_dir:
+ self.csv_dir = self.config_dir
+
+ # 4. 搜索根目录:优先 Resources_moved,其次 Resources
+ if self.game_dir:
+ for root, dirs, _ in os.walk(self.game_dir):
+ for name in ["Resources_moved", "Resources"]:
+ if name in dirs:
+ self.resources_dir = os.path.join(root, name)
+ break
+ if self.resources_dir:
+ break
+
+ # 5. 图标目录 (递归搜索 atlas_* 子目录,只保留包含图片的叶子目录)
+ if self.resources_dir:
+ for root, dirs, files in os.walk(self.resources_dir):
+ for d in dirs:
+ if d.startswith("atlas_"):
+ dp = os.path.join(root, d)
+ # 检查是否包含图片文件
+ has_img = any(f.lower().endswith((".png", ".jpg", ".jpeg")) for f in os.listdir(dp))
+ if has_img:
+ self.icon_dirs[d] = dp
+
+ # 6. Scripts 目录
+ if self.game_dir:
+ unity = os.path.join(self.game_dir, "unity")
+ assets = os.path.join(unity, "Assets") if os.path.isdir(unity) else None
+ if assets:
+ scripts = os.path.join(assets, "Scripts")
+ if os.path.isdir(scripts):
+ self.scripts_dir = scripts
+
+ # 7. GameTextConst.cs
+ if self.scripts_dir:
+ for root, _, files in os.walk(self.scripts_dir):
+ for f in files:
+ if f.lower().endswith(".cs") and "const" in f.lower() and ("text" in f.lower() or "game" in f.lower()):
+ self.game_text_const = os.path.join(root, f)
+ return
+ # 宽松匹配:任意 *Const*.cs
+ for root, _, files in os.walk(self.scripts_dir):
+ for f in files:
+ if "Const" in f and f.endswith(".cs"):
+ self.game_text_const = os.path.join(root, f)
+ return
+
+ def find_file(self, name):
+ """在 csv_dir 或 config_dir 中查找文件(支持 .csv/.xlsx)"""
+ for d in [self.csv_dir, self.config_dir]:
+ if not d:
+ continue
+ for ext in [".csv", ".xlsx"]:
+ p = os.path.join(d, name + ext)
+ if os.path.isfile(p):
+ return p
+ return None
+
+ def summary(self):
+ """打印扫描结果"""
+ lines = [
+ f" 项目名称: {self.project_name}",
+ f" 配置目录: {self.config_dir or '未找到'}",
+ f" 游戏目录: {self.game_dir or '未找到'}",
+ f" CSV目录: {self.csv_dir or '未找到'}",
+ f" 图标目录: {len(self.icon_dirs)}个 {list(self.icon_dirs.keys())}",
+ f" 脚本目录: {self.scripts_dir or '未找到'}",
+ f" 常量文件: {os.path.basename(self.game_text_const) if self.game_text_const else '未找到'}",
+ ]
+ return "\n".join(lines)
+
+
+# ================================================================
+# 二、CSV 数据读取(自动编码 + 动态字段名解析)
+# ================================================================
+
+def read_csv(filepath):
+ """
+ 读取CSV文件,自动检测编码 (utf-8-sig/gbk/gb2312/utf-8)。
+ 返回 (headers, rows):
+ headers: 第3行英文字段名列表
+ rows: 第4行起的数据行列表 (list of list)
+ """
+ for enc in ["utf-8-sig", "gbk", "gb2312", "utf-8"]:
+ try:
+ with open(filepath, "r", encoding=enc) as f:
+ all_rows = list(csv.reader(f))
+ if len(all_rows) < 4:
+ return all_rows[-1] if all_rows else [], []
+ return all_rows[2], all_rows[3:]
+ except (UnicodeDecodeError, UnicodeError):
+ continue
+ return [], []
+
+
+def field_map(headers):
+ """将英文头行转为 {字段名: 列索引} 映射"""
+ return {h.strip(): i for i, h in enumerate(headers) if h.strip()}
+
+
+def field_val(row, fm, name, default=""):
+ """从数据行中按字段名取值"""
+ idx = fm.get(name)
+ if idx is not None and idx < len(row):
+ return row[idx].strip()
+ return default
+
+
+def xlsx_to_csv_batch(config_dir, output_dir):
+ """将 config_dir 中所有 xlsx 批量转为 csv"""
+ from openpyxl import load_workbook
+ os.makedirs(output_dir, exist_ok=True)
+ count = 0
+ for xlsx in glob.glob(os.path.join(config_dir, "*.xlsx")):
+ fname = os.path.basename(xlsx)
+ if fname.startswith("~$"):
+ continue
+ csv_path = os.path.join(output_dir, os.path.splitext(fname)[0] + ".csv")
+ try:
+ wb = load_workbook(xlsx, read_only=True, data_only=True)
+ ws = wb.active
+ with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
+ writer = csv.writer(f)
+ for row in ws.iter_rows(values_only=True):
+ writer.writerow([c if c is not None else "" for c in row])
+ wb.close()
+ count += 1
+ except Exception:
+ pass
+ # 复制非xlsx文件
+ import shutil
+ for entry in os.listdir(config_dir):
+ src = os.path.join(config_dir, entry)
+ if not os.path.isfile(src) or entry.endswith(".xlsx"):
+ continue
+ dst = os.path.join(output_dir, entry)
+ if not os.path.exists(dst):
+ shutil.copy2(src, dst)
+ return count
+
+
+# ================================================================
+# 三、图片资源查找
+# ================================================================
+
+def find_image(icon_name, icon_dirs, icon_key):
+ """根据 icon 名称在指定目录中查找图片文件"""
+ icon_dir = icon_dirs.get(icon_key)
+ if not icon_dir or not os.path.isdir(icon_dir):
+ return None
+ # 直接匹配
+ for ext in [".png", ".jpg"]:
+ p = os.path.join(icon_dir, icon_name + ext)
+ if os.path.isfile(p):
+ return p
+ # 补零匹配 (如 icon_product_1 -> icon_product_01)
+ m = re.match(r"^(.+?)(\d+)$", icon_name)
+ if m:
+ prefix, num = m.group(1), m.group(2)
+ for pad in [2, 3]:
+ padded = prefix + num.zfill(pad)
+ for ext in [".png", ".jpg"]:
+ p = os.path.join(icon_dir, padded + ext)
+ if os.path.isfile(p):
+ return p
+ return None
+
+
+# ================================================================
+# 四、GameTextConst.cs 自动解析
+# ================================================================
+
+def parse_game_text_const(cs_path, scripts_dir):
+ """
+ 自动解析 GameTextConst.cs 中所有 public const string 定义,
+ 扫描项目中其他 .cs 文件确认哪些常量被引用,
+ 返回去重后的文本列表。
+ """
+ if not cs_path or not os.path.isfile(cs_path):
+ return []
+
+ # 读取文件
+ content = None
+ for enc in ["utf-8-sig", "utf-8", "gbk", "gb2312"]:
+ try:
+ with open(cs_path, "r", encoding=enc) as f:
+ content = f.read()
+ break
+ except (UnicodeDecodeError, UnicodeError):
+ continue
+ if content is None:
+ return []
+
+ # 提取常量:public const string Name = "value";
+ pattern = r'public\s+const\s+string\s+(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"\s*;'
+ all_consts = {}
+ for m in re.finditer(pattern, content):
+ name, value = m.group(1), m.group(2)
+ # 还原转义
+ value = value.replace("\\n", "\n").replace("\\t", "\t").replace('\\"', '"')
+ all_consts[name] = value
+
+ if not all_consts:
+ return []
+
+ # 扫描引用
+ const_names = set(all_consts.keys())
+ referenced = set()
+ cs_file = os.path.basename(cs_path)
+ if scripts_dir and os.path.isdir(scripts_dir):
+ for root, _, files in os.walk(scripts_dir):
+ for fname in files:
+ if not fname.endswith(".cs") or fname == cs_file:
+ continue
+ try:
+ with open(os.path.join(root, fname), "r", encoding="utf-8", errors="ignore") as f:
+ src = f.read()
+ for cn in const_names:
+ if cn in src:
+ referenced.add(cn)
+ except Exception:
+ continue
+
+ # 去重文本值
+ seen = set()
+ result = []
+ for name, text in all_consts.items():
+ if name in referenced and text not in seen:
+ seen.add(text)
+ result.append(text)
+ return result
+
+
+# ================================================================
+# 五、文档格式化辅助
+# ================================================================
+
+def _run(para, text, font="仿宋", size=14, bold=False, underline=None):
+ """创建带格式的 run"""
+ r = para.add_run(text)
+ r.font.name = font
+ r._element.rPr.rFonts.set(qn("w:eastAsia"), font)
+ r.font.size = Pt(size)
+ r.font.bold = bold
+ if underline is not None:
+ r.font.underline = underline
+ return r
+
+
+def add_title(doc, text):
+ """标题:宋体 二号(22pt) 加粗 居中"""
+ p = doc.add_paragraph()
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ _run(p, text, font="宋体", size=22, bold=True, underline=False)
+
+
+def add_section_heading(doc, cn_num, title):
+ """小标题:仿宋 四号(14pt) 加粗 中文编号"""
+ p = doc.add_paragraph()
+ _run(p, f"{cn_num}、{title}", font="仿宋", size=14, bold=True)
+
+
+def add_numbered_items(doc, items):
+ """编号列表:仿宋 四号 不加粗"""
+ for i, text in enumerate(items, 1):
+ p = doc.add_paragraph()
+ _run(p, f"{i}.{text}", font="仿宋", size=14, bold=False)
+
+
+def _set_cell(cell, text, bold=False, size=14):
+ """设置表格单元格文本"""
+ cell.text = ""
+ p = cell.paragraphs[0]
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ _run(p, str(text), font="仿宋", size=size, bold=bold)
+
+
+def _insert_img(cell, img_path):
+ """在单元格中插入图片(高度3cm 锁定纵横比)"""
+ p = cell.paragraphs[0]
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ p.add_run().add_picture(img_path, height=Cm(3))
+
+
+def add_data_table(doc, headers, rows, icon_dirs=None, icon_key=None):
+ """通用数据表格(支持图片列)"""
+ table = doc.add_table(rows=1, cols=len(headers), style="Table Grid")
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
+ for i, h in enumerate(headers):
+ _set_cell(table.rows[0].cells[i], h, bold=True, size=10)
+ for rd in rows:
+ row = table.add_row()
+ _set_cell(row.cells[0], rd[0])
+ if len(rd) > 1:
+ icon_name = rd[1]
+ img = find_image(icon_name, icon_dirs, icon_key) if icon_name and icon_dirs else None
+ if img:
+ _insert_img(row.cells[1], img)
+ else:
+ _set_cell(row.cells[1], "", size=10)
+ if len(rd) > 2:
+ _set_cell(row.cells[2], rd[2], size=10)
+
+
+def add_name_table(doc, names):
+ """玩家姓名表格(姓/名 两列)"""
+ table = doc.add_table(rows=1, cols=2, style="Table Grid")
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
+ for i, h in enumerate(["姓", "名"]):
+ _set_cell(table.rows[0].cells[i], h, bold=True)
+ for surname, given in names:
+ row = table.add_row()
+ _set_cell(row.cells[0], surname)
+ _set_cell(row.cells[1], given)
+
+
+# ================================================================
+# 六、数据提取函数
+# ================================================================
+
+def get_player_names(scanner):
+ """获取玩家随机名称(优先 name.csv,否则使用默认列表)"""
+ fp = scanner.find_file("name")
+ if fp and fp.endswith(".csv"):
+ headers, rows = read_csv(fp)
+ fm = field_map(headers)
+ if "first" in fm and "last" in fm:
+ seen = set()
+ result = []
+ for r in rows:
+ first = field_val(r, fm, "first")
+ last = field_val(r, fm, "last")
+ if first and last:
+ key = (first, last)
+ if key not in seen:
+ seen.add(key)
+ result.append(key)
+ if result:
+ return result
+ # 默认名称(100对)
+ return [
+ ("李", "子轩"), ("王", "梓涵"), ("张", "浩然"), ("刘", "欣怡"),
+ ("陈", "宇轩"), ("杨", "晨曦"), ("赵", "俊杰"), ("黄", "雅静"),
+ ("周", "天佑"), ("吴", "梦琪"), ("徐", "博文"), ("孙", "思彤"),
+ ("胡", "睿智"), ("朱", "静怡"), ("高", "昊天"), ("林", "雨桐"),
+ ("何", "泽宇"), ("郭", "婉清"), ("马", "俊熙"), ("罗", "雪柔"),
+ ("梁", "子豪"), ("宋", "依琳"), ("郑", "宇航"), ("谢", "诗涵"),
+ ("韩", "志强"), ("唐", "梦瑶"), ("冯", "文博"), ("于", "雅楠"),
+ ("董", "天宇"), ("萧", "欣妍"), ("程", "子睿"), ("曹", "静雯"),
+ ("袁", "俊豪"), ("邓", "雨欣"), ("许", "浩宇"), ("傅", "语嫣"),
+ ("沈", "子涵"), ("曾", "思雨"), ("彭", "博涛"), ("吕", "雅婷"),
+ ("苏", "天翔"), ("卢", "梦洁"), ("蒋", "文轩"), ("蔡", "雪梅"),
+ ("贾", "志远"), ("丁", "依娜"), ("魏", "子安"), ("薛", "静雅"),
+ ("叶", "俊峰"), ("阎", "雨婷"), ("余", "浩轩"), ("潘", "诗琪"),
+ ("杜", "子恒"), ("戴", "思雅"), ("夏", "博超"), ("钟", "雅琪"),
+ ("汪", "天瑞"), ("田", "梦菲"), ("任", "文昊"), ("姜", "雪莲"),
+ ("范", "志鹏"), ("方", "依婷"), ("石", "子健"), ("姚", "静淑"),
+ ("谭", "俊凯"), ("廖", "雨菲"), ("邹", "浩南"), ("熊", "诗瑶"),
+ ("金", "子宁"), ("陆", "思琪"), ("郝", "博远"), ("孔", "雅静"),
+ ("白", "天浩"), ("崔", "梦婷"), ("康", "文杰"), ("毛", "雪琪"),
+ ("邱", "志伟"), ("秦", "依静"), ("江", "子文"), ("史", "静怡"),
+ ("顾", "俊杰"), ("侯", "雨萱"), ("邵", "浩天"), ("孟", "诗涵"),
+ ("龙", "子辰"), ("万", "思妍"), ("段", "博文"), ("雷", "雅欣"),
+ ("钱", "天佑"), ("汤", "梦瑶"), ("尹", "文豪"), ("黎", "雪莹"),
+ ("易", "志强"), ("常", "依琳"), ("武", "子轩"), ("乔", "静雯"),
+ ("贺", "俊熙"), ("赖", "雨桐"), ("龚", "浩宇"), ("文", "诗琪"),
+ ]
+
+
+def get_csv_list_data(scanner, csv_name, field_name, filter_dict=None):
+ """通用:从CSV中按字段名提取去重文本列表"""
+ fp = scanner.find_file(csv_name)
+ if not fp or not fp.endswith(".csv"):
+ return []
+ headers, rows = read_csv(fp)
+ fm = field_map(headers)
+ seen = set()
+ result = []
+ for r in rows:
+ # 过滤条件
+ if filter_dict:
+ ok = True
+ for fk, fvs in filter_dict.items():
+ if field_val(r, fm, fk) not in fvs:
+ ok = False
+ break
+ if not ok:
+ continue
+ val = field_val(r, fm, field_name)
+ if val and val not in seen:
+ seen.add(val)
+ result.append(val)
+ return result
+
+
+def get_csv_table_data(scanner, csv_name, field_names, dedup_field=None):
+ """通用:从CSV中提取多字段表格数据 [(f1, f2, f3, ...), ...]"""
+ fp = scanner.find_file(csv_name)
+ if not fp or not fp.endswith(".csv"):
+ return []
+ headers, rows = read_csv(fp)
+ fm = field_map(headers)
+ seen = set()
+ result = []
+ for r in rows:
+ vals = tuple(field_val(r, fm, fn) for fn in field_names)
+ if not vals[0]:
+ continue
+ if dedup_field:
+ key = vals[0]
+ if key in seen:
+ continue
+ seen.add(key)
+ result.append(vals)
+ return result
+
+
+# ================================================================
+# 七、文档构建主流程
+# ================================================================
+
+def build_document(scanner, output_path):
+ """根据扫描结果构建完整文档"""
+ print("\n[1/4] 准备数据源...")
+
+ # 确保CSV可用
+ if not scanner.csv_dir or not any(f.endswith(".csv") for f in os.listdir(scanner.csv_dir)):
+ if scanner.config_dir:
+ print(f" CSV目录为空,正在从 {os.path.basename(scanner.config_dir)} 转换xlsx...")
+ cvs_out = os.path.join(scanner.work_dir or scanner.root, "cvs")
+ n = xlsx_to_csv_batch(scanner.config_dir, cvs_out)
+ scanner.csv_dir = cvs_out
+ print(f" 已转换 {n} 个xlsx文件")
+
+ # 预加载数据
+ system_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir)
+ player_names = get_player_names(scanner)
+
+ print(f" 系统提示: {len(system_prompts)}条")
+ print(f" 玩家名称: {len(player_names)}对")
+
+ print("\n[2/4] 创建文档...")
+ doc = Document()
+ style = doc.styles["Normal"]
+ style.font.name = "仿宋"
+ style.font.size = Pt(14)
+ style._element.rPr.rFonts.set(qn("w:eastAsia"), "仿宋")
+
+ doc_title = f"《{scanner.project_name}》文字脚本" if scanner.project_name else "游戏文字脚本"
+ add_title(doc, doc_title)
+
+ print("\n[3/4] 写入章节...")
+ icon_dirs = scanner.icon_dirs
+ sections_done = 0
+
+ # --- 一、系统提示 ---
+ add_section_heading(doc, CN_NUMBERS[0], "系统提示")
+ add_numbered_items(doc, system_prompts)
+ print(f" 一、系统提示: {len(system_prompts)}条")
+ sections_done += 1
+
+ # --- 二、任务文本 ---
+ quest_def = SECTION_DEFS["quest_texts"]
+ quest_texts = get_csv_list_data(scanner, quest_def["csv"], quest_def["field"], quest_def.get("filter"))
+ add_section_heading(doc, CN_NUMBERS[1], "任务文本")
+ add_numbered_items(doc, quest_texts)
+ print(f" 二、任务文本: {len(quest_texts)}条")
+ sections_done += 1
+
+ # --- 三、成就文本 ---
+ ach_def = SECTION_DEFS["achievements"]
+ ach_texts = get_csv_list_data(scanner, ach_def["csv"], ach_def["field"], ach_def.get("filter"))
+ add_section_heading(doc, CN_NUMBERS[2], "成就文本")
+ add_numbered_items(doc, ach_texts)
+ print(f" 三、成就文本: {len(ach_texts)}条")
+ sections_done += 1
+
+ # --- 四、玩家随机名称文本 ---
+ add_section_heading(doc, CN_NUMBERS[3], "玩家随机名称文本")
+ add_name_table(doc, player_names)
+ print(f" 四、玩家随机名称文本: {len(player_names)}对")
+ sections_done += 1
+
+ # --- 五、新手指引文本 ---
+ guide_def = SECTION_DEFS["guide_texts"]
+ guide_texts = get_csv_list_data(scanner, guide_def["csv"], guide_def["field"])
+ add_section_heading(doc, CN_NUMBERS[4], "新手指引文本")
+ add_numbered_items(doc, guide_texts)
+ print(f" 五、新手指引文本: {len(guide_texts)}条")
+ sections_done += 1
+
+ # --- 六、道具说明 ---
+ prop_def = SECTION_DEFS["props"]
+ prop_data = get_csv_table_data(scanner, prop_def["csv"], prop_def["fields"])
+ add_section_heading(doc, CN_NUMBERS[5], "道具说明")
+ add_data_table(doc, ["道具名称", "图标", "道具描述"], prop_data, icon_dirs, prop_def["icon_key"])
+ print(f" 六、道具说明: {len(prop_data)}条")
+ sections_done += 1
+
+ # --- 七、技能 ---
+ skill_def = SECTION_DEFS["skills"]
+ skill_data = get_csv_table_data(scanner, skill_def["csv"], skill_def["fields"])
+ add_section_heading(doc, CN_NUMBERS[6], "技能")
+ add_data_table(doc, ["技能名称", "技能图标", "技能描述"], skill_data, icon_dirs, skill_def["icon_key"])
+ print(f" 七、技能: {len(skill_data)}条")
+ sections_done += 1
+
+ # --- 八、灵石 ---
+ rune_def = SECTION_DEFS["runes"]
+ rune_data = get_csv_table_data(scanner, rune_def["csv"], rune_def["fields"])
+ add_section_heading(doc, CN_NUMBERS[7], "灵石")
+ add_data_table(doc, ["灵石名称", "灵石图标", "灵石介绍"], rune_data, icon_dirs, rune_def["icon_key"])
+ print(f" 八、灵石: {len(rune_data)}条")
+ sections_done += 1
+
+ # --- 九、天赋 ---
+ talent_def = SECTION_DEFS["talents"]
+ talent_data = get_csv_table_data(scanner, talent_def["csv"], talent_def["fields"], talent_def.get("dedup"))
+ add_section_heading(doc, CN_NUMBERS[8], "天赋")
+ add_data_table(doc, ["天赋名称", "天赋图标", "天赋介绍"], talent_data, icon_dirs, talent_def["icon_key"])
+ print(f" 九、天赋: {len(talent_data)}条")
+ sections_done += 1
+
+ # --- 十、装备 ---
+ equip_def = SECTION_DEFS["equips"]
+ equip_data = get_csv_table_data(scanner, equip_def["csv"], equip_def["fields"], equip_def.get("dedup"))
+ add_section_heading(doc, CN_NUMBERS[9], "装备")
+ add_data_table(doc, ["装备名称", "装备图标"], equip_data, icon_dirs, equip_def["icon_key"])
+ print(f" 十、装备: {len(equip_data)}条")
+ sections_done += 1
+
+ # --- 十一、关卡名称 ---
+ level_def = SECTION_DEFS["level_names"]
+ level_names = get_csv_list_data(scanner, level_def["csv"], level_def["field"])
+ add_section_heading(doc, CN_NUMBERS[10], "关卡名称")
+ add_numbered_items(doc, level_names)
+ print(f" 十一、关卡名称: {len(level_names)}条")
+ sections_done += 1
+
+ print(f"\n[4/4] 保存文档...")
+ doc.save(output_path)
+ print(f" 文档已生成: {output_path}")
+ print(f" 共 {sections_done} 个章节")
+
+
+# ================================================================
+# 主入口
+# ================================================================
+
+def main():
+ parser = argparse.ArgumentParser(description="通用游戏文字脚本文档生成工具")
+ parser.add_argument("project_path", nargs="?", help="项目根目录路径 (默认: 当前目录)")
+ parser.add_argument("--output", "-o", help="输出文档路径 (默认: 自动生成)")
+ args = parser.parse_args()
+
+ project_path = args.project_path or os.getcwd()
+ if not os.path.isdir(project_path):
+ print(f"错误: 目录不存在 - {project_path}")
+ sys.exit(1)
+
+ print(f"{'='*50}")
+ print(f" 通用游戏文字脚本生成工具")
+ print(f"{'='*50}")
+ print(f"\n扫描项目: {project_path}")
+
+ scanner = ProjectScanner(project_path)
+ print(f"\n{scanner.summary()}")
+
+ if not scanner.csv_dir and not scanner.config_dir:
+ print("\n错误: 未找到配置目录 (*_config) 或CSV目录")
+ sys.exit(1)
+
+ if args.output:
+ output_path = args.output
+ else:
+ work_dir = scanner.work_dir or os.path.join(project_path, "work")
+ os.makedirs(work_dir, exist_ok=True)
+ title = scanner.project_name or "游戏"
+ output_path = os.path.join(work_dir, f"《{title}》游戏文字脚本.docx")
+
+ build_document(scanner, output_path)
+
+ print(f"\n{'='*50}")
+ print(f" 全部完成!")
+ print(f"{'='*50}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/wenzijiaoben/py/xlsx_to_csv.py b/wenzijiaoben/py/xlsx_to_csv.py
new file mode 100644
index 0000000..51b2083
--- /dev/null
+++ b/wenzijiaoben/py/xlsx_to_csv.py
@@ -0,0 +1,104 @@
+"""
+xlsx转csv工具
+- 将指定目录下所有.xlsx文件转换为.csv格式
+- 输出路径:D:\15CVS
+- 只处理每个xlsx文件的第一个工作表(主数据表)
+- 忽略以~$开头的临时文件
+- CSV编码:UTF-8 with BOM
+- 避免覆盖同名文件(如已存在则跳过并提示)
+"""
+
+import os
+import sys
+import csv
+import glob
+from openpyxl import load_workbook
+
+
+def xlsx_to_csv(src_dir, out_dir):
+ """将源目录下所有xlsx文件转换为csv格式"""
+ # 确保输出目录存在
+ os.makedirs(out_dir, exist_ok=True)
+
+ # 统计计数
+ success_count = 0
+ skip_count = 0
+ fail_count = 0
+
+ # 遍历源目录下所有xlsx文件(不递归子目录)
+ xlsx_files = glob.glob(os.path.join(src_dir, "*.xlsx"))
+
+ for xlsx_path in xlsx_files:
+ filename = os.path.basename(xlsx_path)
+
+ # 跳过以~$开头的临时文件
+ if filename.startswith("~$"):
+ print(f"[跳过] 临时文件: {filename}")
+ skip_count += 1
+ continue
+
+ csv_filename = os.path.splitext(filename)[0] + ".csv"
+ csv_path = os.path.join(out_dir, csv_filename)
+
+ # 检查目标文件是否已存在
+ if os.path.exists(csv_path):
+ print(f"[跳过] 文件已存在: {csv_filename}")
+ skip_count += 1
+ continue
+
+ try:
+ print(f"[转换] {filename} -> {csv_filename}", end=" ... ")
+
+ # 读取xlsx文件(只读模式,提高性能)
+ wb = load_workbook(xlsx_path, read_only=True, data_only=True)
+ ws = wb.active # 获取第一个工作表
+
+ # 写入CSV文件
+ with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
+ writer = csv.writer(f)
+
+ for row in ws.iter_rows(values_only=True):
+ # 将None转为空字符串,其他值保持原样
+ csv_row = [cell if cell is not None else "" for cell in row]
+ writer.writerow(csv_row)
+
+ wb.close()
+ print("成功")
+ success_count += 1
+
+ except Exception as e:
+ print(f"失败")
+ print(f" [错误] {filename}: {e}")
+ fail_count += 1
+ # 清理可能生成的不完整文件
+ if os.path.exists(csv_path):
+ try:
+ os.remove(csv_path)
+ except Exception:
+ pass
+
+ # 打印统计结果
+ print("\n" + "=" * 50)
+ print(f"转换完成!")
+ print(f" 成功: {success_count} 个")
+ print(f" 跳过: {skip_count} 个")
+ print(f" 失败: {fail_count} 个")
+ print(f" 总计: {success_count + skip_count + fail_count} 个")
+ print("=" * 50)
+
+
+if __name__ == "__main__":
+ src_dir = r"d:\A工作\P-L15_config"
+ out_dir = r"D:\15CVS"
+
+ # 支持命令行参数修改源目录
+ if len(sys.argv) > 1:
+ src_dir = sys.argv[1]
+ if len(sys.argv) > 2:
+ out_dir = sys.argv[2]
+
+ print(f"源目录: {src_dir}")
+ print(f"输出目录: {out_dir}")
+ print("-" * 50)
+
+ xlsx_to_csv(src_dir, out_dir)
diff --git a/wenzijiaoben/使用请看.txt b/wenzijiaoben/使用请看.txt
new file mode 100644
index 0000000..67337cc
--- /dev/null
+++ b/wenzijiaoben/使用请看.txt
@@ -0,0 +1 @@
+将PY放入项目文件中,运行universal_tool.py他会自动读取设定好的表格对应关系进行自动生成
\ No newline at end of file