| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095 |
- #!/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
-
- # 检查参数是否有效
- param_types = self.current_mapping.get('param_types', {})
- if param_name in param_types:
- try:
- # 优先使用第3列(默认值),如果不存在则使用第2列
- param_value = row.iloc[2] if len(row) > 2 and not pd.isna(row.iloc[2]) else row.iloc[1]
-
- if pd.isna(param_value):
- continue
-
- param_type = param_types[param_name]
- if param_type == bool:
- config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
- else:
- config[param_name] = param_type(param_value)
- except (ValueError, TypeError, IndexError):
- continue
-
- elif format_type == 'horizontal':
- # 横向表格:第一行是参数名(列名),数据从第0行开始
- print(f"横向表格解析: 总行数={len(df)}")
- print(f"表格列名: {list(df.columns)}")
-
- # 打印前几行数据用于调试
- for i in range(min(3, len(df))):
- print(f"第{i}行数据: {df.iloc[i].to_dict()}")
-
- # 检查第0行是否为有效数据行
- # 如果第0行第一个单元格是描述性文字或空值,则跳过
- data_start_row = 0
- if len(df) > 0:
- first_cell = str(df.iloc[0, 0]).strip().lower()
- # 跳过描述行、空行或标题行
- if (first_cell in ['唯一标识符', '描述', 'description', 'desc', 'nan', ''] or
- first_cell == df.columns[0].lower()):
- data_start_row = 1
- print(f"跳过第0行(描述行或标题行): {first_cell}")
-
- print(f"数据起始行: {data_start_row}")
-
- # 解析多行数据(如敌人配置、武器配置、关卡配置等)
- config_list = []
- for i in range(data_start_row, len(df)):
- row_config = {}
- row_has_data = False
-
- param_types = self.current_mapping.get('param_types', {})
- print(f"可用的参数类型: {list(param_types.keys())}")
-
- for col_idx, col_name in enumerate(df.columns):
- param_name = str(col_name).strip()
- print(f"检查列名: '{param_name}' 是否在参数类型中")
- if param_name in param_types:
- try:
- param_value = df.iloc[i, col_idx]
- if pd.isna(param_value) or str(param_value).strip() == '':
- print(f"跳过空值: {param_name} = {param_value}")
- continue
-
- param_type = param_types[param_name]
- if param_type == bool:
- row_config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
- else:
- row_config[param_name] = param_type(param_value)
- row_has_data = True
- print(f"成功转换字段: {param_name} = {param_value} ({param_type})")
- except (ValueError, TypeError, IndexError) as e:
- print(f"转换字段 {param_name} 时出错: {e}")
- continue
- else:
- print(f"字段 '{param_name}' 不在参数类型定义中,跳过")
-
- if row_config and row_has_data: # 只添加非空且有有效数据的配置
- config_list.append(row_config)
- print(f"成功解析第{i}行,包含{len(row_config)}个字段")
- else:
- print(f"跳过第{i}行(无有效数据)")
-
- print(f"总共解析出{len(config_list)}个有效配置项")
-
- # 对于横向表格,返回配置列表
- config = {'items': config_list}
-
- except Exception as e:
- print(f"解析文件 {filename} 时出错: {e}")
-
- return config
-
- def clear_selection(self):
- """清空选择"""
- self.file_listbox.selection_clear(0, tk.END)
- self.preview_text.delete(1.0, tk.END)
- self.config_data = {}
- self.status_var.set("已清空选择")
- self.load_current_config()
-
- def load_current_config(self):
- """加载当前配置"""
- try:
- if not self.json_config_path:
- self.preview_text.insert(tk.END, "请先选择配置文件\n")
- return
-
- if self.json_config_path.exists():
- with open(self.json_config_path, 'r', encoding='utf-8') as f:
- current_config = json.load(f)
-
- self.preview_text.insert(tk.END, f"=== 当前配置 ({self.json_config_path.name}) ===\n")
-
- # 根据配置类型显示不同的预览格式
- if self.current_mapping and self.current_mapping['format_type'] == 'horizontal':
- # 横向表格配置(如敌人、武器)
- if 'enemies' in current_config:
- self.preview_text.insert(tk.END, f"敌人配置 ({len(current_config['enemies'])} 个):\n")
- for i, enemy in enumerate(current_config['enemies'][:5]): # 只显示前5个
- self.preview_text.insert(tk.END, f" {i+1}. {enemy.get('name', enemy.get('id', 'Unknown'))}\n")
- if len(current_config['enemies']) > 5:
- self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['enemies']) - 5} 个\n")
-
- if 'weapons' in current_config:
- self.preview_text.insert(tk.END, f"\n武器配置 ({len(current_config['weapons'])} 个):\n")
- for i, weapon in enumerate(current_config['weapons'][:5]): # 只显示前5个
- self.preview_text.insert(tk.END, f" {i+1}. {weapon.get('name', weapon.get('id', 'Unknown'))}\n")
- if len(current_config['weapons']) > 5:
- self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['weapons']) - 5} 个\n")
- else:
- # 纵向表格配置(如BallController)
- self.preview_text.insert(tk.END, json.dumps(current_config, indent=2, ensure_ascii=False))
-
- file_hint = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
- self.preview_text.insert(tk.END, f"\n\n请选择{file_hint}进行配置导入...\n")
- else:
- self.preview_text.insert(tk.END, "配置文件不存在\n")
- except Exception as e:
- self.preview_text.insert(tk.END, f"加载当前配置失败: {e}\n")
-
- def backup_config(self):
- """备份当前配置"""
- try:
- if not self.json_config_path.exists():
- messagebox.showwarning("警告", "配置文件不存在")
- return
-
- backup_path = self.json_config_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
-
- with open(self.json_config_path, 'r', encoding='utf-8') as f:
- content = f.read()
-
- with open(backup_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}")
- self.status_var.set(f"配置已备份")
-
- except Exception as e:
- messagebox.showerror("错误", f"备份失败: {e}")
-
- def import_config(self):
- """导入配置到JSON文件"""
- if not self.config_data:
- messagebox.showwarning("警告", "没有配置数据可导入")
- return
-
- if not self.current_mapping:
- messagebox.showwarning("警告", "未检测到有效的配置映射")
- return
-
- try:
- print(f"开始导入配置...")
- print(f"配置数据: {self.config_data}")
- print(f"当前映射: {self.current_mapping}")
- print(f"选中文件: {self.selected_files}")
-
- format_type = self.current_mapping['format_type']
- print(f"格式类型: {format_type}")
-
- if format_type == 'vertical':
- # 处理纵向表格(如BallController)
- print("开始处理纵向表格配置...")
- self._import_vertical_config()
- elif format_type == 'horizontal':
- # 处理横向表格(如敌人配置、武器配置)
- print("开始处理横向表格配置...")
- self._import_horizontal_config()
- else:
- raise ValueError(f"未知的格式类型: {format_type}")
-
- except Exception as e:
- import traceback
- error_details = traceback.format_exc()
- print(f"导入配置失败,详细错误信息:")
- print(error_details)
- messagebox.showerror("错误", f"导入配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
-
- def _import_vertical_config(self):
- """导入纵向表格配置"""
- # 读取现有JSON配置
- if self.json_config_path.exists():
- with open(self.json_config_path, 'r', encoding='utf-8') as f:
- current_config = json.load(f)
- else:
- current_config = self.default_config.copy()
-
- # 合并配置
- updated_count = 0
- for key, value in self.config_data.items():
- if key in current_config and current_config[key] != value:
- current_config[key] = value
- updated_count += 1
- elif key not in current_config:
- current_config[key] = value
- updated_count += 1
-
- # 写入更新后的配置
- with open(self.json_config_path, 'w', encoding='utf-8') as f:
- json.dump(current_config, f, indent=2, ensure_ascii=False)
-
- messagebox.showinfo("成功", f"配置导入成功!\n更新了 {updated_count} 个参数")
- self.status_var.set("配置导入成功")
-
- # 刷新预览
- self.clear_selection()
-
- def _import_horizontal_config(self):
- """导入横向表格配置"""
- print(f"横向表格配置导入开始...")
- print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
-
- if 'items' not in self.config_data:
- print(f"错误: 配置数据中缺少'items'字段")
- print(f"实际配置数据: {self.config_data}")
- messagebox.showwarning("警告", "横向表格数据格式错误:缺少'items'字段")
- return
-
- items = self.config_data['items']
- print(f"配置项数量: {len(items) if items else 0}")
- if not items:
- messagebox.showwarning("警告", "没有有效的配置项")
- return
-
- # 打印前几个配置项用于调试
- for i, item in enumerate(items[:3]):
- print(f"配置项{i}: {item}")
-
- # 读取现有JSON配置
- print(f"JSON配置文件路径: {self.json_config_path}")
-
- # 检查是否是关卡配置(目录类型)
- if str(self.json_config_path).endswith('levels'):
- print("处理关卡配置目录...")
- # 关卡配置是目录,确保目录存在
- if not self.json_config_path.exists():
- print(f"创建关卡配置目录: {self.json_config_path}")
- self.json_config_path.mkdir(parents=True, exist_ok=True)
- current_config = {}
- else:
- # 普通JSON文件配置
- if self.json_config_path.exists():
- print("读取现有JSON配置文件...")
- with open(self.json_config_path, 'r', encoding='utf-8') as f:
- current_config = json.load(f)
- else:
- print("创建新的JSON配置...")
- current_config = {}
-
- # 根据不同的配置类型处理
- if not self.selected_files:
- print("错误: 没有选中的文件")
- messagebox.showerror("错误", "没有选中的文件")
- return
-
- filename = Path(self.selected_files[0]).name
- print(f"处理文件: {filename}")
-
- if '敌人配置' in filename:
- print("处理敌人配置...")
- # 敌人配置:更新enemies数组
- if 'enemies' not in current_config:
- current_config['enemies'] = []
-
- # 将Excel数据转换为JSON格式
- updated_enemies = []
- for i, item in enumerate(items):
- print(f"转换敌人数据 {i+1}/{len(items)}: {item}")
- enemy_data = self._convert_enemy_data(item)
- if enemy_data:
- updated_enemies.append(enemy_data)
- print(f"成功转换敌人数据: {enemy_data['id']}")
- else:
- print(f"跳过无效的敌人数据: {item}")
-
- print(f"总共转换了 {len(updated_enemies)} 个敌人配置")
- current_config['enemies'] = updated_enemies
-
- elif '武器配置' in filename:
- print("处理武器配置...")
- # 武器配置:更新weapons数组
- if 'weapons' not in current_config:
- current_config['weapons'] = []
-
- # 将Excel数据转换为JSON格式
- updated_weapons = []
- for i, item in enumerate(items):
- print(f"转换武器数据 {i+1}/{len(items)}: {item}")
- weapon_data = self._convert_weapon_data(item)
- if weapon_data:
- updated_weapons.append(weapon_data)
- print(f"成功转换武器数据: {weapon_data.get('id', 'Unknown')}")
- else:
- print(f"跳过无效的武器数据: {item}")
-
- print(f"总共转换了 {len(updated_weapons)} 个武器配置")
- current_config['weapons'] = updated_weapons
-
- elif '技能配置' in filename:
- print("处理技能配置...")
- # 技能配置:更新skills数组
- if 'skills' not in current_config:
- current_config['skills'] = []
-
- # 将Excel数据转换为JSON格式
- updated_skills = []
- for i, item in enumerate(items):
- print(f"转换技能数据 {i+1}/{len(items)}: {item}")
- skill_data = self._convert_skill_data(item)
- if skill_data:
- updated_skills.append(skill_data)
- print(f"成功转换技能数据: {skill_data.get('id', 'Unknown')}")
- else:
- print(f"跳过无效的技能数据: {item}")
-
- print(f"总共转换了 {len(updated_skills)} 个技能配置")
- current_config['skills'] = updated_skills
-
- elif '关卡配置' in filename:
- print("处理关卡配置...")
- # 关卡配置:为每个关卡创建单独的JSON文件
- levels_dir = self.json_config_path
- print(f"关卡配置目录: {levels_dir}")
-
- try:
- # 检查并创建目录
- if not levels_dir.exists():
- print(f"创建关卡配置目录: {levels_dir}")
- levels_dir.mkdir(parents=True, exist_ok=True)
- else:
- print(f"关卡配置目录已存在: {levels_dir}")
-
- # 测试目录写入权限
- test_file = levels_dir / "test_permission.tmp"
- try:
- with open(test_file, 'w', encoding='utf-8') as f:
- f.write("test")
- test_file.unlink() # 删除测试文件
- except PermissionError:
- messagebox.showerror("错误", f"没有写入权限到目录: {levels_dir}\n请检查目录权限或以管理员身份运行")
- return
-
- # 将Excel数据转换为JSON格式并保存为单独文件
- updated_count = 0
- failed_count = 0
-
- for item in items:
- try:
- level_data = self._convert_level_data(item)
- if level_data and '关卡ID' in item and item['关卡ID']:
- level_id = item['关卡ID']
- level_file = levels_dir / f"{level_id}.json"
-
- print(f"保存关卡配置: {level_file}")
- with open(level_file, 'w', encoding='utf-8') as f:
- json.dump(level_data, f, indent=2, ensure_ascii=False)
- updated_count += 1
- else:
- print(f"跳过无效的关卡数据: {item}")
- failed_count += 1
- except Exception as e:
- print(f"保存关卡配置时出错: {e}")
- failed_count += 1
- continue
-
- if updated_count > 0:
- message = f"关卡配置导入成功!\n更新了 {updated_count} 个关卡文件"
- if failed_count > 0:
- message += f"\n跳过了 {failed_count} 个无效配置"
- messagebox.showinfo("成功", message)
- self.status_var.set("关卡配置导入成功")
- else:
- messagebox.showerror("错误", "没有成功导入任何关卡配置\n请检查Excel文件格式和数据")
- self.status_var.set("关卡配置导入失败")
-
- except Exception as e:
- error_msg = f"关卡配置导入失败: {str(e)}"
- print(error_msg)
- messagebox.showerror("错误", error_msg)
- self.status_var.set("关卡配置导入失败")
-
- self.clear_selection()
- return
-
- else:
- # 未知配置类型
- print(f"警告: 未知的配置文件类型: {filename}")
- messagebox.showwarning("警告", f"未知的配置文件类型: {filename}\n支持的类型: 敌人配置、武器配置、技能配置、关卡配置")
- return
-
- # 写入更新后的配置(关卡配置已在前面处理,跳过)
- if not str(self.json_config_path).endswith('levels'):
- try:
- print(f"写入配置文件: {self.json_config_path}")
- print(f"配置内容预览: {str(current_config)[:200]}...")
-
- with open(self.json_config_path, 'w', encoding='utf-8') as f:
- json.dump(current_config, f, indent=2, ensure_ascii=False)
-
- print("配置文件写入成功")
- messagebox.showinfo("成功", f"配置导入成功!\n更新了 {len(items)} 个配置项")
- self.status_var.set("配置导入成功")
-
- except Exception as e:
- print(f"写入配置文件失败: {e}")
- messagebox.showerror("错误", f"写入配置文件失败: {str(e)}")
- return
-
- # 刷新预览
- self.clear_selection()
-
- def _convert_enemy_data(self, item):
- """转换敌人数据格式"""
- try:
- print(f"开始转换敌人数据: {item}")
- enemy_id = item.get('敌人ID', '')
- enemy_name = item.get('敌人名称', '')
- print(f"敌人ID: {enemy_id}, 敌人名称: {enemy_name}")
-
- # 检查必要字段
- if not enemy_id:
- print(f"跳过无效敌人数据: 缺少敌人ID - {item}")
- return None
-
- result = {
- 'id': enemy_id,
- 'name': item.get('敌人名称', ''),
- 'type': item.get('敌人类型', ''),
- 'rarity': item.get('稀有度', ''),
- 'weight': item.get('权重', 1),
- 'health': item.get('生命值', 100),
- 'speed': item.get('移动速度', 50),
- 'attack': item.get('攻击力', 10),
- 'range': item.get('攻击范围', 100),
- 'attackSpeed': item.get('攻击速度', 1.0),
- 'defense': item.get('防御力', 0),
- 'goldReward': item.get('金币奖励', 10)
- }
- print(f"成功转换敌人数据: {result}")
- return result
- except Exception as e:
- print(f"转换敌人数据失败: {e} - 数据: {item}")
- return None
-
- def _convert_weapon_data(self, item):
- """转换武器数据格式"""
- try:
- print(f"开始转换武器数据: {item}")
- weapon_id = item.get('ID', '')
- weapon_name = item.get('名称', '')
- print(f"武器ID: {weapon_id}, 武器名称: {weapon_name}")
-
- result = {
- 'id': weapon_id,
- 'name': weapon_name,
- 'type': item.get('类型', ''),
- 'rarity': item.get('稀有度', ''),
- 'weight': item.get('权重', 1),
- 'damage': item.get('伤害', 10),
- 'fireRate': item.get('射速', 1.0),
- 'range': item.get('射程', 100),
- 'bulletSpeed': item.get('子弹速度', 100)
- }
- print(f"成功转换武器数据: {result}")
- return result
- except Exception as e:
- print(f"转换武器数据失败: {e} - 数据: {item}")
- return None
-
- def _convert_skill_data(self, item):
- """转换技能数据格式"""
- try:
- skill_id = item.get('技能ID', '')
- print(f"正在转换技能数据: {skill_id} - {item.get('技能名称', '')}")
-
- # 检查必要字段
- if not skill_id:
- print(f"跳过无效技能数据: 缺少技能ID - {item}")
- return None
-
- result = {
- 'id': skill_id,
- 'name': item.get('技能名称', ''),
- 'description': item.get('技能描述', ''),
- 'iconPath': item.get('图标路径', ''),
- 'maxLevel': item.get('最大等级', 1),
- 'currentLevel': item.get('当前等级', 0),
- 'priceReduction': item.get('价格减少', 0.0),
- 'critChanceIncrease': item.get('暴击几率增加', 0.0),
- 'critDamageBonus': item.get('暴击伤害加成', 0.0),
- 'healthIncrease': item.get('生命值增加', 0.0),
- 'multiShotChance': item.get('多重射击几率', 0.0),
- 'energyGainIncrease': item.get('能量加成', 0.0),
- 'ballSpeedIncrease': item.get('速度提升', 0.0)
- }
- print(f"成功转换技能数据: {result}")
- return result
- except Exception as e:
- print(f"转换技能数据失败: {e} - 数据: {item}")
- return None
-
- def _convert_level_data(self, item):
- """转换关卡数据格式"""
- try:
- # 处理可用武器字符串,转换为数组
- available_weapons = []
- if '可用武器' in item and item['可用武器']:
- weapons_str = str(item['可用武器'])
- available_weapons = [weapon.strip() for weapon in weapons_str.split(',')]
-
- return {
- "levelId": str(item.get('关卡ID', '')),
- "name": str(item.get('关卡名称', '')),
- "scene": str(item.get('场景', '')),
- "description": str(item.get('描述', '')),
- "weapons": available_weapons,
- "timeLimit": int(item.get('时间限制', 300)),
- "difficulty": str(item.get('难度', 'normal')),
- "healthMultiplier": float(item.get('生命倍数', 1.0)),
- "coinReward": int(item.get('金币奖励', 0)),
- "diamondReward": int(item.get('钻石奖励', 0))
- }
- except Exception as e:
- print(f"转换关卡数据时出错: {e}")
- return None
-
- def restore_default_config(self):
- """恢复默认配置"""
- result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!")
- if not result:
- return
-
- try:
- # 备份当前配置
- if self.json_config_path.exists():
- backup_path = self.json_config_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
- with open(self.json_config_path, 'r', encoding='utf-8') as f:
- content = f.read()
- with open(backup_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- # 写入默认配置
- with open(self.json_config_path, 'w', encoding='utf-8') as f:
- json.dump(self.default_config, f, indent=2, ensure_ascii=False)
-
- messagebox.showinfo("成功", "已恢复默认配置")
- self.status_var.set("已恢复默认配置")
-
- # 刷新预览
- self.clear_selection()
-
- except Exception as e:
- messagebox.showerror("错误", f"恢复默认配置失败: {e}")
-
- def run(self):
- """运行应用"""
- self.root.mainloop()
- def main():
- """主函数"""
- try:
- app = ConfigManagerGUI()
- app.run()
- except Exception as e:
- print(f"启动应用失败: {e}")
- input("按回车键退出...")
- if __name__ == "__main__":
- main()
|