config_manager.py 95 KB

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