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