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
-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文档。
无需手动配置路径,只需提供项目根目录即可自动完成全部流程。
配置表直接读取 xlsx(优先)或 csv,无需预先转换。
标准目录结构约定:
<project_root>/
@@ -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):
"""获取文档的 <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):
"""编号列表:仿宋 四号 不加粗"""
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)
-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)