config_manager.py 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095
  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. },
  117. '技能配置表.xlsx': {
  118. 'json_path': self.project_root / "assets/resources/data/skill.json",
  119. 'param_types': {
  120. '技能ID': str,
  121. '技能名称': str,
  122. '技能描述': str,
  123. '图标路径': str,
  124. '最大等级': int,
  125. '当前等级': int,
  126. '价格减少': float,
  127. '暴击几率增加': float,
  128. '暴击伤害加成': float,
  129. '生命值增加': float,
  130. '多重射击几率': float,
  131. '能量加成': float,
  132. '速度提升': float
  133. },
  134. 'format_type': 'horizontal',
  135. 'sheet_name': '技能信息表' # 指定使用的工作表名称
  136. }
  137. }
  138. # 当前选择的配置映射
  139. self.current_mapping = None
  140. self.json_config_path = None
  141. self.param_types = {}
  142. # 默认配置值
  143. self.default_config = {
  144. 'baseSpeed': 60,
  145. 'maxReflectionRandomness': 0.2,
  146. 'antiTrapTimeWindow': 5.0,
  147. 'antiTrapHitThreshold': 5,
  148. 'deflectionAttemptThreshold': 3,
  149. 'antiTrapDeflectionMultiplier': 3.0,
  150. 'FIRE_COOLDOWN': 0.05,
  151. 'ballRadius': 10,
  152. 'gravityScale': 0,
  153. 'linearDamping': 0,
  154. 'angularDamping': 0,
  155. 'colliderGroup': 2,
  156. 'colliderTag': 1,
  157. 'friction': 0,
  158. 'restitution': 1,
  159. 'safeDistance': 50,
  160. 'edgeOffset': 20,
  161. 'sensor': False
  162. }
  163. self.selected_files = []
  164. self.config_data = {}
  165. self.setup_ui()
  166. self.load_current_config()
  167. def setup_ui(self):
  168. """设置用户界面"""
  169. # 主框架
  170. main_frame = ttk.Frame(self.root, padding="10")
  171. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  172. # 配置根窗口的网格权重
  173. self.root.columnconfigure(0, weight=1)
  174. self.root.rowconfigure(0, weight=1)
  175. main_frame.columnconfigure(1, weight=1)
  176. main_frame.rowconfigure(2, weight=1)
  177. # 标题
  178. title_label = ttk.Label(main_frame, text="游戏配置管理工具",
  179. font=("Arial", 16, "bold"))
  180. title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
  181. # 左侧面板 - 文件选择
  182. file_types_text = "Excel/CSV配置文件选择" if PANDAS_AVAILABLE else "CSV配置文件选择"
  183. left_frame = ttk.LabelFrame(main_frame, text=file_types_text, padding="10")
  184. left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
  185. left_frame.columnconfigure(0, weight=1)
  186. left_frame.rowconfigure(1, weight=1)
  187. # 文件浏览按钮
  188. browse_frame = ttk.Frame(left_frame)
  189. browse_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
  190. browse_frame.columnconfigure(0, weight=1)
  191. browse_text = "浏览Excel/CSV文件" if PANDAS_AVAILABLE else "浏览CSV文件"
  192. ttk.Button(browse_frame, text=browse_text,
  193. command=self.browse_files).grid(row=0, column=0, sticky=(tk.W, tk.E))
  194. ttk.Button(browse_frame, text="扫描项目目录",
  195. command=self.scan_project_files).grid(row=0, column=1, padx=(10, 0))
  196. # 文件列表
  197. list_frame = ttk.Frame(left_frame)
  198. list_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  199. list_frame.columnconfigure(0, weight=1)
  200. list_frame.rowconfigure(0, weight=1)
  201. self.file_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, height=15)
  202. scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
  203. self.file_listbox.configure(yscrollcommand=scrollbar.set)
  204. self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  205. scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
  206. # 文件操作按钮
  207. file_btn_frame = ttk.Frame(left_frame)
  208. file_btn_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  209. ttk.Button(file_btn_frame, text="预览选中文件",
  210. command=self.preview_selected_files).pack(side=tk.LEFT)
  211. ttk.Button(file_btn_frame, text="清空选择",
  212. command=self.clear_selection).pack(side=tk.LEFT, padx=(10, 0))
  213. # 右侧面板 - 配置预览和操作
  214. right_frame = ttk.LabelFrame(main_frame, text="配置预览与操作", padding="10")
  215. right_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
  216. right_frame.columnconfigure(0, weight=1)
  217. right_frame.rowconfigure(0, weight=1)
  218. # 配置预览文本框
  219. self.preview_text = scrolledtext.ScrolledText(right_frame, height=20, width=50)
  220. self.preview_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
  221. # 操作按钮
  222. btn_frame = ttk.Frame(right_frame)
  223. btn_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
  224. ttk.Button(btn_frame, text="导入配置",
  225. command=self.import_config).pack(side=tk.LEFT)
  226. ttk.Button(btn_frame, text="备份当前配置",
  227. command=self.backup_config).pack(side=tk.LEFT, padx=(10, 0))
  228. ttk.Button(btn_frame, text="恢复默认配置",
  229. command=self.restore_default_config).pack(side=tk.LEFT, padx=(10, 0))
  230. # 底部状态栏
  231. self.status_var = tk.StringVar()
  232. self.status_var.set("就绪")
  233. status_bar = ttk.Label(main_frame, textvariable=self.status_var,
  234. relief=tk.SUNKEN, anchor=tk.W)
  235. status_bar.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
  236. # 绑定事件
  237. self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
  238. def browse_files(self):
  239. """浏览Excel/CSV文件"""
  240. if PANDAS_AVAILABLE:
  241. title = "选择Excel/CSV配置文件"
  242. filetypes = [("Excel文件", "*.xlsx *.xls"), ("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  243. else:
  244. title = "选择CSV配置文件"
  245. filetypes = [("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  246. files = filedialog.askopenfilenames(
  247. title=title,
  248. filetypes=filetypes,
  249. initialdir=str(self.excel_dir)
  250. )
  251. if files:
  252. self.file_listbox.delete(0, tk.END)
  253. for file in files:
  254. self.file_listbox.insert(tk.END, file)
  255. self.status_var.set(f"已选择 {len(files)} 个文件")
  256. def scan_project_files(self):
  257. """扫描项目目录中的Excel/CSV文件"""
  258. self.status_var.set("正在扫描项目目录...")
  259. def scan_thread():
  260. config_files = []
  261. # 扫描常见的配置目录
  262. scan_dirs = [
  263. self.project_root / "assets/resources/data",
  264. self.project_root / "assets/resources/config",
  265. self.project_root / "assets/excel",
  266. self.project_root / "config",
  267. self.project_root / "data",
  268. self.project_root
  269. ]
  270. # 根据pandas可用性选择扫描的文件类型
  271. patterns = ['*.xlsx', '*.xls', '*.csv', '*.txt'] if PANDAS_AVAILABLE else ['*.csv', '*.txt']
  272. for scan_dir in scan_dirs:
  273. if scan_dir.exists():
  274. for pattern in patterns:
  275. config_files.extend(scan_dir.rglob(pattern))
  276. # 更新UI
  277. self.root.after(0, self.update_file_list, config_files)
  278. threading.Thread(target=scan_thread, daemon=True).start()
  279. def update_file_list(self, files):
  280. """更新文件列表"""
  281. self.file_listbox.delete(0, tk.END)
  282. for file in files:
  283. self.file_listbox.insert(tk.END, str(file))
  284. file_type_text = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  285. self.status_var.set(f"找到 {len(files)} 个{file_type_text}")
  286. def on_file_select(self, event):
  287. """文件选择事件处理"""
  288. selection = self.file_listbox.curselection()
  289. if selection:
  290. self.selected_files = [self.file_listbox.get(i) for i in selection]
  291. # 自动识别配置映射
  292. self.auto_detect_config_mapping()
  293. self.preview_selected_files()
  294. self.status_var.set(f"已选择 {len(selection)} 个文件")
  295. def auto_detect_config_mapping(self):
  296. """自动检测配置映射"""
  297. if not self.selected_files:
  298. return
  299. # 取第一个选中的文件进行映射检测
  300. selected_file = self.selected_files[0]
  301. filename = Path(selected_file).name
  302. # 检查是否有对应的配置映射
  303. for config_name, mapping in self.config_mappings.items():
  304. if config_name in filename or filename in config_name:
  305. self.current_mapping = mapping
  306. self.json_config_path = mapping['json_path']
  307. self.param_types = mapping['param_types']
  308. print(f"检测到配置映射: {config_name} -> {self.json_config_path}")
  309. return
  310. # 如果没有找到映射,使用默认的BallController映射
  311. self.current_mapping = self.config_mappings['BallController配置表.xlsx']
  312. self.json_config_path = self.current_mapping['json_path']
  313. self.param_types = self.current_mapping['param_types']
  314. print(f"使用默认配置映射: BallController -> {self.json_config_path}")
  315. def preview_selected_files(self):
  316. """预览选中的文件"""
  317. selection = self.file_listbox.curselection()
  318. if not selection:
  319. messagebox.showwarning("警告", "请先选择要预览的文件")
  320. return
  321. self.preview_text.delete(1.0, tk.END)
  322. self.config_data = {}
  323. for i in selection:
  324. file_path = Path(self.file_listbox.get(i))
  325. self.preview_text.insert(tk.END, f"=== {file_path.name} ===\n")
  326. try:
  327. # 根据文件扩展名和pandas可用性选择读取方法
  328. if file_path.suffix.lower() in ['.xlsx', '.xls']:
  329. if PANDAS_AVAILABLE:
  330. file_config = self.read_excel_config(file_path)
  331. else:
  332. self.preview_text.insert(tk.END, "Excel文件需要pandas库支持,请安装: pip install pandas openpyxl\n\n")
  333. continue
  334. elif file_path.suffix.lower() in ['.csv', '.txt']:
  335. file_config = self.read_csv_config(file_path)
  336. else:
  337. self.preview_text.insert(tk.END, "不支持的文件格式\n\n")
  338. continue
  339. if file_config:
  340. self.config_data.update(file_config)
  341. # 显示预览
  342. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置参数:\n")
  343. for key, value in file_config.items():
  344. self.preview_text.insert(tk.END, f" {key}: {value}\n")
  345. else:
  346. self.preview_text.insert(tk.END, "未找到有效的配置数据\n")
  347. except Exception as e:
  348. self.preview_text.insert(tk.END, f"读取失败: {str(e)}\n")
  349. self.preview_text.insert(tk.END, "\n")
  350. # 显示合并后的配置
  351. if self.config_data:
  352. self.preview_text.insert(tk.END, "=== 合并后的配置 ===\n")
  353. self.preview_text.insert(tk.END, json.dumps(self.config_data, indent=2, ensure_ascii=False))
  354. self.status_var.set(f"预览完成,共 {len(self.config_data)} 个配置参数")
  355. def read_excel_config(self, file_path):
  356. """读取Excel配置文件"""
  357. config = {}
  358. if not PANDAS_AVAILABLE:
  359. print("错误: pandas未安装,无法读取Excel文件")
  360. return config
  361. try:
  362. # 检查是否有指定的工作表名称
  363. sheet_name = None
  364. if self.current_mapping and 'sheet_name' in self.current_mapping:
  365. sheet_name = self.current_mapping['sheet_name']
  366. # 读取Excel文件
  367. if sheet_name:
  368. df = pd.read_excel(file_path, sheet_name=sheet_name)
  369. else:
  370. df = pd.read_excel(file_path)
  371. config = self.parse_config_data(df, file_path.name)
  372. except Exception as e:
  373. print(f"读取Excel文件 {file_path.name} 时出错: {e}")
  374. return config
  375. def read_csv_config(self, file_path):
  376. """读取CSV配置文件"""
  377. config = {}
  378. try:
  379. # 如果pandas可用,优先使用pandas读取CSV(支持更多格式)
  380. if PANDAS_AVAILABLE:
  381. try:
  382. df = pd.read_csv(file_path)
  383. config = self.parse_config_data(df, file_path.name)
  384. except:
  385. # 如果pandas读取失败,回退到原始CSV读取方法
  386. config = self.read_csv_fallback(file_path)
  387. else:
  388. # 如果pandas不可用,直接使用原始CSV读取方法
  389. config = self.read_csv_fallback(file_path)
  390. except Exception as e:
  391. print(f"读取文件 {file_path.name} 时出错: {e}")
  392. return config
  393. def read_csv_fallback(self, file_path):
  394. """原始CSV读取方法(不依赖pandas)"""
  395. config = {}
  396. try:
  397. with open(file_path, 'r', encoding='utf-8') as f:
  398. reader = csv.reader(f)
  399. for row_num, row in enumerate(reader, 1):
  400. if len(row) < 2:
  401. continue
  402. param_name = row[0].strip()
  403. param_value = row[1].strip()
  404. # 跳过标题行
  405. if param_name in ['参数名', 'parameter', 'name']:
  406. continue
  407. # 检查参数是否有效
  408. if param_name in self.param_types:
  409. try:
  410. param_type = self.param_types[param_name]
  411. if param_type == bool:
  412. config[param_name] = param_value.lower() in ['true', '1', 'yes', 'on']
  413. else:
  414. config[param_name] = param_type(param_value)
  415. except (ValueError, TypeError):
  416. continue
  417. except Exception as e:
  418. print(f"读取CSV文件 {file_path.name} 时出错: {e}")
  419. return config
  420. def parse_config_data(self, df, filename):
  421. """解析配置数据(支持多种格式,需要pandas)"""
  422. config = {}
  423. if not PANDAS_AVAILABLE or not self.current_mapping:
  424. return config
  425. format_type = self.current_mapping['format_type']
  426. try:
  427. if format_type == 'vertical':
  428. # 纵向表格:参数名在第一列,值在第二/三列
  429. for _, row in df.iterrows():
  430. param_name = str(row.iloc[0]).strip()
  431. # 跳过标题行和无效行
  432. if param_name in ['参数名', 'parameter', 'name', 'nan', '球控制器参数'] or param_name == 'nan':
  433. continue
  434. # 检查参数是否有效
  435. param_types = self.current_mapping.get('param_types', {})
  436. if param_name in param_types:
  437. try:
  438. # 优先使用第3列(默认值),如果不存在则使用第2列
  439. param_value = row.iloc[2] if len(row) > 2 and not pd.isna(row.iloc[2]) else row.iloc[1]
  440. if pd.isna(param_value):
  441. continue
  442. param_type = param_types[param_name]
  443. if param_type == bool:
  444. config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  445. else:
  446. config[param_name] = param_type(param_value)
  447. except (ValueError, TypeError, IndexError):
  448. continue
  449. elif format_type == 'horizontal':
  450. # 横向表格:第一行是参数名(列名),数据从第0行开始
  451. print(f"横向表格解析: 总行数={len(df)}")
  452. print(f"表格列名: {list(df.columns)}")
  453. # 打印前几行数据用于调试
  454. for i in range(min(3, len(df))):
  455. print(f"第{i}行数据: {df.iloc[i].to_dict()}")
  456. # 检查第0行是否为有效数据行
  457. # 如果第0行第一个单元格是描述性文字或空值,则跳过
  458. data_start_row = 0
  459. if len(df) > 0:
  460. first_cell = str(df.iloc[0, 0]).strip().lower()
  461. # 跳过描述行、空行或标题行
  462. if (first_cell in ['唯一标识符', '描述', 'description', 'desc', 'nan', ''] or
  463. first_cell == df.columns[0].lower()):
  464. data_start_row = 1
  465. print(f"跳过第0行(描述行或标题行): {first_cell}")
  466. print(f"数据起始行: {data_start_row}")
  467. # 解析多行数据(如敌人配置、武器配置、关卡配置等)
  468. config_list = []
  469. for i in range(data_start_row, len(df)):
  470. row_config = {}
  471. row_has_data = False
  472. param_types = self.current_mapping.get('param_types', {})
  473. print(f"可用的参数类型: {list(param_types.keys())}")
  474. for col_idx, col_name in enumerate(df.columns):
  475. param_name = str(col_name).strip()
  476. print(f"检查列名: '{param_name}' 是否在参数类型中")
  477. if param_name in param_types:
  478. try:
  479. param_value = df.iloc[i, col_idx]
  480. if pd.isna(param_value) or str(param_value).strip() == '':
  481. print(f"跳过空值: {param_name} = {param_value}")
  482. continue
  483. param_type = param_types[param_name]
  484. if param_type == bool:
  485. row_config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  486. else:
  487. row_config[param_name] = param_type(param_value)
  488. row_has_data = True
  489. print(f"成功转换字段: {param_name} = {param_value} ({param_type})")
  490. except (ValueError, TypeError, IndexError) as e:
  491. print(f"转换字段 {param_name} 时出错: {e}")
  492. continue
  493. else:
  494. print(f"字段 '{param_name}' 不在参数类型定义中,跳过")
  495. if row_config and row_has_data: # 只添加非空且有有效数据的配置
  496. config_list.append(row_config)
  497. print(f"成功解析第{i}行,包含{len(row_config)}个字段")
  498. else:
  499. print(f"跳过第{i}行(无有效数据)")
  500. print(f"总共解析出{len(config_list)}个有效配置项")
  501. # 对于横向表格,返回配置列表
  502. config = {'items': config_list}
  503. except Exception as e:
  504. print(f"解析文件 {filename} 时出错: {e}")
  505. return config
  506. def clear_selection(self):
  507. """清空选择"""
  508. self.file_listbox.selection_clear(0, tk.END)
  509. self.preview_text.delete(1.0, tk.END)
  510. self.config_data = {}
  511. self.status_var.set("已清空选择")
  512. self.load_current_config()
  513. def load_current_config(self):
  514. """加载当前配置"""
  515. try:
  516. if not self.json_config_path:
  517. self.preview_text.insert(tk.END, "请先选择配置文件\n")
  518. return
  519. if self.json_config_path.exists():
  520. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  521. current_config = json.load(f)
  522. self.preview_text.insert(tk.END, f"=== 当前配置 ({self.json_config_path.name}) ===\n")
  523. # 根据配置类型显示不同的预览格式
  524. if self.current_mapping and self.current_mapping['format_type'] == 'horizontal':
  525. # 横向表格配置(如敌人、武器)
  526. if 'enemies' in current_config:
  527. self.preview_text.insert(tk.END, f"敌人配置 ({len(current_config['enemies'])} 个):\n")
  528. for i, enemy in enumerate(current_config['enemies'][:5]): # 只显示前5个
  529. self.preview_text.insert(tk.END, f" {i+1}. {enemy.get('name', enemy.get('id', 'Unknown'))}\n")
  530. if len(current_config['enemies']) > 5:
  531. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['enemies']) - 5} 个\n")
  532. if 'weapons' in current_config:
  533. self.preview_text.insert(tk.END, f"\n武器配置 ({len(current_config['weapons'])} 个):\n")
  534. for i, weapon in enumerate(current_config['weapons'][:5]): # 只显示前5个
  535. self.preview_text.insert(tk.END, f" {i+1}. {weapon.get('name', weapon.get('id', 'Unknown'))}\n")
  536. if len(current_config['weapons']) > 5:
  537. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['weapons']) - 5} 个\n")
  538. else:
  539. # 纵向表格配置(如BallController)
  540. self.preview_text.insert(tk.END, json.dumps(current_config, indent=2, ensure_ascii=False))
  541. file_hint = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  542. self.preview_text.insert(tk.END, f"\n\n请选择{file_hint}进行配置导入...\n")
  543. else:
  544. self.preview_text.insert(tk.END, "配置文件不存在\n")
  545. except Exception as e:
  546. self.preview_text.insert(tk.END, f"加载当前配置失败: {e}\n")
  547. def backup_config(self):
  548. """备份当前配置"""
  549. try:
  550. if not self.json_config_path.exists():
  551. messagebox.showwarning("警告", "配置文件不存在")
  552. return
  553. backup_path = self.json_config_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  554. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  555. content = f.read()
  556. with open(backup_path, 'w', encoding='utf-8') as f:
  557. f.write(content)
  558. messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}")
  559. self.status_var.set(f"配置已备份")
  560. except Exception as e:
  561. messagebox.showerror("错误", f"备份失败: {e}")
  562. def import_config(self):
  563. """导入配置到JSON文件"""
  564. if not self.config_data:
  565. messagebox.showwarning("警告", "没有配置数据可导入")
  566. return
  567. if not self.current_mapping:
  568. messagebox.showwarning("警告", "未检测到有效的配置映射")
  569. return
  570. try:
  571. print(f"开始导入配置...")
  572. print(f"配置数据: {self.config_data}")
  573. print(f"当前映射: {self.current_mapping}")
  574. print(f"选中文件: {self.selected_files}")
  575. format_type = self.current_mapping['format_type']
  576. print(f"格式类型: {format_type}")
  577. if format_type == 'vertical':
  578. # 处理纵向表格(如BallController)
  579. print("开始处理纵向表格配置...")
  580. self._import_vertical_config()
  581. elif format_type == 'horizontal':
  582. # 处理横向表格(如敌人配置、武器配置)
  583. print("开始处理横向表格配置...")
  584. self._import_horizontal_config()
  585. else:
  586. raise ValueError(f"未知的格式类型: {format_type}")
  587. except Exception as e:
  588. import traceback
  589. error_details = traceback.format_exc()
  590. print(f"导入配置失败,详细错误信息:")
  591. print(error_details)
  592. messagebox.showerror("错误", f"导入配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  593. def _import_vertical_config(self):
  594. """导入纵向表格配置"""
  595. # 读取现有JSON配置
  596. if self.json_config_path.exists():
  597. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  598. current_config = json.load(f)
  599. else:
  600. current_config = self.default_config.copy()
  601. # 合并配置
  602. updated_count = 0
  603. for key, value in self.config_data.items():
  604. if key in current_config and current_config[key] != value:
  605. current_config[key] = value
  606. updated_count += 1
  607. elif key not in current_config:
  608. current_config[key] = value
  609. updated_count += 1
  610. # 写入更新后的配置
  611. with open(self.json_config_path, 'w', encoding='utf-8') as f:
  612. json.dump(current_config, f, indent=2, ensure_ascii=False)
  613. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {updated_count} 个参数")
  614. self.status_var.set("配置导入成功")
  615. # 刷新预览
  616. self.clear_selection()
  617. def _import_horizontal_config(self):
  618. """导入横向表格配置"""
  619. print(f"横向表格配置导入开始...")
  620. print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
  621. if 'items' not in self.config_data:
  622. print(f"错误: 配置数据中缺少'items'字段")
  623. print(f"实际配置数据: {self.config_data}")
  624. messagebox.showwarning("警告", "横向表格数据格式错误:缺少'items'字段")
  625. return
  626. items = self.config_data['items']
  627. print(f"配置项数量: {len(items) if items else 0}")
  628. if not items:
  629. messagebox.showwarning("警告", "没有有效的配置项")
  630. return
  631. # 打印前几个配置项用于调试
  632. for i, item in enumerate(items[:3]):
  633. print(f"配置项{i}: {item}")
  634. # 读取现有JSON配置
  635. print(f"JSON配置文件路径: {self.json_config_path}")
  636. # 检查是否是关卡配置(目录类型)
  637. if str(self.json_config_path).endswith('levels'):
  638. print("处理关卡配置目录...")
  639. # 关卡配置是目录,确保目录存在
  640. if not self.json_config_path.exists():
  641. print(f"创建关卡配置目录: {self.json_config_path}")
  642. self.json_config_path.mkdir(parents=True, exist_ok=True)
  643. current_config = {}
  644. else:
  645. # 普通JSON文件配置
  646. if self.json_config_path.exists():
  647. print("读取现有JSON配置文件...")
  648. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  649. current_config = json.load(f)
  650. else:
  651. print("创建新的JSON配置...")
  652. current_config = {}
  653. # 根据不同的配置类型处理
  654. if not self.selected_files:
  655. print("错误: 没有选中的文件")
  656. messagebox.showerror("错误", "没有选中的文件")
  657. return
  658. filename = Path(self.selected_files[0]).name
  659. print(f"处理文件: {filename}")
  660. if '敌人配置' in filename:
  661. print("处理敌人配置...")
  662. # 敌人配置:更新enemies数组
  663. if 'enemies' not in current_config:
  664. current_config['enemies'] = []
  665. # 将Excel数据转换为JSON格式
  666. updated_enemies = []
  667. for i, item in enumerate(items):
  668. print(f"转换敌人数据 {i+1}/{len(items)}: {item}")
  669. enemy_data = self._convert_enemy_data(item)
  670. if enemy_data:
  671. updated_enemies.append(enemy_data)
  672. print(f"成功转换敌人数据: {enemy_data['id']}")
  673. else:
  674. print(f"跳过无效的敌人数据: {item}")
  675. print(f"总共转换了 {len(updated_enemies)} 个敌人配置")
  676. current_config['enemies'] = updated_enemies
  677. elif '武器配置' in filename:
  678. print("处理武器配置...")
  679. # 武器配置:更新weapons数组
  680. if 'weapons' not in current_config:
  681. current_config['weapons'] = []
  682. # 将Excel数据转换为JSON格式
  683. updated_weapons = []
  684. for i, item in enumerate(items):
  685. print(f"转换武器数据 {i+1}/{len(items)}: {item}")
  686. weapon_data = self._convert_weapon_data(item)
  687. if weapon_data:
  688. updated_weapons.append(weapon_data)
  689. print(f"成功转换武器数据: {weapon_data.get('id', 'Unknown')}")
  690. else:
  691. print(f"跳过无效的武器数据: {item}")
  692. print(f"总共转换了 {len(updated_weapons)} 个武器配置")
  693. current_config['weapons'] = updated_weapons
  694. elif '技能配置' in filename:
  695. print("处理技能配置...")
  696. # 技能配置:更新skills数组
  697. if 'skills' not in current_config:
  698. current_config['skills'] = []
  699. # 将Excel数据转换为JSON格式
  700. updated_skills = []
  701. for i, item in enumerate(items):
  702. print(f"转换技能数据 {i+1}/{len(items)}: {item}")
  703. skill_data = self._convert_skill_data(item)
  704. if skill_data:
  705. updated_skills.append(skill_data)
  706. print(f"成功转换技能数据: {skill_data.get('id', 'Unknown')}")
  707. else:
  708. print(f"跳过无效的技能数据: {item}")
  709. print(f"总共转换了 {len(updated_skills)} 个技能配置")
  710. current_config['skills'] = updated_skills
  711. elif '关卡配置' in filename:
  712. print("处理关卡配置...")
  713. # 关卡配置:为每个关卡创建单独的JSON文件
  714. levels_dir = self.json_config_path
  715. print(f"关卡配置目录: {levels_dir}")
  716. try:
  717. # 检查并创建目录
  718. if not levels_dir.exists():
  719. print(f"创建关卡配置目录: {levels_dir}")
  720. levels_dir.mkdir(parents=True, exist_ok=True)
  721. else:
  722. print(f"关卡配置目录已存在: {levels_dir}")
  723. # 测试目录写入权限
  724. test_file = levels_dir / "test_permission.tmp"
  725. try:
  726. with open(test_file, 'w', encoding='utf-8') as f:
  727. f.write("test")
  728. test_file.unlink() # 删除测试文件
  729. except PermissionError:
  730. messagebox.showerror("错误", f"没有写入权限到目录: {levels_dir}\n请检查目录权限或以管理员身份运行")
  731. return
  732. # 将Excel数据转换为JSON格式并保存为单独文件
  733. updated_count = 0
  734. failed_count = 0
  735. for item in items:
  736. try:
  737. level_data = self._convert_level_data(item)
  738. if level_data and '关卡ID' in item and item['关卡ID']:
  739. level_id = item['关卡ID']
  740. level_file = levels_dir / f"{level_id}.json"
  741. print(f"保存关卡配置: {level_file}")
  742. with open(level_file, 'w', encoding='utf-8') as f:
  743. json.dump(level_data, f, indent=2, ensure_ascii=False)
  744. updated_count += 1
  745. else:
  746. print(f"跳过无效的关卡数据: {item}")
  747. failed_count += 1
  748. except Exception as e:
  749. print(f"保存关卡配置时出错: {e}")
  750. failed_count += 1
  751. continue
  752. if updated_count > 0:
  753. message = f"关卡配置导入成功!\n更新了 {updated_count} 个关卡文件"
  754. if failed_count > 0:
  755. message += f"\n跳过了 {failed_count} 个无效配置"
  756. messagebox.showinfo("成功", message)
  757. self.status_var.set("关卡配置导入成功")
  758. else:
  759. messagebox.showerror("错误", "没有成功导入任何关卡配置\n请检查Excel文件格式和数据")
  760. self.status_var.set("关卡配置导入失败")
  761. except Exception as e:
  762. error_msg = f"关卡配置导入失败: {str(e)}"
  763. print(error_msg)
  764. messagebox.showerror("错误", error_msg)
  765. self.status_var.set("关卡配置导入失败")
  766. self.clear_selection()
  767. return
  768. else:
  769. # 未知配置类型
  770. print(f"警告: 未知的配置文件类型: {filename}")
  771. messagebox.showwarning("警告", f"未知的配置文件类型: {filename}\n支持的类型: 敌人配置、武器配置、技能配置、关卡配置")
  772. return
  773. # 写入更新后的配置(关卡配置已在前面处理,跳过)
  774. if not str(self.json_config_path).endswith('levels'):
  775. try:
  776. print(f"写入配置文件: {self.json_config_path}")
  777. print(f"配置内容预览: {str(current_config)[:200]}...")
  778. with open(self.json_config_path, 'w', encoding='utf-8') as f:
  779. json.dump(current_config, f, indent=2, ensure_ascii=False)
  780. print("配置文件写入成功")
  781. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {len(items)} 个配置项")
  782. self.status_var.set("配置导入成功")
  783. except Exception as e:
  784. print(f"写入配置文件失败: {e}")
  785. messagebox.showerror("错误", f"写入配置文件失败: {str(e)}")
  786. return
  787. # 刷新预览
  788. self.clear_selection()
  789. def _convert_enemy_data(self, item):
  790. """转换敌人数据格式"""
  791. try:
  792. print(f"开始转换敌人数据: {item}")
  793. enemy_id = item.get('敌人ID', '')
  794. enemy_name = item.get('敌人名称', '')
  795. print(f"敌人ID: {enemy_id}, 敌人名称: {enemy_name}")
  796. # 检查必要字段
  797. if not enemy_id:
  798. print(f"跳过无效敌人数据: 缺少敌人ID - {item}")
  799. return None
  800. result = {
  801. 'id': enemy_id,
  802. 'name': item.get('敌人名称', ''),
  803. 'type': item.get('敌人类型', ''),
  804. 'rarity': item.get('稀有度', ''),
  805. 'weight': item.get('权重', 1),
  806. 'health': item.get('生命值', 100),
  807. 'speed': item.get('移动速度', 50),
  808. 'attack': item.get('攻击力', 10),
  809. 'range': item.get('攻击范围', 100),
  810. 'attackSpeed': item.get('攻击速度', 1.0),
  811. 'defense': item.get('防御力', 0),
  812. 'goldReward': item.get('金币奖励', 10)
  813. }
  814. print(f"成功转换敌人数据: {result}")
  815. return result
  816. except Exception as e:
  817. print(f"转换敌人数据失败: {e} - 数据: {item}")
  818. return None
  819. def _convert_weapon_data(self, item):
  820. """转换武器数据格式"""
  821. try:
  822. print(f"开始转换武器数据: {item}")
  823. weapon_id = item.get('ID', '')
  824. weapon_name = item.get('名称', '')
  825. print(f"武器ID: {weapon_id}, 武器名称: {weapon_name}")
  826. result = {
  827. 'id': weapon_id,
  828. 'name': weapon_name,
  829. 'type': item.get('类型', ''),
  830. 'rarity': item.get('稀有度', ''),
  831. 'weight': item.get('权重', 1),
  832. 'damage': item.get('伤害', 10),
  833. 'fireRate': item.get('射速', 1.0),
  834. 'range': item.get('射程', 100),
  835. 'bulletSpeed': item.get('子弹速度', 100)
  836. }
  837. print(f"成功转换武器数据: {result}")
  838. return result
  839. except Exception as e:
  840. print(f"转换武器数据失败: {e} - 数据: {item}")
  841. return None
  842. def _convert_skill_data(self, item):
  843. """转换技能数据格式"""
  844. try:
  845. skill_id = item.get('技能ID', '')
  846. print(f"正在转换技能数据: {skill_id} - {item.get('技能名称', '')}")
  847. # 检查必要字段
  848. if not skill_id:
  849. print(f"跳过无效技能数据: 缺少技能ID - {item}")
  850. return None
  851. result = {
  852. 'id': skill_id,
  853. 'name': item.get('技能名称', ''),
  854. 'description': item.get('技能描述', ''),
  855. 'iconPath': item.get('图标路径', ''),
  856. 'maxLevel': item.get('最大等级', 1),
  857. 'currentLevel': item.get('当前等级', 0),
  858. 'priceReduction': item.get('价格减少', 0.0),
  859. 'critChanceIncrease': item.get('暴击几率增加', 0.0),
  860. 'critDamageBonus': item.get('暴击伤害加成', 0.0),
  861. 'healthIncrease': item.get('生命值增加', 0.0),
  862. 'multiShotChance': item.get('多重射击几率', 0.0),
  863. 'energyGainIncrease': item.get('能量加成', 0.0),
  864. 'ballSpeedIncrease': item.get('速度提升', 0.0)
  865. }
  866. print(f"成功转换技能数据: {result}")
  867. return result
  868. except Exception as e:
  869. print(f"转换技能数据失败: {e} - 数据: {item}")
  870. return None
  871. def _convert_level_data(self, item):
  872. """转换关卡数据格式"""
  873. try:
  874. # 处理可用武器字符串,转换为数组
  875. available_weapons = []
  876. if '可用武器' in item and item['可用武器']:
  877. weapons_str = str(item['可用武器'])
  878. available_weapons = [weapon.strip() for weapon in weapons_str.split(',')]
  879. return {
  880. "levelId": str(item.get('关卡ID', '')),
  881. "name": str(item.get('关卡名称', '')),
  882. "scene": str(item.get('场景', '')),
  883. "description": str(item.get('描述', '')),
  884. "weapons": available_weapons,
  885. "timeLimit": int(item.get('时间限制', 300)),
  886. "difficulty": str(item.get('难度', 'normal')),
  887. "healthMultiplier": float(item.get('生命倍数', 1.0)),
  888. "coinReward": int(item.get('金币奖励', 0)),
  889. "diamondReward": int(item.get('钻石奖励', 0))
  890. }
  891. except Exception as e:
  892. print(f"转换关卡数据时出错: {e}")
  893. return None
  894. def restore_default_config(self):
  895. """恢复默认配置"""
  896. result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!")
  897. if not result:
  898. return
  899. try:
  900. # 备份当前配置
  901. if self.json_config_path.exists():
  902. backup_path = self.json_config_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  903. with open(self.json_config_path, 'r', encoding='utf-8') as f:
  904. content = f.read()
  905. with open(backup_path, 'w', encoding='utf-8') as f:
  906. f.write(content)
  907. # 写入默认配置
  908. with open(self.json_config_path, 'w', encoding='utf-8') as f:
  909. json.dump(self.default_config, f, indent=2, ensure_ascii=False)
  910. messagebox.showinfo("成功", "已恢复默认配置")
  911. self.status_var.set("已恢复默认配置")
  912. # 刷新预览
  913. self.clear_selection()
  914. except Exception as e:
  915. messagebox.showerror("错误", f"恢复默认配置失败: {e}")
  916. def run(self):
  917. """运行应用"""
  918. self.root.mainloop()
  919. def main():
  920. """主函数"""
  921. try:
  922. app = ConfigManagerGUI()
  923. app.run()
  924. except Exception as e:
  925. print(f"启动应用失败: {e}")
  926. input("按回车键退出...")
  927. if __name__ == "__main__":
  928. main()