#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ BallController配置管理工具 - 简化版 图形界面版本,不依赖pandas,支持浏览文件并选择Excel/CSV表格进行配置导入 功能: 1. 图形界面浏览项目文件 2. 多选Excel/CSV配置表格 3. 预览配置内容 4. 一键导入配置到JSON 5. 配置备份和恢复 作者: AI Assistant 日期: 2024 """ import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import json import os import csv from datetime import datetime from pathlib import Path import threading # 尝试导入pandas,如果失败则使用纯CSV模式 try: import pandas as pd PANDAS_AVAILABLE = True except ImportError: PANDAS_AVAILABLE = False print("警告: pandas未安装,将使用纯CSV模式(不支持Excel文件)") class ConfigManagerGUI: def __init__(self): self.root = tk.Tk() self.root.title("游戏配置管理工具") self.root.geometry("1000x700") self.root.resizable(True, True) # 配置文件路径 - 动态获取项目根目录 # 从当前脚本位置向上查找项目根目录 current_dir = Path(__file__).parent self.project_root = current_dir.parent.parent.parent.parent # 从excel目录向上4级到项目根目录 self.excel_dir = current_dir # 配置表映射 - 定义每种表格对应的JSON文件和参数类型 self.config_mappings = { 'BallController配置表.xlsx': { 'json_path': self.project_root / "assets/resources/data/ballController.json", 'param_types': { 'baseSpeed': float, 'maxReflectionRandomness': float, 'antiTrapTimeWindow': float, 'antiTrapHitThreshold': int, 'deflectionAttemptThreshold': int, 'antiTrapDeflectionMultiplier': float, 'FIRE_COOLDOWN': float, 'ballRadius': float, 'gravityScale': float, 'linearDamping': float, 'angularDamping': float, 'colliderGroup': int, 'colliderTag': int, 'friction': float, 'restitution': float, 'safeDistance': float, 'edgeOffset': float, 'sensor': bool }, 'format_type': 'vertical' # 纵向表格:参数名在第一列,值在第二/三列 }, '敌人配置表.xlsx': { 'json_path': self.project_root / "assets/resources/data/enemies.json", 'param_types': { '敌人ID': str, '敌人名称': str, '敌人类型': str, '稀有度': str, '权重': int, '生命值': int, '移动速度': int, '攻击力': int, '攻击范围': int, '攻击速度': float, '防御力': int, '金币奖励': int }, 'format_type': 'horizontal' # 横向表格:第一行是参数名,下面是数据行 }, '方块武器配置表_更新_v2.xlsx': { 'json_path': self.project_root / "assets/resources/data/weapons.json", 'param_types': { 'ID': str, '名称': str, '类型': str, '稀有度': str, '权重': int, '伤害': int, '射速': float, '射程': int, '子弹速度': int }, 'format_type': 'horizontal' }, '关卡配置表_完整版_更新_v2.xlsx': { 'json_path': self.project_root / "assets/resources/data/levels", # 目录路径,包含多个关卡JSON文件 'param_types': { '关卡ID': str, '关卡名称': str, '场景': str, '描述': str, '可用武器': str, '初始生命': int, '时间限制': int, '难度': str, '生命倍数': float, '金币奖励': int, '钻石奖励': int }, 'format_type': 'horizontal' }, '技能配置表.xlsx': { 'json_path': self.project_root / "assets/resources/data/skill.json", 'param_types': { '技能ID': str, '技能名称': str, '技能描述': str, '图标路径': str, '最大等级': int, '当前等级': int, '价格减少': float, '暴击几率增加': float, '暴击伤害加成': float, '生命值增加': float, '多重射击几率': float, '能量加成': float, '速度提升': float }, 'format_type': 'horizontal', 'sheet_name': '技能信息表' # 指定使用的工作表名称 } } # 当前选择的配置映射 self.current_mapping = None self.json_config_path = None self.param_types = {} # 默认配置值 self.default_config = { 'baseSpeed': 60, 'maxReflectionRandomness': 0.2, 'antiTrapTimeWindow': 5.0, 'antiTrapHitThreshold': 5, 'deflectionAttemptThreshold': 3, 'antiTrapDeflectionMultiplier': 3.0, 'FIRE_COOLDOWN': 0.05, 'ballRadius': 10, 'gravityScale': 0, 'linearDamping': 0, 'angularDamping': 0, 'colliderGroup': 2, 'colliderTag': 1, 'friction': 0, 'restitution': 1, 'safeDistance': 50, 'edgeOffset': 20, 'sensor': False } self.selected_files = [] self.config_data = {} self.setup_ui() self.load_current_config() def setup_ui(self): """设置用户界面""" # 主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # 配置根窗口的网格权重 self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) main_frame.rowconfigure(2, weight=1) # 标题 title_label = ttk.Label(main_frame, text="游戏配置管理工具", font=("Arial", 16, "bold")) title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) # 左侧面板 - 文件选择 file_types_text = "Excel/CSV配置文件选择" if PANDAS_AVAILABLE else "CSV配置文件选择" left_frame = ttk.LabelFrame(main_frame, text=file_types_text, padding="10") left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10)) left_frame.columnconfigure(0, weight=1) left_frame.rowconfigure(1, weight=1) # 文件浏览按钮 browse_frame = ttk.Frame(left_frame) browse_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10)) browse_frame.columnconfigure(0, weight=1) browse_text = "浏览Excel/CSV文件" if PANDAS_AVAILABLE else "浏览CSV文件" ttk.Button(browse_frame, text=browse_text, command=self.browse_files).grid(row=0, column=0, sticky=(tk.W, tk.E)) ttk.Button(browse_frame, text="扫描项目目录", command=self.scan_project_files).grid(row=0, column=1, padx=(10, 0)) # 文件列表 list_frame = ttk.Frame(left_frame) list_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) list_frame.columnconfigure(0, weight=1) list_frame.rowconfigure(0, weight=1) self.file_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, height=15) scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview) self.file_listbox.configure(yscrollcommand=scrollbar.set) self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) # 文件操作按钮 file_btn_frame = ttk.Frame(left_frame) file_btn_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0)) ttk.Button(file_btn_frame, text="预览选中文件", command=self.preview_selected_files).pack(side=tk.LEFT) ttk.Button(file_btn_frame, text="清空选择", command=self.clear_selection).pack(side=tk.LEFT, padx=(10, 0)) # 右侧面板 - 配置预览和操作 right_frame = ttk.LabelFrame(main_frame, text="配置预览与操作", padding="10") right_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S)) right_frame.columnconfigure(0, weight=1) right_frame.rowconfigure(0, weight=1) # 配置预览文本框 self.preview_text = scrolledtext.ScrolledText(right_frame, height=20, width=50) self.preview_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) # 操作按钮 btn_frame = ttk.Frame(right_frame) btn_frame.grid(row=1, column=0, sticky=(tk.W, tk.E)) ttk.Button(btn_frame, text="导入配置", command=self.import_config).pack(side=tk.LEFT) ttk.Button(btn_frame, text="备份当前配置", command=self.backup_config).pack(side=tk.LEFT, padx=(10, 0)) ttk.Button(btn_frame, text="恢复默认配置", command=self.restore_default_config).pack(side=tk.LEFT, padx=(10, 0)) # 底部状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪") status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0)) # 绑定事件 self.file_listbox.bind('<>', self.on_file_select) def browse_files(self): """浏览Excel/CSV文件""" if PANDAS_AVAILABLE: title = "选择Excel/CSV配置文件" filetypes = [("Excel文件", "*.xlsx *.xls"), ("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")] else: title = "选择CSV配置文件" filetypes = [("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")] files = filedialog.askopenfilenames( title=title, filetypes=filetypes, initialdir=str(self.excel_dir) ) if files: self.file_listbox.delete(0, tk.END) for file in files: self.file_listbox.insert(tk.END, file) self.status_var.set(f"已选择 {len(files)} 个文件") def scan_project_files(self): """扫描项目目录中的Excel/CSV文件""" self.status_var.set("正在扫描项目目录...") def scan_thread(): config_files = [] # 扫描常见的配置目录 scan_dirs = [ self.project_root / "assets/resources/data", self.project_root / "assets/resources/config", self.project_root / "assets/excel", self.project_root / "config", self.project_root / "data", self.project_root ] # 根据pandas可用性选择扫描的文件类型 patterns = ['*.xlsx', '*.xls', '*.csv', '*.txt'] if PANDAS_AVAILABLE else ['*.csv', '*.txt'] for scan_dir in scan_dirs: if scan_dir.exists(): for pattern in patterns: config_files.extend(scan_dir.rglob(pattern)) # 更新UI self.root.after(0, self.update_file_list, config_files) threading.Thread(target=scan_thread, daemon=True).start() def update_file_list(self, files): """更新文件列表""" self.file_listbox.delete(0, tk.END) for file in files: self.file_listbox.insert(tk.END, str(file)) file_type_text = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件" self.status_var.set(f"找到 {len(files)} 个{file_type_text}") def on_file_select(self, event): """文件选择事件处理""" selection = self.file_listbox.curselection() if selection: self.selected_files = [self.file_listbox.get(i) for i in selection] # 自动识别配置映射 self.auto_detect_config_mapping() self.preview_selected_files() self.status_var.set(f"已选择 {len(selection)} 个文件") def auto_detect_config_mapping(self): """自动检测配置映射""" if not self.selected_files: return # 取第一个选中的文件进行映射检测 selected_file = self.selected_files[0] filename = Path(selected_file).name # 检查是否有对应的配置映射 for config_name, mapping in self.config_mappings.items(): if config_name in filename or filename in config_name: self.current_mapping = mapping self.json_config_path = mapping['json_path'] self.param_types = mapping['param_types'] print(f"检测到配置映射: {config_name} -> {self.json_config_path}") return # 如果没有找到映射,使用默认的BallController映射 self.current_mapping = self.config_mappings['BallController配置表.xlsx'] self.json_config_path = self.current_mapping['json_path'] self.param_types = self.current_mapping['param_types'] print(f"使用默认配置映射: BallController -> {self.json_config_path}") def preview_selected_files(self): """预览选中的文件""" selection = self.file_listbox.curselection() if not selection: messagebox.showwarning("警告", "请先选择要预览的文件") return self.preview_text.delete(1.0, tk.END) self.config_data = {} for i in selection: file_path = Path(self.file_listbox.get(i)) self.preview_text.insert(tk.END, f"=== {file_path.name} ===\n") try: # 根据文件扩展名和pandas可用性选择读取方法 if file_path.suffix.lower() in ['.xlsx', '.xls']: if PANDAS_AVAILABLE: file_config = self.read_excel_config(file_path) else: self.preview_text.insert(tk.END, "Excel文件需要pandas库支持,请安装: pip install pandas openpyxl\n\n") continue elif file_path.suffix.lower() in ['.csv', '.txt']: file_config = self.read_csv_config(file_path) else: self.preview_text.insert(tk.END, "不支持的文件格式\n\n") continue if file_config: self.config_data.update(file_config) # 显示预览 self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置参数:\n") for key, value in file_config.items(): self.preview_text.insert(tk.END, f" {key}: {value}\n") else: self.preview_text.insert(tk.END, "未找到有效的配置数据\n") except Exception as e: self.preview_text.insert(tk.END, f"读取失败: {str(e)}\n") self.preview_text.insert(tk.END, "\n") # 显示合并后的配置 if self.config_data: self.preview_text.insert(tk.END, "=== 合并后的配置 ===\n") self.preview_text.insert(tk.END, json.dumps(self.config_data, indent=2, ensure_ascii=False)) self.status_var.set(f"预览完成,共 {len(self.config_data)} 个配置参数") def read_excel_config(self, file_path): """读取Excel配置文件""" config = {} if not PANDAS_AVAILABLE: print("错误: pandas未安装,无法读取Excel文件") return config try: # 检查是否有指定的工作表名称 sheet_name = None if self.current_mapping and 'sheet_name' in self.current_mapping: sheet_name = self.current_mapping['sheet_name'] # 读取Excel文件 if sheet_name: df = pd.read_excel(file_path, sheet_name=sheet_name) else: df = pd.read_excel(file_path) config = self.parse_config_data(df, file_path.name) except Exception as e: print(f"读取Excel文件 {file_path.name} 时出错: {e}") return config def read_csv_config(self, file_path): """读取CSV配置文件""" config = {} try: # 如果pandas可用,优先使用pandas读取CSV(支持更多格式) if PANDAS_AVAILABLE: try: df = pd.read_csv(file_path) config = self.parse_config_data(df, file_path.name) except: # 如果pandas读取失败,回退到原始CSV读取方法 config = self.read_csv_fallback(file_path) else: # 如果pandas不可用,直接使用原始CSV读取方法 config = self.read_csv_fallback(file_path) except Exception as e: print(f"读取文件 {file_path.name} 时出错: {e}") return config def read_csv_fallback(self, file_path): """原始CSV读取方法(不依赖pandas)""" config = {} try: with open(file_path, 'r', encoding='utf-8') as f: reader = csv.reader(f) for row_num, row in enumerate(reader, 1): if len(row) < 2: continue param_name = row[0].strip() param_value = row[1].strip() # 跳过标题行 if param_name in ['参数名', 'parameter', 'name']: continue # 检查参数是否有效 if param_name in self.param_types: try: param_type = self.param_types[param_name] if param_type == bool: config[param_name] = param_value.lower() in ['true', '1', 'yes', 'on'] else: config[param_name] = param_type(param_value) except (ValueError, TypeError): continue except Exception as e: print(f"读取CSV文件 {file_path.name} 时出错: {e}") return config def parse_config_data(self, df, filename): """解析配置数据(支持多种格式,需要pandas)""" config = {} if not PANDAS_AVAILABLE or not self.current_mapping: return config format_type = self.current_mapping['format_type'] try: if format_type == 'vertical': # 纵向表格:参数名在第一列,值在第二/三列 for _, row in df.iterrows(): param_name = str(row.iloc[0]).strip() # 跳过标题行和无效行 if param_name in ['参数名', 'parameter', 'name', 'nan', '球控制器参数'] or param_name == 'nan': continue # 检查参数是否有效 param_types = self.current_mapping.get('param_types', {}) if param_name in param_types: try: # 优先使用第3列(默认值),如果不存在则使用第2列 param_value = row.iloc[2] if len(row) > 2 and not pd.isna(row.iloc[2]) else row.iloc[1] if pd.isna(param_value): continue param_type = param_types[param_name] if param_type == bool: config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on'] else: config[param_name] = param_type(param_value) except (ValueError, TypeError, IndexError): continue elif format_type == 'horizontal': # 横向表格:第一行是参数名(列名),数据从第0行开始 print(f"横向表格解析: 总行数={len(df)}") print(f"表格列名: {list(df.columns)}") # 打印前几行数据用于调试 for i in range(min(3, len(df))): print(f"第{i}行数据: {df.iloc[i].to_dict()}") # 检查第0行是否为有效数据行 # 如果第0行第一个单元格是描述性文字或空值,则跳过 data_start_row = 0 if len(df) > 0: first_cell = str(df.iloc[0, 0]).strip().lower() # 跳过描述行、空行或标题行 if (first_cell in ['唯一标识符', '描述', 'description', 'desc', 'nan', ''] or first_cell == df.columns[0].lower()): data_start_row = 1 print(f"跳过第0行(描述行或标题行): {first_cell}") print(f"数据起始行: {data_start_row}") # 解析多行数据(如敌人配置、武器配置、关卡配置等) config_list = [] for i in range(data_start_row, len(df)): row_config = {} row_has_data = False param_types = self.current_mapping.get('param_types', {}) print(f"可用的参数类型: {list(param_types.keys())}") for col_idx, col_name in enumerate(df.columns): param_name = str(col_name).strip() print(f"检查列名: '{param_name}' 是否在参数类型中") if param_name in param_types: try: param_value = df.iloc[i, col_idx] if pd.isna(param_value) or str(param_value).strip() == '': print(f"跳过空值: {param_name} = {param_value}") continue param_type = param_types[param_name] if param_type == bool: row_config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on'] else: row_config[param_name] = param_type(param_value) row_has_data = True print(f"成功转换字段: {param_name} = {param_value} ({param_type})") except (ValueError, TypeError, IndexError) as e: print(f"转换字段 {param_name} 时出错: {e}") continue else: print(f"字段 '{param_name}' 不在参数类型定义中,跳过") if row_config and row_has_data: # 只添加非空且有有效数据的配置 config_list.append(row_config) print(f"成功解析第{i}行,包含{len(row_config)}个字段") else: print(f"跳过第{i}行(无有效数据)") print(f"总共解析出{len(config_list)}个有效配置项") # 对于横向表格,返回配置列表 config = {'items': config_list} except Exception as e: print(f"解析文件 {filename} 时出错: {e}") return config def clear_selection(self): """清空选择""" self.file_listbox.selection_clear(0, tk.END) self.preview_text.delete(1.0, tk.END) self.config_data = {} self.status_var.set("已清空选择") self.load_current_config() def load_current_config(self): """加载当前配置""" try: if not self.json_config_path: self.preview_text.insert(tk.END, "请先选择配置文件\n") return if self.json_config_path.exists(): with open(self.json_config_path, 'r', encoding='utf-8') as f: current_config = json.load(f) self.preview_text.insert(tk.END, f"=== 当前配置 ({self.json_config_path.name}) ===\n") # 根据配置类型显示不同的预览格式 if self.current_mapping and self.current_mapping['format_type'] == 'horizontal': # 横向表格配置(如敌人、武器) if 'enemies' in current_config: self.preview_text.insert(tk.END, f"敌人配置 ({len(current_config['enemies'])} 个):\n") for i, enemy in enumerate(current_config['enemies'][:5]): # 只显示前5个 self.preview_text.insert(tk.END, f" {i+1}. {enemy.get('name', enemy.get('id', 'Unknown'))}\n") if len(current_config['enemies']) > 5: self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['enemies']) - 5} 个\n") if 'weapons' in current_config: self.preview_text.insert(tk.END, f"\n武器配置 ({len(current_config['weapons'])} 个):\n") for i, weapon in enumerate(current_config['weapons'][:5]): # 只显示前5个 self.preview_text.insert(tk.END, f" {i+1}. {weapon.get('name', weapon.get('id', 'Unknown'))}\n") if len(current_config['weapons']) > 5: self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['weapons']) - 5} 个\n") else: # 纵向表格配置(如BallController) self.preview_text.insert(tk.END, json.dumps(current_config, indent=2, ensure_ascii=False)) file_hint = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件" self.preview_text.insert(tk.END, f"\n\n请选择{file_hint}进行配置导入...\n") else: self.preview_text.insert(tk.END, "配置文件不存在\n") except Exception as e: self.preview_text.insert(tk.END, f"加载当前配置失败: {e}\n") def backup_config(self): """备份当前配置""" try: if not self.json_config_path.exists(): messagebox.showwarning("警告", "配置文件不存在") return backup_path = self.json_config_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(self.json_config_path, 'r', encoding='utf-8') as f: content = f.read() with open(backup_path, 'w', encoding='utf-8') as f: f.write(content) messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}") self.status_var.set(f"配置已备份") except Exception as e: messagebox.showerror("错误", f"备份失败: {e}") def import_config(self): """导入配置到JSON文件""" if not self.config_data: messagebox.showwarning("警告", "没有配置数据可导入") return if not self.current_mapping: messagebox.showwarning("警告", "未检测到有效的配置映射") return try: print(f"开始导入配置...") print(f"配置数据: {self.config_data}") print(f"当前映射: {self.current_mapping}") print(f"选中文件: {self.selected_files}") format_type = self.current_mapping['format_type'] print(f"格式类型: {format_type}") if format_type == 'vertical': # 处理纵向表格(如BallController) print("开始处理纵向表格配置...") self._import_vertical_config() elif format_type == 'horizontal': # 处理横向表格(如敌人配置、武器配置) print("开始处理横向表格配置...") self._import_horizontal_config() else: raise ValueError(f"未知的格式类型: {format_type}") except Exception as e: import traceback error_details = traceback.format_exc() print(f"导入配置失败,详细错误信息:") print(error_details) messagebox.showerror("错误", f"导入配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。") def _import_vertical_config(self): """导入纵向表格配置""" # 读取现有JSON配置 if self.json_config_path.exists(): with open(self.json_config_path, 'r', encoding='utf-8') as f: current_config = json.load(f) else: current_config = self.default_config.copy() # 合并配置 updated_count = 0 for key, value in self.config_data.items(): if key in current_config and current_config[key] != value: current_config[key] = value updated_count += 1 elif key not in current_config: current_config[key] = value updated_count += 1 # 写入更新后的配置 with open(self.json_config_path, 'w', encoding='utf-8') as f: json.dump(current_config, f, indent=2, ensure_ascii=False) messagebox.showinfo("成功", f"配置导入成功!\n更新了 {updated_count} 个参数") self.status_var.set("配置导入成功") # 刷新预览 self.clear_selection() def _import_horizontal_config(self): """导入横向表格配置""" print(f"横向表格配置导入开始...") print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}") if 'items' not in self.config_data: print(f"错误: 配置数据中缺少'items'字段") print(f"实际配置数据: {self.config_data}") messagebox.showwarning("警告", "横向表格数据格式错误:缺少'items'字段") return items = self.config_data['items'] print(f"配置项数量: {len(items) if items else 0}") if not items: messagebox.showwarning("警告", "没有有效的配置项") return # 打印前几个配置项用于调试 for i, item in enumerate(items[:3]): print(f"配置项{i}: {item}") # 读取现有JSON配置 print(f"JSON配置文件路径: {self.json_config_path}") # 检查是否是关卡配置(目录类型) if str(self.json_config_path).endswith('levels'): print("处理关卡配置目录...") # 关卡配置是目录,确保目录存在 if not self.json_config_path.exists(): print(f"创建关卡配置目录: {self.json_config_path}") self.json_config_path.mkdir(parents=True, exist_ok=True) current_config = {} else: # 普通JSON文件配置 if self.json_config_path.exists(): print("读取现有JSON配置文件...") with open(self.json_config_path, 'r', encoding='utf-8') as f: current_config = json.load(f) else: print("创建新的JSON配置...") current_config = {} # 根据不同的配置类型处理 if not self.selected_files: print("错误: 没有选中的文件") messagebox.showerror("错误", "没有选中的文件") return filename = Path(self.selected_files[0]).name print(f"处理文件: {filename}") if '敌人配置' in filename: print("处理敌人配置...") # 敌人配置:更新enemies数组 if 'enemies' not in current_config: current_config['enemies'] = [] # 将Excel数据转换为JSON格式 updated_enemies = [] for i, item in enumerate(items): print(f"转换敌人数据 {i+1}/{len(items)}: {item}") enemy_data = self._convert_enemy_data(item) if enemy_data: updated_enemies.append(enemy_data) print(f"成功转换敌人数据: {enemy_data['id']}") else: print(f"跳过无效的敌人数据: {item}") print(f"总共转换了 {len(updated_enemies)} 个敌人配置") current_config['enemies'] = updated_enemies elif '武器配置' in filename: print("处理武器配置...") # 武器配置:更新weapons数组 if 'weapons' not in current_config: current_config['weapons'] = [] # 将Excel数据转换为JSON格式 updated_weapons = [] for i, item in enumerate(items): print(f"转换武器数据 {i+1}/{len(items)}: {item}") weapon_data = self._convert_weapon_data(item) if weapon_data: updated_weapons.append(weapon_data) print(f"成功转换武器数据: {weapon_data.get('id', 'Unknown')}") else: print(f"跳过无效的武器数据: {item}") print(f"总共转换了 {len(updated_weapons)} 个武器配置") current_config['weapons'] = updated_weapons elif '技能配置' in filename: print("处理技能配置...") # 技能配置:更新skills数组 if 'skills' not in current_config: current_config['skills'] = [] # 将Excel数据转换为JSON格式 updated_skills = [] for i, item in enumerate(items): print(f"转换技能数据 {i+1}/{len(items)}: {item}") skill_data = self._convert_skill_data(item) if skill_data: updated_skills.append(skill_data) print(f"成功转换技能数据: {skill_data.get('id', 'Unknown')}") else: print(f"跳过无效的技能数据: {item}") print(f"总共转换了 {len(updated_skills)} 个技能配置") current_config['skills'] = updated_skills elif '关卡配置' in filename: print("处理关卡配置...") # 关卡配置:为每个关卡创建单独的JSON文件 levels_dir = self.json_config_path print(f"关卡配置目录: {levels_dir}") try: # 检查并创建目录 if not levels_dir.exists(): print(f"创建关卡配置目录: {levels_dir}") levels_dir.mkdir(parents=True, exist_ok=True) else: print(f"关卡配置目录已存在: {levels_dir}") # 测试目录写入权限 test_file = levels_dir / "test_permission.tmp" try: with open(test_file, 'w', encoding='utf-8') as f: f.write("test") test_file.unlink() # 删除测试文件 except PermissionError: messagebox.showerror("错误", f"没有写入权限到目录: {levels_dir}\n请检查目录权限或以管理员身份运行") return # 将Excel数据转换为JSON格式并保存为单独文件 updated_count = 0 failed_count = 0 for item in items: try: level_data = self._convert_level_data(item) if level_data and '关卡ID' in item and item['关卡ID']: level_id = item['关卡ID'] level_file = levels_dir / f"{level_id}.json" print(f"保存关卡配置: {level_file}") with open(level_file, 'w', encoding='utf-8') as f: json.dump(level_data, f, indent=2, ensure_ascii=False) updated_count += 1 else: print(f"跳过无效的关卡数据: {item}") failed_count += 1 except Exception as e: print(f"保存关卡配置时出错: {e}") failed_count += 1 continue if updated_count > 0: message = f"关卡配置导入成功!\n更新了 {updated_count} 个关卡文件" if failed_count > 0: message += f"\n跳过了 {failed_count} 个无效配置" messagebox.showinfo("成功", message) self.status_var.set("关卡配置导入成功") else: messagebox.showerror("错误", "没有成功导入任何关卡配置\n请检查Excel文件格式和数据") self.status_var.set("关卡配置导入失败") except Exception as e: error_msg = f"关卡配置导入失败: {str(e)}" print(error_msg) messagebox.showerror("错误", error_msg) self.status_var.set("关卡配置导入失败") self.clear_selection() return else: # 未知配置类型 print(f"警告: 未知的配置文件类型: {filename}") messagebox.showwarning("警告", f"未知的配置文件类型: {filename}\n支持的类型: 敌人配置、武器配置、技能配置、关卡配置") return # 写入更新后的配置(关卡配置已在前面处理,跳过) if not str(self.json_config_path).endswith('levels'): try: print(f"写入配置文件: {self.json_config_path}") print(f"配置内容预览: {str(current_config)[:200]}...") with open(self.json_config_path, 'w', encoding='utf-8') as f: json.dump(current_config, f, indent=2, ensure_ascii=False) print("配置文件写入成功") messagebox.showinfo("成功", f"配置导入成功!\n更新了 {len(items)} 个配置项") self.status_var.set("配置导入成功") except Exception as e: print(f"写入配置文件失败: {e}") messagebox.showerror("错误", f"写入配置文件失败: {str(e)}") return # 刷新预览 self.clear_selection() def _convert_enemy_data(self, item): """转换敌人数据格式""" try: print(f"开始转换敌人数据: {item}") enemy_id = item.get('敌人ID', '') enemy_name = item.get('敌人名称', '') print(f"敌人ID: {enemy_id}, 敌人名称: {enemy_name}") # 检查必要字段 if not enemy_id: print(f"跳过无效敌人数据: 缺少敌人ID - {item}") return None result = { 'id': enemy_id, 'name': item.get('敌人名称', ''), 'type': item.get('敌人类型', ''), 'rarity': item.get('稀有度', ''), 'weight': item.get('权重', 1), 'health': item.get('生命值', 100), 'speed': item.get('移动速度', 50), 'attack': item.get('攻击力', 10), 'range': item.get('攻击范围', 100), 'attackSpeed': item.get('攻击速度', 1.0), 'defense': item.get('防御力', 0), 'goldReward': item.get('金币奖励', 10) } print(f"成功转换敌人数据: {result}") return result except Exception as e: print(f"转换敌人数据失败: {e} - 数据: {item}") return None def _convert_weapon_data(self, item): """转换武器数据格式""" try: print(f"开始转换武器数据: {item}") weapon_id = item.get('ID', '') weapon_name = item.get('名称', '') print(f"武器ID: {weapon_id}, 武器名称: {weapon_name}") result = { 'id': weapon_id, 'name': weapon_name, 'type': item.get('类型', ''), 'rarity': item.get('稀有度', ''), 'weight': item.get('权重', 1), 'damage': item.get('伤害', 10), 'fireRate': item.get('射速', 1.0), 'range': item.get('射程', 100), 'bulletSpeed': item.get('子弹速度', 100) } print(f"成功转换武器数据: {result}") return result except Exception as e: print(f"转换武器数据失败: {e} - 数据: {item}") return None def _convert_skill_data(self, item): """转换技能数据格式""" try: skill_id = item.get('技能ID', '') print(f"正在转换技能数据: {skill_id} - {item.get('技能名称', '')}") # 检查必要字段 if not skill_id: print(f"跳过无效技能数据: 缺少技能ID - {item}") return None result = { 'id': skill_id, 'name': item.get('技能名称', ''), 'description': item.get('技能描述', ''), 'iconPath': item.get('图标路径', ''), 'maxLevel': item.get('最大等级', 1), 'currentLevel': item.get('当前等级', 0), 'priceReduction': item.get('价格减少', 0.0), 'critChanceIncrease': item.get('暴击几率增加', 0.0), 'critDamageBonus': item.get('暴击伤害加成', 0.0), 'healthIncrease': item.get('生命值增加', 0.0), 'multiShotChance': item.get('多重射击几率', 0.0), 'energyGainIncrease': item.get('能量加成', 0.0), 'ballSpeedIncrease': item.get('速度提升', 0.0) } print(f"成功转换技能数据: {result}") return result except Exception as e: print(f"转换技能数据失败: {e} - 数据: {item}") return None def _convert_level_data(self, item): """转换关卡数据格式""" try: # 处理可用武器字符串,转换为数组 available_weapons = [] if '可用武器' in item and item['可用武器']: weapons_str = str(item['可用武器']) available_weapons = [weapon.strip() for weapon in weapons_str.split(',')] return { "levelId": str(item.get('关卡ID', '')), "name": str(item.get('关卡名称', '')), "scene": str(item.get('场景', '')), "description": str(item.get('描述', '')), "weapons": available_weapons, "timeLimit": int(item.get('时间限制', 300)), "difficulty": str(item.get('难度', 'normal')), "healthMultiplier": float(item.get('生命倍数', 1.0)), "coinReward": int(item.get('金币奖励', 0)), "diamondReward": int(item.get('钻石奖励', 0)) } except Exception as e: print(f"转换关卡数据时出错: {e}") return None def restore_default_config(self): """恢复默认配置""" result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!") if not result: return try: # 备份当前配置 if self.json_config_path.exists(): backup_path = self.json_config_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(self.json_config_path, 'r', encoding='utf-8') as f: content = f.read() with open(backup_path, 'w', encoding='utf-8') as f: f.write(content) # 写入默认配置 with open(self.json_config_path, 'w', encoding='utf-8') as f: json.dump(self.default_config, f, indent=2, ensure_ascii=False) messagebox.showinfo("成功", "已恢复默认配置") self.status_var.set("已恢复默认配置") # 刷新预览 self.clear_selection() except Exception as e: messagebox.showerror("错误", f"恢复默认配置失败: {e}") def run(self): """运行应用""" self.root.mainloop() def main(): """主函数""" try: app = ConfigManagerGUI() app.run() except Exception as e: print(f"启动应用失败: {e}") input("按回车键退出...") if __name__ == "__main__": main()