config_manager.py 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347
  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. class ConfigManagerGUI:
  31. def __init__(self):
  32. self.root = tk.Tk()
  33. self.root.title("游戏配置管理工具")
  34. self.root.geometry("1000x700")
  35. self.root.resizable(True, True)
  36. # 配置文件路径 - 动态获取项目根目录
  37. # 从当前脚本位置向上查找项目根目录
  38. current_dir = Path(__file__).parent
  39. self.project_root = current_dir.parent.parent.parent.parent # 从excel目录向上4级到项目根目录
  40. self.excel_dir = current_dir
  41. # 配置表映射 - 定义每种表格对应的JSON文件和参数类型
  42. self.config_mappings = {
  43. 'BallController配置表.xlsx': {
  44. 'json_path': self.project_root / "assets/resources/data/ballController.json",
  45. 'param_types': {
  46. 'baseSpeed': float,
  47. 'maxReflectionRandomness': float,
  48. 'antiTrapTimeWindow': float,
  49. 'antiTrapHitThreshold': int,
  50. 'deflectionAttemptThreshold': int,
  51. 'antiTrapDeflectionMultiplier': float,
  52. 'FIRE_COOLDOWN': float,
  53. 'ballRadius': float,
  54. 'gravityScale': float,
  55. 'linearDamping': float,
  56. 'angularDamping': float,
  57. 'colliderGroup': int,
  58. 'colliderTag': int,
  59. 'friction': float,
  60. 'restitution': float,
  61. 'safeDistance': float,
  62. 'edgeOffset': float,
  63. 'sensor': bool
  64. },
  65. 'format_type': 'vertical' # 纵向表格:参数名在第一列,值在第二/三列
  66. },
  67. '敌人配置表.xlsx': {
  68. 'json_path': self.project_root / "assets/resources/data/enemies.json",
  69. 'param_types': {
  70. '敌人ID': str,
  71. '敌人名称': str,
  72. '敌人类型': str,
  73. '稀有度': str,
  74. '权重': int,
  75. '生命值': int,
  76. '移动速度': int,
  77. '攻击力': int,
  78. '攻击范围': int,
  79. '攻击速度': float,
  80. '防御力': int,
  81. '金币奖励': int
  82. },
  83. 'format_type': 'horizontal' # 横向表格:第一行是参数名,下面是数据行
  84. },
  85. '方块武器配置表_更新_v2.xlsx': {
  86. 'json_path': self.project_root / "assets/resources/data/weapons.json",
  87. 'param_types': {
  88. 'ID': str,
  89. '名称': str,
  90. '类型': str,
  91. '稀有度': str,
  92. '权重': int,
  93. '伤害': int,
  94. '射速': float,
  95. '射程': int,
  96. '子弹速度': int
  97. },
  98. 'format_type': 'horizontal'
  99. },
  100. '关卡配置表_完整版_更新_v2.xlsx': {
  101. 'json_path': self.project_root / "assets/resources/data/levels", # 目录路径,包含多个关卡JSON文件
  102. 'param_types': {
  103. '关卡ID': str,
  104. '关卡名称': str,
  105. '场景': str,
  106. '描述': str,
  107. '可用武器': str,
  108. '初始生命': int,
  109. '时间限制': int,
  110. '难度': str,
  111. '生命倍数': float,
  112. '金币奖励': int,
  113. '钻石奖励': int
  114. },
  115. 'format_type': 'horizontal',
  116. 'multi_sheet': True, # 标记为多工作表文件
  117. 'sheet_names': ['关卡基础配置', '波次配置', '敌人配置', '敌人名称映射'] # 需要读取的工作表
  118. },
  119. '技能配置表.xlsx': {
  120. 'json_path': self.project_root / "assets/resources/data/skill.json",
  121. 'param_types': {
  122. '技能ID': str,
  123. '技能名称': str,
  124. '技能描述': str,
  125. '图标路径': str,
  126. '最大等级': int,
  127. '当前等级': int,
  128. '价格减少': float,
  129. '暴击几率增加': float,
  130. '暴击伤害加成': float,
  131. '生命值增加': float,
  132. '多重射击几率': float,
  133. '能量加成': float,
  134. '速度提升': float
  135. },
  136. 'format_type': 'horizontal',
  137. 'sheet_name': '技能信息表' # 指定使用的工作表名称
  138. }
  139. }
  140. # 当前选择的配置映射
  141. self.current_mapping = None
  142. self.json_config_path = None
  143. self.param_types = {}
  144. # 默认配置值
  145. self.default_config = {
  146. 'baseSpeed': 60,
  147. 'maxReflectionRandomness': 0.2,
  148. 'antiTrapTimeWindow': 5.0,
  149. 'antiTrapHitThreshold': 5,
  150. 'deflectionAttemptThreshold': 3,
  151. 'antiTrapDeflectionMultiplier': 3.0,
  152. 'FIRE_COOLDOWN': 0.05,
  153. 'ballRadius': 10,
  154. 'gravityScale': 0,
  155. 'linearDamping': 0,
  156. 'angularDamping': 0,
  157. 'colliderGroup': 2,
  158. 'colliderTag': 1,
  159. 'friction': 0,
  160. 'restitution': 1,
  161. 'safeDistance': 50,
  162. 'edgeOffset': 20,
  163. 'sensor': False
  164. }
  165. self.selected_files = []
  166. self.config_data = {}
  167. self.setup_ui()
  168. self.load_current_config()
  169. def setup_ui(self):
  170. """设置用户界面"""
  171. # 主框架
  172. main_frame = ttk.Frame(self.root, padding="10")
  173. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  174. # 配置根窗口的网格权重
  175. self.root.columnconfigure(0, weight=1)
  176. self.root.rowconfigure(0, weight=1)
  177. main_frame.columnconfigure(1, weight=1)
  178. main_frame.rowconfigure(2, weight=1)
  179. # 标题
  180. title_label = ttk.Label(main_frame, text="游戏配置管理工具",
  181. font=("Arial", 16, "bold"))
  182. title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
  183. # 左侧面板 - 文件选择
  184. file_types_text = "Excel/CSV配置文件选择" if PANDAS_AVAILABLE else "CSV配置文件选择"
  185. left_frame = ttk.LabelFrame(main_frame, text=file_types_text, padding="10")
  186. left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
  187. left_frame.columnconfigure(0, weight=1)
  188. left_frame.rowconfigure(1, weight=1)
  189. # 文件浏览按钮
  190. browse_frame = ttk.Frame(left_frame)
  191. browse_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
  192. browse_frame.columnconfigure(0, weight=1)
  193. browse_text = "浏览Excel/CSV文件" if PANDAS_AVAILABLE else "浏览CSV文件"
  194. ttk.Button(browse_frame, text=browse_text,
  195. command=self.browse_files).grid(row=0, column=0, sticky=(tk.W, tk.E))
  196. ttk.Button(browse_frame, text="扫描项目目录",
  197. command=self.scan_project_files).grid(row=0, column=1, padx=(10, 0))
  198. # 文件列表
  199. list_frame = ttk.Frame(left_frame)
  200. list_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  201. list_frame.columnconfigure(0, weight=1)
  202. list_frame.rowconfigure(0, weight=1)
  203. self.file_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, height=15)
  204. scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
  205. self.file_listbox.configure(yscrollcommand=scrollbar.set)
  206. self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  207. scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
  208. # 文件操作按钮
  209. file_btn_frame = ttk.Frame(left_frame)
  210. file_btn_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  211. ttk.Button(file_btn_frame, text="预览选中文件",
  212. command=self.preview_selected_files).pack(side=tk.LEFT)
  213. ttk.Button(file_btn_frame, text="清空选择",
  214. command=self.clear_selection).pack(side=tk.LEFT, padx=(10, 0))
  215. # 右侧面板 - 配置预览和操作
  216. right_frame = ttk.LabelFrame(main_frame, text="配置预览与操作", padding="10")
  217. right_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
  218. right_frame.columnconfigure(0, weight=1)
  219. right_frame.rowconfigure(0, weight=1)
  220. # 配置预览文本框
  221. self.preview_text = scrolledtext.ScrolledText(right_frame, height=20, width=50)
  222. self.preview_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
  223. # 操作按钮
  224. btn_frame = ttk.Frame(right_frame)
  225. btn_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
  226. ttk.Button(btn_frame, text="导入配置",
  227. command=self.import_config).pack(side=tk.LEFT)
  228. ttk.Button(btn_frame, text="备份当前配置",
  229. command=self.backup_config).pack(side=tk.LEFT, padx=(10, 0))
  230. ttk.Button(btn_frame, text="恢复默认配置",
  231. command=self.restore_default_config).pack(side=tk.LEFT, padx=(10, 0))
  232. # 底部状态栏
  233. self.status_var = tk.StringVar()
  234. self.status_var.set("就绪")
  235. status_bar = ttk.Label(main_frame, textvariable=self.status_var,
  236. relief=tk.SUNKEN, anchor=tk.W)
  237. status_bar.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
  238. # 绑定事件
  239. self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
  240. def browse_files(self):
  241. """浏览Excel/CSV文件"""
  242. if PANDAS_AVAILABLE:
  243. title = "选择Excel/CSV配置文件"
  244. filetypes = [("Excel文件", "*.xlsx *.xls"), ("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  245. else:
  246. title = "选择CSV配置文件"
  247. filetypes = [("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  248. files = filedialog.askopenfilenames(
  249. title=title,
  250. filetypes=filetypes,
  251. initialdir=str(self.excel_dir)
  252. )
  253. if files:
  254. self.file_listbox.delete(0, tk.END)
  255. for file in files:
  256. self.file_listbox.insert(tk.END, file)
  257. self.status_var.set(f"已选择 {len(files)} 个文件")
  258. def scan_project_files(self):
  259. """扫描项目目录中的Excel/CSV文件"""
  260. self.status_var.set("正在扫描项目目录...")
  261. def scan_thread():
  262. config_files = []
  263. # 扫描常见的配置目录
  264. scan_dirs = [
  265. self.project_root / "assets/resources/data",
  266. self.project_root / "assets/resources/config",
  267. self.project_root / "assets/excel",
  268. self.project_root / "config",
  269. self.project_root / "data",
  270. self.project_root
  271. ]
  272. # 根据pandas可用性选择扫描的文件类型
  273. patterns = ['*.xlsx', '*.xls', '*.csv', '*.txt'] if PANDAS_AVAILABLE else ['*.csv', '*.txt']
  274. for scan_dir in scan_dirs:
  275. if scan_dir.exists():
  276. for pattern in patterns:
  277. config_files.extend(scan_dir.rglob(pattern))
  278. # 更新UI
  279. self.root.after(0, self.update_file_list, config_files)
  280. threading.Thread(target=scan_thread, daemon=True).start()
  281. def update_file_list(self, files):
  282. """更新文件列表"""
  283. self.file_listbox.delete(0, tk.END)
  284. for file in files:
  285. self.file_listbox.insert(tk.END, str(file))
  286. file_type_text = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  287. self.status_var.set(f"找到 {len(files)} 个{file_type_text}")
  288. def on_file_select(self, event):
  289. """文件选择事件处理"""
  290. selection = self.file_listbox.curselection()
  291. if selection:
  292. self.selected_files = [self.file_listbox.get(i) for i in selection]
  293. # 自动识别配置映射
  294. self.auto_detect_config_mapping()
  295. self.preview_selected_files()
  296. self.status_var.set(f"已选择 {len(selection)} 个文件")
  297. def auto_detect_config_mapping(self):
  298. """自动检测配置映射"""
  299. if not self.selected_files:
  300. return
  301. # 取第一个选中的文件进行映射检测
  302. selected_file = self.selected_files[0]
  303. filename = Path(selected_file).name
  304. # 检查是否有对应的配置映射
  305. for config_name, mapping in self.config_mappings.items():
  306. if config_name in filename or filename in config_name:
  307. self.current_mapping = mapping
  308. self.json_config_path = mapping['json_path']
  309. self.param_types = mapping['param_types']
  310. print(f"检测到配置映射: {config_name} -> {self.json_config_path}")
  311. return
  312. # 如果没有找到映射,使用默认的BallController映射
  313. self.current_mapping = self.config_mappings['BallController配置表.xlsx']
  314. self.json_config_path = self.current_mapping['json_path']
  315. self.param_types = self.current_mapping['param_types']
  316. print(f"使用默认配置映射: BallController -> {self.json_config_path}")
  317. def preview_selected_files(self):
  318. """预览选中的文件"""
  319. selection = self.file_listbox.curselection()
  320. if not selection:
  321. messagebox.showwarning("警告", "请先选择要预览的文件")
  322. return
  323. self.preview_text.delete(1.0, tk.END)
  324. self.config_data = {}
  325. for i in selection:
  326. file_path = Path(self.file_listbox.get(i))
  327. self.preview_text.insert(tk.END, f"=== {file_path.name} ===\n")
  328. try:
  329. # 根据文件扩展名和pandas可用性选择读取方法
  330. if file_path.suffix.lower() in ['.xlsx', '.xls']:
  331. if PANDAS_AVAILABLE:
  332. file_config = self.read_excel_config(file_path)
  333. else:
  334. self.preview_text.insert(tk.END, "Excel文件需要pandas库支持,请安装: pip install pandas openpyxl\n\n")
  335. continue
  336. elif file_path.suffix.lower() in ['.csv', '.txt']:
  337. file_config = self.read_csv_config(file_path)
  338. else:
  339. self.preview_text.insert(tk.END, "不支持的文件格式\n\n")
  340. continue
  341. if file_config:
  342. # 检查file_config的类型,如果是列表则特殊处理
  343. if isinstance(file_config, list):
  344. # 对于关卡配置等返回列表的情况
  345. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置项:\n")
  346. for i, item in enumerate(file_config):
  347. self.preview_text.insert(tk.END, f" 配置项 {i+1}: {item}\n")
  348. # 将列表数据存储到config_data中
  349. self.config_data[file_path.stem] = file_config
  350. else:
  351. # 原有的字典处理逻辑
  352. self.config_data.update(file_config)
  353. # 显示预览
  354. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置参数:\n")
  355. for key, value in file_config.items():
  356. self.preview_text.insert(tk.END, f" {key}: {value}\n")
  357. else:
  358. self.preview_text.insert(tk.END, "未找到有效的配置数据\n")
  359. except Exception as e:
  360. self.preview_text.insert(tk.END, f"读取失败: {str(e)}\n")
  361. self.preview_text.insert(tk.END, "\n")
  362. # 显示合并后的配置
  363. if self.config_data:
  364. self.preview_text.insert(tk.END, "=== 合并后的配置 ===\n")
  365. self.preview_text.insert(tk.END, json.dumps(self.config_data, indent=2, ensure_ascii=False))
  366. self.status_var.set(f"预览完成,共 {len(self.config_data)} 个配置参数")
  367. def read_excel_config(self, file_path):
  368. """读取Excel配置文件"""
  369. config = {}
  370. if not PANDAS_AVAILABLE:
  371. print("错误: pandas未安装,无法读取Excel文件")
  372. return config
  373. try:
  374. # 检查是否为多工作表文件
  375. if self.current_mapping and self.current_mapping.get('multi_sheet', False):
  376. # 处理多工作表文件
  377. sheet_names = self.current_mapping.get('sheet_names', [])
  378. all_sheets_data = {}
  379. for sheet_name in sheet_names:
  380. try:
  381. df = pd.read_excel(file_path, sheet_name=sheet_name)
  382. all_sheets_data[sheet_name] = df
  383. print(f"成功读取工作表: {sheet_name}, 数据行数: {len(df)}")
  384. except Exception as e:
  385. print(f"读取工作表 {sheet_name} 时出错: {e}")
  386. # 对于关卡配置,需要特殊处理多工作表数据
  387. if '关卡配置表' in file_path.name:
  388. config = self.parse_level_multi_sheet_data(all_sheets_data, file_path.name)
  389. else:
  390. # 其他多工作表文件的处理逻辑
  391. config = self.parse_multi_sheet_data(all_sheets_data, file_path.name)
  392. else:
  393. # 处理单工作表文件
  394. sheet_name = None
  395. if self.current_mapping and 'sheet_name' in self.current_mapping:
  396. sheet_name = self.current_mapping['sheet_name']
  397. # 读取Excel文件
  398. if sheet_name:
  399. df = pd.read_excel(file_path, sheet_name=sheet_name)
  400. else:
  401. df = pd.read_excel(file_path)
  402. config = self.parse_config_data(df, file_path.name)
  403. except Exception as e:
  404. print(f"读取Excel文件 {file_path.name} 时出错: {e}")
  405. return config
  406. def read_csv_config(self, file_path):
  407. """读取CSV配置文件"""
  408. config = {}
  409. try:
  410. # 如果pandas可用,优先使用pandas读取CSV(支持更多格式)
  411. if PANDAS_AVAILABLE:
  412. try:
  413. df = pd.read_csv(file_path)
  414. config = self.parse_config_data(df, file_path.name)
  415. except:
  416. # 如果pandas读取失败,回退到原始CSV读取方法
  417. config = self.read_csv_fallback(file_path)
  418. else:
  419. # 如果pandas不可用,直接使用原始CSV读取方法
  420. config = self.read_csv_fallback(file_path)
  421. except Exception as e:
  422. print(f"读取文件 {file_path.name} 时出错: {e}")
  423. return config
  424. def read_csv_fallback(self, file_path):
  425. """原始CSV读取方法(不依赖pandas)"""
  426. config = {}
  427. try:
  428. with open(file_path, 'r', encoding='utf-8') as f:
  429. reader = csv.reader(f)
  430. for row_num, row in enumerate(reader, 1):
  431. if len(row) < 2:
  432. continue
  433. param_name = row[0].strip()
  434. param_value = row[1].strip()
  435. # 跳过标题行
  436. if param_name in ['参数名', 'parameter', 'name']:
  437. continue
  438. # 检查参数是否有效
  439. if param_name in self.param_types:
  440. try:
  441. param_type = self.param_types[param_name]
  442. if param_type == bool:
  443. config[param_name] = param_value.lower() in ['true', '1', 'yes', 'on']
  444. else:
  445. config[param_name] = param_type(param_value)
  446. except (ValueError, TypeError):
  447. continue
  448. except Exception as e:
  449. print(f"读取CSV文件 {file_path.name} 时出错: {e}")
  450. return config
  451. def parse_level_multi_sheet_data(self, all_sheets_data, filename):
  452. """解析关卡配置的多工作表数据"""
  453. config = []
  454. try:
  455. # 获取各个工作表的数据
  456. basic_config = all_sheets_data.get('关卡基础配置')
  457. wave_config = all_sheets_data.get('波次配置')
  458. enemy_config = all_sheets_data.get('敌人配置')
  459. enemy_mapping = all_sheets_data.get('敌人名称映射')
  460. if basic_config is None:
  461. print("错误: 未找到关卡基础配置工作表")
  462. return config
  463. # 创建敌人名称映射字典
  464. enemy_name_map = {}
  465. if enemy_mapping is not None:
  466. for _, row in enemy_mapping.iterrows():
  467. if pd.notna(row['中文名称']) and pd.notna(row['英文ID']):
  468. enemy_name_map[row['中文名称']] = row['英文ID']
  469. # 处理每个关卡
  470. for _, basic_row in basic_config.iterrows():
  471. if pd.isna(basic_row['关卡ID']):
  472. continue
  473. level_id = str(basic_row['关卡ID'])
  474. level_data = {
  475. 'levelId': level_id,
  476. 'name': str(basic_row['关卡名称']) if pd.notna(basic_row['关卡名称']) else '',
  477. 'scene': str(basic_row['场景']) if pd.notna(basic_row['场景']) else 'grassland',
  478. 'description': str(basic_row['描述']) if pd.notna(basic_row['描述']) else '',
  479. 'availableWeapons': str(basic_row['可用武器']).split(', ') if pd.notna(basic_row['可用武器']) else [],
  480. 'timeLimit': 300, # 默认值
  481. 'difficulty': 'normal', # 默认值
  482. 'healthMultiplier': 1.0, # 默认值
  483. 'waves': []
  484. }
  485. # 获取该关卡的波次配置
  486. if wave_config is not None:
  487. level_waves = wave_config[wave_config['关卡ID'] == level_id]
  488. for _, wave_row in level_waves.iterrows():
  489. wave_id = int(wave_row['波次ID']) if pd.notna(wave_row['波次ID']) else 1
  490. # 获取该波次的敌人配置
  491. wave_enemies = []
  492. if enemy_config is not None:
  493. wave_enemy_data = enemy_config[
  494. (enemy_config['关卡ID'] == level_id) &
  495. (enemy_config['波次ID'] == wave_id)
  496. ]
  497. for _, enemy_row in wave_enemy_data.iterrows():
  498. enemy_type = str(enemy_row['敌人类型']) if pd.notna(enemy_row['敌人类型']) else '普通僵尸'
  499. # 使用映射表转换敌人名称
  500. enemy_id = enemy_name_map.get(enemy_type, enemy_type)
  501. enemy_data = {
  502. 'enemyType': enemy_id,
  503. 'count': int(enemy_row['数量']) if pd.notna(enemy_row['数量']) else 1,
  504. 'spawnInterval': float(enemy_row['生成间隔']) if pd.notna(enemy_row['生成间隔']) else 2.0,
  505. 'spawnDelay': float(enemy_row['生成延迟']) if pd.notna(enemy_row['生成延迟']) else 0.0,
  506. 'characteristics': str(enemy_row['特性']) if pd.notna(enemy_row['特性']) else ''
  507. }
  508. wave_enemies.append(enemy_data)
  509. wave_data = {
  510. 'waveId': wave_id,
  511. 'enemies': wave_enemies
  512. }
  513. level_data['waves'].append(wave_data)
  514. config.append(level_data)
  515. print(f"处理关卡: {level_id}, 波次数: {len(level_data['waves'])}")
  516. print(f"成功解析关卡配置,共 {len(config)} 个关卡")
  517. except Exception as e:
  518. print(f"解析关卡多工作表数据时出错: {e}")
  519. import traceback
  520. traceback.print_exc()
  521. return config
  522. def parse_multi_sheet_data(self, all_sheets_data, filename):
  523. """解析通用多工作表数据"""
  524. # 这里可以添加其他多工作表文件的处理逻辑
  525. # 目前只返回第一个工作表的数据
  526. if all_sheets_data:
  527. first_sheet_name = list(all_sheets_data.keys())[0]
  528. first_sheet_data = all_sheets_data[first_sheet_name]
  529. return self.parse_config_data(first_sheet_data, filename)
  530. return []
  531. def parse_config_data(self, df, filename):
  532. """解析配置数据(支持多种格式,需要pandas)"""
  533. config = {}
  534. if not PANDAS_AVAILABLE or not self.current_mapping:
  535. return config
  536. format_type = self.current_mapping['format_type']
  537. try:
  538. if format_type == 'vertical':
  539. # 纵向表格:参数名在第一列,值在第二/三列
  540. for _, row in df.iterrows():
  541. param_name = str(row.iloc[0]).strip()
  542. # 跳过标题行和无效行
  543. if param_name in ['参数名', 'parameter', 'name', 'nan', '球控制器参数'] or param_name == 'nan':
  544. continue
  545. # 检查参数是否有效
  546. param_types = self.current_mapping.get('param_types', {})
  547. if param_name in param_types:
  548. try:
  549. # 优先使用第3列(默认值),如果不存在则使用第2列
  550. param_value = row.iloc[2] if len(row) > 2 and not pd.isna(row.iloc[2]) else row.iloc[1]
  551. if pd.isna(param_value):
  552. continue
  553. param_type = param_types[param_name]
  554. if param_type == bool:
  555. config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  556. else:
  557. config[param_name] = param_type(param_value)
  558. except (ValueError, TypeError, IndexError):
  559. continue
  560. elif format_type == 'horizontal':
  561. # 横向表格:第一行是参数名(列名),数据从第0行开始
  562. print(f"横向表格解析: 总行数={len(df)}")
  563. print(f"表格列名: {list(df.columns)}")
  564. # 打印前几行数据用于调试
  565. for i in range(min(3, len(df))):
  566. print(f"第{i}行数据: {df.iloc[i].to_dict()}")
  567. # 检查第0行是否为有效数据行
  568. # 如果第0行第一个单元格是描述性文字或空值,则跳过
  569. data_start_row = 0
  570. if len(df) > 0:
  571. first_cell = str(df.iloc[0, 0]).strip().lower()
  572. # 跳过描述行、空行或标题行
  573. if (first_cell in ['唯一标识符', '描述', 'description', 'desc', 'nan', ''] or
  574. first_cell == df.columns[0].lower()):
  575. data_start_row = 1
  576. print(f"跳过第0行(描述行或标题行): {first_cell}")
  577. print(f"数据起始行: {data_start_row}")
  578. # 解析多行数据(如敌人配置、武器配置、关卡配置等)
  579. config_list = []
  580. for i in range(data_start_row, len(df)):
  581. row_config = {}
  582. row_has_data = False
  583. param_types = self.current_mapping.get('param_types', {})
  584. print(f"可用的参数类型: {list(param_types.keys())}")
  585. for col_idx, col_name in enumerate(df.columns):
  586. param_name = str(col_name).strip()
  587. print(f"检查列名: '{param_name}' 是否在参数类型中")
  588. if param_name in param_types:
  589. try:
  590. param_value = df.iloc[i, col_idx]
  591. if pd.isna(param_value) or str(param_value).strip() == '':
  592. print(f"跳过空值: {param_name} = {param_value}")
  593. continue
  594. param_type = param_types[param_name]
  595. if param_type == bool:
  596. row_config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  597. else:
  598. row_config[param_name] = param_type(param_value)
  599. row_has_data = True
  600. print(f"成功转换字段: {param_name} = {param_value} ({param_type})")
  601. except (ValueError, TypeError, IndexError) as e:
  602. print(f"转换字段 {param_name} 时出错: {e}")
  603. continue
  604. else:
  605. print(f"字段 '{param_name}' 不在参数类型定义中,跳过")
  606. if row_config and row_has_data: # 只添加非空且有有效数据的配置
  607. config_list.append(row_config)
  608. print(f"成功解析第{i}行,包含{len(row_config)}个字段")
  609. else:
  610. print(f"跳过第{i}行(无有效数据)")
  611. print(f"总共解析出{len(config_list)}个有效配置项")
  612. # 对于横向表格,返回配置列表
  613. config = {'items': config_list}
  614. except Exception as e:
  615. print(f"解析文件 {filename} 时出错: {e}")
  616. return config
  617. def clear_selection(self):
  618. """清空选择"""
  619. self.file_listbox.selection_clear(0, tk.END)
  620. self.preview_text.delete(1.0, tk.END)
  621. self.config_data = {}
  622. self.status_var.set("已清空选择")
  623. self.load_current_config()
  624. def load_current_config(self):
  625. """加载当前配置"""
  626. try:
  627. if not self.json_config_path:
  628. self.preview_text.insert(tk.END, "请先选择配置文件\n")
  629. return
  630. if self.json_config_path.exists():
  631. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  632. current_config = json.load(f)
  633. self.preview_text.insert(tk.END, f"=== 当前配置 ({self.json_config_path.name}) ===\n")
  634. # 根据配置类型显示不同的预览格式
  635. if self.current_mapping and self.current_mapping['format_type'] == 'horizontal':
  636. # 横向表格配置(如敌人、武器)
  637. if 'enemies' in current_config:
  638. self.preview_text.insert(tk.END, f"敌人配置 ({len(current_config['enemies'])} 个):\n")
  639. for i, enemy in enumerate(current_config['enemies'][:5]): # 只显示前5个
  640. self.preview_text.insert(tk.END, f" {i+1}. {enemy.get('name', enemy.get('id', 'Unknown'))}\n")
  641. if len(current_config['enemies']) > 5:
  642. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['enemies']) - 5} 个\n")
  643. if 'weapons' in current_config:
  644. self.preview_text.insert(tk.END, f"\n武器配置 ({len(current_config['weapons'])} 个):\n")
  645. for i, weapon in enumerate(current_config['weapons'][:5]): # 只显示前5个
  646. self.preview_text.insert(tk.END, f" {i+1}. {weapon.get('name', weapon.get('id', 'Unknown'))}\n")
  647. if len(current_config['weapons']) > 5:
  648. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['weapons']) - 5} 个\n")
  649. else:
  650. # 纵向表格配置(如BallController)
  651. self.preview_text.insert(tk.END, json.dumps(current_config, indent=2, ensure_ascii=False))
  652. file_hint = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  653. self.preview_text.insert(tk.END, f"\n\n请选择{file_hint}进行配置导入...\n")
  654. else:
  655. self.preview_text.insert(tk.END, "配置文件不存在\n")
  656. except Exception as e:
  657. self.preview_text.insert(tk.END, f"加载当前配置失败: {e}\n")
  658. def backup_config(self):
  659. """备份当前配置"""
  660. try:
  661. if not self.json_config_path.exists():
  662. messagebox.showwarning("警告", "配置文件不存在")
  663. return
  664. backup_path = self.json_config_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  665. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  666. content = f.read()
  667. with open(backup_path, 'w', encoding='utf-8') as f:
  668. f.write(content)
  669. messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}")
  670. self.status_var.set(f"配置已备份")
  671. except Exception as e:
  672. messagebox.showerror("错误", f"备份失败: {e}")
  673. def import_config(self):
  674. """导入配置到JSON文件"""
  675. if not self.config_data:
  676. messagebox.showwarning("警告", "没有配置数据可导入")
  677. return
  678. if not self.current_mapping:
  679. messagebox.showwarning("警告", "未检测到有效的配置映射")
  680. return
  681. try:
  682. print(f"开始导入配置...")
  683. print(f"配置数据: {self.config_data}")
  684. print(f"当前映射: {self.current_mapping}")
  685. print(f"选中文件: {self.selected_files}")
  686. format_type = self.current_mapping['format_type']
  687. print(f"格式类型: {format_type}")
  688. if format_type == 'vertical':
  689. # 处理纵向表格(如BallController)
  690. print("开始处理纵向表格配置...")
  691. self._import_vertical_config()
  692. elif format_type == 'horizontal':
  693. # 处理横向表格(如敌人配置、武器配置)
  694. print("开始处理横向表格配置...")
  695. self._import_horizontal_config()
  696. else:
  697. raise ValueError(f"未知的格式类型: {format_type}")
  698. except Exception as e:
  699. import traceback
  700. error_details = traceback.format_exc()
  701. print(f"导入配置失败,详细错误信息:")
  702. print(error_details)
  703. messagebox.showerror("错误", f"导入配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  704. def _import_vertical_config(self):
  705. """导入纵向表格配置"""
  706. # 读取现有JSON配置
  707. if self.json_config_path.exists():
  708. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  709. current_config = json.load(f)
  710. else:
  711. current_config = self.default_config.copy()
  712. # 合并配置
  713. updated_count = 0
  714. for key, value in self.config_data.items():
  715. if key in current_config and current_config[key] != value:
  716. current_config[key] = value
  717. updated_count += 1
  718. elif key not in current_config:
  719. current_config[key] = value
  720. updated_count += 1
  721. # 写入更新后的配置
  722. with open(self.json_config_path, 'w', encoding='utf-8') as f:
  723. json.dump(current_config, f, indent=2, ensure_ascii=False)
  724. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {updated_count} 个参数")
  725. self.status_var.set("配置导入成功")
  726. # 刷新预览
  727. self.clear_selection()
  728. def _import_horizontal_config(self):
  729. """导入横向表格配置"""
  730. print(f"横向表格配置导入开始...")
  731. print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
  732. # 检查是否是关卡配置(目录类型)
  733. is_level_config = str(self.json_config_path).endswith('levels')
  734. # 对于关卡配置,支持多种数据格式
  735. if is_level_config:
  736. if isinstance(self.config_data, list):
  737. # 多工作表数据格式:直接是关卡数据列表
  738. print("检测到关卡配置的多工作表数据格式")
  739. items = None # 不需要items字段
  740. elif 'items' in self.config_data:
  741. # 传统的items数据格式
  742. print("检测到关卡配置的传统items数据格式")
  743. items = self.config_data['items']
  744. elif isinstance(self.config_data, dict) and len(self.config_data) == 1:
  745. # 检查是否是包装在单个键中的配置数据
  746. key = list(self.config_data.keys())[0]
  747. wrapped_data = self.config_data[key]
  748. print(f"检测到包装的关卡配置数据,键名: {key}")
  749. if isinstance(wrapped_data, list):
  750. print("提取到关卡配置的多工作表数据格式")
  751. self.config_data = wrapped_data # 更新配置数据为实际内容
  752. items = None
  753. elif isinstance(wrapped_data, dict) and 'items' in wrapped_data:
  754. print("提取到关卡配置的传统items数据格式")
  755. self.config_data = wrapped_data
  756. items = wrapped_data['items']
  757. else:
  758. print(f"错误: 包装数据格式不正确: {type(wrapped_data)}")
  759. error_msg = f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n\n包装键: {key}\n数据类型: {type(wrapped_data)}"
  760. messagebox.showwarning("警告", error_msg)
  761. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  762. self.preview_text.insert(tk.END, f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n")
  763. self.preview_text.insert(tk.END, f"包装键: {key}\n数据类型: {type(wrapped_data)}\n")
  764. return
  765. else:
  766. print(f"错误: 关卡配置数据格式不正确")
  767. print(f"实际配置数据: {self.config_data}")
  768. print(f"配置数据类型: {type(self.config_data)}")
  769. print(f"配置数据键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}")
  770. error_msg = f"关卡配置数据格式错误:需要多工作表数据或items字段\n\n数据类型: {type(self.config_data)}\n数据内容: {str(self.config_data)[:200]}..."
  771. messagebox.showwarning("警告", error_msg)
  772. # 同时在GUI中显示错误信息
  773. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  774. self.preview_text.insert(tk.END, f"关卡配置数据格式错误:需要多工作表数据或items字段\n")
  775. self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n")
  776. self.preview_text.insert(tk.END, f"数据内容: {str(self.config_data)[:500]}...\n")
  777. return
  778. else:
  779. # 其他配置类型必须有items字段
  780. if 'items' not in self.config_data:
  781. print(f"错误: 配置数据中缺少'items'字段")
  782. print(f"实际配置数据: {self.config_data}")
  783. print(f"配置数据类型: {type(self.config_data)}")
  784. print(f"配置数据键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}")
  785. error_msg = f"横向表格数据格式错误:缺少'items'字段\n\n数据类型: {type(self.config_data)}\n可用键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}"
  786. messagebox.showwarning("警告", error_msg)
  787. # 同时在GUI中显示错误信息
  788. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  789. self.preview_text.insert(tk.END, f"横向表格数据格式错误:缺少'items'字段\n")
  790. self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n")
  791. self.preview_text.insert(tk.END, f"可用键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}\n")
  792. return
  793. items = self.config_data['items']
  794. # 验证数据有效性
  795. if items is not None:
  796. print(f"配置项数量: {len(items) if items else 0}")
  797. if not items:
  798. messagebox.showwarning("警告", "没有有效的配置项")
  799. return
  800. elif isinstance(self.config_data, list):
  801. print(f"关卡配置数量: {len(self.config_data)}")
  802. if not self.config_data:
  803. messagebox.showwarning("警告", "没有有效的关卡配置")
  804. return
  805. # 打印前几个配置项用于调试
  806. if items is not None:
  807. for i, item in enumerate(items[:3]):
  808. print(f"配置项{i}: {item}")
  809. elif isinstance(self.config_data, list):
  810. for i, item in enumerate(self.config_data[:3]):
  811. print(f"关卡配置{i}: {item.get('levelId', 'Unknown')} - {item.get('name', 'Unknown')}")
  812. # 读取现有JSON配置
  813. print(f"JSON配置文件路径: {self.json_config_path}")
  814. # 检查是否是关卡配置(目录类型)
  815. if str(self.json_config_path).endswith('levels'):
  816. print("处理关卡配置目录...")
  817. # 关卡配置是目录,确保目录存在
  818. if not self.json_config_path.exists():
  819. print(f"创建关卡配置目录: {self.json_config_path}")
  820. self.json_config_path.mkdir(parents=True, exist_ok=True)
  821. current_config = {}
  822. else:
  823. # 普通JSON文件配置
  824. if self.json_config_path.exists():
  825. print("读取现有JSON配置文件...")
  826. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  827. current_config = json.load(f)
  828. else:
  829. print("创建新的JSON配置...")
  830. current_config = {}
  831. # 根据不同的配置类型处理
  832. if not self.selected_files:
  833. print("错误: 没有选中的文件")
  834. messagebox.showerror("错误", "没有选中的文件")
  835. return
  836. filename = Path(self.selected_files[0]).name
  837. print(f"处理文件: {filename}")
  838. if '敌人配置' in filename:
  839. print("处理敌人配置...")
  840. # 敌人配置:更新enemies数组
  841. if 'enemies' not in current_config:
  842. current_config['enemies'] = []
  843. # 将Excel数据转换为JSON格式
  844. updated_enemies = []
  845. for i, item in enumerate(items):
  846. print(f"转换敌人数据 {i+1}/{len(items)}: {item}")
  847. enemy_data = self._convert_enemy_data(item)
  848. if enemy_data:
  849. updated_enemies.append(enemy_data)
  850. print(f"成功转换敌人数据: {enemy_data['id']}")
  851. else:
  852. print(f"跳过无效的敌人数据: {item}")
  853. print(f"总共转换了 {len(updated_enemies)} 个敌人配置")
  854. current_config['enemies'] = updated_enemies
  855. elif '武器配置' in filename:
  856. print("处理武器配置...")
  857. # 武器配置:更新weapons数组
  858. if 'weapons' not in current_config:
  859. current_config['weapons'] = []
  860. # 将Excel数据转换为JSON格式
  861. updated_weapons = []
  862. for i, item in enumerate(items):
  863. print(f"转换武器数据 {i+1}/{len(items)}: {item}")
  864. weapon_data = self._convert_weapon_data(item)
  865. if weapon_data:
  866. updated_weapons.append(weapon_data)
  867. print(f"成功转换武器数据: {weapon_data.get('id', 'Unknown')}")
  868. else:
  869. print(f"跳过无效的武器数据: {item}")
  870. print(f"总共转换了 {len(updated_weapons)} 个武器配置")
  871. current_config['weapons'] = updated_weapons
  872. elif '技能配置' in filename:
  873. print("处理技能配置...")
  874. # 技能配置:更新skills数组
  875. if 'skills' not in current_config:
  876. current_config['skills'] = []
  877. # 将Excel数据转换为JSON格式
  878. updated_skills = []
  879. for i, item in enumerate(items):
  880. print(f"转换技能数据 {i+1}/{len(items)}: {item}")
  881. skill_data = self._convert_skill_data(item)
  882. if skill_data:
  883. updated_skills.append(skill_data)
  884. print(f"成功转换技能数据: {skill_data.get('id', 'Unknown')}")
  885. else:
  886. print(f"跳过无效的技能数据: {item}")
  887. print(f"总共转换了 {len(updated_skills)} 个技能配置")
  888. current_config['skills'] = updated_skills
  889. elif '关卡配置' in filename:
  890. print("处理关卡配置...")
  891. # 关卡配置:为每个关卡创建单独的JSON文件
  892. levels_dir = self.json_config_path
  893. print(f"关卡配置目录: {levels_dir}")
  894. try:
  895. # 检查并创建目录
  896. if not levels_dir.exists():
  897. print(f"创建关卡配置目录: {levels_dir}")
  898. levels_dir.mkdir(parents=True, exist_ok=True)
  899. else:
  900. print(f"关卡配置目录已存在: {levels_dir}")
  901. # 测试目录写入权限
  902. test_file = levels_dir / "test_permission.tmp"
  903. try:
  904. with open(test_file, 'w', encoding='utf-8') as f:
  905. f.write("test")
  906. test_file.unlink() # 删除测试文件
  907. except PermissionError:
  908. messagebox.showerror("错误", f"没有写入权限到目录: {levels_dir}\n请检查目录权限或以管理员身份运行")
  909. return
  910. # 检查数据格式:多工作表数据 vs 传统items数据
  911. if isinstance(self.config_data, list):
  912. # 新的多工作表数据格式:直接是关卡数据列表
  913. print("使用多工作表数据格式")
  914. level_configs = self.config_data
  915. elif 'items' in self.config_data:
  916. # 传统的items数据格式:需要转换
  917. print("使用传统items数据格式")
  918. level_configs = []
  919. for item in items:
  920. level_data = self._convert_level_data(item)
  921. if level_data:
  922. level_configs.append(level_data)
  923. else:
  924. print(f"错误: 未知的关卡配置数据格式: {type(self.config_data)}")
  925. messagebox.showerror("错误", "关卡配置数据格式错误")
  926. return
  927. # 保存关卡配置文件
  928. updated_count = 0
  929. failed_count = 0
  930. for level_data in level_configs:
  931. try:
  932. if level_data and 'levelId' in level_data and level_data['levelId']:
  933. level_id = level_data['levelId']
  934. level_file = levels_dir / f"{level_id}.json"
  935. print(f"保存关卡配置: {level_file}")
  936. with open(level_file, 'w', encoding='utf-8') as f:
  937. json.dump(level_data, f, indent=2, ensure_ascii=False)
  938. updated_count += 1
  939. else:
  940. print(f"跳过无效的关卡数据: {level_data}")
  941. failed_count += 1
  942. except Exception as e:
  943. print(f"保存关卡配置时出错: {e}")
  944. failed_count += 1
  945. continue
  946. if updated_count > 0:
  947. message = f"关卡配置导入成功!\n更新了 {updated_count} 个关卡文件"
  948. if failed_count > 0:
  949. message += f"\n跳过了 {failed_count} 个无效配置"
  950. messagebox.showinfo("成功", message)
  951. self.status_var.set("关卡配置导入成功")
  952. else:
  953. messagebox.showerror("错误", "没有成功导入任何关卡配置\n请检查Excel文件格式和数据")
  954. self.status_var.set("关卡配置导入失败")
  955. except Exception as e:
  956. error_msg = f"关卡配置导入失败: {str(e)}"
  957. print(error_msg)
  958. messagebox.showerror("错误", error_msg)
  959. self.status_var.set("关卡配置导入失败")
  960. self.clear_selection()
  961. return
  962. else:
  963. # 未知配置类型
  964. print(f"警告: 未知的配置文件类型: {filename}")
  965. messagebox.showwarning("警告", f"未知的配置文件类型: {filename}\n支持的类型: 敌人配置、武器配置、技能配置、关卡配置")
  966. return
  967. # 写入更新后的配置(关卡配置已在前面处理,跳过)
  968. if not str(self.json_config_path).endswith('levels'):
  969. try:
  970. print(f"写入配置文件: {self.json_config_path}")
  971. print(f"配置内容预览: {str(current_config)[:200]}...")
  972. with open(self.json_config_path, 'w', encoding='utf-8') as f:
  973. json.dump(current_config, f, indent=2, ensure_ascii=False)
  974. print("配置文件写入成功")
  975. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {len(items)} 个配置项")
  976. self.status_var.set("配置导入成功")
  977. except Exception as e:
  978. print(f"写入配置文件失败: {e}")
  979. messagebox.showerror("错误", f"写入配置文件失败: {str(e)}")
  980. return
  981. # 刷新预览
  982. self.clear_selection()
  983. def _convert_enemy_data(self, item):
  984. """转换敌人数据格式"""
  985. try:
  986. print(f"开始转换敌人数据: {item}")
  987. enemy_id = item.get('敌人ID', '')
  988. enemy_name = item.get('敌人名称', '')
  989. print(f"敌人ID: {enemy_id}, 敌人名称: {enemy_name}")
  990. # 检查必要字段
  991. if not enemy_id:
  992. print(f"跳过无效敌人数据: 缺少敌人ID - {item}")
  993. return None
  994. result = {
  995. 'id': enemy_id,
  996. 'name': item.get('敌人名称', ''),
  997. 'type': item.get('敌人类型', ''),
  998. 'rarity': item.get('稀有度', ''),
  999. 'weight': item.get('权重', 1),
  1000. 'health': item.get('生命值', 100),
  1001. 'speed': item.get('移动速度', 50),
  1002. 'attack': item.get('攻击力', 10),
  1003. 'range': item.get('攻击范围', 100),
  1004. 'attackSpeed': item.get('攻击速度', 1.0),
  1005. 'defense': item.get('防御力', 0),
  1006. 'goldReward': item.get('金币奖励', 10)
  1007. }
  1008. print(f"成功转换敌人数据: {result}")
  1009. return result
  1010. except Exception as e:
  1011. print(f"转换敌人数据失败: {e} - 数据: {item}")
  1012. return None
  1013. def _convert_weapon_data(self, item):
  1014. """转换武器数据格式"""
  1015. try:
  1016. print(f"开始转换武器数据: {item}")
  1017. weapon_id = item.get('ID', '')
  1018. weapon_name = item.get('名称', '')
  1019. print(f"武器ID: {weapon_id}, 武器名称: {weapon_name}")
  1020. result = {
  1021. 'id': weapon_id,
  1022. 'name': weapon_name,
  1023. 'type': item.get('类型', ''),
  1024. 'rarity': item.get('稀有度', ''),
  1025. 'weight': item.get('权重', 1),
  1026. 'damage': item.get('伤害', 10),
  1027. 'fireRate': item.get('射速', 1.0),
  1028. 'range': item.get('射程', 100),
  1029. 'bulletSpeed': item.get('子弹速度', 100)
  1030. }
  1031. print(f"成功转换武器数据: {result}")
  1032. return result
  1033. except Exception as e:
  1034. print(f"转换武器数据失败: {e} - 数据: {item}")
  1035. return None
  1036. def _convert_skill_data(self, item):
  1037. """转换技能数据格式"""
  1038. try:
  1039. skill_id = item.get('技能ID', '')
  1040. print(f"正在转换技能数据: {skill_id} - {item.get('技能名称', '')}")
  1041. # 检查必要字段
  1042. if not skill_id:
  1043. print(f"跳过无效技能数据: 缺少技能ID - {item}")
  1044. return None
  1045. result = {
  1046. 'id': skill_id,
  1047. 'name': item.get('技能名称', ''),
  1048. 'description': item.get('技能描述', ''),
  1049. 'iconPath': item.get('图标路径', ''),
  1050. 'maxLevel': item.get('最大等级', 1),
  1051. 'currentLevel': item.get('当前等级', 0),
  1052. 'priceReduction': item.get('价格减少', 0.0),
  1053. 'critChanceIncrease': item.get('暴击几率增加', 0.0),
  1054. 'critDamageBonus': item.get('暴击伤害加成', 0.0),
  1055. 'healthIncrease': item.get('生命值增加', 0.0),
  1056. 'multiShotChance': item.get('多重射击几率', 0.0),
  1057. 'energyGainIncrease': item.get('能量加成', 0.0),
  1058. 'ballSpeedIncrease': item.get('速度提升', 0.0)
  1059. }
  1060. print(f"成功转换技能数据: {result}")
  1061. return result
  1062. except Exception as e:
  1063. print(f"转换技能数据失败: {e} - 数据: {item}")
  1064. return None
  1065. def _convert_level_data(self, item):
  1066. """转换关卡数据格式"""
  1067. try:
  1068. # 如果item已经是完整的关卡数据结构(来自多工作表解析),直接返回
  1069. if isinstance(item, dict) and 'waves' in item:
  1070. return item
  1071. # 处理传统的单行数据格式
  1072. # 处理可用武器字符串,转换为数组
  1073. available_weapons = []
  1074. if '可用武器' in item and item['可用武器']:
  1075. weapons_str = str(item['可用武器'])
  1076. available_weapons = [weapon.strip() for weapon in weapons_str.split(',')]
  1077. # 获取关卡ID,用于读取现有配置
  1078. level_id = str(item.get('关卡ID', ''))
  1079. # 尝试读取现有的关卡配置文件,保留waves数据
  1080. existing_waves = []
  1081. existing_data = {}
  1082. if level_id:
  1083. try:
  1084. levels_dir = self.project_root / "assets/resources/data/levels"
  1085. level_file = levels_dir / f"{level_id}.json"
  1086. if level_file.exists():
  1087. with open(level_file, 'r', encoding='utf-8') as f:
  1088. existing_data = json.load(f)
  1089. existing_waves = existing_data.get('waves', [])
  1090. print(f"保留现有关卡 {level_id} 的 {len(existing_waves)} 个波次数据")
  1091. except Exception as e:
  1092. print(f"读取现有关卡配置时出错: {e}")
  1093. # 构建新的关卡数据,保留现有的waves
  1094. level_data = {
  1095. "levelId": level_id,
  1096. "name": str(item.get('关卡名称', existing_data.get('name', ''))),
  1097. "scene": str(item.get('场景', existing_data.get('scene', ''))),
  1098. "description": str(item.get('描述', existing_data.get('description', ''))),
  1099. "weapons": available_weapons if available_weapons else existing_data.get('weapons', existing_data.get('availableWeapons', [])),
  1100. "timeLimit": int(item.get('时间限制', existing_data.get('timeLimit', 300))),
  1101. "difficulty": str(item.get('难度', existing_data.get('difficulty', 'normal'))),
  1102. "healthMultiplier": float(item.get('生命倍数', existing_data.get('healthMultiplier', 1.0))),
  1103. "waves": existing_waves # 保留现有的waves数据
  1104. }
  1105. # 添加可选字段(如果存在)
  1106. if '金币奖励' in item:
  1107. level_data["coinReward"] = int(item.get('金币奖励', 0))
  1108. elif 'coinReward' in existing_data:
  1109. level_data["coinReward"] = existing_data['coinReward']
  1110. if '钻石奖励' in item:
  1111. level_data["diamondReward"] = int(item.get('钻石奖励', 0))
  1112. elif 'diamondReward' in existing_data:
  1113. level_data["diamondReward"] = existing_data['diamondReward']
  1114. return level_data
  1115. except Exception as e:
  1116. print(f"转换关卡数据时出错: {e}")
  1117. return None
  1118. def restore_default_config(self):
  1119. """恢复默认配置"""
  1120. result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!")
  1121. if not result:
  1122. return
  1123. try:
  1124. # 备份当前配置
  1125. if self.json_config_path.exists():
  1126. backup_path = self.json_config_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  1127. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  1128. content = f.read()
  1129. with open(backup_path, 'w', encoding='utf-8') as f:
  1130. f.write(content)
  1131. # 写入默认配置
  1132. with open(self.json_config_path, 'w', encoding='utf-8') as f:
  1133. json.dump(self.default_config, f, indent=2, ensure_ascii=False)
  1134. messagebox.showinfo("成功", "已恢复默认配置")
  1135. self.status_var.set("已恢复默认配置")
  1136. # 刷新预览
  1137. self.clear_selection()
  1138. except Exception as e:
  1139. messagebox.showerror("错误", f"恢复默认配置失败: {e}")
  1140. def run(self):
  1141. """运行应用"""
  1142. self.root.mainloop()
  1143. def main():
  1144. """主函数"""
  1145. try:
  1146. app = ConfigManagerGUI()
  1147. app.run()
  1148. except Exception as e:
  1149. print(f"启动应用失败: {e}")
  1150. input("按回车键退出...")
  1151. if __name__ == "__main__":
  1152. main()