config_manager.py 111 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. BallController配置管理工具 - 简化版
  5. 图形界面版本,不依赖pandas,支持浏览文件并选择Excel/CSV表格进行配置导入
  6. 功能:
  7. 1. 图形界面浏览项目文件
  8. 2. 多选Excel/CSV配置表格
  9. 3. 预览配置内容
  10. 4. 一键导入配置到JSON
  11. 5. 配置备份和恢复
  12. 作者: AI Assistant
  13. 日期: 2024
  14. """
  15. import tkinter as tk
  16. from tkinter import ttk, filedialog, messagebox, scrolledtext
  17. import json
  18. import os
  19. import csv
  20. from datetime import datetime
  21. from pathlib import Path
  22. import threading
  23. # 尝试导入pandas,如果失败则使用纯CSV模式
  24. try:
  25. import pandas as pd
  26. PANDAS_AVAILABLE = True
  27. except ImportError:
  28. PANDAS_AVAILABLE = False
  29. print("警告: pandas未安装,将使用纯CSV模式(不支持Excel文件)")
  30. # 导入武器配置管理器
  31. try:
  32. from weapon_config_manager import WeaponConfigManager
  33. WEAPON_MANAGER_AVAILABLE = True
  34. except ImportError:
  35. WEAPON_MANAGER_AVAILABLE = False
  36. print("警告: weapon_config_manager.py 未找到,武器配置功能将不可用")
  37. try:
  38. from enemy_config_manager import EnemyConfigManager
  39. ENEMY_MANAGER_AVAILABLE = True
  40. except ImportError:
  41. ENEMY_MANAGER_AVAILABLE = False
  42. print("警告: enemy_config_manager.py 未找到,敌人配置功能将不可用")
  43. class SkillConfigImporter:
  44. """技能配置表导入工具类"""
  45. def __init__(self, excel_dir=None):
  46. # 获取当前脚本所在目录
  47. if excel_dir:
  48. self.script_dir = Path(excel_dir)
  49. else:
  50. self.script_dir = Path(__file__).parent
  51. self.excel_file = self.script_dir / "局外技能配置表.xlsx"
  52. self.json_file = self.script_dir.parent / "skill_config.json"
  53. # 默认配置结构
  54. self.default_config = {
  55. "skillTypes": [],
  56. "skillGroups": [],
  57. "totalGroups": 12,
  58. "skillsPerGroup": 3
  59. }
  60. def read_excel_config(self):
  61. """从Excel文件读取技能配置"""
  62. if not self.excel_file.exists():
  63. print(f"错误: Excel文件不存在: {self.excel_file}")
  64. return None
  65. try:
  66. # 读取所有工作表
  67. excel_data = pd.read_excel(self.excel_file, sheet_name=None)
  68. print(f"找到工作表: {list(excel_data.keys())}")
  69. config = self.default_config.copy()
  70. # 读取技能类型配置
  71. if "技能类型配置" in excel_data:
  72. skill_types = self.parse_skill_types(excel_data["技能类型配置"])
  73. config["skillTypes"] = skill_types
  74. print(f"读取到 {len(skill_types)} 个技能类型")
  75. elif "JSON技能类型配置" in excel_data:
  76. skill_types = self.parse_skill_types(excel_data["JSON技能类型配置"])
  77. config["skillTypes"] = skill_types
  78. print(f"读取到 {len(skill_types)} 个技能类型")
  79. # 读取技能组配置
  80. if "技能组配置" in excel_data:
  81. skill_groups = self.parse_skill_groups(excel_data["技能组配置"])
  82. config["skillGroups"] = skill_groups
  83. config["totalGroups"] = len(skill_groups)
  84. print(f"读取到 {len(skill_groups)} 个技能组")
  85. elif "JSON技能组配置" in excel_data:
  86. skill_groups = self.parse_skill_groups(excel_data["JSON技能组配置"])
  87. config["skillGroups"] = skill_groups
  88. config["totalGroups"] = len(skill_groups)
  89. print(f"读取到 {len(skill_groups)} 个技能组")
  90. # 读取基本配置
  91. if "基本配置" in excel_data:
  92. basic_config = self.parse_basic_config(excel_data["基本配置"])
  93. config.update(basic_config)
  94. elif "JSON基本配置" in excel_data:
  95. basic_config = self.parse_basic_config(excel_data["JSON基本配置"])
  96. config.update(basic_config)
  97. return config
  98. except Exception as e:
  99. print(f"读取Excel文件失败: {e}")
  100. return None
  101. def parse_skill_types(self, df):
  102. """解析技能类型配置"""
  103. skill_types = []
  104. # 读取所有数据行(pandas已经自动处理了标题行)
  105. for index, row in df.iterrows():
  106. # 检查是否为空行
  107. if pd.isna(row.iloc[0]):
  108. continue
  109. try:
  110. skill_type = {
  111. "id": int(row.iloc[0]) if not pd.isna(row.iloc[0]) else 0,
  112. "name": str(row.iloc[1]) if not pd.isna(row.iloc[1]) else "",
  113. "displayName": str(row.iloc[2]) if not pd.isna(row.iloc[2]) else "",
  114. "nameTemplate": str(row.iloc[3]) if not pd.isna(row.iloc[3]) else "",
  115. "type": str(row.iloc[4]) if not pd.isna(row.iloc[4]) else ""
  116. }
  117. skill_types.append(skill_type)
  118. except Exception as e:
  119. print(f"解析技能类型第 {index+1} 行失败: {e}")
  120. continue
  121. return skill_types
  122. def parse_skill_groups(self, df):
  123. """解析技能组配置"""
  124. skill_groups = []
  125. # 读取所有数据行(pandas已经自动处理了标题行)
  126. for index, row in df.iterrows():
  127. # 检查是否为空行
  128. if pd.isna(row.iloc[0]):
  129. continue
  130. try:
  131. skill_group = {
  132. "group": int(row.iloc[0]) if not pd.isna(row.iloc[0]) else 1,
  133. "effectPercent": int(row.iloc[1]) if not pd.isna(row.iloc[1]) else 0,
  134. "diamondCost": int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 0
  135. }
  136. skill_groups.append(skill_group)
  137. except Exception as e:
  138. print(f"解析技能组第 {index+1} 行失败: {e}")
  139. continue
  140. return skill_groups
  141. def parse_basic_config(self, df):
  142. """解析基本配置"""
  143. basic_config = {}
  144. # 假设基本配置是纵向格式:配置项名称 | 数值
  145. for index, row in df.iterrows():
  146. if index == 0: # 跳过标题行
  147. continue
  148. # 检查是否为空行
  149. if pd.isna(row.iloc[0]):
  150. continue
  151. try:
  152. param_name = str(row.iloc[0]).strip()
  153. param_value = row.iloc[1]
  154. if param_name == "总组数" or param_name == "totalGroups":
  155. basic_config["totalGroups"] = int(param_value) if not pd.isna(param_value) else 12
  156. elif param_name == "每组技能数" or param_name == "skillsPerGroup":
  157. basic_config["skillsPerGroup"] = int(param_value) if not pd.isna(param_value) else 3
  158. except Exception as e:
  159. print(f"解析基本配置第 {index+1} 行失败: {e}")
  160. continue
  161. return basic_config
  162. def validate_config(self, config):
  163. """验证配置数据"""
  164. errors = []
  165. # 验证技能类型
  166. if not config.get("skillTypes"):
  167. errors.append("技能类型配置为空")
  168. else:
  169. for i, skill_type in enumerate(config["skillTypes"]):
  170. if not skill_type.get("name"):
  171. errors.append(f"技能类型 {i+1} 缺少名称")
  172. if not skill_type.get("type"):
  173. errors.append(f"技能类型 {i+1} 缺少类型标识")
  174. # 验证技能组
  175. if not config.get("skillGroups"):
  176. errors.append("技能组配置为空")
  177. else:
  178. groups = [sg["group"] for sg in config["skillGroups"]]
  179. if len(groups) != len(set(groups)):
  180. errors.append("技能组编号存在重复")
  181. # 验证基本配置
  182. if config.get("totalGroups", 0) <= 0:
  183. errors.append("总组数必须大于0")
  184. if config.get("skillsPerGroup", 0) <= 0:
  185. errors.append("每组技能数必须大于0")
  186. return errors
  187. def backup_json(self):
  188. """备份当前JSON文件(已禁用)"""
  189. # 不再创建备份文件,直接返回成功
  190. return True
  191. def save_json_config(self, config):
  192. """保存配置到JSON文件"""
  193. try:
  194. with open(self.json_file, 'w', encoding='utf-8') as f:
  195. json.dump(config, f, ensure_ascii=False, indent=2)
  196. print(f"配置已保存到: {self.json_file}")
  197. return True
  198. except Exception as e:
  199. print(f"保存JSON文件失败: {e}")
  200. return False
  201. def import_config(self):
  202. """执行配置导入"""
  203. print("=== 技能配置导入工具 ===")
  204. print(f"Excel文件: {self.excel_file}")
  205. print(f"JSON文件: {self.json_file}")
  206. print()
  207. # 读取Excel配置
  208. config = self.read_excel_config()
  209. if not config:
  210. print("读取Excel配置失败")
  211. return False
  212. # 验证配置
  213. errors = self.validate_config(config)
  214. if errors:
  215. print("配置验证失败:")
  216. for error in errors:
  217. print(f" - {error}")
  218. return False
  219. print("配置验证通过")
  220. # 显示配置摘要
  221. print(f"\n配置摘要:")
  222. print(f" 技能类型数量: {len(config['skillTypes'])}")
  223. print(f" 技能组数量: {len(config['skillGroups'])}")
  224. print(f" 总组数: {config['totalGroups']}")
  225. print(f" 每组技能数: {config['skillsPerGroup']}")
  226. # 备份原文件
  227. if not self.backup_json():
  228. print("备份失败,是否继续?(y/n): ", end="")
  229. # 在GUI环境中,我们默认继续
  230. print("y (自动继续)")
  231. # 保存新配置
  232. if self.save_json_config(config):
  233. print("\n配置导入成功!")
  234. return True
  235. else:
  236. print("\n配置导入失败!")
  237. return False
  238. class ConfigManagerGUI:
  239. def __init__(self):
  240. self.root = tk.Tk()
  241. self.root.title("游戏配置管理工具")
  242. self.root.geometry("1000x700")
  243. self.root.resizable(True, True)
  244. # 配置文件路径 - 动态获取项目根目录
  245. # 从当前脚本位置向上查找项目根目录
  246. current_dir = Path(__file__).parent
  247. self.project_root = current_dir.parent.parent.parent.parent # 从excel目录向上4级到项目根目录
  248. self.excel_dir = current_dir
  249. # 手动配置变量
  250. self.selected_json_path = None
  251. self.format_type = 'horizontal' # 默认为横向表格
  252. self.multi_sheet = False
  253. self.sheet_names = []
  254. self.param_types = {}
  255. # 当前选择的配置映射
  256. self.current_mapping = None
  257. # 移除json_config_path,现在使用selected_json_path
  258. self.param_types = {}
  259. # 默认配置值
  260. self.default_config = {
  261. 'baseSpeed': 60,
  262. 'maxReflectionRandomness': 0.2,
  263. 'antiTrapTimeWindow': 5.0,
  264. 'antiTrapHitThreshold': 5,
  265. 'deflectionAttemptThreshold': 3,
  266. 'antiTrapDeflectionMultiplier': 3.0,
  267. 'FIRE_COOLDOWN': 0.05,
  268. 'ballRadius': 10,
  269. 'gravityScale': 0,
  270. 'linearDamping': 0,
  271. 'angularDamping': 0,
  272. 'colliderGroup': 2,
  273. 'colliderTag': 1,
  274. 'friction': 0,
  275. 'restitution': 1,
  276. 'safeDistance': 50,
  277. 'edgeOffset': 20,
  278. 'sensor': False
  279. }
  280. self.selected_files = []
  281. self.config_data = {}
  282. self.setup_ui()
  283. self.load_current_config()
  284. def setup_ui(self):
  285. """设置用户界面"""
  286. # 主框架
  287. main_frame = ttk.Frame(self.root, padding="10")
  288. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  289. # 配置根窗口的网格权重
  290. self.root.columnconfigure(0, weight=1)
  291. self.root.rowconfigure(0, weight=1)
  292. main_frame.columnconfigure(1, weight=1)
  293. main_frame.rowconfigure(2, weight=1)
  294. # 标题
  295. title_label = ttk.Label(main_frame, text="游戏配置管理工具",
  296. font=("Arial", 16, "bold"))
  297. title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
  298. # 左侧面板 - 文件选择
  299. file_types_text = "Excel/CSV配置文件选择" if PANDAS_AVAILABLE else "CSV配置文件选择"
  300. left_frame = ttk.LabelFrame(main_frame, text=file_types_text, padding="10")
  301. left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
  302. left_frame.columnconfigure(0, weight=1)
  303. left_frame.rowconfigure(3, weight=1)
  304. # 文件浏览按钮
  305. browse_frame = ttk.Frame(left_frame)
  306. browse_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
  307. browse_frame.columnconfigure(0, weight=1)
  308. browse_text = "浏览Excel/CSV文件" if PANDAS_AVAILABLE else "浏览CSV文件"
  309. ttk.Button(browse_frame, text=browse_text,
  310. command=self.browse_files).grid(row=0, column=0, sticky=(tk.W, tk.E))
  311. ttk.Button(browse_frame, text="扫描项目目录",
  312. command=self.scan_project_files).grid(row=0, column=1, padx=(10, 0))
  313. # JSON文件选择区域
  314. json_frame = ttk.LabelFrame(left_frame, text="JSON输出路径选择", padding="5")
  315. json_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  316. json_frame.columnconfigure(1, weight=1)
  317. ttk.Label(json_frame, text="输出路径:").grid(row=0, column=0, sticky=tk.W)
  318. self.json_path_var = tk.StringVar()
  319. self.json_path_entry = ttk.Entry(json_frame, textvariable=self.json_path_var, state='readonly')
  320. self.json_path_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0))
  321. ttk.Button(json_frame, text="浏览", command=self.browse_json_path).grid(row=0, column=2, padx=(5, 0))
  322. # 配置选项
  323. config_frame = ttk.LabelFrame(left_frame, text="配置选项", padding="5")
  324. config_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  325. # 表格格式选择
  326. ttk.Label(config_frame, text="表格格式:").grid(row=0, column=0, sticky=tk.W)
  327. self.format_var = tk.StringVar(value="horizontal")
  328. format_frame = ttk.Frame(config_frame)
  329. format_frame.grid(row=0, column=1, sticky=tk.W, padx=(5, 0))
  330. ttk.Radiobutton(format_frame, text="横向", variable=self.format_var, value="horizontal").pack(side=tk.LEFT)
  331. ttk.Radiobutton(format_frame, text="纵向", variable=self.format_var, value="vertical").pack(side=tk.LEFT, padx=(10, 0))
  332. # 多工作表选项
  333. self.multi_sheet_var = tk.BooleanVar()
  334. ttk.Checkbutton(config_frame, text="多工作表文件", variable=self.multi_sheet_var,
  335. command=self.on_multi_sheet_change).grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
  336. # 单个JSON选项
  337. self.single_json_var = tk.BooleanVar(value=True)
  338. ttk.Checkbutton(config_frame, text="单个json", variable=self.single_json_var,
  339. command=self.on_single_json_change).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
  340. # 文件列表
  341. list_frame = ttk.Frame(left_frame)
  342. list_frame.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  343. list_frame.columnconfigure(0, weight=1)
  344. list_frame.rowconfigure(0, weight=1)
  345. self.file_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, height=15)
  346. scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
  347. self.file_listbox.configure(yscrollcommand=scrollbar.set)
  348. self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  349. scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
  350. # 文件操作按钮
  351. file_btn_frame = ttk.Frame(left_frame)
  352. file_btn_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  353. ttk.Button(file_btn_frame, text="预览选中文件",
  354. command=self.preview_selected_files).pack(side=tk.LEFT)
  355. ttk.Button(file_btn_frame, text="清空选择",
  356. command=self.clear_selection).pack(side=tk.LEFT, padx=(10, 0))
  357. # 右侧面板 - 配置预览和操作
  358. right_frame = ttk.LabelFrame(main_frame, text="配置预览与操作", padding="10")
  359. right_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
  360. right_frame.columnconfigure(0, weight=1)
  361. right_frame.rowconfigure(0, weight=1)
  362. # 配置预览文本框
  363. self.preview_text = scrolledtext.ScrolledText(right_frame, height=20, width=50)
  364. self.preview_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
  365. # 操作按钮
  366. btn_frame = ttk.Frame(right_frame)
  367. btn_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
  368. ttk.Button(btn_frame, text="导入配置",
  369. command=self.import_config).pack(side=tk.LEFT)
  370. ttk.Button(btn_frame, text="导入技能配置",
  371. command=self.import_skill_config).pack(side=tk.LEFT, padx=(10, 0))
  372. ttk.Button(btn_frame, text="备份当前配置",
  373. command=self.backup_config).pack(side=tk.LEFT, padx=(10, 0))
  374. ttk.Button(btn_frame, text="恢复默认配置",
  375. command=self.restore_default_config).pack(side=tk.LEFT, padx=(10, 0))
  376. # 武器配置专用按钮
  377. weapon_btn_frame = ttk.Frame(right_frame)
  378. weapon_btn_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  379. if WEAPON_MANAGER_AVAILABLE:
  380. ttk.Button(weapon_btn_frame, text="导入武器配置",
  381. command=self.import_weapon_config).pack(side=tk.LEFT)
  382. else:
  383. ttk.Label(weapon_btn_frame, text="武器配置管理器不可用",
  384. foreground="red").pack(side=tk.LEFT)
  385. # 敌人配置专用按钮
  386. enemy_btn_frame = ttk.Frame(right_frame)
  387. enemy_btn_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  388. if ENEMY_MANAGER_AVAILABLE:
  389. ttk.Button(enemy_btn_frame, text="导入敌人配置",
  390. command=self.import_enemy_config).pack(side=tk.LEFT)
  391. else:
  392. ttk.Label(enemy_btn_frame, text="敌人配置管理器不可用",
  393. foreground="red").pack(side=tk.LEFT)
  394. # 底部状态栏
  395. self.status_var = tk.StringVar()
  396. self.status_var.set("就绪")
  397. status_bar = ttk.Label(main_frame, textvariable=self.status_var,
  398. relief=tk.SUNKEN, anchor=tk.W)
  399. status_bar.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
  400. # 绑定事件
  401. self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
  402. def browse_files(self):
  403. """浏览Excel/CSV文件"""
  404. if PANDAS_AVAILABLE:
  405. title = "选择Excel/CSV配置文件"
  406. filetypes = [("Excel文件", "*.xlsx *.xls"), ("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  407. else:
  408. title = "选择CSV配置文件"
  409. filetypes = [("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  410. files = filedialog.askopenfilenames(
  411. title=title,
  412. filetypes=filetypes,
  413. initialdir=str(self.excel_dir)
  414. )
  415. if files:
  416. self.file_listbox.delete(0, tk.END)
  417. for file in files:
  418. self.file_listbox.insert(tk.END, file)
  419. self.status_var.set(f"已选择 {len(files)} 个文件")
  420. def scan_project_files(self):
  421. """扫描项目目录中的Excel/CSV文件"""
  422. self.status_var.set("正在扫描项目目录...")
  423. def scan_thread():
  424. config_files = []
  425. # 扫描常见的配置目录
  426. scan_dirs = [
  427. self.project_root / "assets/resources/data",
  428. self.project_root / "assets/resources/config",
  429. self.project_root / "assets/excel",
  430. self.project_root / "config",
  431. self.project_root / "data",
  432. self.project_root
  433. ]
  434. # 根据pandas可用性选择扫描的文件类型
  435. patterns = ['*.xlsx', '*.xls', '*.csv', '*.txt'] if PANDAS_AVAILABLE else ['*.csv', '*.txt']
  436. for scan_dir in scan_dirs:
  437. if scan_dir.exists():
  438. for pattern in patterns:
  439. config_files.extend(scan_dir.rglob(pattern))
  440. # 更新UI
  441. self.root.after(0, self.update_file_list, config_files)
  442. threading.Thread(target=scan_thread, daemon=True).start()
  443. def browse_json_path(self):
  444. """浏览JSON输出路径"""
  445. # 根据用户选择的选项组合决定选择文件还是目录
  446. multi_sheet = self.multi_sheet_var.get()
  447. single_json = self.single_json_var.get()
  448. if multi_sheet and not single_json:
  449. # 多工作表文件且不是单个JSON:输出到目录(每个sheet一个JSON文件)
  450. path = filedialog.askdirectory(
  451. title="选择JSON输出目录",
  452. initialdir=str(self.project_root / "assets/resources/data")
  453. )
  454. else:
  455. # 其他情况:输出单个JSON文件
  456. path = filedialog.askopenfilename(
  457. title="选择JSON输出文件",
  458. filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")],
  459. initialdir=str(self.project_root / "assets/resources/data")
  460. )
  461. if path:
  462. self.json_path_var.set(path)
  463. self.selected_json_path = Path(path)
  464. self.status_var.set(f"已选择输出路径: {Path(path).name}")
  465. def on_multi_sheet_change(self):
  466. """多工作表选项变化处理"""
  467. # 清空当前选择的JSON路径,让用户重新选择
  468. self.json_path_var.set("")
  469. def on_single_json_change(self):
  470. """单个JSON选项变化处理"""
  471. # 清空当前选择的JSON路径,让用户重新选择
  472. self.json_path_var.set("")
  473. self.selected_json_path = None
  474. if self.multi_sheet_var.get():
  475. self.status_var.set("多工作表模式:请选择输出目录")
  476. else:
  477. self.status_var.set("单工作表模式:请选择输出JSON文件")
  478. def update_file_list(self, files):
  479. """更新文件列表"""
  480. self.file_listbox.delete(0, tk.END)
  481. for file in files:
  482. self.file_listbox.insert(tk.END, str(file))
  483. file_type_text = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  484. self.status_var.set(f"找到 {len(files)} 个{file_type_text}")
  485. def on_file_select(self, event):
  486. """文件选择事件处理"""
  487. selection = self.file_listbox.curselection()
  488. if selection:
  489. self.selected_files = [self.file_listbox.get(i) for i in selection]
  490. # 根据选择的文件设置配置映射
  491. self._set_config_mapping_for_files()
  492. self.preview_selected_files()
  493. self.status_var.set(f"已选择 {len(selection)} 个文件")
  494. def _set_config_mapping_for_files(self):
  495. """根据选择的文件设置配置映射"""
  496. if not self.selected_files:
  497. return
  498. # 获取第一个文件名来判断配置类型
  499. first_file = Path(self.selected_files[0])
  500. filename = first_file.name.lower()
  501. # 根据文件名设置配置映射
  502. if '武器' in filename or 'weapon' in filename:
  503. self.current_mapping = {
  504. 'format_type': 'horizontal',
  505. 'param_types': {
  506. 'ID': str,
  507. '名称': str,
  508. '伤害': int,
  509. '射程': int,
  510. '射速': float,
  511. '弹药容量': int,
  512. '重装时间': float,
  513. '解锁等级': int,
  514. '价格': int,
  515. '描述': str,
  516. '最大等级': int,
  517. # 英文字段支持
  518. 'weaponId': str,
  519. 'name': str,
  520. 'damage': int,
  521. 'range': int,
  522. 'fireRate': float,
  523. 'ammoCapacity': int,
  524. 'reloadTime': float,
  525. 'unlockLevel': int,
  526. 'price': int,
  527. 'description': str,
  528. 'maxLevel': int
  529. }
  530. }
  531. elif '敌人' in filename or 'enemy' in filename:
  532. self.current_mapping = {
  533. 'format_type': 'multi_sheet',
  534. 'multi_sheet': True,
  535. 'param_types': {
  536. # 基础配置字段
  537. '敌人ID': ('id', str),
  538. '敌人名称': ('name', str),
  539. '敌人类型': ('type', str),
  540. '生命值': ('stats.health', int),
  541. '最大生命值': ('stats.maxHealth', int),
  542. '防御力': ('stats.defense', int),
  543. '移动速度': ('stats.speed', float),
  544. # 战斗配置字段
  545. '攻击伤害': ('combat.attackDamage', int),
  546. '攻击范围': ('combat.attackRange', int),
  547. '攻击速度': ('combat.attackSpeed', float),
  548. '能否格挡': ('combat.canBlock', bool),
  549. '格挡几率': ('combat.blockChance', float),
  550. '格挡伤害减免': ('combat.blockDamageReduction', float),
  551. '攻击冷却': ('combat.attackCooldown', float),
  552. '攻击类型': ('combat.attackType', str),
  553. '攻击延迟': ('combat.attackDelay', float),
  554. '武器类型': ('combat.weaponType', str),
  555. '投射物类型': ('combat.projectileType', str),
  556. '投射物速度': ('combat.projectileSpeed', float),
  557. # 移动配置字段
  558. '移动模式': ('movement.pattern', str),
  559. '巡逻范围': ('movement.patrolRange', int),
  560. '追击范围': ('movement.chaseRange', int),
  561. '旋转速度': ('movement.rotationSpeed', float),
  562. '移动类型': ('movement.moveType', str),
  563. '摆动幅度': ('movement.swingAmplitude', float),
  564. '摆动频率': ('movement.swingFrequency', float),
  565. '速度变化': ('movement.speedVariation', float),
  566. # 视觉配置字段
  567. '精灵路径': ('visualConfig.spritePath', str),
  568. '缩放比例': ('visualConfig.scale', float),
  569. '动画速度': ('visualConfig.animationSpeed', float),
  570. '水平翻转': ('visualConfig.flipX', bool),
  571. '待机动画': ('visualConfig.animations.idle', str),
  572. '行走动画': ('visualConfig.animations.walk', str),
  573. '攻击动画': ('visualConfig.animations.attack', str),
  574. '死亡动画': ('visualConfig.animations.death', str),
  575. '武器道具': ('visualConfig.weaponProp', str),
  576. # 音频配置字段
  577. '攻击音效': ('audioConfig.attackSound', str),
  578. '死亡音效': ('audioConfig.deathSound', str),
  579. '受击音效': ('audioConfig.hitSound', str),
  580. '行走音效': ('audioConfig.walkSound', str),
  581. '格挡音效': ('audioConfig.blockSound', str),
  582. '隐身音效': ('audioConfig.stealthSound', str),
  583. '护甲破碎音效': ('audioConfig.armorBreakSound', str),
  584. '引信音效': ('audioConfig.fuseSound', str),
  585. # 特殊能力配置字段
  586. '能力类型': ('specialAbilities.type', str),
  587. '伤害': ('specialAbilities.damage', int),
  588. '范围': ('specialAbilities.range', float),
  589. '冷却时间': ('specialAbilities.cooldown', float),
  590. # BOSS配置字段
  591. '是否BOSS': ('bossConfig.isBoss', bool),
  592. '阶段数': ('bossConfig.phases', int),
  593. '狂暴阈值': ('bossConfig.enrageThreshold', float),
  594. '狂暴伤害倍数': ('bossConfig.enrageDamageMultiplier', float),
  595. '狂暴速度倍数': ('bossConfig.enrageSpeedMultiplier', float),
  596. # 英文字段支持
  597. 'id': ('id', str),
  598. 'name': ('name', str),
  599. 'type': ('type', str),
  600. 'health': ('stats.health', int),
  601. 'maxHealth': ('stats.maxHealth', int),
  602. 'defense': ('stats.defense', int),
  603. 'speed': ('stats.speed', float),
  604. 'attackDamage': ('combat.attackDamage', int),
  605. 'attackRange': ('combat.attackRange', int),
  606. 'attackSpeed': ('combat.attackSpeed', float)
  607. }
  608. }
  609. elif '技能' in filename or 'skill' in filename:
  610. self.current_mapping = {
  611. 'format_type': 'horizontal',
  612. 'param_types': {
  613. 'ID': str,
  614. '名称': str,
  615. '伤害': int,
  616. '冷却时间': float,
  617. '消耗': int,
  618. '描述': str,
  619. # 英文字段支持
  620. 'skillId': str,
  621. 'skillName': str,
  622. 'damage': int,
  623. 'cooldown': float,
  624. 'cost': int,
  625. 'description': str
  626. }
  627. }
  628. elif '关卡' in filename or 'level' in filename:
  629. self.current_mapping = {
  630. 'format_type': 'horizontal',
  631. 'param_types': {
  632. 'ID': str,
  633. '名称': str,
  634. '敌人列表': str,
  635. '波数': int,
  636. '难度': int,
  637. # 英文字段支持
  638. 'levelId': str,
  639. 'name': str,
  640. 'enemies': str,
  641. 'waves': int,
  642. 'difficulty': int
  643. }
  644. }
  645. elif 'ballcontroller' in filename or '球控制' in filename or '标准配置表' in filename:
  646. # BallController配置映射(纵向格式:参数名-数值)
  647. self.current_mapping = {
  648. 'format_type': 'vertical',
  649. 'param_types': {
  650. 'baseSpeed': float,
  651. 'maxReflectionRandomness': float,
  652. 'antiTrapTimeWindow': float,
  653. 'antiTrapHitThreshold': int,
  654. 'deflectionAttemptThreshold': int,
  655. 'antiTrapDeflectionMultiplier': float,
  656. 'FIRE_COOLDOWN': float,
  657. 'ballRadius': float,
  658. 'gravityScale': float,
  659. 'linearDamping': float,
  660. 'angularDamping': float,
  661. 'colliderGroup': int,
  662. 'colliderTag': int,
  663. 'friction': float,
  664. 'restitution': float,
  665. 'safeDistance': float,
  666. 'edgeOffset': float,
  667. 'sensor': bool,
  668. 'maxAttempts': int
  669. }
  670. }
  671. else:
  672. # 默认配置映射
  673. self.current_mapping = {
  674. 'format_type': 'horizontal',
  675. 'param_types': {}
  676. }
  677. print(f"为文件 {filename} 设置配置映射: {self.current_mapping['format_type']} 格式")
  678. print(f"支持的参数类型: {list(self.current_mapping['param_types'].keys())}")
  679. def preview_selected_files(self):
  680. """预览选中的文件"""
  681. selection = self.file_listbox.curselection()
  682. if not selection:
  683. messagebox.showwarning("警告", "请先选择要预览的文件")
  684. return
  685. self.preview_text.delete(1.0, tk.END)
  686. self.config_data = {}
  687. for i in selection:
  688. file_path = Path(self.file_listbox.get(i))
  689. self.preview_text.insert(tk.END, f"=== {file_path.name} ===\n")
  690. try:
  691. # 根据文件扩展名和pandas可用性选择读取方法
  692. if file_path.suffix.lower() in ['.xlsx', '.xls']:
  693. if PANDAS_AVAILABLE:
  694. file_config = self.read_excel_config(file_path)
  695. else:
  696. self.preview_text.insert(tk.END, "Excel文件需要pandas库支持,请安装: pip install pandas openpyxl\n\n")
  697. continue
  698. elif file_path.suffix.lower() in ['.csv', '.txt']:
  699. file_config = self.read_csv_config(file_path)
  700. else:
  701. self.preview_text.insert(tk.END, "不支持的文件格式\n\n")
  702. continue
  703. if file_config:
  704. # 检查file_config的类型,如果是列表则特殊处理
  705. if isinstance(file_config, list):
  706. # 对于关卡配置等返回列表的情况
  707. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置项:\n")
  708. for i, item in enumerate(file_config):
  709. self.preview_text.insert(tk.END, f" 配置项 {i+1}: {item}\n")
  710. # 将列表数据存储到config_data中
  711. self.config_data[file_path.stem] = file_config
  712. else:
  713. # 原有的字典处理逻辑
  714. self.config_data.update(file_config)
  715. # 显示预览
  716. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置参数:\n")
  717. for key, value in file_config.items():
  718. self.preview_text.insert(tk.END, f" {key}: {value}\n")
  719. else:
  720. self.preview_text.insert(tk.END, "未找到有效的配置数据\n")
  721. except Exception as e:
  722. self.preview_text.insert(tk.END, f"读取失败: {str(e)}\n")
  723. self.preview_text.insert(tk.END, "\n")
  724. # 显示合并后的配置
  725. if self.config_data:
  726. self.preview_text.insert(tk.END, "=== 合并后的配置 ===\n")
  727. self.preview_text.insert(tk.END, json.dumps(self.config_data, indent=2, ensure_ascii=False))
  728. self.status_var.set(f"预览完成,共 {len(self.config_data)} 个配置参数")
  729. def read_excel_config(self, file_path):
  730. """读取Excel配置文件"""
  731. config = {}
  732. if not PANDAS_AVAILABLE:
  733. print("错误: pandas未安装,无法读取Excel文件")
  734. return config
  735. try:
  736. # 根据用户选择的"多工作表文件"选项决定处理方式
  737. if self.multi_sheet_var.get():
  738. # 多工作表模式:读取所有工作表
  739. print(f"多工作表模式处理文件: {file_path.name}")
  740. # 获取所有工作表名称(避免缓存问题)
  741. try:
  742. # 先尝试读取第一个工作表来获取所有工作表名称
  743. excel_file = pd.ExcelFile(file_path)
  744. sheet_names = excel_file.sheet_names
  745. excel_file.close() # 关闭文件句柄
  746. print(f"发现工作表: {sheet_names}")
  747. except Exception as e:
  748. print(f"获取工作表名称失败: {e}")
  749. return config
  750. all_sheets_data = {}
  751. for sheet_name in sheet_names:
  752. try:
  753. # 每次都重新读取文件,避免缓存
  754. df = pd.read_excel(file_path, sheet_name=sheet_name, engine='openpyxl')
  755. all_sheets_data[sheet_name] = df
  756. print(f"成功读取工作表: {sheet_name}, 数据行数: {len(df)}")
  757. # 打印第一行数据用于调试
  758. if len(df) > 0 and sheet_name == '敌人基础配置':
  759. print(f"敌人基础配置第一行数据: {df.iloc[0].to_dict()}")
  760. except Exception as e:
  761. print(f"读取工作表 {sheet_name} 时出错: {e}")
  762. # 根据工作表内容判断配置类型
  763. if any('关卡' in name for name in sheet_names):
  764. config = self.parse_level_multi_sheet_data(all_sheets_data, file_path.name)
  765. elif any('技能' in name for name in sheet_names):
  766. config = self.parse_skill_multi_sheet_data(all_sheets_data, file_path.name)
  767. elif any('敌人' in name for name in sheet_names):
  768. config = self.parse_enemy_multi_sheet_data(all_sheets_data, file_path.name)
  769. elif any('武器' in name for name in sheet_names):
  770. config = self.parse_weapon_multi_sheet_data(all_sheets_data, file_path.name)
  771. else:
  772. # 通用多工作表处理
  773. config = self.parse_multi_sheet_data(all_sheets_data, file_path.name)
  774. else:
  775. # 单工作表模式:读取第一个工作表或指定工作表
  776. print(f"单工作表模式处理文件: {file_path.name}")
  777. sheet_name = None
  778. if self.current_mapping and 'sheet_name' in self.current_mapping:
  779. sheet_name = self.current_mapping['sheet_name']
  780. # 读取Excel文件
  781. if sheet_name:
  782. df = pd.read_excel(file_path, sheet_name=sheet_name)
  783. print(f"读取指定工作表: {sheet_name}, 数据行数: {len(df)}")
  784. else:
  785. df = pd.read_excel(file_path)
  786. print(f"读取默认工作表, 数据行数: {len(df)}")
  787. config = self.parse_config_data(df, file_path.name)
  788. except Exception as e:
  789. print(f"读取Excel文件 {file_path.name} 时出错: {e}")
  790. return config
  791. def read_csv_config(self, file_path):
  792. """读取CSV配置文件"""
  793. config = {}
  794. try:
  795. # 如果pandas可用,优先使用pandas读取CSV(支持更多格式)
  796. if PANDAS_AVAILABLE:
  797. try:
  798. df = pd.read_csv(file_path)
  799. config = self.parse_config_data(df, file_path.name)
  800. except:
  801. # 如果pandas读取失败,回退到原始CSV读取方法
  802. config = self.read_csv_fallback(file_path)
  803. else:
  804. # 如果pandas不可用,直接使用原始CSV读取方法
  805. config = self.read_csv_fallback(file_path)
  806. except Exception as e:
  807. print(f"读取文件 {file_path.name} 时出错: {e}")
  808. return config
  809. def read_csv_fallback(self, file_path):
  810. """原始CSV读取方法(不依赖pandas)"""
  811. config = {}
  812. try:
  813. with open(file_path, 'r', encoding='utf-8') as f:
  814. reader = csv.reader(f)
  815. for row_num, row in enumerate(reader, 1):
  816. if len(row) < 2:
  817. continue
  818. param_name = row[0].strip()
  819. param_value = row[1].strip()
  820. # 跳过标题行
  821. if param_name in ['参数名', 'parameter', 'name']:
  822. continue
  823. # 检查参数是否有效
  824. if param_name in self.param_types:
  825. try:
  826. param_type = self.param_types[param_name]
  827. if param_type == bool:
  828. config[param_name] = param_value.lower() in ['true', '1', 'yes', 'on']
  829. else:
  830. config[param_name] = param_type(param_value)
  831. except (ValueError, TypeError):
  832. continue
  833. except Exception as e:
  834. print(f"读取CSV文件 {file_path.name} 时出错: {e}")
  835. return config
  836. def parse_level_multi_sheet_data(self, all_sheets_data, filename):
  837. """解析关卡配置的多工作表数据"""
  838. config = []
  839. try:
  840. # 获取各个工作表的数据(支持中英文工作表名)
  841. basic_config = None
  842. for sheet_name in ['关卡基础配置', 'Level Config', 'levels', '关卡配置']:
  843. if sheet_name in all_sheets_data:
  844. basic_config = all_sheets_data[sheet_name]
  845. break
  846. weapon_config = None
  847. for sheet_name in ['关卡武器配置', 'Level Weapon Config', 'level_weapons', '武器配置']:
  848. if sheet_name in all_sheets_data:
  849. weapon_config = all_sheets_data[sheet_name]
  850. break
  851. wave_config = None
  852. for sheet_name in ['关卡波次配置', 'Wave Config', 'waves', '波次配置']:
  853. if sheet_name in all_sheets_data:
  854. wave_config = all_sheets_data[sheet_name]
  855. break
  856. enemy_config = None
  857. for sheet_name in ['敌人详细配置', 'Enemy Detail Config', 'enemy_details', '敌人配置']:
  858. if sheet_name in all_sheets_data:
  859. enemy_config = all_sheets_data[sheet_name]
  860. break
  861. if basic_config is None:
  862. print("错误: 未找到关卡基础配置工作表(支持的工作表名: 关卡基础配置, Level Config, levels, 关卡配置)")
  863. return config
  864. # 处理每个关卡
  865. for _, basic_row in basic_config.iterrows():
  866. if pd.isna(basic_row['关卡ID']):
  867. continue
  868. level_id = str(basic_row['关卡ID'])
  869. # 获取武器配置
  870. available_weapons = []
  871. if weapon_config is not None:
  872. weapon_rows = weapon_config[weapon_config['关卡ID'] == level_id]
  873. for _, weapon_row in weapon_rows.iterrows():
  874. if pd.notna(weapon_row['可用武器']):
  875. weapons_str = str(weapon_row['可用武器'])
  876. # 支持多种分隔符
  877. import re
  878. weapons = re.split(r'[、,,;;]', weapons_str)
  879. available_weapons.extend([w.strip() for w in weapons if w.strip()])
  880. level_data = {
  881. 'levelId': level_id,
  882. 'name': str(basic_row['关卡名称']) if pd.notna(basic_row['关卡名称']) else '',
  883. 'scene': str(basic_row['场景']) if pd.notna(basic_row['场景']) else 'grassland',
  884. 'description': str(basic_row['描述']) if pd.notna(basic_row['描述']) else '',
  885. 'backgroundImage': str(basic_row['关卡背景图路径']) if pd.notna(basic_row['关卡背景图路径']) else 'images/LevelBackground/BG1',
  886. 'availableWeapons': available_weapons,
  887. 'coinReward': int(basic_row['钞票奖励']) if pd.notna(basic_row['钞票奖励']) else 100,
  888. 'diamondReward': int(basic_row['钻石奖励']) if pd.notna(basic_row['钻石奖励']) else 0,
  889. 'timeLimit': 300, # 默认值
  890. 'difficulty': 'normal', # 默认值
  891. 'healthMultiplier': float(basic_row['生命倍数']) if pd.notna(basic_row['生命倍数']) else 1.0, # 从Excel读取生命倍数
  892. 'waves': []
  893. }
  894. # 获取该关卡的波次配置
  895. if wave_config is not None:
  896. level_waves = wave_config[wave_config['关卡ID'] == level_id]
  897. for _, wave_row in level_waves.iterrows():
  898. wave_id = int(wave_row['波次ID']) if pd.notna(wave_row['波次ID']) else 1
  899. # 获取该波次的敌人配置
  900. wave_enemies = []
  901. if enemy_config is not None:
  902. wave_enemy_data = enemy_config[
  903. (enemy_config['关卡ID'] == level_id) &
  904. (enemy_config['波次ID'] == wave_id)
  905. ]
  906. for _, enemy_row in wave_enemy_data.iterrows():
  907. enemy_type = str(enemy_row['敌人类型']) if pd.notna(enemy_row['敌人类型']) else '普通僵尸'
  908. enemy_data = {
  909. 'enemyType': enemy_type,
  910. 'count': int(enemy_row['数量']) if pd.notna(enemy_row['数量']) else 1,
  911. 'spawnInterval': float(enemy_row['生成间隔']) if pd.notna(enemy_row['生成间隔']) else 2.0,
  912. 'spawnDelay': float(enemy_row['生成延迟']) if pd.notna(enemy_row['生成延迟']) else 0.0,
  913. 'characteristics': str(enemy_row['特征描述']) if pd.notna(enemy_row['特征描述']) else ''
  914. }
  915. wave_enemies.append(enemy_data)
  916. wave_data = {
  917. 'waveId': wave_id,
  918. 'enemies': wave_enemies
  919. }
  920. level_data['waves'].append(wave_data)
  921. config.append(level_data)
  922. print(f"处理关卡: {level_id}, 波次数: {len(level_data['waves'])}")
  923. print(f"成功解析关卡配置,共 {len(config)} 个关卡")
  924. except Exception as e:
  925. print(f"解析关卡多工作表数据时出错: {e}")
  926. import traceback
  927. traceback.print_exc()
  928. return config
  929. def parse_skill_multi_sheet_data(self, all_sheets_data, filename):
  930. """解析技能配置的多工作表数据"""
  931. skills_config = []
  932. try:
  933. # 检查是否为局外技能配置表格式(包含技能类型配置、技能组配置、基本配置)
  934. if '技能类型配置' in all_sheets_data and '技能组配置' in all_sheets_data and '基本配置' in all_sheets_data:
  935. print("检测到局外技能配置表格式,使用集成的技能配置导入工具处理")
  936. # 使用集成的SkillConfigImporter处理
  937. try:
  938. skill_importer = SkillConfigImporter(self.excel_dir)
  939. success = skill_importer.import_config()
  940. if success:
  941. print("技能配置导入成功!")
  942. # 返回成功标识,让调用者知道已经处理完成
  943. return [{'status': 'imported', 'message': '技能配置已成功导入到skill_config.json'}]
  944. else:
  945. print("技能配置导入失败")
  946. return [{'status': 'error', 'message': '技能配置导入失败'}]
  947. except Exception as e:
  948. print(f"技能配置导入过程中出错: {e}")
  949. return [{'status': 'error', 'message': f'技能配置导入出错: {e}'}]
  950. return skills_config
  951. # 获取技能信息表数据(支持中英文工作表名)
  952. skill_info = None
  953. for sheet_name in ['技能信息表', 'Skill Config', 'skills', '技能配置']:
  954. if sheet_name in all_sheets_data:
  955. skill_info = all_sheets_data[sheet_name]
  956. break
  957. if skill_info is None:
  958. print("错误: 未找到技能信息表工作表(支持的工作表名: 技能信息表, Skill Config, skills, 技能配置)")
  959. return skills_config
  960. # 处理每个技能(支持中英文字段名)
  961. for _, skill_row in skill_info.iterrows():
  962. # 获取技能ID(支持中英文字段名)
  963. skill_id_field = None
  964. for field in ['id', '技能ID', 'skill_id', 'ID']:
  965. if field in skill_row and pd.notna(skill_row[field]):
  966. skill_id_field = field
  967. break
  968. if skill_id_field is None:
  969. continue
  970. def get_skill_field_value(row, cn_field, en_fields, default_value, value_type=str):
  971. """获取技能字段值,支持中英文字段名"""
  972. for field in [cn_field] + (en_fields if isinstance(en_fields, list) else [en_fields]):
  973. if field in row and pd.notna(row[field]):
  974. try:
  975. return value_type(row[field])
  976. except (ValueError, TypeError):
  977. continue
  978. return default_value
  979. skill_data = {
  980. 'id': str(skill_row[skill_id_field]),
  981. 'name': get_skill_field_value(skill_row, '技能名称', ['name', 'skill_name'], ''),
  982. 'description': get_skill_field_value(skill_row, '技能描述', ['description', 'desc'], ''),
  983. 'iconPath': get_skill_field_value(skill_row, '图标路径', ['iconPath', 'icon_path', 'icon'], ''),
  984. 'maxLevel': get_skill_field_value(skill_row, '最大等级', ['maxLevel', 'max_level'], 5, int),
  985. 'currentLevel': get_skill_field_value(skill_row, '当前等级', ['currentLevel', 'current_level'], 0, int),
  986. 'priceReduction': get_skill_field_value(skill_row, '价格减少', ['priceReduction', 'price_reduction'], 0.0, float),
  987. 'critChanceIncrease': get_skill_field_value(skill_row, '暴击几率增加', ['critChanceIncrease', 'crit_chance_increase'], 0.0, float),
  988. 'critDamageBonus': get_skill_field_value(skill_row, '暴击伤害加成', ['critDamageBonus', 'crit_damage_bonus'], 0.0, float),
  989. 'healthIncrease': get_skill_field_value(skill_row, '生命值增加', ['healthIncrease', 'health_increase'], 0.0, float),
  990. 'multiShotChance': get_skill_field_value(skill_row, '多重射击几率', ['multiShotChance', 'multi_shot_chance'], 0.0, float),
  991. 'energyGainIncrease': get_skill_field_value(skill_row, '能量加成', ['energyGainIncrease', 'energy_gain_increase'], 0.0, float),
  992. 'ballSpeedIncrease': get_skill_field_value(skill_row, '速度提升', ['ballSpeedIncrease', 'ball_speed_increase'], 0.0, float)
  993. }
  994. skills_config.append(skill_data)
  995. print(f"处理技能: {skill_data['id']} - {skill_data['name']}")
  996. print(f"成功解析技能配置,共 {len(skills_config)} 个技能")
  997. except Exception as e:
  998. print(f"解析技能多工作表数据时出错: {e}")
  999. import traceback
  1000. traceback.print_exc()
  1001. return skills_config
  1002. def parse_multi_sheet_data(self, all_sheets_data, filename):
  1003. """解析通用多工作表数据"""
  1004. # 处理武器配置表的多工作表
  1005. if '方块武器配置表' in filename:
  1006. return self.parse_weapon_multi_sheet_data(all_sheets_data, filename)
  1007. # 处理敌人配置表的多工作表
  1008. if '敌人配置表' in filename or '敌人' in filename:
  1009. return self.parse_enemy_multi_sheet_data(all_sheets_data, filename)
  1010. # 这里可以添加其他多工作表文件的处理逻辑
  1011. # 目前只返回第一个工作表的数据
  1012. if all_sheets_data:
  1013. first_sheet_name = list(all_sheets_data.keys())[0]
  1014. first_sheet_data = all_sheets_data[first_sheet_name]
  1015. return self.parse_config_data(first_sheet_data, filename)
  1016. return []
  1017. def parse_config_data(self, df, filename):
  1018. """解析配置数据(支持多种格式,需要pandas)"""
  1019. config = {}
  1020. if not PANDAS_AVAILABLE or not self.current_mapping:
  1021. return config
  1022. format_type = self.current_mapping['format_type']
  1023. try:
  1024. if format_type == 'vertical':
  1025. # 纵向表格:参数名在第一列,值在第二/三列
  1026. for _, row in df.iterrows():
  1027. param_name = str(row.iloc[0]).strip()
  1028. # 跳过标题行和无效行
  1029. if param_name in ['参数名', 'parameter', 'name', 'nan', '球控制器参数'] or param_name == 'nan':
  1030. continue
  1031. # 检查参数是否有效
  1032. param_types = self.current_mapping.get('param_types', {})
  1033. if param_name in param_types:
  1034. try:
  1035. # 优先使用第3列(默认值),如果不存在则使用第2列
  1036. param_value = row.iloc[2] if len(row) > 2 and not pd.isna(row.iloc[2]) else row.iloc[1]
  1037. if pd.isna(param_value):
  1038. continue
  1039. param_type = param_types[param_name]
  1040. if param_type == bool:
  1041. config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  1042. else:
  1043. config[param_name] = param_type(param_value)
  1044. except (ValueError, TypeError, IndexError):
  1045. continue
  1046. elif format_type == 'horizontal':
  1047. # 横向表格:第一行是参数名(列名),数据从第0行开始
  1048. print(f"横向表格解析: 总行数={len(df)}")
  1049. print(f"表格列名: {list(df.columns)}")
  1050. # 打印前几行数据用于调试
  1051. for i in range(min(3, len(df))):
  1052. print(f"第{i}行数据: {df.iloc[i].to_dict()}")
  1053. # 检查第0行是否为有效数据行
  1054. # 如果第0行第一个单元格是描述性文字或空值,则跳过
  1055. data_start_row = 0
  1056. if len(df) > 0:
  1057. first_cell = str(df.iloc[0, 0]).strip().lower()
  1058. # 跳过描述行、空行或标题行
  1059. if (first_cell in ['唯一标识符', '描述', 'description', 'desc', 'nan', ''] or
  1060. first_cell == df.columns[0].lower()):
  1061. data_start_row = 1
  1062. print(f"跳过第0行(描述行或标题行): {first_cell}")
  1063. print(f"数据起始行: {data_start_row}")
  1064. # 解析多行数据(如敌人配置、武器配置、关卡配置等)
  1065. config_list = []
  1066. for i in range(data_start_row, len(df)):
  1067. row_config = {}
  1068. row_has_data = False
  1069. param_types = self.current_mapping.get('param_types', {})
  1070. print(f"可用的参数类型: {list(param_types.keys())}")
  1071. print(f"当前配置映射: {self.current_mapping}")
  1072. print(f"DataFrame列名: {list(df.columns)}")
  1073. # 如果参数类型为空,尝试从DataFrame列名自动推断
  1074. if not param_types:
  1075. print("参数类型为空,尝试自动推断...")
  1076. for col_name in df.columns:
  1077. col_str = str(col_name).strip()
  1078. if col_str and col_str != 'nan':
  1079. # 根据列名推断数据类型
  1080. if any(keyword in col_str for keyword in ['ID', 'id', '编号']):
  1081. param_types[col_str] = str
  1082. elif any(keyword in col_str for keyword in ['名称', 'name', '名字']):
  1083. param_types[col_str] = str
  1084. elif any(keyword in col_str for keyword in ['伤害', 'damage', '攻击', 'attack']):
  1085. param_types[col_str] = int
  1086. elif any(keyword in col_str for keyword in ['射程', 'range', '范围']):
  1087. param_types[col_str] = int
  1088. elif any(keyword in col_str for keyword in ['射速', 'fireRate', '频率']):
  1089. param_types[col_str] = float
  1090. elif any(keyword in col_str for keyword in ['价格', 'price', '金币', 'cost']):
  1091. param_types[col_str] = int
  1092. elif any(keyword in col_str for keyword in ['等级', 'level', 'Level']):
  1093. param_types[col_str] = int
  1094. elif any(keyword in col_str for keyword in ['时间', 'time', 'Time']):
  1095. param_types[col_str] = float
  1096. elif any(keyword in col_str for keyword in ['描述', 'description', '说明']):
  1097. param_types[col_str] = str
  1098. else:
  1099. # 默认为字符串类型
  1100. param_types[col_str] = str
  1101. # 更新配置映射
  1102. if param_types:
  1103. self.current_mapping['param_types'] = param_types
  1104. print(f"自动推断的参数类型: {param_types}")
  1105. for col_idx, col_name in enumerate(df.columns):
  1106. param_name = str(col_name).strip()
  1107. print(f"检查列名: '{param_name}' 是否在参数类型中")
  1108. if param_name in param_types:
  1109. try:
  1110. param_value = df.iloc[i, col_idx]
  1111. if pd.isna(param_value) or str(param_value).strip() == '':
  1112. print(f"跳过空值: {param_name} = {param_value}")
  1113. continue
  1114. param_type = param_types[param_name]
  1115. if param_type == bool:
  1116. row_config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  1117. else:
  1118. row_config[param_name] = param_type(param_value)
  1119. row_has_data = True
  1120. print(f"成功转换字段: {param_name} = {param_value} ({param_type})")
  1121. except (ValueError, TypeError, IndexError) as e:
  1122. print(f"转换字段 {param_name} 时出错: {e}")
  1123. continue
  1124. else:
  1125. print(f"字段 '{param_name}' 不在参数类型定义中,跳过")
  1126. if row_config and row_has_data: # 只添加非空且有有效数据的配置
  1127. config_list.append(row_config)
  1128. print(f"成功解析第{i}行,包含{len(row_config)}个字段")
  1129. else:
  1130. print(f"跳过第{i}行(无有效数据)")
  1131. print(f"总共解析出{len(config_list)}个有效配置项")
  1132. # 对于横向表格,返回配置列表
  1133. config = {'items': config_list}
  1134. except Exception as e:
  1135. print(f"解析文件 {filename} 时出错: {e}")
  1136. return config
  1137. def clear_selection(self):
  1138. """清空选择"""
  1139. self.file_listbox.selection_clear(0, tk.END)
  1140. self.preview_text.delete(1.0, tk.END)
  1141. self.config_data = {}
  1142. self.status_var.set("已清空选择")
  1143. self.load_current_config()
  1144. def load_current_config(self):
  1145. """加载当前配置"""
  1146. try:
  1147. if not self.selected_json_path:
  1148. self.preview_text.insert(tk.END, "请先选择JSON输出路径\n")
  1149. return
  1150. json_path = Path(self.selected_json_path)
  1151. if json_path.exists():
  1152. with open(json_path, 'r', encoding='utf-8') as f:
  1153. current_config = json.load(f)
  1154. self.preview_text.insert(tk.END, f"=== 当前配置 ({json_path.name}) ===\n")
  1155. # 根据配置类型显示不同的预览格式
  1156. if self.format_var.get() == 'horizontal':
  1157. # 横向表格配置(如敌人、武器)
  1158. if 'enemies' in current_config:
  1159. self.preview_text.insert(tk.END, f"敌人配置 ({len(current_config['enemies'])} 个):\n")
  1160. for i, enemy in enumerate(current_config['enemies'][:5]): # 只显示前5个
  1161. self.preview_text.insert(tk.END, f" {i+1}. {enemy.get('name', enemy.get('id', 'Unknown'))}\n")
  1162. if len(current_config['enemies']) > 5:
  1163. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['enemies']) - 5} 个\n")
  1164. if 'weapons' in current_config:
  1165. self.preview_text.insert(tk.END, f"\n武器配置 ({len(current_config['weapons'])} 个):\n")
  1166. for i, weapon in enumerate(current_config['weapons'][:5]): # 只显示前5个
  1167. self.preview_text.insert(tk.END, f" {i+1}. {weapon.get('name', weapon.get('id', 'Unknown'))}\n")
  1168. if len(current_config['weapons']) > 5:
  1169. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['weapons']) - 5} 个\n")
  1170. else:
  1171. # 纵向表格配置(如BallController)
  1172. self.preview_text.insert(tk.END, json.dumps(current_config, indent=2, ensure_ascii=False))
  1173. file_hint = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  1174. self.preview_text.insert(tk.END, f"\n\n请选择{file_hint}进行配置导入...\n")
  1175. else:
  1176. self.preview_text.insert(tk.END, "配置文件不存在\n")
  1177. except Exception as e:
  1178. self.preview_text.insert(tk.END, f"加载当前配置失败: {e}\n")
  1179. def backup_config(self):
  1180. """备份当前配置"""
  1181. try:
  1182. if not self.selected_json_path:
  1183. messagebox.showwarning("警告", "请先选择JSON输出路径")
  1184. return
  1185. json_path = Path(self.selected_json_path)
  1186. if not json_path.exists():
  1187. messagebox.showwarning("警告", "配置文件不存在")
  1188. return
  1189. backup_path = json_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  1190. with open(json_path, 'r', encoding='utf-8') as f:
  1191. content = f.read()
  1192. with open(backup_path, 'w', encoding='utf-8') as f:
  1193. f.write(content)
  1194. messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}")
  1195. self.status_var.set(f"配置已备份")
  1196. except Exception as e:
  1197. messagebox.showerror("错误", f"备份失败: {e}")
  1198. def import_config(self):
  1199. """导入配置到JSON文件"""
  1200. if not self.config_data:
  1201. messagebox.showwarning("警告", "没有配置数据可导入")
  1202. return
  1203. if not self.selected_json_path:
  1204. messagebox.showwarning("警告", "请先选择JSON输出路径")
  1205. return
  1206. try:
  1207. print(f"开始导入配置...")
  1208. print(f"配置数据: {self.config_data}")
  1209. print(f"输出路径: {self.selected_json_path}")
  1210. print(f"选中文件: {self.selected_files}")
  1211. # 优先使用配置映射中的格式类型,如果没有则使用GUI设置
  1212. format_type = self.current_mapping.get('format_type', self.format_var.get()) if hasattr(self, 'current_mapping') and self.current_mapping else self.format_var.get()
  1213. print(f"格式类型: {format_type}")
  1214. print(f"配置映射格式: {self.current_mapping.get('format_type') if hasattr(self, 'current_mapping') and self.current_mapping else 'None'}")
  1215. print(f"GUI格式设置: {self.format_var.get()}")
  1216. if format_type == 'vertical':
  1217. # 处理纵向表格(如BallController)
  1218. print("开始处理纵向表格配置...")
  1219. self._import_vertical_config()
  1220. elif format_type == 'horizontal':
  1221. # 处理横向表格(如武器配置)
  1222. print("开始处理横向表格配置...")
  1223. self._import_horizontal_config()
  1224. elif format_type == 'multi_sheet':
  1225. # 处理多工作表格式(如敌人配置)
  1226. print("开始处理多工作表配置...")
  1227. self._import_multi_sheet_config() # 使用专门的多工作表处理逻辑
  1228. else:
  1229. raise ValueError(f"未知的格式类型: {format_type}")
  1230. except Exception as e:
  1231. import traceback
  1232. error_details = traceback.format_exc()
  1233. print(f"导入配置失败,详细错误信息:")
  1234. print(error_details)
  1235. messagebox.showerror("错误", f"导入配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1236. def import_skill_config(self):
  1237. """导入技能配置到skill_config.json"""
  1238. try:
  1239. # 查找局外技能配置表.xlsx文件
  1240. excel_dir = Path(self.excel_dir)
  1241. skill_excel_file = excel_dir / "局外技能配置表.xlsx"
  1242. if not skill_excel_file.exists():
  1243. messagebox.showwarning("警告", f"未找到技能配置文件: {skill_excel_file}")
  1244. return
  1245. # 使用SkillConfigImporter导入配置
  1246. importer = SkillConfigImporter(str(excel_dir))
  1247. success, message = importer.import_config()
  1248. if success:
  1249. messagebox.showinfo("成功", message)
  1250. self.status_var.set("技能配置导入成功")
  1251. # 更新预览文本
  1252. self.preview_text.delete(1.0, tk.END)
  1253. self.preview_text.insert(tk.END, f"技能配置导入成功\n\n{message}")
  1254. else:
  1255. messagebox.showerror("错误", f"技能配置导入失败: {message}")
  1256. self.status_var.set("技能配置导入失败")
  1257. except Exception as e:
  1258. import traceback
  1259. error_details = traceback.format_exc()
  1260. print(f"导入技能配置失败,详细错误信息:")
  1261. print(error_details)
  1262. messagebox.showerror("错误", f"导入技能配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1263. def import_weapon_config(self):
  1264. """导入武器配置"""
  1265. try:
  1266. if not WEAPON_MANAGER_AVAILABLE:
  1267. messagebox.showerror("错误", "武器配置管理器不可用,请检查weapon_config_manager.py文件是否存在")
  1268. return
  1269. # 创建武器配置管理器
  1270. excel_dir = Path(self.excel_dir)
  1271. weapon_excel_file = excel_dir / "方块武器配置" / "方块武器配置表.xlsx"
  1272. weapon_json_file = excel_dir.parent / "weapons.json"
  1273. if not weapon_excel_file.exists():
  1274. messagebox.showwarning("警告", f"未找到武器配置文件: {weapon_excel_file}")
  1275. return
  1276. # 使用WeaponConfigManager导入配置
  1277. weapon_manager = WeaponConfigManager(
  1278. excel_file_path=str(weapon_excel_file),
  1279. json_file_path=str(weapon_json_file)
  1280. )
  1281. # 在后台线程中执行导入
  1282. def import_thread():
  1283. try:
  1284. success = weapon_manager.import_weapon_config()
  1285. # 在主线程中更新UI
  1286. def update_ui():
  1287. if success:
  1288. messagebox.showinfo("成功", "武器配置导入成功!")
  1289. self.status_var.set("武器配置导入成功")
  1290. # 更新预览文本
  1291. self.preview_text.delete(1.0, tk.END)
  1292. self.preview_text.insert(tk.END, "武器配置导入成功\n\n配置已从Excel文件合并到JSON文件中")
  1293. else:
  1294. messagebox.showerror("错误", "武器配置导入失败,请查看控制台输出")
  1295. self.status_var.set("武器配置导入失败")
  1296. self.root.after(0, update_ui)
  1297. except Exception as e:
  1298. def show_error():
  1299. import traceback
  1300. error_details = traceback.format_exc()
  1301. print(f"导入武器配置失败,详细错误信息:")
  1302. print(error_details)
  1303. messagebox.showerror("错误", f"导入武器配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1304. self.status_var.set("武器配置导入失败")
  1305. self.root.after(0, show_error)
  1306. # 启动导入线程
  1307. self.status_var.set("正在导入武器配置...")
  1308. thread = threading.Thread(target=import_thread)
  1309. thread.daemon = True
  1310. thread.start()
  1311. except Exception as e:
  1312. import traceback
  1313. error_details = traceback.format_exc()
  1314. print(f"启动武器配置导入失败,详细错误信息:")
  1315. print(error_details)
  1316. messagebox.showerror("错误", f"启动武器配置导入失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1317. def import_enemy_config(self):
  1318. """导入敌人配置"""
  1319. try:
  1320. if not ENEMY_MANAGER_AVAILABLE:
  1321. messagebox.showerror("错误", "敌人配置管理器不可用,请检查enemy_config_manager.py文件是否存在")
  1322. return
  1323. # 创建敌人配置管理器
  1324. excel_dir = Path(self.excel_dir)
  1325. enemy_excel_file = excel_dir / "敌人配置表.xlsx"
  1326. enemy_json_file = excel_dir.parent / "enemies.json"
  1327. if not enemy_excel_file.exists():
  1328. messagebox.showwarning("警告", f"未找到敌人配置文件: {enemy_excel_file}")
  1329. return
  1330. # 使用EnemyConfigManager导入配置
  1331. enemy_manager = EnemyConfigManager(
  1332. excel_path=str(enemy_excel_file),
  1333. json_path=str(enemy_json_file)
  1334. )
  1335. # 在后台线程中执行导入
  1336. def import_thread():
  1337. try:
  1338. success = enemy_manager.import_config()
  1339. # 在主线程中更新UI
  1340. def update_ui():
  1341. if success:
  1342. messagebox.showinfo("成功", "敌人配置导入成功!")
  1343. self.status_var.set("敌人配置导入成功")
  1344. # 更新预览文本
  1345. self.preview_text.delete(1.0, tk.END)
  1346. self.preview_text.insert(tk.END, "敌人配置导入成功\n\n配置已从Excel文件合并到JSON文件中")
  1347. else:
  1348. messagebox.showerror("错误", "敌人配置导入失败,请查看控制台输出")
  1349. self.status_var.set("敌人配置导入失败")
  1350. self.root.after(0, update_ui)
  1351. except Exception as e:
  1352. def show_error():
  1353. import traceback
  1354. error_details = traceback.format_exc()
  1355. print(f"导入敌人配置失败,详细错误信息:")
  1356. print(error_details)
  1357. messagebox.showerror("错误", f"导入敌人配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1358. self.status_var.set("敌人配置导入失败")
  1359. self.root.after(0, show_error)
  1360. # 启动导入线程
  1361. self.status_var.set("正在导入敌人配置...")
  1362. thread = threading.Thread(target=import_thread)
  1363. thread.daemon = True
  1364. thread.start()
  1365. except Exception as e:
  1366. import traceback
  1367. error_details = traceback.format_exc()
  1368. print(f"启动敌人配置导入失败,详细错误信息:")
  1369. print(error_details)
  1370. messagebox.showerror("错误", f"启动敌人配置导入失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1371. def _import_vertical_config(self):
  1372. """导入纵向表格配置"""
  1373. # 读取现有JSON配置
  1374. if self.selected_json_path.exists():
  1375. with open(self.selected_json_path, 'r', encoding='utf-8') as f:
  1376. current_config = json.load(f)
  1377. else:
  1378. current_config = self.default_config.copy()
  1379. # 合并配置
  1380. updated_count = 0
  1381. for key, value in self.config_data.items():
  1382. if key in current_config and current_config[key] != value:
  1383. current_config[key] = value
  1384. updated_count += 1
  1385. elif key not in current_config:
  1386. current_config[key] = value
  1387. updated_count += 1
  1388. # 写入更新后的配置
  1389. with open(self.selected_json_path, 'w', encoding='utf-8') as f:
  1390. json.dump(current_config, f, indent=2, ensure_ascii=False)
  1391. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {updated_count} 个参数")
  1392. self.status_var.set("配置导入成功")
  1393. # 刷新预览
  1394. self.clear_selection()
  1395. def _import_horizontal_config(self):
  1396. """导入横向表格配置"""
  1397. print(f"横向表格配置导入开始...")
  1398. print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
  1399. # 检查是否是关卡配置(目录类型)
  1400. is_level_config = str(self.selected_json_path).endswith('levels') or self.selected_json_path.is_dir()
  1401. # 对于关卡配置,支持多种数据格式
  1402. if is_level_config:
  1403. if isinstance(self.config_data, list):
  1404. # 多工作表数据格式:直接是关卡数据列表
  1405. print("检测到关卡配置的多工作表数据格式")
  1406. items = None # 不需要items字段
  1407. elif 'items' in self.config_data:
  1408. # 传统的items数据格式
  1409. print("检测到关卡配置的传统items数据格式")
  1410. items = self.config_data['items']
  1411. elif isinstance(self.config_data, dict) and len(self.config_data) == 1:
  1412. # 检查是否是包装在单个键中的配置数据
  1413. key = list(self.config_data.keys())[0]
  1414. wrapped_data = self.config_data[key]
  1415. print(f"检测到包装的关卡配置数据,键名: {key}")
  1416. if isinstance(wrapped_data, list):
  1417. print("提取到关卡配置的多工作表数据格式")
  1418. self.config_data = wrapped_data # 更新配置数据为实际内容
  1419. items = None
  1420. elif isinstance(wrapped_data, dict) and 'items' in wrapped_data:
  1421. print("提取到关卡配置的传统items数据格式")
  1422. self.config_data = wrapped_data
  1423. items = wrapped_data['items']
  1424. else:
  1425. print(f"错误: 包装数据格式不正确: {type(wrapped_data)}")
  1426. error_msg = f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n\n包装键: {key}\n数据类型: {type(wrapped_data)}"
  1427. messagebox.showwarning("警告", error_msg)
  1428. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  1429. self.preview_text.insert(tk.END, f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n")
  1430. self.preview_text.insert(tk.END, f"包装键: {key}\n数据类型: {type(wrapped_data)}\n")
  1431. return
  1432. else:
  1433. print(f"错误: 关卡配置数据格式不正确")
  1434. print(f"实际配置数据: {self.config_data}")
  1435. print(f"配置数据类型: {type(self.config_data)}")
  1436. print(f"配置数据键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}")
  1437. error_msg = f"关卡配置数据格式错误:需要多工作表数据或items字段\n\n数据类型: {type(self.config_data)}\n数据内容: {str(self.config_data)[:200]}..."
  1438. messagebox.showwarning("警告", error_msg)
  1439. # 同时在GUI中显示错误信息
  1440. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  1441. self.preview_text.insert(tk.END, f"关卡配置数据格式错误:需要多工作表数据或items字段\n")
  1442. self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n")
  1443. self.preview_text.insert(tk.END, f"数据内容: {str(self.config_data)[:500]}...\n")
  1444. return
  1445. else:
  1446. # 其他配置类型:检查数据格式
  1447. if isinstance(self.config_data, dict):
  1448. # 检查是否是多工作表数据格式(如:{'技能配置表': [...]})
  1449. if len(self.config_data) == 1:
  1450. key = list(self.config_data.keys())[0]
  1451. data_content = self.config_data[key]
  1452. if isinstance(data_content, list):
  1453. print(f"检测到多工作表数据格式,键名: {key}")
  1454. items = data_content
  1455. elif isinstance(data_content, dict) and 'items' in data_content:
  1456. print(f"检测到包装的items数据格式,键名: {key}")
  1457. items = data_content['items']
  1458. else:
  1459. print(f"错误: 无法识别的数据格式")
  1460. error_msg = f"配置数据格式错误:无法识别的数据结构\n\n键名: {key}\n数据类型: {type(data_content)}"
  1461. messagebox.showwarning("警告", error_msg)
  1462. return
  1463. elif 'items' in self.config_data:
  1464. print("检测到传统items数据格式")
  1465. items = self.config_data['items']
  1466. else:
  1467. print(f"错误: 配置数据格式不正确")
  1468. print(f"实际配置数据: {self.config_data}")
  1469. print(f"配置数据类型: {type(self.config_data)}")
  1470. print(f"配置数据键: {list(self.config_data.keys())}")
  1471. error_msg = f"配置数据格式错误:需要items字段或多工作表数据格式\n\n数据类型: {type(self.config_data)}\n可用键: {list(self.config_data.keys())}"
  1472. messagebox.showwarning("警告", error_msg)
  1473. # 同时在GUI中显示错误信息
  1474. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  1475. self.preview_text.insert(tk.END, f"配置数据格式错误:需要items字段或多工作表数据格式\n")
  1476. self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n")
  1477. self.preview_text.insert(tk.END, f"可用键: {list(self.config_data.keys())}\n")
  1478. return
  1479. elif isinstance(self.config_data, list):
  1480. print("检测到直接列表数据格式")
  1481. items = self.config_data
  1482. else:
  1483. print(f"错误: 不支持的数据类型: {type(self.config_data)}")
  1484. error_msg = f"不支持的数据类型: {type(self.config_data)}"
  1485. messagebox.showwarning("警告", error_msg)
  1486. return
  1487. # 验证数据有效性
  1488. if items is not None:
  1489. print(f"配置项数量: {len(items) if items else 0}")
  1490. if not items:
  1491. messagebox.showwarning("警告", "没有有效的配置项")
  1492. return
  1493. elif isinstance(self.config_data, list):
  1494. print(f"关卡配置数量: {len(self.config_data)}")
  1495. if not self.config_data:
  1496. messagebox.showwarning("警告", "没有有效的关卡配置")
  1497. return
  1498. # 打印前几个配置项用于调试
  1499. if items is not None:
  1500. for i, item in enumerate(items[:3]):
  1501. print(f"配置项{i}: {item}")
  1502. elif isinstance(self.config_data, list):
  1503. for i, item in enumerate(self.config_data[:3]):
  1504. print(f"关卡配置{i}: {item.get('levelId', 'Unknown')} - {item.get('name', 'Unknown')}")
  1505. # 读取现有JSON配置
  1506. print(f"JSON配置文件路径: {self.selected_json_path}")
  1507. # 检查是否是关卡配置(目录类型)
  1508. if is_level_config:
  1509. print("处理关卡配置目录...")
  1510. # 关卡配置是目录,确保目录存在
  1511. if not self.selected_json_path.exists():
  1512. print(f"创建关卡配置目录: {self.selected_json_path}")
  1513. self.selected_json_path.mkdir(parents=True, exist_ok=True)
  1514. current_config = {}
  1515. else:
  1516. # 普通JSON文件配置
  1517. if self.selected_json_path.exists():
  1518. print("读取现有JSON配置文件...")
  1519. with open(self.selected_json_path, 'r', encoding='utf-8') as f:
  1520. current_config = json.load(f)
  1521. else:
  1522. print("创建新的JSON配置...")
  1523. current_config = {}
  1524. # 根据用户选择的处理模式和数据类型处理
  1525. if not self.selected_files:
  1526. print("错误: 没有选中的文件")
  1527. messagebox.showerror("错误", "没有选中的文件")
  1528. return
  1529. filename = Path(self.selected_files[0]).name
  1530. print(f"处理文件: {filename}")
  1531. # 获取用户选择的处理模式
  1532. is_multi_sheet = self.multi_sheet_var.get()
  1533. is_single_json = self.single_json_var.get()
  1534. print(f"处理模式 - 多工作表: {is_multi_sheet}, 单个JSON: {is_single_json}")
  1535. # 根据数据类型和用户选择的模式进行处理
  1536. if is_level_config:
  1537. # 关卡配置:始终为目录模式,每个关卡一个JSON文件
  1538. print("处理关卡配置...")
  1539. # 关卡配置:为每个关卡创建单独的JSON文件
  1540. levels_dir = self.selected_json_path
  1541. print(f"关卡配置目录: {levels_dir}")
  1542. try:
  1543. # 检查并创建目录
  1544. if not levels_dir.exists():
  1545. print(f"创建关卡配置目录: {levels_dir}")
  1546. levels_dir.mkdir(parents=True, exist_ok=True)
  1547. else:
  1548. print(f"关卡配置目录已存在: {levels_dir}")
  1549. # 测试目录写入权限
  1550. test_file = levels_dir / "test_permission.tmp"
  1551. try:
  1552. with open(test_file, 'w', encoding='utf-8') as f:
  1553. f.write("test")
  1554. test_file.unlink() # 删除测试文件
  1555. except PermissionError:
  1556. messagebox.showerror("错误", f"没有写入权限到目录: {levels_dir}\n请检查目录权限或以管理员身份运行")
  1557. return
  1558. # 检查数据格式:多工作表数据 vs 传统items数据
  1559. if isinstance(self.config_data, list):
  1560. # 新的多工作表数据格式:直接是关卡数据列表
  1561. print("使用多工作表数据格式")
  1562. level_configs = self.config_data
  1563. elif 'items' in self.config_data:
  1564. # 传统的items数据格式:需要转换
  1565. print("使用传统items数据格式")
  1566. level_configs = []
  1567. for item in items:
  1568. level_data = self._convert_level_data(item)
  1569. if level_data:
  1570. level_configs.append(level_data)
  1571. else:
  1572. print(f"错误: 未知的关卡配置数据格式: {type(self.config_data)}")
  1573. messagebox.showerror("错误", "关卡配置数据格式错误")
  1574. return
  1575. # 保存关卡配置文件
  1576. updated_count = 0
  1577. failed_count = 0
  1578. for level_data in level_configs:
  1579. try:
  1580. if level_data and 'levelId' in level_data and level_data['levelId']:
  1581. level_id = level_data['levelId']
  1582. level_file = levels_dir / f"{level_id}.json"
  1583. print(f"保存关卡配置: {level_file}")
  1584. with open(level_file, 'w', encoding='utf-8') as f:
  1585. json.dump(level_data, f, indent=2, ensure_ascii=False)
  1586. updated_count += 1
  1587. else:
  1588. print(f"跳过无效的关卡数据: {level_data}")
  1589. failed_count += 1
  1590. except Exception as e:
  1591. print(f"保存关卡配置时出错: {e}")
  1592. failed_count += 1
  1593. continue
  1594. if updated_count > 0:
  1595. message = f"关卡配置导入成功!\n更新了 {updated_count} 个关卡文件"
  1596. if failed_count > 0:
  1597. message += f"\n跳过了 {failed_count} 个无效配置"
  1598. messagebox.showinfo("成功", message)
  1599. self.status_var.set("关卡配置导入成功")
  1600. else:
  1601. messagebox.showerror("错误", "没有成功导入任何关卡配置\n请检查Excel文件格式和数据")
  1602. self.status_var.set("关卡配置导入失败")
  1603. except Exception as e:
  1604. error_msg = f"关卡配置导入失败: {str(e)}"
  1605. print(error_msg)
  1606. messagebox.showerror("错误", error_msg)
  1607. self.status_var.set("关卡配置导入失败")
  1608. self.clear_selection()
  1609. return
  1610. else:
  1611. # 非关卡配置:根据用户选择的模式和数据内容进行处理
  1612. print("处理非关卡配置...")
  1613. # 根据数据类型自动识别配置类型
  1614. config_type = self._detect_config_type()
  1615. print(f"检测到配置类型: {config_type}")
  1616. if config_type == 'unknown':
  1617. print(f"警告: 无法识别的配置类型")
  1618. messagebox.showwarning("警告", f"无法识别配置类型\n请检查Excel文件的工作表名称或数据格式")
  1619. return
  1620. # 根据配置类型处理数据
  1621. if config_type == 'enemy':
  1622. print("处理敌人配置...")
  1623. if 'enemies' not in current_config:
  1624. current_config['enemies'] = []
  1625. updated_enemies = []
  1626. for i, item in enumerate(items):
  1627. print(f"转换敌人数据 {i+1}/{len(items)}: {item}")
  1628. enemy_data = self._convert_enemy_data(item)
  1629. if enemy_data:
  1630. updated_enemies.append(enemy_data)
  1631. print(f"成功转换敌人数据: {enemy_data['id']}")
  1632. else:
  1633. print(f"跳过无效的敌人数据: {item}")
  1634. print(f"总共转换了 {len(updated_enemies)} 个敌人配置")
  1635. current_config['enemies'] = updated_enemies
  1636. elif config_type == 'weapon':
  1637. print("处理武器配置...")
  1638. if 'weapons' not in current_config:
  1639. current_config['weapons'] = []
  1640. updated_weapons = []
  1641. for i, item in enumerate(items):
  1642. print(f"转换武器数据 {i+1}/{len(items)}: {item}")
  1643. weapon_data = self._convert_weapon_data(item)
  1644. if weapon_data:
  1645. updated_weapons.append(weapon_data)
  1646. print(f"成功转换武器数据: {weapon_data.get('id', 'Unknown')}")
  1647. else:
  1648. print(f"跳过无效的武器数据: {item}")
  1649. print(f"总共转换了 {len(updated_weapons)} 个武器配置")
  1650. current_config['weapons'] = updated_weapons
  1651. elif config_type == 'skill':
  1652. print("处理技能配置...")
  1653. if 'skills' not in current_config:
  1654. current_config['skills'] = []
  1655. updated_skills = []
  1656. for i, item in enumerate(items):
  1657. print(f"转换技能数据 {i+1}/{len(items)}: {item}")
  1658. skill_data = self._convert_skill_data(item)
  1659. if skill_data:
  1660. updated_skills.append(skill_data)
  1661. print(f"成功转换技能数据: {skill_data.get('id', 'Unknown')}")
  1662. else:
  1663. print(f"跳过无效的技能数据: {item}")
  1664. print(f"总共转换了 {len(updated_skills)} 个技能配置")
  1665. current_config['skills'] = updated_skills
  1666. else:
  1667. # 通用配置处理
  1668. print(f"处理通用配置类型: {config_type}")
  1669. # 直接使用items数据
  1670. current_config['items'] = items
  1671. # 写入更新后的配置(关卡配置已在前面处理,跳过)
  1672. if not is_level_config:
  1673. try:
  1674. print(f"写入配置文件: {self.selected_json_path}")
  1675. print(f"配置内容预览: {str(current_config)[:200]}...")
  1676. with open(self.selected_json_path, 'w', encoding='utf-8') as f:
  1677. json.dump(current_config, f, indent=2, ensure_ascii=False)
  1678. print("配置文件写入成功")
  1679. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {len(items)} 个配置项")
  1680. self.status_var.set("配置导入成功")
  1681. except Exception as e:
  1682. print(f"写入配置文件失败: {e}")
  1683. messagebox.showerror("错误", f"写入配置文件失败: {str(e)}")
  1684. return
  1685. # 刷新预览
  1686. self.clear_selection()
  1687. def _import_multi_sheet_config(self):
  1688. """导入多工作表配置(直接使用解析后的数据,不再通过_convert_enemy_data转换)"""
  1689. print(f"多工作表配置导入开始...")
  1690. print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
  1691. try:
  1692. # 读取现有JSON配置
  1693. if self.selected_json_path.exists():
  1694. with open(self.selected_json_path, 'r', encoding='utf-8') as f:
  1695. current_config = json.load(f)
  1696. else:
  1697. current_config = self.default_config.copy()
  1698. # 直接使用解析后的多工作表数据
  1699. if isinstance(self.config_data, list):
  1700. # 如果config_data是列表(如敌人配置),直接使用
  1701. print(f"使用解析后的敌人配置数据,共 {len(self.config_data)} 个敌人")
  1702. current_config['enemies'] = self.config_data
  1703. # 添加元数据
  1704. current_config['metadata'] = {
  1705. 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  1706. 'totalEnemies': len(self.config_data),
  1707. 'conversionMethod': 'excel_multi_sheet_direct'
  1708. }
  1709. elif isinstance(self.config_data, dict):
  1710. # 如果config_data是字典,合并到current_config
  1711. print(f"合并字典格式的配置数据")
  1712. # 特殊处理武器配置,保留现有武器中表格没有的字段
  1713. if 'weapons' in self.config_data and 'weapons' in current_config:
  1714. print("检测到武器配置,执行智能合并以保留现有字段")
  1715. new_weapons = self.config_data['weapons']
  1716. existing_weapons = current_config['weapons']
  1717. # 创建现有武器的ID映射
  1718. existing_weapons_map = {}
  1719. for weapon in existing_weapons:
  1720. weapon_id = weapon.get('ID', '') or weapon.get('id', '')
  1721. if weapon_id:
  1722. existing_weapons_map[weapon_id] = weapon
  1723. # 合并武器配置,保留现有字段
  1724. merged_weapons = []
  1725. for new_weapon_data in new_weapons:
  1726. weapon_id = new_weapon_data.get('ID', '') or new_weapon_data.get('id', '')
  1727. if weapon_id and weapon_id in existing_weapons_map:
  1728. # 找到对应的现有武器,转换新数据时传入现有武器配置
  1729. existing_weapon = existing_weapons_map[weapon_id]
  1730. # 转换新武器数据,传入现有武器配置以保留特效字段
  1731. converted_weapon = self._convert_weapon_data(new_weapon_data, existing_weapon)
  1732. if converted_weapon:
  1733. merged_weapons.append(converted_weapon)
  1734. print(f"✓ 武器 {weapon_id} 已转换并保留了现有的特效字段")
  1735. else:
  1736. # 新武器,直接转换
  1737. converted_weapon = self._convert_weapon_data(new_weapon_data)
  1738. if converted_weapon:
  1739. merged_weapons.append(converted_weapon)
  1740. print(f"+ 新武器 {weapon_id} 已添加")
  1741. # 更新武器配置
  1742. self.config_data['weapons'] = merged_weapons
  1743. print(f"武器配置合并完成,共 {len(merged_weapons)} 个武器")
  1744. current_config.update(self.config_data)
  1745. # 写入更新后的配置
  1746. print(f"写入配置文件: {self.selected_json_path}")
  1747. print(f"配置内容预览: {str(current_config)[:200]}...")
  1748. with open(self.selected_json_path, 'w', encoding='utf-8') as f:
  1749. json.dump(current_config, f, indent=2, ensure_ascii=False)
  1750. print("多工作表配置文件写入成功")
  1751. messagebox.showinfo("成功", f"多工作表配置导入成功!\n更新了配置数据")
  1752. self.status_var.set("多工作表配置导入成功")
  1753. except Exception as e:
  1754. print(f"导入多工作表配置失败: {e}")
  1755. import traceback
  1756. traceback.print_exc()
  1757. messagebox.showerror("错误", f"导入多工作表配置失败: {str(e)}")
  1758. return
  1759. # 刷新预览
  1760. self.clear_selection()
  1761. def _deep_merge_weapon_config(self, existing_weapon, new_weapon):
  1762. """深度合并武器配置,保留现有武器中表格没有的字段"""
  1763. import copy
  1764. # 创建新武器的深拷贝作为基础
  1765. merged = copy.deepcopy(new_weapon)
  1766. # 递归合并函数,将现有武器中新武器没有的字段添加到合并结果中
  1767. def preserve_existing_fields(target, existing):
  1768. for key, value in existing.items():
  1769. if key not in target:
  1770. # 现有武器有但新武器没有的字段,保留
  1771. target[key] = copy.deepcopy(value)
  1772. elif isinstance(target[key], dict) and isinstance(value, dict):
  1773. # 如果两者都是字典,递归处理
  1774. preserve_existing_fields(target[key], value)
  1775. # 执行深度合并,保留现有武器中的额外字段
  1776. preserve_existing_fields(merged, existing_weapon)
  1777. return merged
  1778. def _detect_config_type(self):
  1779. """根据数据内容和工作表名称自动识别配置类型"""
  1780. try:
  1781. # 检查是否是多工作表数据
  1782. if self.multi_sheet_var.get() and hasattr(self, 'config_data') and isinstance(self.config_data, dict):
  1783. # 多工作表模式:根据工作表名称判断
  1784. if '敌人基础配置' in self.config_data or '敌人配置' in str(self.config_data.keys()):
  1785. return 'enemy'
  1786. elif '技能信息表' in self.config_data or '技能配置' in str(self.config_data.keys()):
  1787. return 'skill'
  1788. elif '武器配置' in str(self.config_data.keys()) or '武器信息' in self.config_data:
  1789. return 'weapon'
  1790. elif any('关卡' in str(key) for key in self.config_data.keys()):
  1791. return 'level'
  1792. # 检查数据内容中的字段来判断类型
  1793. if hasattr(self, 'config_data'):
  1794. sample_data = None
  1795. # 获取样本数据
  1796. if isinstance(self.config_data, dict) and 'items' in self.config_data:
  1797. items = self.config_data['items']
  1798. if items and len(items) > 0:
  1799. sample_data = items[0]
  1800. elif isinstance(self.config_data, list) and len(self.config_data) > 0:
  1801. sample_data = self.config_data[0]
  1802. if sample_data and isinstance(sample_data, dict):
  1803. # 根据字段名判断配置类型
  1804. fields = set(sample_data.keys())
  1805. # 敌人配置特征字段
  1806. enemy_fields = {'enemyId', 'name', 'hp', 'attack', 'defense', 'speed'}
  1807. if enemy_fields.intersection(fields):
  1808. return 'enemy'
  1809. # 技能配置特征字段
  1810. skill_fields = {'skillId', 'skillName', 'damage', 'cooldown', 'description'}
  1811. if skill_fields.intersection(fields):
  1812. return 'skill'
  1813. # 武器配置特征字段
  1814. weapon_fields = {'weaponId', 'weaponName', 'damage', 'range', 'fireRate'}
  1815. if weapon_fields.intersection(fields):
  1816. return 'weapon'
  1817. # 关卡配置特征字段
  1818. level_fields = {'levelId', 'name', 'enemies', 'waves'}
  1819. if level_fields.intersection(fields):
  1820. return 'level'
  1821. # 如果无法通过数据内容判断,尝试通过文件名判断(作为后备方案)
  1822. if hasattr(self, 'selected_files') and self.selected_files:
  1823. filename = Path(self.selected_files[0]).name.lower()
  1824. if '敌人' in filename:
  1825. return 'enemy'
  1826. elif '技能' in filename:
  1827. return 'skill'
  1828. elif '武器' in filename:
  1829. return 'weapon'
  1830. elif '关卡' in filename:
  1831. return 'level'
  1832. return 'unknown'
  1833. except Exception as e:
  1834. print(f"配置类型检测出错: {e}")
  1835. return 'unknown'
  1836. def _convert_skill_data(self, item):
  1837. """转换技能数据格式"""
  1838. try:
  1839. # 支持多种字段名格式(中文和英文)
  1840. skill_id = item.get('id', item.get('技能ID', ''))
  1841. skill_name = item.get('name', item.get('技能名称', ''))
  1842. print(f"正在转换技能数据: {skill_id} - {skill_name}")
  1843. # 检查必要字段
  1844. if not skill_id:
  1845. print(f"跳过无效技能数据: 缺少技能ID - {item}")
  1846. return None
  1847. result = {
  1848. 'id': skill_id,
  1849. 'name': skill_name,
  1850. 'description': item.get('description', item.get('技能描述', '')),
  1851. 'iconPath': item.get('iconPath', item.get('图标路径', '')),
  1852. 'maxLevel': item.get('maxLevel', item.get('最大等级', 1)),
  1853. 'currentLevel': item.get('currentLevel', item.get('当前等级', 0)),
  1854. 'priceReduction': item.get('priceReduction', item.get('价格减少', 0.0)),
  1855. 'critChanceIncrease': item.get('critChanceIncrease', item.get('暴击几率增加', 0.0)),
  1856. 'critDamageBonus': item.get('critDamageBonus', item.get('暴击伤害加成', 0.0)),
  1857. 'healthIncrease': item.get('healthIncrease', item.get('生命值增加', 0.0)),
  1858. 'multiShotChance': item.get('multiShotChance', item.get('多重射击几率', 0.0)),
  1859. 'energyGainIncrease': item.get('energyGainIncrease', item.get('能量加成', 0.0)),
  1860. 'ballSpeedIncrease': item.get('ballSpeedIncrease', item.get('速度提升', 0.0))
  1861. }
  1862. print(f"成功转换技能数据: {result}")
  1863. return result
  1864. except Exception as e:
  1865. print(f"转换技能数据失败: {e} - 数据: {item}")
  1866. return None
  1867. def _convert_level_data(self, item):
  1868. """转换关卡数据格式"""
  1869. try:
  1870. # 如果item已经是完整的关卡数据结构(来自多工作表解析),直接返回
  1871. if isinstance(item, dict) and 'waves' in item:
  1872. return item
  1873. # 处理传统的单行数据格式
  1874. # 处理可用武器字符串,转换为数组(支持中英文字段名)
  1875. available_weapons = []
  1876. weapons_field = item.get('weapons', item.get('可用武器', ''))
  1877. if weapons_field:
  1878. weapons_str = str(weapons_field)
  1879. # 支持逗号、顿号、分号等多种分隔符
  1880. for separator in ['、', ',', ',', ';', ';']:
  1881. if separator in weapons_str:
  1882. available_weapons = [weapon.strip() for weapon in weapons_str.split(separator)]
  1883. break
  1884. # 如果没有找到分隔符,则作为单个武器处理
  1885. if not available_weapons:
  1886. available_weapons = [weapons_str.strip()]
  1887. # 获取关卡ID,用于读取现有配置(支持中英文字段名)
  1888. level_id = str(item.get('levelId', item.get('关卡ID', '')))
  1889. # 尝试读取现有的关卡配置文件,保留waves数据
  1890. existing_waves = []
  1891. existing_data = {}
  1892. if level_id:
  1893. try:
  1894. levels_dir = self.project_root / "assets/resources/data/levels"
  1895. level_file = levels_dir / f"{level_id}.json"
  1896. if level_file.exists():
  1897. with open(level_file, 'r', encoding='utf-8') as f:
  1898. existing_data = json.load(f)
  1899. existing_waves = existing_data.get('waves', [])
  1900. print(f"保留现有关卡 {level_id} 的 {len(existing_waves)} 个波次数据")
  1901. except Exception as e:
  1902. print(f"读取现有关卡配置时出错: {e}")
  1903. # 构建新的关卡数据,保留现有的waves(支持中英文字段名)
  1904. level_data = {
  1905. "levelId": level_id,
  1906. "name": str(item.get('name', item.get('关卡名称', existing_data.get('name', '')))),
  1907. "scene": str(item.get('scene', item.get('场景', existing_data.get('scene', '')))),
  1908. "description": str(item.get('description', item.get('描述', existing_data.get('description', '')))),
  1909. "backgroundImage": str(item.get('backgroundImage', item.get('关卡背景图路径', existing_data.get('backgroundImage', 'images/LevelBackground/BG1')))),
  1910. "weapons": available_weapons if available_weapons else existing_data.get('weapons', existing_data.get('availableWeapons', [])),
  1911. "timeLimit": int(item.get('timeLimit', item.get('时间限制', existing_data.get('timeLimit', 300)))),
  1912. "difficulty": str(item.get('difficulty', item.get('难度', existing_data.get('difficulty', 'normal')))),
  1913. "healthMultiplier": float(item.get('healthMultiplier', item.get('生命倍数', existing_data.get('healthMultiplier', 1.0)))),
  1914. "waves": existing_waves # 保留现有的waves数据
  1915. }
  1916. # 添加可选字段(如果存在,支持中英文字段名)
  1917. coin_reward = item.get('coinReward', item.get('钞票奖励'))
  1918. if coin_reward is not None:
  1919. level_data["coinReward"] = int(coin_reward)
  1920. elif 'coinReward' in existing_data:
  1921. level_data["coinReward"] = existing_data['coinReward']
  1922. diamond_reward = item.get('diamondReward', item.get('钻石奖励'))
  1923. if diamond_reward is not None:
  1924. level_data["diamondReward"] = int(diamond_reward)
  1925. elif 'diamondReward' in existing_data:
  1926. level_data["diamondReward"] = existing_data['diamondReward']
  1927. return level_data
  1928. except Exception as e:
  1929. print(f"转换关卡数据时出错: {e}")
  1930. return None
  1931. def restore_default_config(self):
  1932. """恢复默认配置"""
  1933. result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!")
  1934. if not result:
  1935. return
  1936. try:
  1937. if not self.selected_json_path:
  1938. messagebox.showwarning("警告", "请先选择JSON输出路径")
  1939. return
  1940. json_path = Path(self.selected_json_path)
  1941. # 备份当前配置
  1942. if json_path.exists():
  1943. backup_path = json_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  1944. with open(json_path, 'r', encoding='utf-8') as f:
  1945. content = f.read()
  1946. with open(backup_path, 'w', encoding='utf-8') as f:
  1947. f.write(content)
  1948. # 写入默认配置
  1949. with open(json_path, 'w', encoding='utf-8') as f:
  1950. json.dump(self.default_config, f, indent=2, ensure_ascii=False)
  1951. messagebox.showinfo("成功", "已恢复默认配置")
  1952. self.status_var.set("已恢复默认配置")
  1953. # 刷新预览
  1954. self.clear_selection()
  1955. except Exception as e:
  1956. messagebox.showerror("错误", f"恢复默认配置失败: {e}")
  1957. def run(self):
  1958. """运行应用"""
  1959. self.root.mainloop()
  1960. def main():
  1961. """主函数"""
  1962. try:
  1963. app = ConfigManagerGUI()
  1964. app.run()
  1965. except Exception as e:
  1966. print(f"启动应用失败: {e}")
  1967. input("按回车键退出...")
  1968. if __name__ == "__main__":
  1969. main()