This commit is contained in:
JA
2026-08-21 10:39:51 +08:00
parent 46e4f8471a
commit c18f9d5536
12 changed files with 285 additions and 783 deletions
@@ -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`:每次调用新建独立 numIddecimal, "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 仅为参考不被脚本读取。
+15
View File
@@ -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 + venvC:\Users\Administrator\.workbuddy\binaries\python\envs\default),依赖:python-docx、openpyxl。
- 项目配置记忆在 `memories/*.json`(数据源映射、格式规范、图标映射、项目配置)。
+19 -17
View File
@@ -1,42 +1,43 @@
{ {
"description": "CSV数据源到文档章节的映射关系", "description": "配置表数据源到文档章节的映射关系",
"csv_base_dir": "d:\\p-L12\\work\\cvs", "table_base_dir": "*_config 目录(优先读 .xlsx,其次 .csv,无需预先转换)",
"read_strategy": "universal_tool.find_file 优先查找 name.xlsx,找不到再找 name.csv;两种格式都直接读取,表头结构一致",
"mappings": { "mappings": {
"system_prompts": { "system_prompts": {
"source": "GameTextConst.cs (代码引用分析)", "source": "GameTextConst.cs (代码引用分析)",
"section": "一、系统提示", "section": "一、系统提示",
"field": "常量值文本", "field": "常量值文本",
"filter": "仅保留在代码中被实际引用的常量排除仅在注释中出现的", "filter": "仅保留在代码中被实际引用的常量(排除仅在注释中出现的)",
"dedup": "按文本值去重" "dedup": "按文本值去重"
}, },
"quest_texts": { "quest_texts": {
"source": "quest.csv", "source": "quest.xlsx/csv",
"section": "二、任务文本", "section": "二、任务文本",
"field": "desc (第4列, index=3)", "field": "desc (第4列, index=3)",
"filter": "category == 1 或 category == 2", "filter": "category == 1 或 category == 2",
"dedup": "按desc文本去重" "dedup": "按desc文本去重"
}, },
"achievement_texts": { "achievement_texts": {
"source": "quest.csv", "source": "quest.xlsx/csv",
"section": "三、成就文本", "section": "三、成就文本",
"field": "desc (第4列, index=3)", "field": "desc (第4列, index=3)",
"filter": "category == 9", "filter": "category == 9",
"dedup": "按desc文本去重" "dedup": "按desc文本去重"
}, },
"player_names": { "player_names": {
"source": "原始文档固定数据 (100对姓名)", "source": "name.xlsx/csv (找不到则用内置默认100对姓名)",
"section": "四、玩家随机名称文本", "section": "四、玩家随机名称文本",
"format": "表格: 姓 | 名" "format": "表格: 姓 | 名"
}, },
"guide_texts": { "guide_texts": {
"source": "guide.csv", "source": "guide.xlsx/csv",
"section": "五、新手指引文本", "section": "五、新手指引文本",
"field": "desc (第8列, index=7)", "field": "desc (第8列, index=7)",
"filter": "无", "filter": "无",
"dedup": "按desc文本去重" "dedup": "按desc文本去重"
}, },
"props": { "props": {
"source": "prop.csv", "source": "prop.xlsx/csv",
"section": "六、道具说明", "section": "六、道具说明",
"fields": { "fields": {
"name": "第3列 (index=2)", "name": "第3列 (index=2)",
@@ -47,7 +48,7 @@
"icon_naming": "icon_product_XX.png (XX为补零两位数, 如 icon_product_01)" "icon_naming": "icon_product_XX.png (XX为补零两位数, 如 icon_product_01)"
}, },
"skills": { "skills": {
"source": "skill.csv", "source": "skill.xlsx/csv",
"section": "七、技能", "section": "七、技能",
"fields": { "fields": {
"name": "第2列 (index=1)", "name": "第2列 (index=1)",
@@ -58,7 +59,7 @@
"icon_naming": "skillX.png (X为数字, 不补零, 如 skill1, skill10)" "icon_naming": "skillX.png (X为数字, 不补零, 如 skill1, skill10)"
}, },
"runes": { "runes": {
"source": "rune.csv", "source": "rune.xlsx/csv",
"section": "八、灵石", "section": "八、灵石",
"fields": { "fields": {
"name": "第2列 (index=1)", "name": "第2列 (index=1)",
@@ -69,7 +70,7 @@
"icon_naming": "rune_icon_X.png (X为数字, 不补零)" "icon_naming": "rune_icon_X.png (X为数字, 不补零)"
}, },
"talents": { "talents": {
"source": "playerlevel.csv", "source": "playerlevel.xlsx/csv",
"section": "九、天赋", "section": "九、天赋",
"fields": { "fields": {
"name": "第14列 (index=13)", "name": "第14列 (index=13)",
@@ -81,7 +82,7 @@
"dedup": "按name去重" "dedup": "按name去重"
}, },
"equipment": { "equipment": {
"source": "equip.csv", "source": "equip.xlsx/csv",
"section": "十、装备", "section": "十、装备",
"fields": { "fields": {
"name": "第2列 (index=1)", "name": "第2列 (index=1)",
@@ -90,19 +91,20 @@
"icon_dir": "atlas_equip_icons", "icon_dir": "atlas_equip_icons",
"icon_naming": "equip_icon_X.png (X为数字, 不补零)", "icon_naming": "equip_icon_X.png (X为数字, 不补零)",
"dedup": "按name去重", "dedup": "按name去重",
"note": "equip.csv无desc字段" "note": "equip无desc字段"
}, },
"level_names": { "level_names": {
"source": "fight_sample.csv", "source": "fight_sample.xlsx/csv",
"section": "十一、关卡名称", "section": "十一、关卡名称",
"field": "name (第3列, index=2)", "field": "name (第3列, index=2)",
"filter": "无", "filter": "无",
"dedup": "按name去重" "dedup": "按name去重"
} }
}, },
"csv_format": { "table_format": {
"header_rows": 3, "header_rows": 3,
"header_structure": "第1行:中文头, 第2行:类型定义, 第3行:英文字段名", "header_structure": "第1行:中文头, 第2行:类型定义, 第3行:英文字段名(用作字段映射)",
"encoding_note": "部分文件为GBK编码,需自动检测" "data_start_row": "第4行起",
"encoding_note": "csv需自动检测(优先级 utf-8-sig -> gbk -> gb2312 -> utf-8)xlsx由openpyxl直接读取无编码问题"
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"document_title": "《不朽封仙》文字脚本", "document_title_example": "《<项目名>》文字脚本 (universal_tool 用扫描到的项目名生成)",
"sections": [ "sections": [
{"number": "一", "title": "系统提示", "type": "numbered_list"}, {"number": "一", "title": "系统提示", "type": "numbered_list"},
{"number": "二", "title": "任务文本", "type": "numbered_list"}, {"number": "二", "title": "任务文本", "type": "numbered_list"},
@@ -35,7 +35,7 @@
"font_en": "FangSong", "font_en": "FangSong",
"size": "四号 (14pt)", "size": "四号 (14pt)",
"bold": false, "bold": false,
"numbering": "阿拉伯数字 (1. 2. 3. ...)" "numbering": "Word自动编号列表 (decimal, 每章节独立numId从1开始),增删条目后编号自动重排;段落文本不再硬编码 1. 2. 3."
}, },
"table_images": { "table_images": {
"height": "3cm", "height": "3cm",
@@ -1,10 +1,10 @@
{ {
"description": "图标资源名称到图片文件的映射规则与目录结构", "description": "图标资源名称到图片文件的映射规则与目录结构",
"base_dir": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Resources\\Resources_moved\\gui\\atlas_ui", "base_dir_example": "<game_dir>/unity/Assets/Resources/Resources_moved/gui/atlas_ui (universal_tool 自动扫描 atlas_* 子目录)",
"directories": { "directories": {
"atlas_product_icons": { "atlas_product_icons": {
"section": "六、道具说明", "section": "六、道具说明",
"csv_field": "prop.csv icon (index=7)", "table_field": "prop.xlsx/csv icon (index=7)",
"naming_pattern": "icon_product_XX.png", "naming_pattern": "icon_product_XX.png",
"naming_note": "XX为补零两位数 (01-63), 如 icon_product_1 -> icon_product_01.png", "naming_note": "XX为补零两位数 (01-63), 如 icon_product_1 -> icon_product_01.png",
"file_count": 63, "file_count": 63,
@@ -12,7 +12,7 @@
}, },
"atlas_skill": { "atlas_skill": {
"section": "七、技能", "section": "七、技能",
"csv_field": "skill.csv icon (index=6)", "table_field": "skill.xlsx/csv icon (index=6)",
"naming_pattern": "skillX.png", "naming_pattern": "skillX.png",
"naming_note": "X为数字不补零 (1-99), 如 skill1, skill10, skill99", "naming_note": "X为数字不补零 (1-99), 如 skill1, skill10, skill99",
"file_count": 99, "file_count": 99,
@@ -20,7 +20,7 @@
}, },
"atlas_rune_icons": { "atlas_rune_icons": {
"section": "八、灵石", "section": "八、灵石",
"csv_field": "rune.csv icon (index=2)", "table_field": "rune.xlsx/csv icon (index=2)",
"naming_pattern": "rune_icon_X.png", "naming_pattern": "rune_icon_X.png",
"naming_note": "X为数字不补零 (1-18)", "naming_note": "X为数字不补零 (1-18)",
"file_count": 12, "file_count": 12,
@@ -28,7 +28,7 @@
}, },
"atlas_talent_icons": { "atlas_talent_icons": {
"section": "九、天赋", "section": "九、天赋",
"csv_field": "playerlevel.csv icon (index=15)", "table_field": "playerlevel.xlsx/csv icon (index=15)",
"naming_pattern": "icon_talent_X.png", "naming_pattern": "icon_talent_X.png",
"naming_note": "X为数字不补零 (1-6)", "naming_note": "X为数字不补零 (1-6)",
"file_count": 6, "file_count": 6,
@@ -36,7 +36,7 @@
}, },
"atlas_equip_icons": { "atlas_equip_icons": {
"section": "十、装备", "section": "十、装备",
"csv_field": "equip.csv icon (index=5)", "table_field": "equip.xlsx/csv icon (index=5)",
"naming_pattern": "equip_icon_X.png", "naming_pattern": "equip_icon_X.png",
"naming_note": "X为数字不补零 (1-6)", "naming_note": "X为数字不补零 (1-6)",
"file_count": 6, "file_count": 6,
+17 -19
View File
@@ -1,17 +1,15 @@
{ {
"project_name": "P-L12《不朽封仙》", "project_name": "P-L12《不朽封仙》(历史参考项目)",
"workspace_root": "d:\\p-L12", "note": "本文件记录历史项目的路径约定,作为通用工具 universal_tool.py 的参考。当前工具自动扫描项目结构,不再依赖固定路径。",
"paths": { "paths_example": {
"config_dir": "d:\\p-L12\\P-L12_config", "config_dir": "<project_root>/<项目名>_config (xlsx/csv 配置表)",
"game_dir": "d:\\p-L12\\P-L12_game", "game_dir": "<project_root>/<项目名>_game",
"csv_dir": "d:\\p-L12\\work\\cvs", "scripts_dir": "<project_root>/<项目名>_game/unity/Assets/Scripts",
"scripts_dir": "d:\\p-L12\\work\\py", "memories_dir": "<本项目>/memories",
"memories_dir": "d:\\p-L12\\work\\memories", "output_doc": "<project_root>/work/《项目名》游戏文字脚本.docx"
"output_doc": "d:\\p-L12\\work\\《不朽封仙》游戏文字脚本.docx"
}, },
"scripts": { "scripts": {
"xlsx_to_csv": "d:\\p-L12\\work\\py\\xlsx_to_csv.py", "universal_tool": "<本项目>/py/universal_tool.py (唯一入口,自动扫描+生成)"
"generate_doc": "d:\\p-L12\\work\\py\\generate_doc.py"
}, },
"dependencies": { "dependencies": {
"python_packages": ["openpyxl", "python-docx"], "python_packages": ["openpyxl", "python-docx"],
@@ -20,14 +18,14 @@
"pip install python-docx" "pip install python-docx"
] ]
}, },
"csv_encoding": { "data_reading": {
"note": "部分CSV文件使用GBK/GB2312编码,非UTF-8", "strategy": "直接读取 xlsx(优先)或 csv,无需预先转换",
"read_strategy": "按优先级尝试 utf-8-sig -> gbk -> gb2312 -> utf-8", "csv_encoding": "按优先级尝试 utf-8-sig -> gbk -> gb2312 -> utf-8 (仅csv需要)",
"write_encoding": "xlsx_to_csv.py 输出使用 UTF-8 with BOM (utf-8-sig)" "xlsx_reading": "openpyxl read_only/data_only 直接读取"
}, },
"game_code_paths": { "game_code_paths_example": {
"game_text_const": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts\\mgame\\error\\GameTextConst.cs", "game_text_const": "<game_dir>/unity/Assets/Scripts/.../GameTextConst.cs",
"scripts_root": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts" "scripts_root": "<game_dir>/unity/Assets/Scripts"
}, },
"icon_resource_base": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Resources\\Resources_moved\\gui\\atlas_ui" "icon_resource_base_example": "<game_dir>/unity/Assets/Resources/Resources_moved/gui/atlas_ui"
} }
+3 -2
View File
@@ -1,6 +1,7 @@
{ {
"description": "GameTextConst.cs 中被代码实际引用的系统提示常量", "description": "GameTextConst.cs 中被代码实际引用的系统提示常量(历史项目参考)",
"source_file": "d:\\p-L12\\P-L12_game\\unity\\Assets\\Scripts\\mgame\\error\\GameTextConst.cs", "source_file_example": "<game_dir>/unity/Assets/Scripts/.../GameTextConst.cs (universal_tool 自动扫描定位)",
"note": "universal_tool.parse_game_text_const 自动解析 *Const*.cs 中所有 public const string,扫描其他 .cs 确认被引用后按文本去重;本文件为历史项目的静态快照参考",
"total_referenced": 85, "total_referenced": 85,
"total_unique_text": 83, "total_unique_text": 83,
"note": "85个被引用常量中,有2对文本值重复('{0}不足。' 和 '保存失败。'),去重后为83条", "note": "85个被引用常量中,有2对文本值重复('{0}不足。' 和 '保存失败。'),去重后为83条",
-568
View File
@@ -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今日剩余(<color=#19F45C>{1}</color>/{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()
+151 -64
View File
@@ -4,6 +4,7 @@
功能:自动扫描项目目录,识别标准结构,生成游戏文字脚本Word文档。 功能:自动扫描项目目录,识别标准结构,生成游戏文字脚本Word文档。
无需手动配置路径,只需提供项目根目录即可自动完成全部流程。 无需手动配置路径,只需提供项目根目录即可自动完成全部流程。
配置表直接读取 xlsx(优先)或 csv,无需预先转换。
标准目录结构约定: 标准目录结构约定:
<project_root>/ <project_root>/
@@ -11,7 +12,9 @@
├── *_game/ ← 游戏客户端目录 ├── *_game/ ← 游戏客户端目录
│ └── unity/Assets/Resources/.../atlas_ui/ ← 图标资源 │ └── unity/Assets/Resources/.../atlas_ui/ ← 图标资源
│ └── unity/Assets/Scripts/.../*Const*.cs ← 系统提示常量 │ └── unity/Assets/Scripts/.../*Const*.cs ← 系统提示常量
└── work/ ← 工作目录 (CSV缓存、输出等) └── work/ ← 工作目录 (输出等)
正文编号使用 Word 自动编号列表,在 Word 中增删条目后编号会自动重排。
用法: 用法:
python universal_tool.py [项目路径] [--output 输出路径] python universal_tool.py [项目路径] [--output 输出路径]
@@ -23,18 +26,18 @@ import csv
import os import os
import re import re
import sys import sys
import glob
import argparse import argparse
from docx import Document from docx import Document
from docx.shared import Pt, Cm from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn from docx.oxml.ns import qn
from docx.oxml import OxmlElement
# ============ 常量 ============ # ============ 常量 ============
CN_NUMBERS = ["", "", "", "", "", "", "", "", "", "", "十一"] CN_NUMBERS = ["", "", "", "", "", "", "", "", "", "", "十一"]
# 11章节数据源定义(字段名取自CSV英文头行第3行) # 11章节数据源定义(字段名取自表格英文头行第3行;优先读 xlsx,其次 csv
SECTION_DEFS = { SECTION_DEFS = {
"quest_texts": {"csv": "quest", "field": "desc", "filter": {"category": ["1", "2"]}}, "quest_texts": {"csv": "quest", "field": "desc", "filter": {"category": ["1", "2"]}},
"achievements": {"csv": "quest", "field": "desc", "filter": {"category": ["9"]}}, "achievements": {"csv": "quest", "field": "desc", "filter": {"category": ["9"]}},
@@ -86,7 +89,8 @@ class ProjectScanner:
base = os.path.basename(self.config_dir) base = os.path.basename(self.config_dir)
self.project_name = base.replace("_config", "") 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: if self.work_dir:
cvs = os.path.join(self.work_dir, "cvs") cvs = os.path.join(self.work_dir, "cvs")
if os.path.isdir(cvs) and os.listdir(cvs): if os.path.isdir(cvs) and os.listdir(cvs):
@@ -139,11 +143,11 @@ class ProjectScanner:
return return
def find_file(self, name): 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]: for d in [self.csv_dir, self.config_dir]:
if not d: if not d:
continue continue
for ext in [".csv", ".xlsx"]: for ext in [".xlsx", ".csv"]:
p = os.path.join(d, name + ext) p = os.path.join(d, name + ext)
if os.path.isfile(p): if os.path.isfile(p):
return p return p
@@ -155,7 +159,7 @@ class ProjectScanner:
f" 项目名称: {self.project_name}", f" 项目名称: {self.project_name}",
f" 配置目录: {self.config_dir or '未找到'}", f" 配置目录: {self.config_dir or '未找到'}",
f" 游戏目录: {self.game_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" 图标目录: {len(self.icon_dirs)}{list(self.icon_dirs.keys())}",
f" 脚本目录: {self.scripts_dir or '未找到'}", f" 脚本目录: {self.scripts_dir or '未找到'}",
f" 常量文件: {os.path.basename(self.game_text_const) if self.game_text_const else '未找到'}", 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)。 统一表格读取(支持 .xlsx / .csv),返回 (headers, rows)
返回 (headers, rows)
headers: 第3行英文字段名列表 headers: 第3行英文字段名列表
rows: 第4行起的数据行列表 (list of list) 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"]: for enc in ["utf-8-sig", "gbk", "gb2312", "utf-8"]:
try: try:
with open(filepath, "r", encoding=enc) as f: with open(filepath, "r", encoding=enc) as f:
@@ -186,6 +198,20 @@ def read_csv(filepath):
return [], [] 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): def field_map(headers):
"""将英文头行转为 {字段名: 列索引} 映射""" """将英文头行转为 {字段名: 列索引} 映射"""
return {h.strip(): i for i, h in enumerate(headers) if h.strip()} 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 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) _run(p, f"{cn_num}{title}", font="仿宋", size=14, bold=True)
# ---- Word 自动编号辅助 ----
# 用 Word 的 numbering 机制生成 1. 2. 3. ... 列表,
# 这样在 Word 中增删条目后编号会自动重排,无需手改数字。
def _get_numbering_element(doc):
"""获取文档的 <w:numbering> 根元素(默认模板已含 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
# <w:abstractNum>
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)
# <w:num>
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): 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() 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): def _set_cell(cell, text, bold=False, size=14):
@@ -412,10 +500,10 @@ def add_name_table(doc, names):
# ================================================================ # ================================================================
def get_player_names(scanner): def get_player_names(scanner):
"""获取玩家随机名称(优先 name.csv,否则使用默认列表)""" """获取玩家随机名称(优先 name.xlsx/csv,否则使用默认列表)"""
fp = scanner.find_file("name") fp = scanner.find_file("name")
if fp and fp.endswith(".csv"): if fp:
headers, rows = read_csv(fp) headers, rows = read_table(fp)
fm = field_map(headers) fm = field_map(headers)
if "first" in fm and "last" in fm: if "first" in fm and "last" in fm:
seen = set() seen = set()
@@ -461,11 +549,11 @@ def get_player_names(scanner):
def get_csv_list_data(scanner, csv_name, field_name, filter_dict=None): def get_csv_list_data(scanner, csv_name, field_name, filter_dict=None):
"""通用:从CSV中按字段名提取去重文本列表""" """通用:从表格(xlsx/csv)中按字段名提取去重文本列表"""
fp = scanner.find_file(csv_name) fp = scanner.find_file(csv_name)
if not fp or not fp.endswith(".csv"): if not fp:
return [] return []
headers, rows = read_csv(fp) headers, rows = read_table(fp)
fm = field_map(headers) fm = field_map(headers)
seen = set() seen = set()
result = [] 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): 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) fp = scanner.find_file(csv_name)
if not fp or not fp.endswith(".csv"): if not fp:
return [] return []
headers, rows = read_csv(fp) headers, rows = read_table(fp)
fm = field_map(headers) fm = field_map(headers)
seen = set() seen = set()
result = [] result = []
@@ -516,14 +604,13 @@ def build_document(scanner, output_path):
"""根据扫描结果构建完整文档""" """根据扫描结果构建完整文档"""
print("\n[1/4] 准备数据源...") print("\n[1/4] 准备数据源...")
# 确保CSV可用 # 直接从 config_dir / csv_dir 读取 xlsx(优先) 或 csv,不再转换。
if not scanner.csv_dir or not any(f.endswith(".csv") for f in os.listdir(scanner.csv_dir)): data_dir = scanner.csv_dir or scanner.config_dir
if scanner.config_dir: if data_dir:
print(f" CSV目录为空,正在从 {os.path.basename(scanner.config_dir)} 转换xlsx...") files = [f for f in os.listdir(data_dir) if not f.startswith("~$")]
cvs_out = os.path.join(scanner.work_dir or scanner.root, "cvs") xlsx_n = sum(1 for f in files if f.endswith(".xlsx"))
n = xlsx_to_csv_batch(scanner.config_dir, cvs_out) csv_n = sum(1 for f in files if f.endswith(".csv"))
scanner.csv_dir = cvs_out print(f" 数据目录: {data_dir} (xlsx {xlsx_n} 个 / csv {csv_n} 个)")
print(f" 已转换 {n} 个xlsx文件")
# 预加载数据 # 预加载数据
system_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir) system_prompts = parse_game_text_const(scanner.game_text_const, scanner.scripts_dir)
-104
View File
@@ -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)
+30 -1
View File
@@ -1 +1,30 @@
将PY放入项目文件中,运行universal_tool.py他会自动读取设定好的表格对应关系进行自动生成 通用游戏文字脚本文档生成工具 使用说明
入口脚本: 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 仅为开发参考(数据源映射/格式规范/图标规则等),
脚本运行时不读取它们,全部信息由自动扫描项目目录获得。