diff --git a/wenzijiaoben/.workbuddy/memory/2026-08-21.md b/wenzijiaoben/.workbuddy/memory/2026-08-21.md new file mode 100644 index 0000000..4b9b916 --- /dev/null +++ b/wenzijiaoben/.workbuddy/memory/2026-08-21.md @@ -0,0 +1,42 @@ +# 2026-08-21 工作日志 + +## universal_tool.py 改造(两项) + +### 1. 直接读取 xlsx,不再转 csv +- 新增 `read_table(filepath)` 统一入口 + `_read_xlsx` / `_read_csv`。(`read_csv` 兼容别名后已删除) +- `find_file` 改为优先 `.xlsx`,其次 `.csv`。 +- `get_csv_list_data` / `get_csv_table_data` / `get_player_names` 去掉 `.csv` 限制,支持任意表格格式。 +- `build_document` 删除 `xlsx_to_csv_batch` 自动转换逻辑,直接从 config_dir / csv_dir 读 xlsx。 +- `xlsx_to_csv_batch` 函数定义后已在第二轮清理中删除。 + +### 2. Word 自动编号替代硬编码数字 +- 新增 `_get_numbering_element` / `_create_decimal_numbering` / `_apply_numbering`。 +- 重写 `add_numbered_items`:每次调用新建独立 numId(decimal, "1." 格式),段落用 numPr,文本不再带 "1." 前缀。 +- 效果:在 Word 中增删条目后编号自动重排。 +- 章节标题 `add_section_heading` 仍用硬编码中文数字("一、""二、"),因章节固定不增删。 + +## 验证 +- 用 managed python venv (3.13.12) + python-docx/openpyxl 实测: + - 6 个编号段落均带 numPr;2 个章节用 2 个不同 numId 独立从 1 计数;无硬编码数字前缀。 +- venv 位置:C:\Users\Administrator\.workbuddy\binaries\python\envs\default + +## 项目清理(第二轮:删无用 + 更新文档) + +### 删除 +- `py/generate_doc.py`(旧硬编码版,路径写死)、`py/xlsx_to_csv.py`(不再需要转 csv)、`py/__pycache__`。 +- 清理后 `py/` 仅剩 `universal_tool.py`。 + +### 清理 universal_tool.py +- 删除 `xlsx_to_csv_batch` 函数、`read_csv` 兼容别名、`import glob`。 +- 更新文件头/章节注释:去除 "CSV 缓存" 说法,注明直接读 xlsx、Word 自动编号。 +- py_compile + import 验证通过(read_table 存在,xlsx_to_csv_batch/read_csv 已无)。 + +### 更新 memories/*.json +- `data_source_mapping.json`:`csv_base_dir`→`table_base_dir`,各 source `.csv`→`.xlsx/csv`,`csv_format`→`table_format` 补充 xlsx 直读。 +- `project_config.json`:去掉被删脚本引用,路径改为示例说明,补充 data_reading 策略。 +- `system_prompts.json`:`source_file`→`source_file_example` + note(自动解析说明)。 +- `icon_resource_mapping.json`:`base_dir`→示例,`csv_field`→`table_field` (.xlsx/csv)。 +- `doc_format_spec.json`:标题改示例标注(格式规范本身通用,numbering 已是 Word 自动编号)。 + +### 更新 使用请看.txt +- 改为准确说明:自动扫描项目结构、直接读 xlsx、Word 自动编号、memories 仅为参考不被脚本读取。 diff --git a/wenzijiaoben/.workbuddy/memory/MEMORY.md b/wenzijiaoben/.workbuddy/memory/MEMORY.md new file mode 100644 index 0000000..1000e53 --- /dev/null +++ b/wenzijiaoben/.workbuddy/memory/MEMORY.md @@ -0,0 +1,15 @@ +# 项目长期记忆 + +## 项目概述 +游戏文字脚本文档生成工具:扫描项目目录,从配置表(xlsx/csv)和 GameTextConst.cs 提取文本,生成 Word 文档。 +入口:`py/universal_tool.py`(通用版);`py/generate_doc.py` 为早期硬编码版(路径写死,仅参考)。 + +## 关键约定 +- **数据读取**:直接读取 xlsx(优先),无需先转 csv。表头结构固定为第3行英文字段名、第4行起数据。 +- **文档编号**:正文条目用 Word 自动编号(numPr + decimal),每章节独立 numId 从 1 开始;段落文本不硬编码 "1.",便于增删后自动重排。章节标题仍用硬编码中文数字。 +- **字体**:标题宋体二号加粗居中;章节标题/正文仿宋四号。 +- **图标**:表格图标列高 3cm 锁定纵横比,按 atlas_* 目录匹配,支持末尾数字补零。 + +## 环境 +- 推荐用 managed python 3.13.12 + venv(C:\Users\Administrator\.workbuddy\binaries\python\envs\default),依赖:python-docx、openpyxl。 +- 项目配置记忆在 `memories/*.json`(数据源映射、格式规范、图标映射、项目配置)。 diff --git a/wenzijiaoben/memories/data_source_mapping.json b/wenzijiaoben/memories/data_source_mapping.json index 62193be..b1ce026 100644 --- a/wenzijiaoben/memories/data_source_mapping.json +++ b/wenzijiaoben/memories/data_source_mapping.json @@ -1,42 +1,43 @@ { - "description": "CSV数据源到文档章节的映射关系", - "csv_base_dir": "d:\\p-L12\\work\\cvs", + "description": "配置表数据源到文档章节的映射关系", + "table_base_dir": "*_config 目录(优先读 .xlsx,其次 .csv,无需预先转换)", + "read_strategy": "universal_tool.find_file 优先查找 name.xlsx,找不到再找 name.csv;两种格式都直接读取,表头结构一致", "mappings": { "system_prompts": { "source": "GameTextConst.cs (代码引用分析)", "section": "一、系统提示", "field": "常量值文本", - "filter": "仅保留在代码中被实际引用的常量(排除仅在注释中出现的)", + "filter": "仅保留在代码中被实际引用的常量(排除仅在注释中出现的)", "dedup": "按文本值去重" }, "quest_texts": { - "source": "quest.csv", + "source": "quest.xlsx/csv", "section": "二、任务文本", "field": "desc (第4列, index=3)", "filter": "category == 1 或 category == 2", "dedup": "按desc文本去重" }, "achievement_texts": { - "source": "quest.csv", + "source": "quest.xlsx/csv", "section": "三、成就文本", "field": "desc (第4列, index=3)", "filter": "category == 9", "dedup": "按desc文本去重" }, "player_names": { - "source": "原始文档固定数据 (100对姓名)", + "source": "name.xlsx/csv (找不到则用内置默认100对姓名)", "section": "四、玩家随机名称文本", "format": "表格: 姓 | 名" }, "guide_texts": { - "source": "guide.csv", + "source": "guide.xlsx/csv", "section": "五、新手指引文本", "field": "desc (第8列, index=7)", "filter": "无", "dedup": "按desc文本去重" }, "props": { - "source": "prop.csv", + "source": "prop.xlsx/csv", "section": "六、道具说明", "fields": { "name": "第3列 (index=2)", @@ -47,7 +48,7 @@ "icon_naming": "icon_product_XX.png (XX为补零两位数, 如 icon_product_01)" }, "skills": { - "source": "skill.csv", + "source": "skill.xlsx/csv", "section": "七、技能", "fields": { "name": "第2列 (index=1)", @@ -58,7 +59,7 @@ "icon_naming": "skillX.png (X为数字, 不补零, 如 skill1, skill10)" }, "runes": { - "source": "rune.csv", + "source": "rune.xlsx/csv", "section": "八、灵石", "fields": { "name": "第2列 (index=1)", @@ -69,7 +70,7 @@ "icon_naming": "rune_icon_X.png (X为数字, 不补零)" }, "talents": { - "source": "playerlevel.csv", + "source": "playerlevel.xlsx/csv", "section": "九、天赋", "fields": { "name": "第14列 (index=13)", @@ -81,7 +82,7 @@ "dedup": "按name去重" }, "equipment": { - "source": "equip.csv", + "source": "equip.xlsx/csv", "section": "十、装备", "fields": { "name": "第2列 (index=1)", @@ -90,19 +91,20 @@ "icon_dir": "atlas_equip_icons", "icon_naming": "equip_icon_X.png (X为数字, 不补零)", "dedup": "按name去重", - "note": "equip.csv无desc字段" + "note": "equip表无desc字段" }, "level_names": { - "source": "fight_sample.csv", + "source": "fight_sample.xlsx/csv", "section": "十一、关卡名称", "field": "name (第3列, index=2)", "filter": "无", "dedup": "按name去重" } }, - "csv_format": { + "table_format": { "header_rows": 3, - "header_structure": "第1行:中文头, 第2行:类型定义, 第3行:英文字段名", - "encoding_note": "部分文件为GBK编码,需自动检测" + "header_structure": "第1行:中文头, 第2行:类型定义, 第3行:英文字段名(用作字段映射)", + "data_start_row": "第4行起", + "encoding_note": "csv需自动检测(优先级 utf-8-sig -> gbk -> gb2312 -> utf-8);xlsx由openpyxl直接读取无编码问题" } } diff --git a/wenzijiaoben/memories/doc_format_spec.json b/wenzijiaoben/memories/doc_format_spec.json index 185edd1..021ae97 100644 --- a/wenzijiaoben/memories/doc_format_spec.json +++ b/wenzijiaoben/memories/doc_format_spec.json @@ -1,5 +1,5 @@ { - "document_title": "《不朽封仙》文字脚本", + "document_title_example": "《<项目名>》文字脚本 (universal_tool 用扫描到的项目名生成)", "sections": [ {"number": "一", "title": "系统提示", "type": "numbered_list"}, {"number": "二", "title": "任务文本", "type": "numbered_list"}, @@ -35,7 +35,7 @@ "font_en": "FangSong", "size": "四号 (14pt)", "bold": false, - "numbering": "阿拉伯数字 (1. 2. 3. ...)" + "numbering": "Word自动编号列表 (decimal, 每章节独立numId从1开始),增删条目后编号自动重排;段落文本不再硬编码 1. 2. 3." }, "table_images": { "height": "3cm", diff --git a/wenzijiaoben/memories/icon_resource_mapping.json b/wenzijiaoben/memories/icon_resource_mapping.json index 3640e3e..df10b76 100644 --- a/wenzijiaoben/memories/icon_resource_mapping.json +++ b/wenzijiaoben/memories/icon_resource_mapping.json @@ -1,10 +1,10 @@ { "description": "图标资源名称到图片文件的映射规则与目录结构", - "base_dir": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Resources\\Resources_moved\\gui\\atlas_ui", + "base_dir_example": "/unity/Assets/Resources/Resources_moved/gui/atlas_ui (universal_tool 自动扫描 atlas_* 子目录)", "directories": { "atlas_product_icons": { "section": "六、道具说明", - "csv_field": "prop.csv icon (index=7)", + "table_field": "prop.xlsx/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, @@ -12,7 +12,7 @@ }, "atlas_skill": { "section": "七、技能", - "csv_field": "skill.csv icon (index=6)", + "table_field": "skill.xlsx/csv icon (index=6)", "naming_pattern": "skillX.png", "naming_note": "X为数字不补零 (1-99), 如 skill1, skill10, skill99", "file_count": 99, @@ -20,7 +20,7 @@ }, "atlas_rune_icons": { "section": "八、灵石", - "csv_field": "rune.csv icon (index=2)", + "table_field": "rune.xlsx/csv icon (index=2)", "naming_pattern": "rune_icon_X.png", "naming_note": "X为数字不补零 (1-18)", "file_count": 12, @@ -28,7 +28,7 @@ }, "atlas_talent_icons": { "section": "九、天赋", - "csv_field": "playerlevel.csv icon (index=15)", + "table_field": "playerlevel.xlsx/csv icon (index=15)", "naming_pattern": "icon_talent_X.png", "naming_note": "X为数字不补零 (1-6)", "file_count": 6, @@ -36,7 +36,7 @@ }, "atlas_equip_icons": { "section": "十、装备", - "csv_field": "equip.csv icon (index=5)", + "table_field": "equip.xlsx/csv icon (index=5)", "naming_pattern": "equip_icon_X.png", "naming_note": "X为数字不补零 (1-6)", "file_count": 6, diff --git a/wenzijiaoben/memories/project_config.json b/wenzijiaoben/memories/project_config.json index ffe1ee8..2c5d2b3 100644 --- a/wenzijiaoben/memories/project_config.json +++ b/wenzijiaoben/memories/project_config.json @@ -1,17 +1,15 @@ { - "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" + "project_name": "P-L12《不朽封仙》(历史参考项目)", + "note": "本文件记录历史项目的路径约定,作为通用工具 universal_tool.py 的参考。当前工具自动扫描项目结构,不再依赖固定路径。", + "paths_example": { + "config_dir": "/<项目名>_config (xlsx/csv 配置表)", + "game_dir": "/<项目名>_game", + "scripts_dir": "/<项目名>_game/unity/Assets/Scripts", + "memories_dir": "<本项目>/memories", + "output_doc": "/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" + "universal_tool": "<本项目>/py/universal_tool.py (唯一入口,自动扫描+生成)" }, "dependencies": { "python_packages": ["openpyxl", "python-docx"], @@ -20,14 +18,14 @@ "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)" + "data_reading": { + "strategy": "直接读取 xlsx(优先)或 csv,无需预先转换", + "csv_encoding": "按优先级尝试 utf-8-sig -> gbk -> gb2312 -> utf-8 (仅csv需要)", + "xlsx_reading": "openpyxl read_only/data_only 直接读取" }, - "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" + "game_code_paths_example": { + "game_text_const": "/unity/Assets/Scripts/.../GameTextConst.cs", + "scripts_root": "/unity/Assets/Scripts" }, - "icon_resource_base": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Resources\\Resources_moved\\gui\\atlas_ui" + "icon_resource_base_example": "/unity/Assets/Resources/Resources_moved/gui/atlas_ui" } diff --git a/wenzijiaoben/memories/system_prompts.json b/wenzijiaoben/memories/system_prompts.json index 179d32d..0025e93 100644 --- a/wenzijiaoben/memories/system_prompts.json +++ b/wenzijiaoben/memories/system_prompts.json @@ -1,6 +1,7 @@ { - "description": "GameTextConst.cs 中被代码实际引用的系统提示常量", - "source_file": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts\\mgame\\error\\GameTextConst.cs", + "description": "GameTextConst.cs 中被代码实际引用的系统提示常量(历史项目参考)", + "source_file_example": "/unity/Assets/Scripts/.../GameTextConst.cs (universal_tool 自动扫描定位)", + "note": "universal_tool.parse_game_text_const 自动解析 *Const*.cs 中所有 public const string,扫描其他 .cs 确认被引用后按文本去重;本文件为历史项目的静态快照参考", "total_referenced": 85, "total_unique_text": 83, "note": "85个被引用常量中,有2对文本值重复('{0}不足。' 和 '保存失败。'),去重后为83条", diff --git a/wenzijiaoben/py/__pycache__/generate_doc.cpython-311.pyc b/wenzijiaoben/py/__pycache__/generate_doc.cpython-311.pyc deleted file mode 100644 index b73db0b..0000000 Binary files a/wenzijiaoben/py/__pycache__/generate_doc.cpython-311.pyc and /dev/null differ diff --git a/wenzijiaoben/py/generate_doc.py b/wenzijiaoben/py/generate_doc.py deleted file mode 100644 index 960fbfe..0000000 --- a/wenzijiaoben/py/generate_doc.py +++ /dev/null @@ -1,568 +0,0 @@ -# -*- 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 index 94d9609..5e68119 100644 --- a/wenzijiaoben/py/universal_tool.py +++ b/wenzijiaoben/py/universal_tool.py @@ -4,6 +4,7 @@ 功能:自动扫描项目目录,识别标准结构,生成游戏文字脚本Word文档。 无需手动配置路径,只需提供项目根目录即可自动完成全部流程。 +配置表直接读取 xlsx(优先)或 csv,无需预先转换。 标准目录结构约定: / @@ -11,7 +12,9 @@ ├── *_game/ ← 游戏客户端目录 │ └── unity/Assets/Resources/.../atlas_ui/ ← 图标资源 │ └── unity/Assets/Scripts/.../*Const*.cs ← 系统提示常量 - └── work/ ← 工作目录 (CSV缓存、输出等) + └── work/ ← 工作目录 (输出等) + +正文编号使用 Word 自动编号列表,在 Word 中增删条目后编号会自动重排。 用法: python universal_tool.py [项目路径] [--output 输出路径] @@ -23,18 +26,18 @@ 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 +from docx.oxml import OxmlElement # ============ 常量 ============ CN_NUMBERS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一"] -# 11章节数据源定义(字段名取自CSV英文头行第3行) +# 11章节数据源定义(字段名取自表格英文头行第3行;优先读 xlsx,其次 csv) SECTION_DEFS = { "quest_texts": {"csv": "quest", "field": "desc", "filter": {"category": ["1", "2"]}}, "achievements": {"csv": "quest", "field": "desc", "filter": {"category": ["9"]}}, @@ -86,7 +89,8 @@ class ProjectScanner: base = os.path.basename(self.config_dir) self.project_name = base.replace("_config", "") - # 3. CSV目录: work/cvs 优先,其次 config_dir 本身 + # 3. 数据表目录(csv_dir): work/cvs 优先(若存在),否则用 config_dir 本身(含xlsx) + # find_file 优先查找 .xlsx,其次 .csv,两种格式都直接读取 if self.work_dir: cvs = os.path.join(self.work_dir, "cvs") if os.path.isdir(cvs) and os.listdir(cvs): @@ -139,11 +143,11 @@ class ProjectScanner: return def find_file(self, name): - """在 csv_dir 或 config_dir 中查找文件(支持 .csv/.xlsx)""" + """在 csv_dir 或 config_dir 中查找文件(优先 .xlsx,其次 .csv)""" for d in [self.csv_dir, self.config_dir]: if not d: continue - for ext in [".csv", ".xlsx"]: + for ext in [".xlsx", ".csv"]: p = os.path.join(d, name + ext) if os.path.isfile(p): return p @@ -155,7 +159,7 @@ class ProjectScanner: f" 项目名称: {self.project_name}", f" 配置目录: {self.config_dir or '未找到'}", f" 游戏目录: {self.game_dir or '未找到'}", - f" CSV目录: {self.csv_dir or '未找到'}", + f" 数据表目录: {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 '未找到'}", @@ -164,16 +168,24 @@ class ProjectScanner: # ================================================================ -# 二、CSV 数据读取(自动编码 + 动态字段名解析) +# 二、表格数据读取(xlsx/csv 自动识别 + 动态字段名解析) # ================================================================ -def read_csv(filepath): +def read_table(filepath): """ - 读取CSV文件,自动检测编码 (utf-8-sig/gbk/gb2312/utf-8)。 - 返回 (headers, rows): + 统一表格读取(支持 .xlsx / .csv),返回 (headers, rows): headers: 第3行英文字段名列表 rows: 第4行起的数据行列表 (list of list) + xlsx 直接读取,无需先转 csv。 """ + ext = os.path.splitext(filepath)[1].lower() + if ext == ".xlsx": + return _read_xlsx(filepath) + return _read_csv(filepath) + + +def _read_csv(filepath): + """读取CSV文件,自动检测编码 (utf-8-sig/gbk/gb2312/utf-8)。""" for enc in ["utf-8-sig", "gbk", "gb2312", "utf-8"]: try: with open(filepath, "r", encoding=enc) as f: @@ -186,6 +198,20 @@ def read_csv(filepath): return [], [] +def _read_xlsx(filepath): + """直接读取 xlsx(无需先转csv),表头结构与csv一致:第3行字段名,第4行起数据。""" + from openpyxl import load_workbook + wb = load_workbook(filepath, read_only=True, data_only=True) + ws = wb.active + all_rows = [] + for row in ws.iter_rows(values_only=True): + all_rows.append([str(c) if c is not None else "" for c in row]) + wb.close() + if len(all_rows) < 4: + return all_rows[-1] if all_rows else [], [] + return all_rows[2], all_rows[3:] + + def field_map(headers): """将英文头行转为 {字段名: 列索引} 映射""" return {h.strip(): i for i, h in enumerate(headers) if h.strip()} @@ -199,39 +225,6 @@ def field_val(row, fm, name, default=""): 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 - - # ================================================================ # 三、图片资源查找 # ================================================================ @@ -353,11 +346,106 @@ def add_section_heading(doc, cn_num, title): _run(p, f"{cn_num}、{title}", font="仿宋", size=14, bold=True) +# ---- Word 自动编号辅助 ---- +# 用 Word 的 numbering 机制生成 1. 2. 3. ... 列表, +# 这样在 Word 中增删条目后编号会自动重排,无需手改数字。 + +def _get_numbering_element(doc): + """获取文档的 根元素(默认模板已含 numbering part)。""" + try: + return doc.part.numbering_part.element + except KeyError: + raise RuntimeError("当前文档模板缺少 numbering part,无法使用自动编号。") + + +def _create_decimal_numbering(doc): + """ + 新建一个十进制自动编号定义 (1. 2. 3. ...),返回新的 numId。 + 每次调用返回独立的 numId,编号从 1 开始, + 因此每个章节调用一次即可获得独立计数。 + """ + numbering = _get_numbering_element(doc) + + abs_ids = [int(an.get(qn('w:abstractNumId'))) for an in numbering.findall(qn('w:abstractNum'))] + num_ids = [int(n.get(qn('w:numId'))) for n in numbering.findall(qn('w:num'))] + next_abs = (max(abs_ids) + 1) if abs_ids else 0 + next_num = (max(num_ids) + 1) if num_ids else 1 + + # + abstract_num = OxmlElement('w:abstractNum') + abstract_num.set(qn('w:abstractNumId'), str(next_abs)) + multi_level = OxmlElement('w:multiLevelType') + multi_level.set(qn('w:val'), 'singleLevel') + abstract_num.append(multi_level) + + lvl = OxmlElement('w:lvl') + lvl.set(qn('w:ilvl'), '0') + start = OxmlElement('w:start') + start.set(qn('w:val'), '1') + lvl.append(start) + num_fmt = OxmlElement('w:numFmt') + num_fmt.set(qn('w:val'), 'decimal') + lvl.append(num_fmt) + lvl_text = OxmlElement('w:lvlText') + lvl_text.set(qn('w:val'), '%1.') + lvl.append(lvl_text) + lvl_jc = OxmlElement('w:lvlJc') + lvl_jc.set(qn('w:val'), 'left') + lvl.append(lvl_jc) + p_pr = OxmlElement('w:pPr') + ind = OxmlElement('w:ind') + ind.set(qn('w:left'), '420') + ind.set(qn('w:hanging'), '420') + p_pr.append(ind) + lvl.append(p_pr) + abstract_num.append(lvl) + + # + num = OxmlElement('w:num') + num.set(qn('w:numId'), str(next_num)) + abs_ref = OxmlElement('w:abstractNumId') + abs_ref.set(qn('w:val'), str(next_abs)) + num.append(abs_ref) + + # abstractNum 必须排在所有 num 之前 + last_abs = numbering.findall(qn('w:abstractNum')) + if last_abs: + last_abs[-1].addnext(abstract_num) + else: + numbering.insert(0, abstract_num) + numbering.append(num) + + return next_num + + +def _apply_numbering(paragraph, num_id, ilvl=0): + """给段落应用自动编号 (设置 numPr)。""" + p_pr = paragraph._p.get_or_add_pPr() + for existing in p_pr.findall(qn('w:numPr')): + p_pr.remove(existing) + num_pr = OxmlElement('w:numPr') + ilvl_el = OxmlElement('w:ilvl') + ilvl_el.set(qn('w:val'), str(ilvl)) + num_pr.append(ilvl_el) + num_id_el = OxmlElement('w:numId') + num_id_el.set(qn('w:val'), str(num_id)) + num_pr.append(num_id_el) + p_pr.append(num_pr) + + def add_numbered_items(doc, items): - """编号列表:仿宋 四号 不加粗""" - for i, text in enumerate(items, 1): + """ + 编号列表:仿宋 四号 不加粗,使用 Word 自动编号。 + 每次调用创建独立编号序列(从 1 开始), + 在 Word 中增删条目后编号会自动重排。 + """ + if not items: + return + num_id = _create_decimal_numbering(doc) + for text in items: p = doc.add_paragraph() - _run(p, f"{i}.{text}", font="仿宋", size=14, bold=False) + _apply_numbering(p, num_id) + _run(p, str(text), font="仿宋", size=14, bold=False) def _set_cell(cell, text, bold=False, size=14): @@ -412,10 +500,10 @@ def add_name_table(doc, names): # ================================================================ def get_player_names(scanner): - """获取玩家随机名称(优先 name.csv,否则使用默认列表)""" + """获取玩家随机名称(优先 name.xlsx/csv,否则使用默认列表)""" fp = scanner.find_file("name") - if fp and fp.endswith(".csv"): - headers, rows = read_csv(fp) + if fp: + headers, rows = read_table(fp) fm = field_map(headers) if "first" in fm and "last" in fm: seen = set() @@ -461,11 +549,11 @@ def get_player_names(scanner): def get_csv_list_data(scanner, csv_name, field_name, filter_dict=None): - """通用:从CSV中按字段名提取去重文本列表""" + """通用:从表格(xlsx/csv)中按字段名提取去重文本列表""" fp = scanner.find_file(csv_name) - if not fp or not fp.endswith(".csv"): + if not fp: return [] - headers, rows = read_csv(fp) + headers, rows = read_table(fp) fm = field_map(headers) seen = set() result = [] @@ -487,11 +575,11 @@ def get_csv_list_data(scanner, csv_name, field_name, filter_dict=None): def get_csv_table_data(scanner, csv_name, field_names, dedup_field=None): - """通用:从CSV中提取多字段表格数据 [(f1, f2, f3, ...), ...]""" + """通用:从表格(xlsx/csv)中提取多字段表格数据 [(f1, f2, f3, ...), ...]""" fp = scanner.find_file(csv_name) - if not fp or not fp.endswith(".csv"): + if not fp: return [] - headers, rows = read_csv(fp) + headers, rows = read_table(fp) fm = field_map(headers) seen = set() result = [] @@ -516,14 +604,13 @@ 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文件") + # 直接从 config_dir / csv_dir 读取 xlsx(优先) 或 csv,不再转换。 + data_dir = scanner.csv_dir or scanner.config_dir + if data_dir: + files = [f for f in os.listdir(data_dir) if not f.startswith("~$")] + xlsx_n = sum(1 for f in files if f.endswith(".xlsx")) + csv_n = sum(1 for f in files if f.endswith(".csv")) + print(f" 数据目录: {data_dir} (xlsx {xlsx_n} 个 / csv {csv_n} 个)") # 预加载数据 system_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir) diff --git a/wenzijiaoben/py/xlsx_to_csv.py b/wenzijiaoben/py/xlsx_to_csv.py deleted file mode 100644 index 51b2083..0000000 --- a/wenzijiaoben/py/xlsx_to_csv.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -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 index 67337cc..885da93 100644 --- a/wenzijiaoben/使用请看.txt +++ b/wenzijiaoben/使用请看.txt @@ -1 +1,30 @@ -将PY放入项目文件中,运行universal_tool.py他会自动读取设定好的表格对应关系进行自动生成 \ No newline at end of file +通用游戏文字脚本文档生成工具 使用说明 +入口脚本: py/universal_tool.py + +一、用法 + python universal_tool.py <项目根目录> [--output 输出路径] + 不带参数则用当前目录作为项目根目录。 + +二、它会自动扫描项目目录结构: + <项目根目录>/ + ├── *_config/ 配置表目录 (优先读 .xlsx,其次 .csv,无需预先转换) + ├── *_game/ 游戏客户端目录 + │ └── .../atlas_ui/atlas_* 图标资源 + │ └── .../Scripts/*Const*.cs 系统提示常量 + └── work/ 输出目录 (文档默认生成到此) + +三、输出 + 默认: <项目根目录>/work/《<项目名>》游戏文字脚本.docx + 可用 --output 指定路径。 + +四、特性 + - 配置表直接读 xlsx,不再需要先转成 csv。 + - 正文条目使用 Word 自动编号,在 Word 中增删条目后编号会自动重排。 + - 依赖: pip install openpyxl python-docx + +五、示例 + python universal_tool.py d:\p-L12 + python universal_tool.py d:\p-L12 --output d:\out\脚本.docx + +注: memories/ 目录下的 json 仅为开发参考(数据源映射/格式规范/图标规则等), + 脚本运行时不读取它们,全部信息由自动扫描项目目录获得。