#!/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 SkillConfigImporter: """技能配置表导入工具类""" def __init__(self, excel_dir=None): # 获取当前脚本所在目录 if excel_dir: self.script_dir = Path(excel_dir) else: self.script_dir = Path(__file__).parent self.excel_file = self.script_dir / "局外技能配置表.xlsx" self.json_file = self.script_dir.parent / "skill_config.json" # 默认配置结构 self.default_config = { "skillTypes": [], "skillGroups": [], "totalGroups": 12, "skillsPerGroup": 3 } def read_excel_config(self): """从Excel文件读取技能配置""" if not self.excel_file.exists(): print(f"错误: Excel文件不存在: {self.excel_file}") return None try: # 读取所有工作表 excel_data = pd.read_excel(self.excel_file, sheet_name=None) print(f"找到工作表: {list(excel_data.keys())}") config = self.default_config.copy() # 读取技能类型配置 if "技能类型配置" in excel_data: skill_types = self.parse_skill_types(excel_data["技能类型配置"]) config["skillTypes"] = skill_types print(f"读取到 {len(skill_types)} 个技能类型") elif "JSON技能类型配置" in excel_data: skill_types = self.parse_skill_types(excel_data["JSON技能类型配置"]) config["skillTypes"] = skill_types print(f"读取到 {len(skill_types)} 个技能类型") # 读取技能组配置 if "技能组配置" in excel_data: skill_groups = self.parse_skill_groups(excel_data["技能组配置"]) config["skillGroups"] = skill_groups config["totalGroups"] = len(skill_groups) print(f"读取到 {len(skill_groups)} 个技能组") elif "JSON技能组配置" in excel_data: skill_groups = self.parse_skill_groups(excel_data["JSON技能组配置"]) config["skillGroups"] = skill_groups config["totalGroups"] = len(skill_groups) print(f"读取到 {len(skill_groups)} 个技能组") # 读取基本配置 if "基本配置" in excel_data: basic_config = self.parse_basic_config(excel_data["基本配置"]) config.update(basic_config) elif "JSON基本配置" in excel_data: basic_config = self.parse_basic_config(excel_data["JSON基本配置"]) config.update(basic_config) return config except Exception as e: print(f"读取Excel文件失败: {e}") return None def parse_skill_types(self, df): """解析技能类型配置""" skill_types = [] # 读取所有数据行(pandas已经自动处理了标题行) for index, row in df.iterrows(): # 检查是否为空行 if pd.isna(row.iloc[0]): continue try: skill_type = { "id": int(row.iloc[0]) if not pd.isna(row.iloc[0]) else 0, "name": str(row.iloc[1]) if not pd.isna(row.iloc[1]) else "", "displayName": str(row.iloc[2]) if not pd.isna(row.iloc[2]) else "", "nameTemplate": str(row.iloc[3]) if not pd.isna(row.iloc[3]) else "", "type": str(row.iloc[4]) if not pd.isna(row.iloc[4]) else "" } skill_types.append(skill_type) except Exception as e: print(f"解析技能类型第 {index+1} 行失败: {e}") continue return skill_types def parse_skill_groups(self, df): """解析技能组配置""" skill_groups = [] # 读取所有数据行(pandas已经自动处理了标题行) for index, row in df.iterrows(): # 检查是否为空行 if pd.isna(row.iloc[0]): continue try: skill_group = { "group": int(row.iloc[0]) if not pd.isna(row.iloc[0]) else 1, "effectPercent": int(row.iloc[1]) if not pd.isna(row.iloc[1]) else 0, "diamondCost": int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 0 } skill_groups.append(skill_group) except Exception as e: print(f"解析技能组第 {index+1} 行失败: {e}") continue return skill_groups def parse_basic_config(self, df): """解析基本配置""" basic_config = {} # 假设基本配置是纵向格式:配置项名称 | 数值 for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue # 检查是否为空行 if pd.isna(row.iloc[0]): continue try: param_name = str(row.iloc[0]).strip() param_value = row.iloc[1] if param_name == "总组数" or param_name == "totalGroups": basic_config["totalGroups"] = int(param_value) if not pd.isna(param_value) else 12 elif param_name == "每组技能数" or param_name == "skillsPerGroup": basic_config["skillsPerGroup"] = int(param_value) if not pd.isna(param_value) else 3 except Exception as e: print(f"解析基本配置第 {index+1} 行失败: {e}") continue return basic_config def validate_config(self, config): """验证配置数据""" errors = [] # 验证技能类型 if not config.get("skillTypes"): errors.append("技能类型配置为空") else: for i, skill_type in enumerate(config["skillTypes"]): if not skill_type.get("name"): errors.append(f"技能类型 {i+1} 缺少名称") if not skill_type.get("type"): errors.append(f"技能类型 {i+1} 缺少类型标识") # 验证技能组 if not config.get("skillGroups"): errors.append("技能组配置为空") else: groups = [sg["group"] for sg in config["skillGroups"]] if len(groups) != len(set(groups)): errors.append("技能组编号存在重复") # 验证基本配置 if config.get("totalGroups", 0) <= 0: errors.append("总组数必须大于0") if config.get("skillsPerGroup", 0) <= 0: errors.append("每组技能数必须大于0") return errors def backup_json(self): """备份当前JSON文件(已禁用)""" # 不再创建备份文件,直接返回成功 return True def save_json_config(self, config): """保存配置到JSON文件""" try: with open(self.json_file, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=2) print(f"配置已保存到: {self.json_file}") return True except Exception as e: print(f"保存JSON文件失败: {e}") return False def import_config(self): """执行配置导入""" print("=== 技能配置导入工具 ===") print(f"Excel文件: {self.excel_file}") print(f"JSON文件: {self.json_file}") print() # 读取Excel配置 config = self.read_excel_config() if not config: print("读取Excel配置失败") return False # 验证配置 errors = self.validate_config(config) if errors: print("配置验证失败:") for error in errors: print(f" - {error}") return False print("配置验证通过") # 显示配置摘要 print(f"\n配置摘要:") print(f" 技能类型数量: {len(config['skillTypes'])}") print(f" 技能组数量: {len(config['skillGroups'])}") print(f" 总组数: {config['totalGroups']}") print(f" 每组技能数: {config['skillsPerGroup']}") # 备份原文件 if not self.backup_json(): print("备份失败,是否继续?(y/n): ", end="") # 在GUI环境中,我们默认继续 print("y (自动继续)") # 保存新配置 if self.save_json_config(config): print("\n配置导入成功!") return True else: print("\n配置导入失败!") return False 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 # 手动配置变量 self.selected_json_path = None self.format_type = 'horizontal' # 默认为横向表格 self.multi_sheet = False self.sheet_names = [] self.param_types = {} # 当前选择的配置映射 self.current_mapping = None # 移除json_config_path,现在使用selected_json_path 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(3, 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)) # JSON文件选择区域 json_frame = ttk.LabelFrame(left_frame, text="JSON输出路径选择", padding="5") json_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(10, 0)) json_frame.columnconfigure(1, weight=1) ttk.Label(json_frame, text="输出路径:").grid(row=0, column=0, sticky=tk.W) self.json_path_var = tk.StringVar() self.json_path_entry = ttk.Entry(json_frame, textvariable=self.json_path_var, state='readonly') self.json_path_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0)) ttk.Button(json_frame, text="浏览", command=self.browse_json_path).grid(row=0, column=2, padx=(5, 0)) # 配置选项 config_frame = ttk.LabelFrame(left_frame, text="配置选项", padding="5") config_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0)) # 表格格式选择 ttk.Label(config_frame, text="表格格式:").grid(row=0, column=0, sticky=tk.W) self.format_var = tk.StringVar(value="horizontal") format_frame = ttk.Frame(config_frame) format_frame.grid(row=0, column=1, sticky=tk.W, padx=(5, 0)) ttk.Radiobutton(format_frame, text="横向", variable=self.format_var, value="horizontal").pack(side=tk.LEFT) ttk.Radiobutton(format_frame, text="纵向", variable=self.format_var, value="vertical").pack(side=tk.LEFT, padx=(10, 0)) # 多工作表选项 self.multi_sheet_var = tk.BooleanVar() ttk.Checkbutton(config_frame, text="多工作表文件", variable=self.multi_sheet_var, command=self.on_multi_sheet_change).grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(5, 0)) # 单个JSON选项 self.single_json_var = tk.BooleanVar(value=True) ttk.Checkbutton(config_frame, text="单个json", variable=self.single_json_var, command=self.on_single_json_change).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(5, 0)) # 文件列表 list_frame = ttk.Frame(left_frame) list_frame.grid(row=3, 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=4, 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.import_skill_config).pack(side=tk.LEFT, padx=(10, 0)) 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)) # 敌人配置专用按钮 enemy_btn_frame = ttk.Frame(right_frame) enemy_btn_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0)) ttk.Button(enemy_btn_frame, text="修复敌人JSON", command=self.fix_enemies_json).pack(side=tk.LEFT) ttk.Button(enemy_btn_frame, text="自动导入敌人配置", command=self.auto_import_enemies_from_excel).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 browse_json_path(self): """浏览JSON输出路径""" # 根据用户选择的选项组合决定选择文件还是目录 multi_sheet = self.multi_sheet_var.get() single_json = self.single_json_var.get() if multi_sheet and not single_json: # 多工作表文件且不是单个JSON:输出到目录(每个sheet一个JSON文件) path = filedialog.askdirectory( title="选择JSON输出目录", initialdir=str(self.project_root / "assets/resources/data") ) else: # 其他情况:输出单个JSON文件 path = filedialog.askopenfilename( title="选择JSON输出文件", filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")], initialdir=str(self.project_root / "assets/resources/data") ) if path: self.json_path_var.set(path) self.selected_json_path = Path(path) self.status_var.set(f"已选择输出路径: {Path(path).name}") def on_multi_sheet_change(self): """多工作表选项变化处理""" # 清空当前选择的JSON路径,让用户重新选择 self.json_path_var.set("") def on_single_json_change(self): """单个JSON选项变化处理""" # 清空当前选择的JSON路径,让用户重新选择 self.json_path_var.set("") self.selected_json_path = None if self.multi_sheet_var.get(): self.status_var.set("多工作表模式:请选择输出目录") else: self.status_var.set("单工作表模式:请选择输出JSON文件") 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._set_config_mapping_for_files() self.preview_selected_files() self.status_var.set(f"已选择 {len(selection)} 个文件") def _set_config_mapping_for_files(self): """根据选择的文件设置配置映射""" if not self.selected_files: return # 获取第一个文件名来判断配置类型 first_file = Path(self.selected_files[0]) filename = first_file.name.lower() # 根据文件名设置配置映射 if '武器' in filename or 'weapon' in filename: self.current_mapping = { 'format_type': 'horizontal', 'param_types': { 'ID': str, '名称': str, '伤害': int, '射程': int, '射速': float, '弹药容量': int, '重装时间': float, '解锁等级': int, '价格': int, '描述': str, '最大等级': int, # 英文字段支持 'weaponId': str, 'name': str, 'damage': int, 'range': int, 'fireRate': float, 'ammoCapacity': int, 'reloadTime': float, 'unlockLevel': int, 'price': int, 'description': str, 'maxLevel': int } } elif '敌人' in filename or 'enemy' in filename: self.current_mapping = { 'format_type': 'multi_sheet', 'multi_sheet': True, 'param_types': { # 基础配置字段 '敌人ID': ('id', str), '敌人名称': ('name', str), '敌人类型': ('type', str), '生命值': ('stats.health', int), '最大生命值': ('stats.maxHealth', int), '防御力': ('stats.defense', int), '移动速度': ('stats.speed', float), # 战斗配置字段 '攻击伤害': ('combat.attackDamage', int), '攻击范围': ('combat.attackRange', int), '攻击速度': ('combat.attackSpeed', float), '能否格挡': ('combat.canBlock', bool), '格挡几率': ('combat.blockChance', float), '格挡伤害减免': ('combat.blockDamageReduction', float), '攻击冷却': ('combat.attackCooldown', float), '攻击类型': ('combat.attackType', str), '攻击延迟': ('combat.attackDelay', float), '武器类型': ('combat.weaponType', str), '投射物类型': ('combat.projectileType', str), '投射物速度': ('combat.projectileSpeed', float), # 移动配置字段 '移动模式': ('movement.pattern', str), '巡逻范围': ('movement.patrolRange', int), '追击范围': ('movement.chaseRange', int), '旋转速度': ('movement.rotationSpeed', float), '移动类型': ('movement.moveType', str), '摆动幅度': ('movement.swingAmplitude', float), '摆动频率': ('movement.swingFrequency', float), '速度变化': ('movement.speedVariation', float), # 视觉配置字段 '精灵路径': ('visualConfig.spritePath', str), '缩放比例': ('visualConfig.scale', float), '动画速度': ('visualConfig.animationSpeed', float), '水平翻转': ('visualConfig.flipX', bool), '待机动画': ('visualConfig.animations.idle', str), '行走动画': ('visualConfig.animations.walk', str), '攻击动画': ('visualConfig.animations.attack', str), '死亡动画': ('visualConfig.animations.death', str), '武器道具': ('visualConfig.weaponProp', str), # 音频配置字段 '攻击音效': ('audioConfig.attackSound', str), '死亡音效': ('audioConfig.deathSound', str), '受击音效': ('audioConfig.hitSound', str), '行走音效': ('audioConfig.walkSound', str), '格挡音效': ('audioConfig.blockSound', str), '隐身音效': ('audioConfig.stealthSound', str), '护甲破碎音效': ('audioConfig.armorBreakSound', str), '引信音效': ('audioConfig.fuseSound', str), # 特殊能力配置字段 '能力类型': ('specialAbilities.type', str), '伤害': ('specialAbilities.damage', int), '范围': ('specialAbilities.range', float), '冷却时间': ('specialAbilities.cooldown', float), # BOSS配置字段 '是否BOSS': ('bossConfig.isBoss', bool), '阶段数': ('bossConfig.phases', int), '狂暴阈值': ('bossConfig.enrageThreshold', float), '狂暴伤害倍数': ('bossConfig.enrageDamageMultiplier', float), '狂暴速度倍数': ('bossConfig.enrageSpeedMultiplier', float), # 英文字段支持 'id': ('id', str), 'name': ('name', str), 'type': ('type', str), 'health': ('stats.health', int), 'maxHealth': ('stats.maxHealth', int), 'defense': ('stats.defense', int), 'speed': ('stats.speed', float), 'attackDamage': ('combat.attackDamage', int), 'attackRange': ('combat.attackRange', int), 'attackSpeed': ('combat.attackSpeed', float) } } elif '技能' in filename or 'skill' in filename: self.current_mapping = { 'format_type': 'horizontal', 'param_types': { 'ID': str, '名称': str, '伤害': int, '冷却时间': float, '消耗': int, '描述': str, # 英文字段支持 'skillId': str, 'skillName': str, 'damage': int, 'cooldown': float, 'cost': int, 'description': str } } elif '关卡' in filename or 'level' in filename: self.current_mapping = { 'format_type': 'horizontal', 'param_types': { 'ID': str, '名称': str, '敌人列表': str, '波数': int, '难度': int, # 英文字段支持 'levelId': str, 'name': str, 'enemies': str, 'waves': int, 'difficulty': int } } elif 'ballcontroller' in filename or '球控制' in filename or '标准配置表' in filename: # BallController配置映射(纵向格式:参数名-数值) self.current_mapping = { 'format_type': 'vertical', '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, 'maxAttempts': int } } else: # 默认配置映射 self.current_mapping = { 'format_type': 'horizontal', 'param_types': {} } print(f"为文件 {filename} 设置配置映射: {self.current_mapping['format_type']} 格式") print(f"支持的参数类型: {list(self.current_mapping['param_types'].keys())}") 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: # 检查file_config的类型,如果是列表则特殊处理 if isinstance(file_config, list): # 对于关卡配置等返回列表的情况 self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置项:\n") for i, item in enumerate(file_config): self.preview_text.insert(tk.END, f" 配置项 {i+1}: {item}\n") # 将列表数据存储到config_data中 self.config_data[file_path.stem] = file_config else: # 原有的字典处理逻辑 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: # 根据用户选择的"多工作表文件"选项决定处理方式 if self.multi_sheet_var.get(): # 多工作表模式:读取所有工作表 print(f"多工作表模式处理文件: {file_path.name}") # 获取所有工作表名称(避免缓存问题) try: # 先尝试读取第一个工作表来获取所有工作表名称 excel_file = pd.ExcelFile(file_path) sheet_names = excel_file.sheet_names excel_file.close() # 关闭文件句柄 print(f"发现工作表: {sheet_names}") except Exception as e: print(f"获取工作表名称失败: {e}") return config all_sheets_data = {} for sheet_name in sheet_names: try: # 每次都重新读取文件,避免缓存 df = pd.read_excel(file_path, sheet_name=sheet_name, engine='openpyxl') all_sheets_data[sheet_name] = df print(f"成功读取工作表: {sheet_name}, 数据行数: {len(df)}") # 打印第一行数据用于调试 if len(df) > 0 and sheet_name == '敌人基础配置': print(f"敌人基础配置第一行数据: {df.iloc[0].to_dict()}") except Exception as e: print(f"读取工作表 {sheet_name} 时出错: {e}") # 根据工作表内容判断配置类型 if any('关卡' in name for name in sheet_names): config = self.parse_level_multi_sheet_data(all_sheets_data, file_path.name) elif any('技能' in name for name in sheet_names): config = self.parse_skill_multi_sheet_data(all_sheets_data, file_path.name) elif any('敌人' in name for name in sheet_names): config = self.parse_enemy_multi_sheet_data(all_sheets_data, file_path.name) elif any('武器' in name for name in sheet_names): config = self.parse_weapon_multi_sheet_data(all_sheets_data, file_path.name) else: # 通用多工作表处理 config = self.parse_multi_sheet_data(all_sheets_data, file_path.name) else: # 单工作表模式:读取第一个工作表或指定工作表 print(f"单工作表模式处理文件: {file_path.name}") 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) print(f"读取指定工作表: {sheet_name}, 数据行数: {len(df)}") else: df = pd.read_excel(file_path) print(f"读取默认工作表, 数据行数: {len(df)}") 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_level_multi_sheet_data(self, all_sheets_data, filename): """解析关卡配置的多工作表数据""" config = [] try: # 获取各个工作表的数据(支持中英文工作表名) basic_config = None for sheet_name in ['关卡基础配置', 'Level Config', 'levels', '关卡配置']: if sheet_name in all_sheets_data: basic_config = all_sheets_data[sheet_name] break weapon_config = None for sheet_name in ['关卡武器配置', 'Level Weapon Config', 'level_weapons', '武器配置']: if sheet_name in all_sheets_data: weapon_config = all_sheets_data[sheet_name] break wave_config = None for sheet_name in ['关卡波次配置', 'Wave Config', 'waves', '波次配置']: if sheet_name in all_sheets_data: wave_config = all_sheets_data[sheet_name] break enemy_config = None for sheet_name in ['敌人详细配置', 'Enemy Detail Config', 'enemy_details', '敌人配置']: if sheet_name in all_sheets_data: enemy_config = all_sheets_data[sheet_name] break if basic_config is None: print("错误: 未找到关卡基础配置工作表(支持的工作表名: 关卡基础配置, Level Config, levels, 关卡配置)") return config # 处理每个关卡 for _, basic_row in basic_config.iterrows(): if pd.isna(basic_row['关卡ID']): continue level_id = str(basic_row['关卡ID']) # 获取武器配置 available_weapons = [] if weapon_config is not None: weapon_rows = weapon_config[weapon_config['关卡ID'] == level_id] for _, weapon_row in weapon_rows.iterrows(): if pd.notna(weapon_row['可用武器']): weapons_str = str(weapon_row['可用武器']) # 支持多种分隔符 import re weapons = re.split(r'[、,,;;]', weapons_str) available_weapons.extend([w.strip() for w in weapons if w.strip()]) level_data = { 'levelId': level_id, 'name': str(basic_row['关卡名称']) if pd.notna(basic_row['关卡名称']) else '', 'scene': str(basic_row['场景']) if pd.notna(basic_row['场景']) else 'grassland', 'description': str(basic_row['描述']) if pd.notna(basic_row['描述']) else '', 'backgroundImage': str(basic_row['关卡背景图路径']) if pd.notna(basic_row['关卡背景图路径']) else 'images/LevelBackground/BG1', 'availableWeapons': available_weapons, 'coinReward': int(basic_row['钞票奖励']) if pd.notna(basic_row['钞票奖励']) else 100, 'diamondReward': int(basic_row['钻石奖励']) if pd.notna(basic_row['钻石奖励']) else 0, 'timeLimit': 300, # 默认值 'difficulty': 'normal', # 默认值 'healthMultiplier': float(basic_row['生命倍数']) if pd.notna(basic_row['生命倍数']) else 1.0, # 从Excel读取生命倍数 'waves': [] } # 获取该关卡的波次配置 if wave_config is not None: level_waves = wave_config[wave_config['关卡ID'] == level_id] for _, wave_row in level_waves.iterrows(): wave_id = int(wave_row['波次ID']) if pd.notna(wave_row['波次ID']) else 1 # 获取该波次的敌人配置 wave_enemies = [] if enemy_config is not None: wave_enemy_data = enemy_config[ (enemy_config['关卡ID'] == level_id) & (enemy_config['波次ID'] == wave_id) ] for _, enemy_row in wave_enemy_data.iterrows(): enemy_type = str(enemy_row['敌人类型']) if pd.notna(enemy_row['敌人类型']) else '普通僵尸' enemy_data = { 'enemyType': enemy_type, 'count': int(enemy_row['数量']) if pd.notna(enemy_row['数量']) else 1, 'spawnInterval': float(enemy_row['生成间隔']) if pd.notna(enemy_row['生成间隔']) else 2.0, 'spawnDelay': float(enemy_row['生成延迟']) if pd.notna(enemy_row['生成延迟']) else 0.0, 'characteristics': str(enemy_row['特征描述']) if pd.notna(enemy_row['特征描述']) else '' } wave_enemies.append(enemy_data) wave_data = { 'waveId': wave_id, 'enemies': wave_enemies } level_data['waves'].append(wave_data) config.append(level_data) print(f"处理关卡: {level_id}, 波次数: {len(level_data['waves'])}") print(f"成功解析关卡配置,共 {len(config)} 个关卡") except Exception as e: print(f"解析关卡多工作表数据时出错: {e}") import traceback traceback.print_exc() return config def parse_skill_multi_sheet_data(self, all_sheets_data, filename): """解析技能配置的多工作表数据""" skills_config = [] try: # 检查是否为局外技能配置表格式(包含技能类型配置、技能组配置、基本配置) if '技能类型配置' in all_sheets_data and '技能组配置' in all_sheets_data and '基本配置' in all_sheets_data: print("检测到局外技能配置表格式,使用集成的技能配置导入工具处理") # 使用集成的SkillConfigImporter处理 try: skill_importer = SkillConfigImporter(self.excel_dir) success = skill_importer.import_config() if success: print("技能配置导入成功!") # 返回成功标识,让调用者知道已经处理完成 return [{'status': 'imported', 'message': '技能配置已成功导入到skill_config.json'}] else: print("技能配置导入失败") return [{'status': 'error', 'message': '技能配置导入失败'}] except Exception as e: print(f"技能配置导入过程中出错: {e}") return [{'status': 'error', 'message': f'技能配置导入出错: {e}'}] return skills_config # 获取技能信息表数据(支持中英文工作表名) skill_info = None for sheet_name in ['技能信息表', 'Skill Config', 'skills', '技能配置']: if sheet_name in all_sheets_data: skill_info = all_sheets_data[sheet_name] break if skill_info is None: print("错误: 未找到技能信息表工作表(支持的工作表名: 技能信息表, Skill Config, skills, 技能配置)") return skills_config # 处理每个技能(支持中英文字段名) for _, skill_row in skill_info.iterrows(): # 获取技能ID(支持中英文字段名) skill_id_field = None for field in ['id', '技能ID', 'skill_id', 'ID']: if field in skill_row and pd.notna(skill_row[field]): skill_id_field = field break if skill_id_field is None: continue def get_skill_field_value(row, cn_field, en_fields, default_value, value_type=str): """获取技能字段值,支持中英文字段名""" for field in [cn_field] + (en_fields if isinstance(en_fields, list) else [en_fields]): if field in row and pd.notna(row[field]): try: return value_type(row[field]) except (ValueError, TypeError): continue return default_value skill_data = { 'id': str(skill_row[skill_id_field]), 'name': get_skill_field_value(skill_row, '技能名称', ['name', 'skill_name'], ''), 'description': get_skill_field_value(skill_row, '技能描述', ['description', 'desc'], ''), 'iconPath': get_skill_field_value(skill_row, '图标路径', ['iconPath', 'icon_path', 'icon'], ''), 'maxLevel': get_skill_field_value(skill_row, '最大等级', ['maxLevel', 'max_level'], 5, int), 'currentLevel': get_skill_field_value(skill_row, '当前等级', ['currentLevel', 'current_level'], 0, int), 'priceReduction': get_skill_field_value(skill_row, '价格减少', ['priceReduction', 'price_reduction'], 0.0, float), 'critChanceIncrease': get_skill_field_value(skill_row, '暴击几率增加', ['critChanceIncrease', 'crit_chance_increase'], 0.0, float), 'critDamageBonus': get_skill_field_value(skill_row, '暴击伤害加成', ['critDamageBonus', 'crit_damage_bonus'], 0.0, float), 'healthIncrease': get_skill_field_value(skill_row, '生命值增加', ['healthIncrease', 'health_increase'], 0.0, float), 'multiShotChance': get_skill_field_value(skill_row, '多重射击几率', ['multiShotChance', 'multi_shot_chance'], 0.0, float), 'energyGainIncrease': get_skill_field_value(skill_row, '能量加成', ['energyGainIncrease', 'energy_gain_increase'], 0.0, float), 'ballSpeedIncrease': get_skill_field_value(skill_row, '速度提升', ['ballSpeedIncrease', 'ball_speed_increase'], 0.0, float) } skills_config.append(skill_data) print(f"处理技能: {skill_data['id']} - {skill_data['name']}") print(f"成功解析技能配置,共 {len(skills_config)} 个技能") except Exception as e: print(f"解析技能多工作表数据时出错: {e}") import traceback traceback.print_exc() return skills_config def parse_enemy_multi_sheet_data(self, all_sheets_data, filename): """解析敌人配置的多工作表数据""" enemies_config = [] try: # 获取敌人基础配置数据(支持中英文工作表名) enemy_info = None for sheet_name in ['敌人基础配置', 'Enemy Config', 'enemies', '敌人配置']: if sheet_name in all_sheets_data: enemy_info = all_sheets_data[sheet_name] break if enemy_info is None: print("错误: 未找到敌人基础配置工作表(支持的工作表名: 敌人基础配置, Enemy Config, enemies, 敌人配置)") return enemies_config # 获取其他配置工作表 combat_sheet = all_sheets_data.get('战斗配置') movement_sheet = all_sheets_data.get('移动配置') visual_sheet = all_sheets_data.get('视觉配置') audio_sheet = all_sheets_data.get('音频配置') special_sheet = all_sheets_data.get('特殊能力配置') boss_sheet = all_sheets_data.get('BOSS配置') def get_field_value(row, cn_field, en_fields, default_value, value_type=str): """获取字段值,支持中英文字段名""" for field in [cn_field] + (en_fields if isinstance(en_fields, list) else [en_fields]): if field in row and pd.notna(row[field]): try: return value_type(row[field]) except (ValueError, TypeError): continue return default_value def find_row_by_id(sheet, enemy_id): """根据敌人ID查找对应行""" if sheet is None: return None for _, row in sheet.iterrows(): if str(row.get('敌人ID', '')) == enemy_id: return row return None # 处理每个敌人(支持中英文字段名) for _, enemy_row in enemy_info.iterrows(): # 获取敌人ID(支持中英文字段名) enemy_id_field = None for field in ['id', '敌人ID', 'enemy_id', 'ID']: if field in enemy_row and pd.notna(enemy_row[field]): enemy_id_field = field break if enemy_id_field is None: continue enemy_id = str(enemy_row[enemy_id_field]) # 基础配置(不包含攻击伤害,攻击伤害从战斗配置表获取) enemy_data = { 'id': enemy_id, 'name': get_field_value(enemy_row, '敌人名称', ['name', 'enemy_name'], ''), 'type': get_field_value(enemy_row, '敌人类型', ['type', 'enemy_type'], 'basic'), 'health': get_field_value(enemy_row, '生命值', ['health', 'hp'], 100, int), 'speed': get_field_value(enemy_row, '移动速度', ['speed', 'move_speed'], 1.0, float), 'attackRange': get_field_value(enemy_row, '攻击范围', ['attackRange', 'attack_range', 'range'], 1.0, float), 'attackSpeed': get_field_value(enemy_row, '攻击速度', ['attackSpeed', 'attack_speed'], 1.0, float), 'defense': get_field_value(enemy_row, '防御力', ['defense'], 0, int), 'attackDamage': 10 # 默认值,将从战斗配置表覆盖 } # 获取战斗配置 combat_row = find_row_by_id(combat_sheet, enemy_id) if combat_row is not None: enemy_data.update({ 'attackDamage': get_field_value(combat_row, '攻击伤害', ['attackDamage', 'attack'], 10, int), 'attackType': get_field_value(combat_row, '攻击类型', ['attackType'], 'melee'), 'attackDelay': get_field_value(combat_row, '攻击延迟', ['attackDelay'], 0.5, float), 'weaponType': get_field_value(combat_row, '武器类型', ['weaponType'], 'none'), 'projectileType': get_field_value(combat_row, '投射物类型', ['projectileType'], 'none'), 'projectileSpeed': get_field_value(combat_row, '投射物速度', ['projectileSpeed'], 100, float), 'canBlock': get_field_value(combat_row, '能否格挡', ['canBlock'], False, bool), 'blockChance': get_field_value(combat_row, '格挡几率', ['blockChance'], 0.0, float), 'blockDamageReduction': get_field_value(combat_row, '格挡伤害减免', ['blockDamageReduction'], 0.5, float) }) # 获取移动配置 movement_row = find_row_by_id(movement_sheet, enemy_id) if movement_row is not None: enemy_data.update({ 'movementPattern': get_field_value(movement_row, '移动模式', ['movementPattern'], 'walk_forward'), 'patrolRange': get_field_value(movement_row, '巡逻范围', ['patrolRange'], 100, int), 'chaseRange': get_field_value(movement_row, '追击范围', ['chaseRange'], 200, int), 'rotationSpeed': get_field_value(movement_row, '旋转速度', ['rotationSpeed'], 180, float), 'moveType': get_field_value(movement_row, '移动类型', ['moveType'], 'straight'), 'swingAmplitude': get_field_value(movement_row, '摆动幅度', ['swingAmplitude'], 0.0, float), 'swingFrequency': get_field_value(movement_row, '摆动频率', ['swingFrequency'], 0.0, float), 'speedVariation': get_field_value(movement_row, '速度变化', ['speedVariation'], 0.1, float) }) # 获取视觉配置 visual_row = find_row_by_id(visual_sheet, enemy_id) if visual_row is not None: enemy_data.update({ 'spritePath': get_field_value(visual_row, '精灵路径', ['spritePath'], f'enemies/{enemy_id}'), 'scale': get_field_value(visual_row, '缩放比例', ['scale'], 1.0, float), 'animationSpeed': get_field_value(visual_row, '动画速度', ['animationSpeed'], 1.0, float), 'flipX': get_field_value(visual_row, '水平翻转', ['flipX'], False, bool), 'idleAnimation': get_field_value(visual_row, '待机动画', ['idleAnimation'], 'idle'), 'walkAnimation': get_field_value(visual_row, '行走动画', ['walkAnimation'], 'walk'), 'attackAnimation': get_field_value(visual_row, '攻击动画', ['attackAnimation'], 'attack'), 'deathAnimation': get_field_value(visual_row, '死亡动画', ['deathAnimation'], 'dead'), 'weaponProp': get_field_value(visual_row, '武器道具', ['weaponProp'], '') }) # 获取音频配置 audio_row = find_row_by_id(audio_sheet, enemy_id) if audio_row is not None: enemy_data.update({ 'attackSound': get_field_value(audio_row, '攻击音效', ['attackSound'], 'enemy_attack'), 'deathSound': get_field_value(audio_row, '死亡音效', ['deathSound'], 'enemy_death'), 'hitSound': get_field_value(audio_row, '受击音效', ['hitSound'], 'enemy_hit'), 'walkSound': get_field_value(audio_row, '行走音效', ['walkSound'], ''), 'blockSound': get_field_value(audio_row, '格挡音效', ['blockSound'], ''), 'stealthSound': get_field_value(audio_row, '隐身音效', ['stealthSound'], ''), 'armorBreakSound': get_field_value(audio_row, '护甲破碎音效', ['armorBreakSound'], ''), 'fuseSound': get_field_value(audio_row, '引信音效', ['fuseSound'], '') }) # 获取特殊能力配置 special_abilities = [] if special_sheet is not None: for _, special_row in special_sheet.iterrows(): if str(special_row.get('敌人ID', '')) == enemy_id: ability_type = get_field_value(special_row, '能力类型', ['abilityType'], '') if ability_type: # 只添加有效的特殊能力 special_abilities.append({ 'type': ability_type, 'damage': get_field_value(special_row, '伤害', ['damage'], 0, int), 'range': get_field_value(special_row, '范围', ['range'], 0, float), 'cooldown': get_field_value(special_row, '冷却时间', ['cooldown'], 0, float) }) if special_abilities: enemy_data['specialAbilities'] = special_abilities # 获取BOSS配置 boss_row = find_row_by_id(boss_sheet, enemy_id) if boss_row is not None: is_boss = get_field_value(boss_row, '是否BOSS', ['isBoss'], False, bool) if is_boss: enemy_data['bossConfig'] = { 'isBoss': True, 'phases': get_field_value(boss_row, '阶段数', ['phases'], 1, int), 'enrageThreshold': get_field_value(boss_row, '狂暴阈值', ['enrageThreshold'], 0.3, float), 'enrageDamageMultiplier': get_field_value(boss_row, '狂暴伤害倍数', ['enrageDamageMultiplier'], 1.5, float), 'enrageSpeedMultiplier': get_field_value(boss_row, '狂暴速度倍数', ['enrageSpeedMultiplier'], 1.3, float) } # 转换为嵌套结构 nested_enemy_data = { 'id': enemy_data['id'], 'name': enemy_data['name'], 'type': enemy_data['type'], 'stats': { 'health': enemy_data.get('health', 100), 'maxHealth': enemy_data.get('health', 100), 'defense': enemy_data.get('defense', 0), 'speed': enemy_data.get('speed', 50) }, 'movement': { 'pattern': enemy_data.get('movementPattern', 'direct'), 'speed': enemy_data.get('speed', 50), 'patrolRange': enemy_data.get('patrolRange', 100), 'chaseRange': enemy_data.get('chaseRange', 200), 'rotationSpeed': enemy_data.get('rotationSpeed', 180), 'moveType': enemy_data.get('moveType', 'straight'), 'swingAmplitude': enemy_data.get('swingAmplitude', 0.0), 'swingFrequency': enemy_data.get('swingFrequency', 0.0), 'speedVariation': enemy_data.get('speedVariation', 0.1) }, 'combat': { 'attackDamage': enemy_data.get('attackDamage', 10), 'attackRange': enemy_data.get('attackRange', 100), 'attackSpeed': enemy_data.get('attackSpeed', 1.0), 'canBlock': enemy_data.get('canBlock', False), 'blockChance': enemy_data.get('blockChance', 0.0), 'blockDamageReduction': enemy_data.get('blockDamageReduction', 0.5), 'attackCooldown': enemy_data.get('attackCooldown', 1.0), 'attackType': enemy_data.get('attackType', 'melee'), 'attackDelay': enemy_data.get('attackDelay', 0.5), 'weaponType': enemy_data.get('weaponType', 'none'), 'projectileType': enemy_data.get('projectileType', 'none'), 'projectileSpeed': enemy_data.get('projectileSpeed', 100.0) }, 'visualConfig': { 'spritePath': enemy_data.get('spritePath', f'Animation/EnemyAni/001'), 'scale': enemy_data.get('scale', 1.0), 'animationSpeed': enemy_data.get('animationSpeed', 1.0), 'flipX': enemy_data.get('flipX', False), 'tint': enemy_data.get('tint', '#FFFFFF'), 'animations': { 'idle': 'idle', 'walk': 'walk', 'attack': 'attack', 'death': 'dead' }, 'weaponProp': enemy_data.get('weaponProp', '') }, 'audioConfig': { 'attackSound': enemy_data.get('attackSound', 'enemy_attack'), 'deathSound': enemy_data.get('deathSound', 'enemy_death'), 'hitSound': enemy_data.get('hitSound', 'enemy_hit'), 'walkSound': enemy_data.get('walkSound', ''), 'blockSound': enemy_data.get('blockSound', ''), 'stealthSound': enemy_data.get('stealthSound', ''), 'armorBreakSound': enemy_data.get('armorBreakSound', ''), 'fuseSound': enemy_data.get('fuseSound', ''), 'volume': enemy_data.get('volume', 1.0) } } # 添加特殊能力配置 if 'specialAbilities' in enemy_data: nested_enemy_data['specialAbilities'] = enemy_data['specialAbilities'] # 添加BOSS配置 if 'bossConfig' in enemy_data: nested_enemy_data['bossConfig'] = enemy_data['bossConfig'] enemies_config.append(nested_enemy_data) print(f"处理敌人: {enemy_data['id']} - {enemy_data['name']}") print(f"敌人 {enemy_data['id']} 的生命值: {nested_enemy_data['stats']['health']}") print(f"成功解析敌人配置,共 {len(enemies_config)} 个敌人") except Exception as e: print(f"解析敌人多工作表数据时出错: {e}") import traceback traceback.print_exc() return enemies_config def parse_multi_sheet_data(self, all_sheets_data, filename): """解析通用多工作表数据""" # 处理武器配置表的多工作表 if '方块武器配置表' in filename: return self.parse_weapon_multi_sheet_data(all_sheets_data, filename) # 处理敌人配置表的多工作表 if '敌人配置表' in filename or '敌人' in filename: return self.parse_enemy_multi_sheet_data(all_sheets_data, filename) # 这里可以添加其他多工作表文件的处理逻辑 # 目前只返回第一个工作表的数据 if all_sheets_data: first_sheet_name = list(all_sheets_data.keys())[0] first_sheet_data = all_sheets_data[first_sheet_name] return self.parse_config_data(first_sheet_data, filename) return [] 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())}") print(f"当前配置映射: {self.current_mapping}") print(f"DataFrame列名: {list(df.columns)}") # 如果参数类型为空,尝试从DataFrame列名自动推断 if not param_types: print("参数类型为空,尝试自动推断...") for col_name in df.columns: col_str = str(col_name).strip() if col_str and col_str != 'nan': # 根据列名推断数据类型 if any(keyword in col_str for keyword in ['ID', 'id', '编号']): param_types[col_str] = str elif any(keyword in col_str for keyword in ['名称', 'name', '名字']): param_types[col_str] = str elif any(keyword in col_str for keyword in ['伤害', 'damage', '攻击', 'attack']): param_types[col_str] = int elif any(keyword in col_str for keyword in ['射程', 'range', '范围']): param_types[col_str] = int elif any(keyword in col_str for keyword in ['射速', 'fireRate', '频率']): param_types[col_str] = float elif any(keyword in col_str for keyword in ['价格', 'price', '金币', 'cost']): param_types[col_str] = int elif any(keyword in col_str for keyword in ['等级', 'level', 'Level']): param_types[col_str] = int elif any(keyword in col_str for keyword in ['时间', 'time', 'Time']): param_types[col_str] = float elif any(keyword in col_str for keyword in ['描述', 'description', '说明']): param_types[col_str] = str else: # 默认为字符串类型 param_types[col_str] = str # 更新配置映射 if param_types: self.current_mapping['param_types'] = param_types print(f"自动推断的参数类型: {param_types}") 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 parse_weapon_multi_sheet_data(self, all_sheets_data, filename): """解析武器配置表的多工作表数据""" weapons_config = {'weapons': []} try: # 解析武器基础配置工作表(支持中英文工作表名) base_sheet = None for sheet_name in ['武器基础配置', 'Weapon Config', 'weapons', '武器配置']: if sheet_name in all_sheets_data: base_sheet = all_sheets_data[sheet_name] break if base_sheet is not None: # 设置武器配置的映射 old_mapping = self.current_mapping self.current_mapping = { 'format_type': 'horizontal', 'param_types': { 'ID': str, '名称': str, '类型': str, '稀有度': str, '权重': int, '伤害': int, '射速': float, '射程': int, '子弹速度': int, # 方块价格配置字段 '基础每格成本': int, 'I形状成本': int, 'H-I形状成本': int, 'L形状成本': int, 'S形状成本': int, 'D-T形状成本': int, # 英文字段支持 'id': str, 'name': str, 'type': str, 'rarity': str, 'weight': int, 'damage': int, 'fireRate': float, 'range': int, 'bulletSpeed': int, 'baseCost': int, 'I_shape_cost': int, 'HI_shape_cost': int, 'L_shape_cost': int, 'S_shape_cost': int, 'DT_shape_cost': int } } base_config = self.parse_config_data(base_sheet, filename) if 'items' in base_config: weapons_config['weapons'] = base_config['items'] print(f"成功解析武器基础配置,共{len(base_config['items'])}个武器") # 恢复原来的映射 self.current_mapping = old_mapping # 解析武器升级费用配置工作表(新增支持) upgrade_cost_sheet = None print(f"可用的工作表: {list(all_sheets_data.keys())}") for sheet_name in ['武器升级费用配置', 'Weapon Upgrade Cost', 'upgrade_costs', '升级费用']: if sheet_name in all_sheets_data: upgrade_cost_sheet = all_sheets_data[sheet_name] print(f"找到升级费用配置工作表: {sheet_name}") break if upgrade_cost_sheet is None: print("未找到升级费用配置工作表") if upgrade_cost_sheet is not None: # 直接处理升级费用数据(横向表格格式) print(f"开始处理升级费用配置,工作表行数: {len(upgrade_cost_sheet)}") upgrade_cost_data = [] # 检查第一行是否为表头 first_row = upgrade_cost_sheet.iloc[0] if len(upgrade_cost_sheet) > 0 else None is_header = False if first_row is not None: first_cell = str(first_row.iloc[0]).strip() if len(first_row) > 0 else "" # 如果第一列是"武器ID"等表头文字,则认为是表头 if first_cell in ['武器ID', 'ID', 'weapon_id', 'weaponId']: is_header = True print(f"检测到表头行,第一列内容: {first_cell}") for index, row in upgrade_cost_sheet.iterrows(): if is_header and index == 0: # 只有确认是表头时才跳过第一行 print(f"跳过表头行 {index}") continue # 支持多种武器ID字段名:武器ID、ID、weapon_id等 weapon_id = None for id_field in ['武器ID', 'ID', 'weapon_id', 'weaponId']: if id_field in row and pd.notna(row[id_field]): weapon_id = row[id_field] break # 如果没有找到命名字段,则使用第一列 if weapon_id is None: weapon_id = row.iloc[0] if len(row) > 0 else None print(f"处理行 {index}: weapon_id = {weapon_id}") if weapon_id and str(weapon_id).strip(): # 确保武器ID不为空 upgrade_levels = {} # 从第5列开始是等级1-10的费用(E列到N列),从第15列开始是等级1-10的伤害(O列到X列) for level in range(1, 11): cost_col_index = 4 + (level - 1) # 等级1费用在第5列(索引4),等级2在第6列(索引5),以此类推 damage_col_index = 14 + (level - 1) # 等级1伤害在第15列(索引14),等级2在第16列(索引15),以此类推 level_config = {} # 处理费用 if cost_col_index < len(row): cost = row.iloc[cost_col_index] if cost and str(cost).strip() and str(cost) != 'nan': try: level_config['cost'] = int(float(cost)) print(f" 等级 {level} 费用: {cost}") except (ValueError, TypeError): print(f" 等级 {level} 费用解析失败: {cost}") # 处理伤害 if damage_col_index < len(row): damage = row.iloc[damage_col_index] if damage and str(damage).strip() and str(damage) != 'nan': try: level_config['damage'] = int(float(damage)) print(f" 等级 {level} 伤害: {damage}") except (ValueError, TypeError): print(f" 等级 {level} 伤害解析失败: {damage}") # 只有当有费用或伤害数据时才添加等级配置 if level_config: upgrade_levels[str(level)] = level_config if upgrade_levels: # 只有当有升级费用数据时才添加 upgrade_cost_data.append({ 'weapon_id': str(weapon_id).strip(), 'levels': upgrade_levels }) print(f"为武器 {weapon_id} 添加了 {len(upgrade_levels)} 个等级的升级费用") else: print(f"武器 {weapon_id} 没有有效的升级费用数据") # 将升级费用配置合并到原始武器数据中(在转换之前) print(f"总共解析到 {len(upgrade_cost_data)} 个武器的升级费用配置") # 检查数据结构,支持两种格式:items 或 weapons weapon_list = None if 'items' in weapons_config: weapon_list = weapons_config['items'] print(f"使用items字段,开始合并升级费用配置到 {len(weapon_list)} 个武器") elif 'weapons' in weapons_config: weapon_list = weapons_config['weapons'] print(f"使用weapons字段,开始合并升级费用配置到 {len(weapon_list)} 个武器") else: print("weapons_config中没有items或weapons字段") if weapon_list: for weapon in weapon_list: # 支持两种字段名格式:ID(原始数据)和id(转换后数据) weapon_id = weapon.get('ID', '') or weapon.get('id', '') if weapon_id: # 查找对应的升级费用配置 matching_upgrade = None for upgrade_data in upgrade_cost_data: if upgrade_data['weapon_id'] == weapon_id: matching_upgrade = upgrade_data break if matching_upgrade: weapon['upgradeConfig'] = { 'maxLevel': 10, 'levels': matching_upgrade['levels'] } print(f"✓ 为武器 {weapon_id} 添加了升级费用配置") else: print(f"✗ 武器 {weapon_id} 未找到匹配的升级费用配置") # 解析旧版升级配置工作表(向后兼容) upgrade_sheet = None for sheet_name in ['升级配置', 'Upgrade Config', 'upgrades', '武器升级']: if sheet_name in all_sheets_data: upgrade_sheet = all_sheets_data[sheet_name] break if upgrade_sheet is not None: upgrade_config = self.parse_config_data(upgrade_sheet, filename) # 将升级配置合并到对应的武器中 if 'items' in upgrade_config: upgrade_items = upgrade_config['items'] # 为每个武器添加升级配置 for weapon in weapons_config['weapons']: weapon_id = weapon.get('ID') if weapon_id and 'upgradeConfig' not in weapon: # 如果还没有升级配置 # 查找对应的升级配置 weapon_upgrades = [item for item in upgrade_items if item.get('ID') == weapon_id] if weapon_upgrades: # 构建升级配置结构 upgrade_config_struct = { 'maxLevel': weapon.get('最大等级', 10), 'levels': {} } # 添加每个等级的升级配置 for upgrade in weapon_upgrades: level = upgrade.get('等级', 1) if level: upgrade_config_struct['levels'][str(level)] = { 'cost': upgrade.get('升级费用', 25), 'damageIncrease': upgrade.get('伤害增加', 1) } weapon['upgradeConfig'] = upgrade_config_struct # 解析视觉配置工作表(新增支持) visual_config_sheet = None print(f"查找视觉配置工作表...") for sheet_name in ['视觉配置', 'Visual Config', 'visual_config', '武器视觉配置']: if sheet_name in all_sheets_data: visual_config_sheet = all_sheets_data[sheet_name] print(f"找到视觉配置工作表: {sheet_name}") break if visual_config_sheet is None: print("未找到视觉配置工作表") if visual_config_sheet is not None: # 直接处理视觉配置数据(横向表格格式) print(f"开始处理视觉配置,工作表行数: {len(visual_config_sheet)}") visual_config_data = [] # 检查第一行是否为表头 first_row = visual_config_sheet.iloc[0] if len(visual_config_sheet) > 0 else None is_header = False if first_row is not None: first_cell = str(first_row.iloc[0]).strip() if len(first_row) > 0 else "" # 如果第一列是"武器ID"等表头文字,则认为是表头 if first_cell in ['武器ID', 'ID', 'weapon_id', 'weaponId']: is_header = True print(f"检测到表头行,第一列内容: {first_cell}") for index, row in visual_config_sheet.iterrows(): if is_header and index == 0: # 只有确认是表头时才跳过第一行 print(f"跳过表头行 {index}") continue # 支持多种武器ID字段名:武器ID、ID、weapon_id等 weapon_id = None for id_field in ['武器ID', 'ID', 'weapon_id', 'weaponId']: if id_field in row and pd.notna(row[id_field]): weapon_id = row[id_field] break # 如果没有找到命名字段,则使用第一列 if weapon_id is None: weapon_id = row.iloc[0] if len(row) > 0 else None print(f"处理行 {index}: weapon_id = {weapon_id}") if weapon_id and str(weapon_id).strip(): # 确保武器ID不为空 visual_config = {} # 读取武器精灵配置 weapon_sprites = {} sprite_fields = { 'I': ['I形状精灵', 'I_sprite', 'I形状图片', 'I'], 'H-I': ['H-I形状精灵', 'HI_sprite', 'H-I形状图片', 'H-I'], 'L': ['L形状精灵', 'L_sprite', 'L形状图片', 'L'], 'S': ['S形状精灵', 'S_sprite', 'S形状图片', 'S'], 'D-T': ['D-T形状精灵', 'DT_sprite', 'D-T形状图片', 'D-T'] } for shape, field_names in sprite_fields.items(): for field_name in field_names: if field_name in row and pd.notna(row[field_name]): sprite_path = str(row[field_name]).strip() if sprite_path: weapon_sprites[shape] = sprite_path print(f" {shape}形状精灵: {sprite_path}") break if weapon_sprites: visual_config['weaponSprites'] = weapon_sprites # 读取开火音效 fire_sound_fields = ['开火音效', 'fireSound', '射击音效', '音效'] for field in fire_sound_fields: if field in row and pd.notna(row[field]): fire_sound = str(row[field]).strip() if fire_sound: visual_config['fireSound'] = fire_sound print(f" 开火音效: {fire_sound}") break # 读取拖尾特效 trail_effect_fields = ['拖尾特效', 'trailEffect', '尾迹特效', '特效'] for field in trail_effect_fields: if field in row and pd.notna(row[field]): trail_effect = str(row[field]).strip() if trail_effect and trail_effect.lower() != 'null': visual_config['trailEffect'] = trail_effect print(f" 拖尾特效: {trail_effect}") break if visual_config: # 只有当有视觉配置数据时才添加 visual_config_data.append({ 'weapon_id': str(weapon_id).strip(), 'visualConfig': visual_config }) print(f"为武器 {weapon_id} 添加了视觉配置") else: print(f"武器 {weapon_id} 没有有效的视觉配置数据") # 将视觉配置合并到原始武器数据中 print(f"总共解析到 {len(visual_config_data)} 个武器的视觉配置") weapon_list = None if 'items' in weapons_config: weapon_list = weapons_config['items'] print(f"使用items字段,开始合并视觉配置到 {len(weapon_list)} 个武器") elif 'weapons' in weapons_config: weapon_list = weapons_config['weapons'] print(f"使用weapons字段,开始合并视觉配置到 {len(weapon_list)} 个武器") else: print("weapons_config中没有items或weapons字段") if weapon_list: for weapon in weapon_list: # 支持两种字段名格式:ID(原始数据)和id(转换后数据) weapon_id = weapon.get('ID', '') or weapon.get('id', '') if weapon_id: # 查找对应的视觉配置 matching_visual = None for visual_data in visual_config_data: if visual_data['weapon_id'] == weapon_id: matching_visual = visual_data break if matching_visual: weapon['visualConfig'] = matching_visual['visualConfig'] print(f"✓ 为武器 {weapon_id} 添加了视觉配置: {matching_visual['visualConfig']}") else: print(f"✗ 武器 {weapon_id} 未找到匹配的视觉配置") # 解析游戏内成本配置工作表(新增支持) cost_config_sheet = None print(f"查找游戏内成本配置工作表...") for sheet_name in ['游戏内成本配置', 'In Game Cost Config', 'cost_config', '成本配置']: if sheet_name in all_sheets_data: cost_config_sheet = all_sheets_data[sheet_name] print(f"找到游戏内成本配置工作表: {sheet_name}") break if cost_config_sheet is None: print("未找到游戏内成本配置工作表") if cost_config_sheet is not None: # 直接处理游戏内成本配置数据(横向表格格式) print(f"开始处理游戏内成本配置,工作表行数: {len(cost_config_sheet)}") cost_config_data = [] # 检查第一行是否为表头 first_row = cost_config_sheet.iloc[0] if len(cost_config_sheet) > 0 else None is_header = False if first_row is not None: first_cell = str(first_row.iloc[0]).strip() if len(first_row) > 0 else "" # 如果第一列是"武器ID"等表头文字,则认为是表头 if first_cell in ['武器ID', 'ID', 'weapon_id', 'weaponId']: is_header = True print(f"检测到表头行,第一列内容: {first_cell}") for index, row in cost_config_sheet.iterrows(): if is_header and index == 0: # 只有确认是表头时才跳过第一行 print(f"跳过表头行 {index}") continue # 支持多种武器ID字段名:武器ID、ID、weapon_id等 weapon_id = None for id_field in ['武器ID', 'ID', 'weapon_id', 'weaponId']: if id_field in row and pd.notna(row[id_field]): weapon_id = row[id_field] break # 如果没有找到命名字段,则使用第一列 if weapon_id is None: weapon_id = row.iloc[0] if len(row) > 0 else None print(f"处理行 {index}: weapon_id = {weapon_id}") if weapon_id and str(weapon_id).strip(): # 确保武器ID不为空 cost_config = {} # 读取基础每格成本 base_cost_fields = ['基础每格成本', 'baseCost', '基础成本', '武器基础售价'] for field in base_cost_fields: if field in row and pd.notna(row[field]): try: cost_config['baseCost'] = int(float(row[field])) print(f" 基础每格成本: {cost_config['baseCost']}") break except (ValueError, TypeError): continue # 读取各形状成本 shape_costs = {} shape_fields = { 'I': ['I形状', 'I形状成本', 'I_shape_cost', 'I'], 'H-I': ['H-I形状', 'H-I形状成本', 'HI_shape_cost', 'H-I'], 'L': ['L形状', 'L形状成本', 'L_shape_cost', 'L'], 'S': ['S形状', 'S形状成本', 'S_shape_cost', 'S'], 'D-T': ['D-T形状', 'D-T形状成本', 'DT_shape_cost', 'D-T'] } for shape, field_names in shape_fields.items(): for field_name in field_names: if field_name in row and pd.notna(row[field_name]): try: shape_costs[shape] = int(float(row[field_name])) print(f" {shape}形状成本: {shape_costs[shape]}") break except (ValueError, TypeError): continue if shape_costs: cost_config['shapeCosts'] = shape_costs if cost_config: # 只有当有成本配置数据时才添加 cost_config_data.append({ 'weapon_id': str(weapon_id).strip(), 'costConfig': cost_config }) print(f"为武器 {weapon_id} 添加了游戏内成本配置") else: print(f"武器 {weapon_id} 没有有效的游戏内成本配置数据") # 将游戏内成本配置合并到原始武器数据中 print(f"总共解析到 {len(cost_config_data)} 个武器的游戏内成本配置") weapon_list = None if 'items' in weapons_config: weapon_list = weapons_config['items'] print(f"使用items字段,开始合并游戏内成本配置到 {len(weapon_list)} 个武器") elif 'weapons' in weapons_config: weapon_list = weapons_config['weapons'] print(f"使用weapons字段,开始合并游戏内成本配置到 {len(weapon_list)} 个武器") else: print("weapons_config中没有items或weapons字段") if weapon_list: for weapon in weapon_list: # 支持两种字段名格式:ID(原始数据)和id(转换后数据) weapon_id = weapon.get('ID', '') or weapon.get('id', '') if weapon_id: # 查找对应的游戏内成本配置 matching_cost = None for cost_data in cost_config_data: if cost_data['weapon_id'] == weapon_id: matching_cost = cost_data break if matching_cost: # 直接将成本配置字段添加到武器数据中 weapon.update(matching_cost['costConfig']) print(f"✓ 为武器 {weapon_id} 添加了游戏内成本配置: {matching_cost['costConfig']}") else: print(f"✗ 武器 {weapon_id} 未找到匹配的游戏内成本配置") print(f"武器配置解析完成,共{len(weapons_config['weapons'])}个武器") return weapons_config except Exception as e: print(f"解析武器配置表时出错: {e}") import traceback traceback.print_exc() return {'weapons': []} 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.selected_json_path: self.preview_text.insert(tk.END, "请先选择JSON输出路径\n") return json_path = Path(self.selected_json_path) if json_path.exists(): with open(json_path, 'r', encoding='utf-8') as f: current_config = json.load(f) self.preview_text.insert(tk.END, f"=== 当前配置 ({json_path.name}) ===\n") # 根据配置类型显示不同的预览格式 if self.format_var.get() == '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.selected_json_path: messagebox.showwarning("警告", "请先选择JSON输出路径") return json_path = Path(self.selected_json_path) if not json_path.exists(): messagebox.showwarning("警告", "配置文件不存在") return backup_path = json_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(json_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.selected_json_path: messagebox.showwarning("警告", "请先选择JSON输出路径") return try: print(f"开始导入配置...") print(f"配置数据: {self.config_data}") print(f"输出路径: {self.selected_json_path}") print(f"选中文件: {self.selected_files}") # 优先使用配置映射中的格式类型,如果没有则使用GUI设置 format_type = self.current_mapping.get('format_type', self.format_var.get()) if hasattr(self, 'current_mapping') and self.current_mapping else self.format_var.get() print(f"格式类型: {format_type}") print(f"配置映射格式: {self.current_mapping.get('format_type') if hasattr(self, 'current_mapping') and self.current_mapping else 'None'}") print(f"GUI格式设置: {self.format_var.get()}") if format_type == 'vertical': # 处理纵向表格(如BallController) print("开始处理纵向表格配置...") self._import_vertical_config() elif format_type == 'horizontal': # 处理横向表格(如武器配置) print("开始处理横向表格配置...") self._import_horizontal_config() elif format_type == 'multi_sheet': # 处理多工作表格式(如敌人配置) print("开始处理多工作表配置...") self._import_multi_sheet_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_skill_config(self): """导入技能配置到skill_config.json""" try: # 查找局外技能配置表.xlsx文件 excel_dir = Path(self.excel_dir) skill_excel_file = excel_dir / "局外技能配置表.xlsx" if not skill_excel_file.exists(): messagebox.showwarning("警告", f"未找到技能配置文件: {skill_excel_file}") return # 使用SkillConfigImporter导入配置 importer = SkillConfigImporter(str(excel_dir)) success, message = importer.import_config() if success: messagebox.showinfo("成功", message) self.status_var.set("技能配置导入成功") # 更新预览文本 self.preview_text.delete(1.0, tk.END) self.preview_text.insert(tk.END, f"技能配置导入成功\n\n{message}") else: messagebox.showerror("错误", f"技能配置导入失败: {message}") self.status_var.set("技能配置导入失败") 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.selected_json_path.exists(): with open(self.selected_json_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.selected_json_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'}") # 检查是否是关卡配置(目录类型) is_level_config = str(self.selected_json_path).endswith('levels') or self.selected_json_path.is_dir() # 对于关卡配置,支持多种数据格式 if is_level_config: if isinstance(self.config_data, list): # 多工作表数据格式:直接是关卡数据列表 print("检测到关卡配置的多工作表数据格式") items = None # 不需要items字段 elif 'items' in self.config_data: # 传统的items数据格式 print("检测到关卡配置的传统items数据格式") items = self.config_data['items'] elif isinstance(self.config_data, dict) and len(self.config_data) == 1: # 检查是否是包装在单个键中的配置数据 key = list(self.config_data.keys())[0] wrapped_data = self.config_data[key] print(f"检测到包装的关卡配置数据,键名: {key}") if isinstance(wrapped_data, list): print("提取到关卡配置的多工作表数据格式") self.config_data = wrapped_data # 更新配置数据为实际内容 items = None elif isinstance(wrapped_data, dict) and 'items' in wrapped_data: print("提取到关卡配置的传统items数据格式") self.config_data = wrapped_data items = wrapped_data['items'] else: print(f"错误: 包装数据格式不正确: {type(wrapped_data)}") error_msg = f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n\n包装键: {key}\n数据类型: {type(wrapped_data)}" messagebox.showwarning("警告", error_msg) self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n") self.preview_text.insert(tk.END, f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n") self.preview_text.insert(tk.END, f"包装键: {key}\n数据类型: {type(wrapped_data)}\n") return else: print(f"错误: 关卡配置数据格式不正确") print(f"实际配置数据: {self.config_data}") print(f"配置数据类型: {type(self.config_data)}") print(f"配置数据键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}") error_msg = f"关卡配置数据格式错误:需要多工作表数据或items字段\n\n数据类型: {type(self.config_data)}\n数据内容: {str(self.config_data)[:200]}..." messagebox.showwarning("警告", error_msg) # 同时在GUI中显示错误信息 self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n") self.preview_text.insert(tk.END, f"关卡配置数据格式错误:需要多工作表数据或items字段\n") self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n") self.preview_text.insert(tk.END, f"数据内容: {str(self.config_data)[:500]}...\n") return else: # 其他配置类型:检查数据格式 if isinstance(self.config_data, dict): # 检查是否是多工作表数据格式(如:{'技能配置表': [...]}) if len(self.config_data) == 1: key = list(self.config_data.keys())[0] data_content = self.config_data[key] if isinstance(data_content, list): print(f"检测到多工作表数据格式,键名: {key}") items = data_content elif isinstance(data_content, dict) and 'items' in data_content: print(f"检测到包装的items数据格式,键名: {key}") items = data_content['items'] else: print(f"错误: 无法识别的数据格式") error_msg = f"配置数据格式错误:无法识别的数据结构\n\n键名: {key}\n数据类型: {type(data_content)}" messagebox.showwarning("警告", error_msg) return elif 'items' in self.config_data: print("检测到传统items数据格式") items = self.config_data['items'] else: print(f"错误: 配置数据格式不正确") print(f"实际配置数据: {self.config_data}") print(f"配置数据类型: {type(self.config_data)}") print(f"配置数据键: {list(self.config_data.keys())}") error_msg = f"配置数据格式错误:需要items字段或多工作表数据格式\n\n数据类型: {type(self.config_data)}\n可用键: {list(self.config_data.keys())}" messagebox.showwarning("警告", error_msg) # 同时在GUI中显示错误信息 self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n") self.preview_text.insert(tk.END, f"配置数据格式错误:需要items字段或多工作表数据格式\n") self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n") self.preview_text.insert(tk.END, f"可用键: {list(self.config_data.keys())}\n") return elif isinstance(self.config_data, list): print("检测到直接列表数据格式") items = self.config_data else: print(f"错误: 不支持的数据类型: {type(self.config_data)}") error_msg = f"不支持的数据类型: {type(self.config_data)}" messagebox.showwarning("警告", error_msg) return # 验证数据有效性 if items is not None: print(f"配置项数量: {len(items) if items else 0}") if not items: messagebox.showwarning("警告", "没有有效的配置项") return elif isinstance(self.config_data, list): print(f"关卡配置数量: {len(self.config_data)}") if not self.config_data: messagebox.showwarning("警告", "没有有效的关卡配置") return # 打印前几个配置项用于调试 if items is not None: for i, item in enumerate(items[:3]): print(f"配置项{i}: {item}") elif isinstance(self.config_data, list): for i, item in enumerate(self.config_data[:3]): print(f"关卡配置{i}: {item.get('levelId', 'Unknown')} - {item.get('name', 'Unknown')}") # 读取现有JSON配置 print(f"JSON配置文件路径: {self.selected_json_path}") # 检查是否是关卡配置(目录类型) if is_level_config: print("处理关卡配置目录...") # 关卡配置是目录,确保目录存在 if not self.selected_json_path.exists(): print(f"创建关卡配置目录: {self.selected_json_path}") self.selected_json_path.mkdir(parents=True, exist_ok=True) current_config = {} else: # 普通JSON文件配置 if self.selected_json_path.exists(): print("读取现有JSON配置文件...") with open(self.selected_json_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}") # 获取用户选择的处理模式 is_multi_sheet = self.multi_sheet_var.get() is_single_json = self.single_json_var.get() print(f"处理模式 - 多工作表: {is_multi_sheet}, 单个JSON: {is_single_json}") # 根据数据类型和用户选择的模式进行处理 if is_level_config: # 关卡配置:始终为目录模式,每个关卡一个JSON文件 print("处理关卡配置...") # 关卡配置:为每个关卡创建单独的JSON文件 levels_dir = self.selected_json_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 # 检查数据格式:多工作表数据 vs 传统items数据 if isinstance(self.config_data, list): # 新的多工作表数据格式:直接是关卡数据列表 print("使用多工作表数据格式") level_configs = self.config_data elif 'items' in self.config_data: # 传统的items数据格式:需要转换 print("使用传统items数据格式") level_configs = [] for item in items: level_data = self._convert_level_data(item) if level_data: level_configs.append(level_data) else: print(f"错误: 未知的关卡配置数据格式: {type(self.config_data)}") messagebox.showerror("错误", "关卡配置数据格式错误") return # 保存关卡配置文件 updated_count = 0 failed_count = 0 for level_data in level_configs: try: if level_data and 'levelId' in level_data and level_data['levelId']: level_id = level_data['levelId'] 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"跳过无效的关卡数据: {level_data}") 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("处理非关卡配置...") # 根据数据类型自动识别配置类型 config_type = self._detect_config_type() print(f"检测到配置类型: {config_type}") if config_type == 'unknown': print(f"警告: 无法识别的配置类型") messagebox.showwarning("警告", f"无法识别配置类型\n请检查Excel文件的工作表名称或数据格式") return # 根据配置类型处理数据 if config_type == 'enemy': print("处理敌人配置...") if 'enemies' not in current_config: current_config['enemies'] = [] 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 config_type == 'weapon': print("处理武器配置...") if 'weapons' not in current_config: current_config['weapons'] = [] 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 config_type == 'skill': print("处理技能配置...") if 'skills' not in current_config: current_config['skills'] = [] 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 else: # 通用配置处理 print(f"处理通用配置类型: {config_type}") # 直接使用items数据 current_config['items'] = items # 写入更新后的配置(关卡配置已在前面处理,跳过) if not is_level_config: try: print(f"写入配置文件: {self.selected_json_path}") print(f"配置内容预览: {str(current_config)[:200]}...") with open(self.selected_json_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 _import_multi_sheet_config(self): """导入多工作表配置(直接使用解析后的数据,不再通过_convert_enemy_data转换)""" print(f"多工作表配置导入开始...") print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}") try: # 读取现有JSON配置 if self.selected_json_path.exists(): with open(self.selected_json_path, 'r', encoding='utf-8') as f: current_config = json.load(f) else: current_config = self.default_config.copy() # 直接使用解析后的多工作表数据 if isinstance(self.config_data, list): # 如果config_data是列表(如敌人配置),直接使用 print(f"使用解析后的敌人配置数据,共 {len(self.config_data)} 个敌人") current_config['enemies'] = self.config_data # 添加元数据 current_config['metadata'] = { 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'totalEnemies': len(self.config_data), 'conversionMethod': 'excel_multi_sheet_direct' } elif isinstance(self.config_data, dict): # 如果config_data是字典,合并到current_config print(f"合并字典格式的配置数据") # 特殊处理武器配置,保留现有武器中表格没有的字段 if 'weapons' in self.config_data and 'weapons' in current_config: print("检测到武器配置,执行智能合并以保留现有字段") new_weapons = self.config_data['weapons'] existing_weapons = current_config['weapons'] # 创建现有武器的ID映射 existing_weapons_map = {} for weapon in existing_weapons: weapon_id = weapon.get('ID', '') or weapon.get('id', '') if weapon_id: existing_weapons_map[weapon_id] = weapon # 合并武器配置,保留现有字段 merged_weapons = [] for new_weapon_data in new_weapons: weapon_id = new_weapon_data.get('ID', '') or new_weapon_data.get('id', '') if weapon_id and weapon_id in existing_weapons_map: # 找到对应的现有武器,转换新数据时传入现有武器配置 existing_weapon = existing_weapons_map[weapon_id] # 转换新武器数据,传入现有武器配置以保留特效字段 converted_weapon = self._convert_weapon_data(new_weapon_data, existing_weapon) if converted_weapon: merged_weapons.append(converted_weapon) print(f"✓ 武器 {weapon_id} 已转换并保留了现有的特效字段") else: # 新武器,直接转换 converted_weapon = self._convert_weapon_data(new_weapon_data) if converted_weapon: merged_weapons.append(converted_weapon) print(f"+ 新武器 {weapon_id} 已添加") # 更新武器配置 self.config_data['weapons'] = merged_weapons print(f"武器配置合并完成,共 {len(merged_weapons)} 个武器") current_config.update(self.config_data) # 写入更新后的配置 print(f"写入配置文件: {self.selected_json_path}") print(f"配置内容预览: {str(current_config)[:200]}...") with open(self.selected_json_path, 'w', encoding='utf-8') as f: json.dump(current_config, f, indent=2, ensure_ascii=False) print("多工作表配置文件写入成功") messagebox.showinfo("成功", f"多工作表配置导入成功!\n更新了配置数据") self.status_var.set("多工作表配置导入成功") except Exception as e: print(f"导入多工作表配置失败: {e}") import traceback traceback.print_exc() messagebox.showerror("错误", f"导入多工作表配置失败: {str(e)}") return # 刷新预览 self.clear_selection() def _deep_merge_weapon_config(self, existing_weapon, new_weapon): """深度合并武器配置,保留现有武器中表格没有的字段""" import copy # 创建新武器的深拷贝作为基础 merged = copy.deepcopy(new_weapon) # 递归合并函数,将现有武器中新武器没有的字段添加到合并结果中 def preserve_existing_fields(target, existing): for key, value in existing.items(): if key not in target: # 现有武器有但新武器没有的字段,保留 target[key] = copy.deepcopy(value) elif isinstance(target[key], dict) and isinstance(value, dict): # 如果两者都是字典,递归处理 preserve_existing_fields(target[key], value) # 执行深度合并,保留现有武器中的额外字段 preserve_existing_fields(merged, existing_weapon) return merged def _detect_config_type(self): """根据数据内容和工作表名称自动识别配置类型""" try: # 检查是否是多工作表数据 if self.multi_sheet_var.get() and hasattr(self, 'config_data') and isinstance(self.config_data, dict): # 多工作表模式:根据工作表名称判断 if '敌人基础配置' in self.config_data or '敌人配置' in str(self.config_data.keys()): return 'enemy' elif '技能信息表' in self.config_data or '技能配置' in str(self.config_data.keys()): return 'skill' elif '武器配置' in str(self.config_data.keys()) or '武器信息' in self.config_data: return 'weapon' elif any('关卡' in str(key) for key in self.config_data.keys()): return 'level' # 检查数据内容中的字段来判断类型 if hasattr(self, 'config_data'): sample_data = None # 获取样本数据 if isinstance(self.config_data, dict) and 'items' in self.config_data: items = self.config_data['items'] if items and len(items) > 0: sample_data = items[0] elif isinstance(self.config_data, list) and len(self.config_data) > 0: sample_data = self.config_data[0] if sample_data and isinstance(sample_data, dict): # 根据字段名判断配置类型 fields = set(sample_data.keys()) # 敌人配置特征字段 enemy_fields = {'enemyId', 'name', 'hp', 'attack', 'defense', 'speed'} if enemy_fields.intersection(fields): return 'enemy' # 技能配置特征字段 skill_fields = {'skillId', 'skillName', 'damage', 'cooldown', 'description'} if skill_fields.intersection(fields): return 'skill' # 武器配置特征字段 weapon_fields = {'weaponId', 'weaponName', 'damage', 'range', 'fireRate'} if weapon_fields.intersection(fields): return 'weapon' # 关卡配置特征字段 level_fields = {'levelId', 'name', 'enemies', 'waves'} if level_fields.intersection(fields): return 'level' # 如果无法通过数据内容判断,尝试通过文件名判断(作为后备方案) if hasattr(self, 'selected_files') and self.selected_files: filename = Path(self.selected_files[0]).name.lower() if '敌人' in filename: return 'enemy' elif '技能' in filename: return 'skill' elif '武器' in filename: return 'weapon' elif '关卡' in filename: return 'level' return 'unknown' except Exception as e: print(f"配置类型检测出错: {e}") return 'unknown' def _convert_enemy_data(self, item): """转换敌人数据格式 - 支持嵌套结构""" try: print(f"开始转换敌人数据: {item}") # 支持中英文字段名 enemy_id = item.get('id', item.get('敌人ID', '')) enemy_name = item.get('name', item.get('敌人名称', '')) enemy_type = item.get('type', item.get('敌人类型', '')) print(f"敌人ID: {enemy_id}, 敌人名称: {enemy_name}, 类型: {enemy_type}") # 检查必要字段 if not enemy_id: print(f"跳过无效敌人数据: 缺少敌人ID - {item}") return None # 获取基础属性 health = item.get('health', item.get('生命值', 100)) speed = item.get('speed', item.get('移动速度', 50)) attack_damage = item.get('attackDamage', item.get('攻击伤害', 10)) attack_range = item.get('range', item.get('攻击范围', 100)) attack_speed = item.get('attackSpeed', item.get('攻击速度', 1.0)) defense = item.get('defense', item.get('防御力', 0)) # 调试信息 print(f"转换敌人 {enemy_id} 的属性: health={health}, speed={speed}, attackDamage={attack_damage}, defense={defense}") print(f"原始数据中的字段: {list(item.keys())}") if '生命值' in item: print(f"找到生命值字段,值为: {item['生命值']}") if 'health' in item: print(f"找到health字段,值为: {item['health']}") # 获取格挡相关属性 can_block = item.get('canBlock', item.get('可格挡', False)) block_chance = item.get('blockChance', item.get('格挡概率', 0.0)) block_damage_reduction = item.get('blockDamageReduction', item.get('格挡伤害减免', 0.5)) # 获取移动相关属性 movement_pattern = item.get('movementPattern', item.get('移动模式', 'direct')) patrol_range = item.get('patrolRange', item.get('巡逻范围', 100)) chase_range = item.get('chaseRange', item.get('追击范围', 200)) # 获取视觉配置 # 敌人ID到动画编号的映射 enemy_to_ani = { 'normal_zombie': '001', 'roadblock_zombie': '002', 'wandering_zombie': '003', 'mage_zombie': '004', 'archer_zombie': '005', 'stealth_zombie': '006', 'bucket_zombie': '007', 'barrel_zombie': '008', 'boss1_gatekeeper': '009', 'boss2_gravedigger': '010', 'boss3_cyborg': '011' } ani_num = enemy_to_ani.get(enemy_id, '001') default_sprite_path = f'Animation/EnemyAni/{ani_num}' sprite_path = item.get('spritePath', item.get('精灵路径', default_sprite_path)) scale = item.get('scale', item.get('缩放', 1.0)) # 获取音效配置 attack_sound = item.get('attackSound', item.get('攻击音效', 'enemy_attack')) death_sound = item.get('deathSound', item.get('死亡音效', 'enemy_death')) # 构建嵌套结构的配置 result = { 'id': enemy_id, 'name': enemy_name, 'type': enemy_type, 'stats': { 'health': health, 'maxHealth': health, 'defense': defense, 'speed': speed }, 'movement': { 'pattern': movement_pattern, 'speed': speed, 'patrolRange': patrol_range, 'chaseRange': chase_range, 'rotationSpeed': item.get('rotationSpeed', item.get('旋转速度', 180)), 'moveType': item.get('moveType', item.get('移动类型', 'straight')), 'swingAmplitude': item.get('swingAmplitude', item.get('摆动幅度', 0.0)), 'swingFrequency': item.get('swingFrequency', item.get('摆动频率', 0.0)), 'speedVariation': item.get('speedVariation', item.get('速度变化', 0.1)) }, 'combat': { 'attackDamage': attack_damage, 'attackRange': attack_range, 'attackSpeed': attack_speed, 'canBlock': can_block, 'blockChance': block_chance, 'blockDamageReduction': block_damage_reduction, 'attackCooldown': 1.0 / attack_speed if attack_speed > 0 else 1.0, 'attackType': item.get('attackType', item.get('攻击类型', 'melee')), 'attackDelay': item.get('attackDelay', item.get('攻击延迟', 0.5)), 'weaponType': item.get('weaponType', item.get('武器类型', 'none')), 'projectileType': item.get('projectileType', item.get('投射物类型', 'none')), 'projectileSpeed': item.get('projectileSpeed', item.get('投射物速度', 100)) }, 'visualConfig': { 'spritePath': sprite_path, 'scale': scale, 'animationSpeed': item.get('animationSpeed', item.get('动画速度', 1.0)), 'flipX': item.get('flipX', item.get('水平翻转', False)), 'tint': item.get('tint', item.get('着色', '#FFFFFF')), 'animations': { 'idle': item.get('idleAnimation', item.get('待机动画', 'idle')), 'walk': item.get('walkAnimation', item.get('行走动画', 'walk')), 'attack': item.get('attackAnimation', item.get('攻击动画', 'attack')), 'death': item.get('deathAnimation', item.get('死亡动画', 'dead')) }, 'weaponProp': item.get('weaponProp', item.get('武器道具', '')) }, 'audioConfig': { 'attackSound': attack_sound, 'deathSound': death_sound, 'hitSound': item.get('hitSound', item.get('受击音效', 'enemy_hit')), 'walkSound': item.get('walkSound', item.get('行走音效', '')), 'blockSound': item.get('blockSound', item.get('格挡音效', '')), 'stealthSound': item.get('stealthSound', item.get('隐身音效', '')), 'armorBreakSound': item.get('armorBreakSound', item.get('护甲破碎音效', '')), 'fuseSound': item.get('fuseSound', item.get('引信音效', '')), 'volume': item.get('volume', item.get('音量', 1.0)) } } # 添加特殊能力配置 special_abilities = item.get('specialAbilities', []) if special_abilities: result['specialAbilities'] = special_abilities # 添加BOSS配置 boss_config = item.get('bossConfig', {}) if boss_config and boss_config.get('isBoss', False): result['bossConfig'] = boss_config # 根据敌人类型添加特殊配置 result = self._add_enemy_special_config(result, enemy_type, item) print(f"成功转换敌人数据: {enemy_id}") return result except Exception as e: print(f"转换敌人数据失败: {e} - 数据: {item}") return None def _add_enemy_special_config(self, result, enemy_type, item): """根据敌人类型添加特殊配置""" try: # 根据敌人类型添加特殊能力和配置 special_abilities = [] if enemy_type == 'stealth_zombie': # 隐身僵尸特殊配置 special_abilities.append({ 'type': 'stealth', 'duration': item.get('stealthDuration', item.get('隐身持续时间', 3.0)), 'cooldown': item.get('stealthCooldown', item.get('隐身冷却时间', 8.0)), 'alpha': item.get('stealthAlpha', item.get('隐身透明度', 0.3)) }) result['stealthConfig'] = { 'canStealth': True, 'stealthDuration': item.get('stealthDuration', item.get('隐身持续时间', 3.0)), 'stealthCooldown': item.get('stealthCooldown', item.get('隐身冷却时间', 8.0)), 'stealthAlpha': item.get('stealthAlpha', item.get('隐身透明度', 0.3)) } elif enemy_type == 'bucket_zombie': # 铁桶僵尸特殊配置 result['armorConfig'] = { 'hasArmor': True, 'armorHealth': item.get('armorHealth', item.get('护甲血量', 50)), 'armorDefense': item.get('armorDefense', item.get('护甲防御', 5)), 'armorSprite': item.get('armorSprite', item.get('护甲精灵', 'bucket_armor')) } elif enemy_type == 'explosive_zombie': # 火药桶僵尸特殊配置 special_abilities.append({ 'type': 'explosion', 'damage': item.get('explosionDamage', item.get('爆炸伤害', 30)), 'radius': item.get('explosionRadius', item.get('爆炸半径', 80)), 'triggerOnDeath': True }) result['explosiveConfig'] = { 'explosionDamage': item.get('explosionDamage', item.get('爆炸伤害', 30)), 'explosionRadius': item.get('explosionRadius', item.get('爆炸半径', 80)), 'fuseTime': item.get('fuseTime', item.get('引爆时间', 2.0)), 'chainExplosion': item.get('chainExplosion', item.get('连锁爆炸', False)) } elif enemy_type == 'ranged_enemy': # 远程敌人特殊配置 result['rangedConfig'] = { 'projectileType': item.get('projectileType', item.get('弹药类型', 'bullet')), 'projectileSpeed': item.get('projectileSpeed', item.get('弹药速度', 100)), 'projectileDamage': item.get('projectileDamage', item.get('弹药伤害', item.get('attack', item.get('攻击力', 10)))), 'fireRate': item.get('fireRate', item.get('射速', 1.0)), 'accuracy': item.get('accuracy', item.get('精度', 0.9)) } elif 'boss' in enemy_type.lower(): # BOSS特殊配置 special_abilities.extend([ { 'type': 'charge_attack', 'damage': item.get('chargeDamage', item.get('冲锋伤害', item.get('attack', item.get('攻击力', 10)) * 2)), 'range': item.get('chargeRange', item.get('冲锋范围', 150)), 'cooldown': item.get('chargeCooldown', item.get('冲锋冷却', 8.0)) }, { 'type': 'area_attack', 'damage': item.get('areaDamage', item.get('范围伤害', item.get('attack', item.get('攻击力', 10)) * 1.5)), 'radius': item.get('areaRadius', item.get('范围半径', 100)), 'cooldown': item.get('areaCooldown', item.get('范围攻击冷却', 12.0)) } ]) result['bossConfig'] = { 'isBoss': True, 'phases': item.get('phases', item.get('阶段数', 1)), 'enrageThreshold': item.get('enrageThreshold', item.get('狂暴阈值', 0.3)), 'enrageDamageMultiplier': item.get('enrageDamageMultiplier', item.get('狂暴伤害倍数', 1.5)), 'enrageSpeedMultiplier': item.get('enrageSpeedMultiplier', item.get('狂暴速度倍数', 1.3)) } # 添加特殊能力到结果中 if special_abilities: result['specialAbilities'] = special_abilities return result except Exception as e: print(f"添加特殊配置失败: {e}") return result def _convert_weapon_data(self, item, existing_weapon=None): """转换武器数据格式""" try: print(f"开始转换武器数据: {item}") # 支持中英文字段名 weapon_id = item.get('id', item.get('ID', '')) weapon_name = item.get('name', item.get('名称', '')) print(f"武器ID: {weapon_id}, 武器名称: {weapon_name}") # 检查必要字段 if not weapon_id: print(f"跳过无效武器数据: 缺少武器ID - {item}") return None # 获取基础属性 damage = item.get('damage', item.get('伤害', 10)) fire_rate = item.get('fireRate', item.get('射速', 1.0)) weapon_range = item.get('range', item.get('射程', 100)) bullet_speed = item.get('bulletSpeed', item.get('子弹速度', 100)) weapon_type = item.get('type', item.get('类型', '')) rarity = item.get('rarity', item.get('稀有度', '')) weight = item.get('weight', item.get('权重', 1)) # 根据武器ID推断武器类型和稀有度(如果为空) if not weapon_type: if 'shotgun' in weapon_id or 'cactus' in weapon_id: weapon_type = 'shotgun' elif 'bomb' in weapon_id or 'pepper' in weapon_id: weapon_type = 'explosive' elif 'missile' in weapon_id: weapon_type = 'homing_missile' elif 'boomerang' in weapon_id: weapon_type = 'boomerang' elif 'saw' in weapon_id: weapon_type = 'ricochet_piercing' elif 'carrot' in weapon_id: weapon_type = 'piercing' else: weapon_type = 'single_shot' if not rarity: if damage >= 60: rarity = 'epic' elif damage >= 40: rarity = 'rare' elif damage >= 25: rarity = 'uncommon' else: rarity = 'common' # 根据稀有度设置权重 if weight == 1: # 默认权重,需要根据稀有度调整 rarity_weights = {'common': 30, 'uncommon': 20, 'rare': 15, 'epic': 8} weight = rarity_weights.get(rarity, 20) result = { 'id': weapon_id, 'name': weapon_name, 'type': weapon_type, 'rarity': rarity, 'weight': weight, 'stats': { 'damage': damage, 'fireRate': fire_rate, 'range': weapon_range, 'bulletSpeed': min(bullet_speed, 50) # 限制子弹速度 }, 'bulletConfig': self._generate_bullet_config(weapon_id, weapon_type, damage, weapon_range, existing_weapon, item), 'visualConfig': self._generate_visual_config(weapon_id, weapon_name, existing_weapon) } # 如果原始数据中有upgradeConfig,则添加到结果中 if 'upgradeConfig' in item: result['upgradeConfig'] = item['upgradeConfig'] print(f"为武器 {weapon_id} 保留了升级配置: {item['upgradeConfig']}") else: print(f"武器 {weapon_id} 没有升级配置数据") # 处理方块价格配置(inGameCostConfig) # 优先使用从游戏内成本配置工作表导入的数据 if 'baseCost' in item or 'shapeCosts' in item: in_game_cost_config = { 'baseCost': item.get('baseCost', 10), 'shapeCosts': item.get('shapeCosts', {}) } print(f"为武器 {weapon_id} 使用Excel导入的方块价格配置: {in_game_cost_config}") else: # 如果没有导入数据,则生成默认配置 in_game_cost_config = self._generate_in_game_cost_config(item, weapon_id, rarity) print(f"为武器 {weapon_id} 生成默认方块价格配置: {in_game_cost_config}") if in_game_cost_config: result['inGameCostConfig'] = in_game_cost_config print(f"成功转换武器数据: {result}") return result except Exception as e: print(f"转换武器数据失败: {e} - 数据: {item}") return None def _generate_bullet_config(self, weapon_id, weapon_type, damage, weapon_range, existing_weapon=None, item=None): """生成子弹配置""" # 从existing_weapon中获取特效配置,保留现有的burnEffect和explosionEffect trail_effect = True burn_effect = None explosion_effect = None if existing_weapon: # 从现有武器的bulletConfig.visual中获取特效配置 if 'bulletConfig' in existing_weapon and 'visual' in existing_weapon['bulletConfig']: existing_visual = existing_weapon['bulletConfig']['visual'] if 'burnEffect' in existing_visual: burn_effect = existing_visual['burnEffect'] print(f"为武器 {weapon_id} 保留现有的灼烧特效: {burn_effect}") if 'explosionEffect' in existing_visual: explosion_effect = existing_visual['explosionEffect'] print(f"为武器 {weapon_id} 保留现有的爆炸特效: {explosion_effect}") if 'trailEffect' in existing_visual: trail_effect = existing_visual['trailEffect'] print(f"为武器 {weapon_id} 保留现有的拖尾特效: {trail_effect}") # 从现有武器的visualConfig中获取特效(作为备选) if not trail_effect and 'visualConfig' in existing_weapon: visual_config = existing_weapon['visualConfig'] if 'trailEffect' in visual_config: trail_effect = visual_config['trailEffect'] print(f"为武器 {weapon_id} 从visualConfig保留拖尾特效: {trail_effect}") # 构建visual配置,保留现有特效 visual_config = { 'bulletPrefab': f'bullets/{weapon_id.title()}Bullet', 'hitEffect': 'Animation/WeaponTx/tx0002/tx0002', 'trailEffect': trail_effect } # 添加保留的特效字段 if burn_effect is not None: visual_config['burnEffect'] = burn_effect if explosion_effect is not None: visual_config['explosionEffect'] = explosion_effect # 基础配置模板 base_config = { 'count': {'type': 'single', 'amount': 1, 'spreadAngle': 0, 'burstCount': 1, 'burstDelay': 0}, 'trajectory': {'type': 'straight', 'speed': 200, 'gravity': 0, 'arcHeight': 0, 'homingStrength': 0, 'homingDelay': 0}, 'hitEffects': [{'type': 'normal_damage', 'priority': 1, 'damage': damage}], 'lifecycle': {'type': 'hit_destroy', 'maxLifetime': 5.0, 'penetration': 1, 'ricochetCount': 0, 'returnToOrigin': False}, 'visual': visual_config } # 根据武器类型调整配置 if weapon_type == 'shotgun': base_config['count'] = {'type': 'spread', 'amount': 5, 'spreadAngle': 30, 'burstCount': 1, 'burstDelay': 0} base_config['lifecycle']['type'] = 'range_limit' base_config['lifecycle']['maxRange'] = weapon_range * 2 elif weapon_type == 'piercing': base_config['hitEffects'] = [{'type': 'pierce_damage', 'priority': 1, 'damage': damage, 'pierceCount': 999}] base_config['lifecycle'] = {'type': 'range_limit', 'maxLifetime': 5.0, 'penetration': 999, 'ricochetCount': 0, 'returnToOrigin': False, 'maxRange': weapon_range * 2} base_config['visual']['trailEffect'] = 'Animation/WeaponTx/tx0001/tx0001' elif weapon_type == 'explosive': base_config['trajectory']['type'] = 'arc' base_config['hitEffects'] = [{'type': 'explosion', 'priority': 1, 'damage': damage + 20, 'radius': 100, 'delay': 0.1}] base_config['lifecycle']['type'] = 'ground_impact' base_config['visual']['hitEffect'] = 'Animation/WeaponTx/tx0007/tx0007' # 为爆炸类武器设置默认爆炸特效(如果没有保留的话) if explosion_effect is None: base_config['visual']['explosionEffect'] = 'Animation/WeaponTx/tx0007/tx0007' elif weapon_type == 'homing_missile': base_config['trajectory'] = {'type': 'homing', 'speed': 20, 'gravity': 0.2, 'arcHeight': 100, 'homingStrength': 0.8, 'homingDelay': 0.3} base_config['hitEffects'] = [{'type': 'explosion', 'priority': 1, 'damage': damage, 'radius': 150, 'delay': 0}] base_config['lifecycle']['type'] = 'target_impact' # 为导弹类武器设置默认爆炸特效(如果没有保留的话) if explosion_effect is None: base_config['visual']['explosionEffect'] = 'Animation/WeaponTx/tx0007/tx0007' elif weapon_type == 'boomerang': base_config['trajectory'] = {'type': 'homing', 'speed': 15, 'gravity': 0, 'homingStrength': 0.5, 'homingDelay': 0.3} base_config['hitEffects'] = [{'type': 'pierce_damage', 'priority': 1, 'damage': damage, 'pierceCount': 999}] base_config['lifecycle'] = {'type': 'return_trip', 'maxLifetime': 10.0, 'penetration': 999, 'ricochetCount': 0, 'returnToOrigin': True, 'returnDelay': 1.0} elif weapon_type == 'ricochet_piercing': base_config['hitEffects'] = [ {'type': 'ricochet_damage', 'priority': 1, 'damage': damage, 'ricochetCount': 2, 'ricochetAngle': 45}, {'type': 'pierce_damage', 'priority': 2, 'damage': damage, 'pierceCount': 3} ] base_config['lifecycle'] = {'type': 'ricochet_counter', 'maxLifetime': 8.0, 'penetration': 3, 'ricochetCount': 3, 'returnToOrigin': False} elif weapon_type == 'area_burn': # 从item中获取灼烧效果配置 if item: burn_damage = item.get('灼烧伤害值', item.get('burnDamage', 5)) burn_duration = item.get('灼烧持续时间', item.get('burnDuration', 3.0)) burn_interval = item.get('灼烧间隔', item.get('burnInterval', 0.5)) base_config['hitEffects'] = [ {'type': 'normal_damage', 'priority': 1, 'damage': damage}, {'type': 'ground_burn', 'priority': 2, 'damage': burn_damage, 'duration': burn_duration, 'tickInterval': burn_interval} ] # 为灼烧类武器设置默认灼烧特效(如果没有保留的话) if burn_effect is None: base_config['visual']['burnEffect'] = 'Animation/WeaponTx/tx0006/tx0006' return base_config def _generate_in_game_cost_config(self, item, weapon_id, rarity): """从Excel表中读取方块价格配置""" try: # 从游戏内成本配置表中读取价格配置 cost_config = self._load_in_game_cost_config(weapon_id) if cost_config: base_cost_per_grid = cost_config.get('基础每格成本', 10) shape_costs = { 'I': cost_config.get('I形状', base_cost_per_grid * 2), 'H-I': cost_config.get('H-I形状', base_cost_per_grid * 2), 'L': cost_config.get('L形状', base_cost_per_grid * 3), 'S': cost_config.get('S形状', base_cost_per_grid * 4), 'D-T': cost_config.get('D-T形状', base_cost_per_grid * 4) } print(f"为武器 {weapon_id} 从Excel表中读取方块价格配置") else: # 如果在Excel表中找不到配置,使用默认值 print(f"警告:武器 {weapon_id} 在游戏内成本配置表中未找到,使用默认配置") base_cost_per_grid = 10 shape_costs = { 'I': 20, 'H-I': 20, 'L': 30, 'S': 40, 'D-T': 40 } return { 'baseCost': base_cost_per_grid, 'shapeCosts': shape_costs } except Exception as e: print(f"生成方块价格配置失败: {e} - 武器: {weapon_id}") # 返回默认配置 return { 'baseCost': 10, 'shapeCosts': { 'I': 20, 'H-I': 20, 'L': 30, 'S': 40, 'D-T': 40 } } def _load_in_game_cost_config(self, weapon_id): """从Excel表中加载指定武器的游戏内成本配置""" try: excel_file = self.script_dir / "方块武器配置" / "方块武器配置表.xlsx" if not excel_file.exists(): print(f"Excel文件不存在: {excel_file}") return None # 读取游戏内成本配置工作表 excel_data = pd.read_excel(excel_file, sheet_name="游戏内成本配置") # 查找指定武器ID的配置 for index, row in excel_data.iterrows(): if index == 0: # 跳过表头 continue # 获取武器ID(支持多种字段名) current_weapon_id = None for id_field in ['武器ID', 'ID', 'weapon_id', 'weaponId']: if id_field in row and pd.notna(row[id_field]): current_weapon_id = str(row[id_field]).strip() break # 如果没有找到命名字段,使用第一列 if current_weapon_id is None: current_weapon_id = str(row.iloc[0]).strip() if len(row) > 0 else None if current_weapon_id == weapon_id: # 构建成本配置 cost_config = {} # 读取基础每格成本 base_cost_fields = ['基础每格成本', 'baseCost', '基础成本', '武器基础售价'] for field in base_cost_fields: if field in row and pd.notna(row[field]): try: cost_config['基础每格成本'] = int(float(row[field])) break except (ValueError, TypeError): continue # 读取各形状成本 shape_fields = { 'I形状': ['I形状', 'I形状成本', 'I_shape_cost', 'I'], 'H-I形状': ['H-I形状', 'H-I形状成本', 'HI_shape_cost', 'H-I'], 'L形状': ['L形状', 'L形状成本', 'L_shape_cost', 'L'], 'S形状': ['S形状', 'S形状成本', 'S_shape_cost', 'S'], 'D-T形状': ['D-T形状', 'D-T形状成本', 'DT_shape_cost', 'D-T'] } for shape_key, field_names in shape_fields.items(): for field_name in field_names: if field_name in row and pd.notna(row[field_name]): try: cost_config[shape_key] = int(float(row[field_name])) break except (ValueError, TypeError): continue return cost_config if cost_config else None print(f"未找到武器 {weapon_id} 的游戏内成本配置") return None except Exception as e: print(f"加载游戏内成本配置失败: {e}") return None def _generate_visual_config(self, weapon_id, weapon_name, existing_weapon=None): """生成视觉配置""" # 根据武器ID生成图片编号 weapon_sprite_map = { 'pea_shooter': '001-1', 'sharp_carrot': '002', 'saw_grass': '003', 'watermelon_bomb': '007', 'boomerang_plant': '004', 'hot_pepper': '005', 'cactus_shotgun': '008', 'okra_missile': '006', 'mace_club': '009' } sprite_id = weapon_sprite_map.get(weapon_id, '001') visual_config = { 'weaponSprites': { 'I': f'images/PlantsSprite/{sprite_id}', 'H-I': f'images/PlantsSprite/{sprite_id}', 'L': f'images/PlantsSprite/{sprite_id}', 'S': f'images/PlantsSprite/{sprite_id}', 'D-T': f'images/PlantsSprite/{sprite_id}' }, 'fireSound': f'audio/{weapon_id}_shot' } # 从existing_weapon中获取特效配置,保留现有的特效字段 if existing_weapon: # 保留现有visualConfig中的特效字段 if 'visualConfig' in existing_weapon: existing_visual = existing_weapon['visualConfig'] # 保留trailEffect if 'trailEffect' in existing_visual: visual_config['trailEffect'] = existing_visual['trailEffect'] print(f"为武器 {weapon_id} 在visualConfig中保留拖尾特效: {existing_visual['trailEffect']}") # 保留burnEffect if 'burnEffect' in existing_visual: visual_config['burnEffect'] = existing_visual['burnEffect'] print(f"为武器 {weapon_id} 在visualConfig中保留灼烧特效: {existing_visual['burnEffect']}") # 保留explosionEffect if 'explosionEffect' in existing_visual: visual_config['explosionEffect'] = existing_visual['explosionEffect'] print(f"为武器 {weapon_id} 在visualConfig中保留爆炸特效: {existing_visual['explosionEffect']}") # 保留其他可能的特效字段 for effect_key in ['muzzleFlash', 'chargeEffect', 'reloadEffect']: if effect_key in existing_visual: visual_config[effect_key] = existing_visual[effect_key] print(f"为武器 {weapon_id} 在visualConfig中保留{effect_key}: {existing_visual[effect_key]}") return visual_config def _convert_skill_data(self, item): """转换技能数据格式""" try: # 支持多种字段名格式(中文和英文) skill_id = item.get('id', item.get('技能ID', '')) skill_name = item.get('name', item.get('技能名称', '')) print(f"正在转换技能数据: {skill_id} - {skill_name}") # 检查必要字段 if not skill_id: print(f"跳过无效技能数据: 缺少技能ID - {item}") return None result = { 'id': skill_id, 'name': skill_name, 'description': item.get('description', item.get('技能描述', '')), 'iconPath': item.get('iconPath', item.get('图标路径', '')), 'maxLevel': item.get('maxLevel', item.get('最大等级', 1)), 'currentLevel': item.get('currentLevel', item.get('当前等级', 0)), 'priceReduction': item.get('priceReduction', item.get('价格减少', 0.0)), 'critChanceIncrease': item.get('critChanceIncrease', item.get('暴击几率增加', 0.0)), 'critDamageBonus': item.get('critDamageBonus', item.get('暴击伤害加成', 0.0)), 'healthIncrease': item.get('healthIncrease', item.get('生命值增加', 0.0)), 'multiShotChance': item.get('multiShotChance', item.get('多重射击几率', 0.0)), 'energyGainIncrease': item.get('energyGainIncrease', item.get('能量加成', 0.0)), 'ballSpeedIncrease': item.get('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: # 如果item已经是完整的关卡数据结构(来自多工作表解析),直接返回 if isinstance(item, dict) and 'waves' in item: return item # 处理传统的单行数据格式 # 处理可用武器字符串,转换为数组(支持中英文字段名) available_weapons = [] weapons_field = item.get('weapons', item.get('可用武器', '')) if weapons_field: weapons_str = str(weapons_field) # 支持逗号、顿号、分号等多种分隔符 for separator in ['、', ',', ',', ';', ';']: if separator in weapons_str: available_weapons = [weapon.strip() for weapon in weapons_str.split(separator)] break # 如果没有找到分隔符,则作为单个武器处理 if not available_weapons: available_weapons = [weapons_str.strip()] # 获取关卡ID,用于读取现有配置(支持中英文字段名) level_id = str(item.get('levelId', item.get('关卡ID', ''))) # 尝试读取现有的关卡配置文件,保留waves数据 existing_waves = [] existing_data = {} if level_id: try: levels_dir = self.project_root / "assets/resources/data/levels" level_file = levels_dir / f"{level_id}.json" if level_file.exists(): with open(level_file, 'r', encoding='utf-8') as f: existing_data = json.load(f) existing_waves = existing_data.get('waves', []) print(f"保留现有关卡 {level_id} 的 {len(existing_waves)} 个波次数据") except Exception as e: print(f"读取现有关卡配置时出错: {e}") # 构建新的关卡数据,保留现有的waves(支持中英文字段名) level_data = { "levelId": level_id, "name": str(item.get('name', item.get('关卡名称', existing_data.get('name', '')))), "scene": str(item.get('scene', item.get('场景', existing_data.get('scene', '')))), "description": str(item.get('description', item.get('描述', existing_data.get('description', '')))), "backgroundImage": str(item.get('backgroundImage', item.get('关卡背景图路径', existing_data.get('backgroundImage', 'images/LevelBackground/BG1')))), "weapons": available_weapons if available_weapons else existing_data.get('weapons', existing_data.get('availableWeapons', [])), "timeLimit": int(item.get('timeLimit', item.get('时间限制', existing_data.get('timeLimit', 300)))), "difficulty": str(item.get('difficulty', item.get('难度', existing_data.get('difficulty', 'normal')))), "healthMultiplier": float(item.get('healthMultiplier', item.get('生命倍数', existing_data.get('healthMultiplier', 1.0)))), "waves": existing_waves # 保留现有的waves数据 } # 添加可选字段(如果存在,支持中英文字段名) coin_reward = item.get('coinReward', item.get('钞票奖励')) if coin_reward is not None: level_data["coinReward"] = int(coin_reward) elif 'coinReward' in existing_data: level_data["coinReward"] = existing_data['coinReward'] diamond_reward = item.get('diamondReward', item.get('钻石奖励')) if diamond_reward is not None: level_data["diamondReward"] = int(diamond_reward) elif 'diamondReward' in existing_data: level_data["diamondReward"] = existing_data['diamondReward'] return level_data except Exception as e: print(f"转换关卡数据时出错: {e}") return None def fix_enemies_json(self): """修复enemies.json文件 - 删除attack字段""" try: # 获取enemies.json路径 enemies_json_path = Path(self.json_path_var.get()) if self.json_path_var.get() else None if not enemies_json_path or not enemies_json_path.exists(): # 尝试默认路径 enemies_json_path = Path(os.path.dirname(__file__)).parent / "enemies.json" if not enemies_json_path.exists(): messagebox.showerror("错误", "未找到enemies.json文件") return False # 备份原文件 backup_path = f"{enemies_json_path}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" with open(enemies_json_path, 'r', encoding='utf-8') as src: with open(backup_path, 'w', encoding='utf-8') as dst: dst.write(src.read()) # 读取JSON文件 with open(enemies_json_path, 'r', encoding='utf-8') as f: data = json.load(f) # 递归删除attack字段 def remove_attack_fields(obj): if isinstance(obj, dict): if 'attack' in obj: del obj['attack'] for value in obj.values(): remove_attack_fields(value) elif isinstance(obj, list): for item in obj: remove_attack_fields(item) remove_attack_fields(data) # 写回文件 with open(enemies_json_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) messagebox.showinfo("成功", f"已修复enemies.json文件\n备份文件: {backup_path}\n已删除所有attack字段") return True except Exception as e: messagebox.showerror("错误", f"修复enemies.json失败: {str(e)}") return False def auto_import_enemies_from_excel(self): """自动从Excel导入敌人配置""" try: # 查找敌人配置表.xlsx excel_dir = Path(os.path.dirname(__file__)) excel_file = excel_dir / "敌人配置表.xlsx" if not excel_file.exists(): messagebox.showerror("错误", f"未找到敌人配置表.xlsx文件: {excel_file}") return False # 读取Excel文件 if not PANDAS_AVAILABLE: messagebox.showerror("错误", "pandas未安装,无法读取Excel文件") return False all_sheets = pd.read_excel(excel_file, sheet_name=None) # 解析敌人配置 enemies_config = self.parse_enemy_multi_sheet_data(all_sheets, "敌人配置表.xlsx") if not enemies_config: messagebox.showerror("错误", "未能解析到任何敌人配置") return False # 获取输出路径 enemies_json_path = Path(self.json_path_var.get()) if self.json_path_var.get() else None if not enemies_json_path: enemies_json_path = excel_dir.parent / "enemies.json" # 备份现有文件 if enemies_json_path.exists(): backup_path = f"{enemies_json_path}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" with open(enemies_json_path, 'r', encoding='utf-8') as src: with open(backup_path, 'w', encoding='utf-8') as dst: dst.write(src.read()) # 写入新配置 with open(enemies_json_path, 'w', encoding='utf-8') as f: json.dump(enemies_config, f, ensure_ascii=False, indent=2) messagebox.showinfo("成功", f"已成功导入{len(enemies_config)}个敌人配置到: {enemies_json_path}") return True except Exception as e: messagebox.showerror("错误", f"自动导入敌人配置失败: {str(e)}") return False def restore_default_config(self): """恢复默认配置""" result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!") if not result: return try: if not self.selected_json_path: messagebox.showwarning("警告", "请先选择JSON输出路径") return json_path = Path(self.selected_json_path) # 备份当前配置 if json_path.exists(): backup_path = json_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" with open(json_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(json_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()