#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 敌人配置管理器 负责从Excel文件读取敌人配置并与现有JSON配置合并 只修改策划配置的值,保持JSON中其他字段不变 作者: AI Assistant 日期: 2024 """ import json import os from pathlib import Path from datetime import datetime import copy try: import pandas as pd PANDAS_AVAILABLE = True except ImportError: PANDAS_AVAILABLE = False print("警告: pandas未安装,敌人配置管理器将不可用") # 新增:统一环境变量大小写不敏感控制 CASE_INSENSITIVE = str(os.environ.get('CFG_CASE_INSENSITIVE', '1')).strip().lower() in ('1', 'true', 'yes', 'on') class EnemyConfigManager: """敌人配置管理器类""" def __init__(self, excel_path=None, json_path=None): """初始化敌人配置管理器""" if not PANDAS_AVAILABLE: raise ImportError("pandas未安装,无法使用敌人配置管理器") # 设置文件路径 self.script_dir = Path(__file__).parent self.excel_path = Path(excel_path) if excel_path else self.script_dir / "敌人配置表.xlsx" self.json_path = Path(json_path) if json_path else self.script_dir.parent / "enemies.json" # 存储配置数据 self.excel_data = {} self.existing_json = [] self.merged_config = [] print(f"敌人配置管理器初始化完成") print(f"Excel文件: {self.excel_path}") print(f"JSON文件: {self.json_path}") def load_existing_json(self): """加载现有的JSON配置""" if self.json_path.exists(): try: with open(self.json_path, 'r', encoding='utf-8') as f: self.existing_json = json.load(f) print(f"成功加载现有JSON配置,包含 {len(self.existing_json)} 个敌人") return True except Exception as e: print(f"加载JSON配置失败: {e}") self.existing_json = [] return False else: print("JSON文件不存在,将创建新配置") self.existing_json = [] return True def read_excel_data(self): """从Excel文件读取敌人配置数据""" if not self.excel_path.exists(): raise FileNotFoundError(f"Excel文件不存在: {self.excel_path}") try: # 读取所有工作表,不指定header以保留所有行 excel_data = pd.read_excel(self.excel_path, sheet_name=None, header=None) print(f"找到工作表: {list(excel_data.keys())}") # 解析各个工作表(支持大小写不敏感与英文别名) self.excel_data = { 'basic': self._parse_basic_config(self._get_sheet(excel_data, ['敌人基础配置','Enemy Basic Config','basic','enemy_basic'])), 'combat': self._parse_combat_config(self._get_sheet(excel_data, ['战斗配置','Combat Config','combat'])), 'movement': self._parse_movement_config(self._get_sheet(excel_data, ['移动配置','Movement Config','movement'])), 'visual': self._parse_visual_config(self._get_sheet(excel_data, ['视觉配置','Visual Config','visual'])), 'audio': self._parse_audio_config(self._get_sheet(excel_data, ['音频配置','Audio Config','audio'])), 'special': self._parse_special_abilities(self._get_sheet(excel_data, ['特殊能力配置','Special Ability Config','special','abilities','特殊能力'])), 'boss': self._parse_boss_config(self._get_sheet(excel_data, ['BOSS配置','Boss Config','boss'])) } print("Excel数据解析完成") return True except Exception as e: print(f"读取Excel文件失败: {e}") return False def _parse_basic_config(self, df): """解析敌人基础配置""" if df is None: return {} config = {} # 从第1行开始处理(跳过第0行标题) for i in range(1, len(df)): row = df.iloc[i] enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue config[enemy_id] = { 'name': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else '', 'type': str(row.iloc[2]) if not pd.isna(row.iloc[2]) else '', 'health': int(row.iloc[3]) if not pd.isna(row.iloc[3]) else 100, 'maxHealth': int(row.iloc[4]) if not pd.isna(row.iloc[4]) else 100, 'defense': int(row.iloc[5]) if not pd.isna(row.iloc[5]) else 0, 'speed': float(row.iloc[6]) if not pd.isna(row.iloc[6]) else 30.0, 'dropEnergy': int(row.iloc[7]) if not pd.isna(row.iloc[7]) else 1, 'dropCoins': int(row.iloc[8]) if not pd.isna(row.iloc[8]) else 5 } print(f"解析基础配置: {len(config)} 个敌人") return config def _parse_combat_config(self, df): """解析战斗配置""" if df is None: return {} config = {} for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue config[enemy_id] = { 'attackDamage': int(row.iloc[1]) if not pd.isna(row.iloc[1]) else 1, 'attackRange': float(row.iloc[2]) if not pd.isna(row.iloc[2]) else 1.0, 'attackSpeed': float(row.iloc[3]) if not pd.isna(row.iloc[3]) else 1.0, 'canBlock': self._parse_boolean(row.iloc[4], False), 'blockChance': float(row.iloc[5]) if not pd.isna(row.iloc[5]) else 0.0, 'blockDamageReduction': float(row.iloc[6]) if not pd.isna(row.iloc[6]) else 0.5, 'attackCooldown': float(row.iloc[7]) if not pd.isna(row.iloc[7]) else 1.0, 'attackType': str(row.iloc[8]) if not pd.isna(row.iloc[8]) else 'melee', # 第9列是"备注"字段,跳过不导入 'causesWallShake': self._parse_boolean(row.iloc[10], False), 'attackDelay': float(row.iloc[11]) if not pd.isna(row.iloc[11]) else 1.0, 'weaponType': str(row.iloc[12]) if not pd.isna(row.iloc[12]) else 'none', 'projectileType': str(row.iloc[13]) if not pd.isna(row.iloc[13]) else 'none', 'projectileSpeed': float(row.iloc[14]) if not pd.isna(row.iloc[14]) else 100.0 } print(f"解析战斗配置: {len(config)} 个敌人") return config def _parse_movement_config(self, df): """解析移动配置""" if df is None: return {} config = {} for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue config[enemy_id] = { 'pattern': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else 'direct', 'speed': float(row.iloc[2]) if not pd.isna(row.iloc[2]) else 30.0, 'patrolRange': int(row.iloc[3]) if not pd.isna(row.iloc[3]) else 100, 'rotationSpeed': float(row.iloc[4]) if not pd.isna(row.iloc[4]) else 180.0, 'moveType': str(row.iloc[5]) if not pd.isna(row.iloc[5]) else 'straight', 'swingAmplitude': float(row.iloc[6]) if not pd.isna(row.iloc[6]) else 0.0, 'swingFrequency': float(row.iloc[7]) if not pd.isna(row.iloc[7]) else 0.0, 'speedVariation': float(row.iloc[8]) if not pd.isna(row.iloc[8]) else 0.1 } print(f"解析移动配置: {len(config)} 个敌人") return config def _parse_visual_config(self, df): """解析视觉配置""" if df is None: return {} visual_config = {} for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue config = { 'sprite_path': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else '', 'scale': float(row.iloc[2]) if not pd.isna(row.iloc[2]) else 1.0, 'animation_speed': float(row.iloc[3]) if not pd.isna(row.iloc[3]) else 1.0, 'flip_horizontal': self._parse_boolean(row.iloc[4], False), 'animations': { 'idle': str(row.iloc[5]) if not pd.isna(row.iloc[5]) else 'idle', 'walk': str(row.iloc[6]) if not pd.isna(row.iloc[6]) else 'walk', 'attack': str(row.iloc[7]) if not pd.isna(row.iloc[7]) else 'attack', 'death': str(row.iloc[8]) if not pd.isna(row.iloc[8]) else 'dead' } } # 武器道具(可选) if len(row) > 9 and not pd.isna(row.iloc[9]): weapon_prop = str(row.iloc[9]).strip() if weapon_prop: config['weapon_prop'] = weapon_prop # 色调(可选),第10列 if len(row) > 10 and not pd.isna(row.iloc[10]): tint_val = str(row.iloc[10]).strip() if tint_val: config['tint'] = tint_val visual_config[enemy_id] = config print(f"解析视觉配置: {len(visual_config)} 个敌人") return visual_config def _parse_audio_config(self, df): """解析音频配置""" if df is None: return {} audio_config = {} for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue config = { 'attack_sound': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else '', 'death_sound': str(row.iloc[2]) if not pd.isna(row.iloc[2]) else '', 'hit_sound': str(row.iloc[3]) if not pd.isna(row.iloc[3]) else '', 'walk_sound': str(row.iloc[4]) if not pd.isna(row.iloc[4]) else '' } audio_config[enemy_id] = config print(f"解析音频配置: {len(audio_config)} 个敌人") return audio_config def _parse_special_abilities(self, df): """解析特殊能力配置""" if df is None: return {} abilities_config = {} for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue ability_type = str(row.iloc[1]).strip() if not pd.isna(row.iloc[1]) else '' if not ability_type: # 没有特殊能力的敌人 if enemy_id not in abilities_config: abilities_config[enemy_id] = [] continue ability = { 'type': ability_type, 'damage': int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 0, 'range': int(row.iloc[3]) if not pd.isna(row.iloc[3]) else 0, 'cooldown': int(row.iloc[4]) if not pd.isna(row.iloc[4]) else 0 } if enemy_id not in abilities_config: abilities_config[enemy_id] = [] abilities_config[enemy_id].append(ability) print(f"解析特殊能力配置: {len(abilities_config)} 个敌人") return abilities_config def _parse_boss_config(self, df): """解析BOSS配置""" if df is None: return {} boss_config = {} for index, row in df.iterrows(): if index == 0: # 跳过标题行 continue enemy_id = str(row.iloc[0]).strip() if pd.isna(row.iloc[0]) or enemy_id == '': continue config = { 'is_boss': self._parse_boolean(row.iloc[1], False), 'phases': int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 1, 'rage_threshold': float(row.iloc[3]) if not pd.isna(row.iloc[3]) else 0.3, 'rage_damage_multiplier': float(row.iloc[4]) if not pd.isna(row.iloc[4]) else 1.0, 'rage_speed_multiplier': float(row.iloc[5]) if not pd.isna(row.iloc[5]) else 1.0 } boss_config[enemy_id] = config print(f"解析BOSS配置: {len(boss_config)} 个敌人") return boss_config # 新增:布尔解析和工作表匹配助手 def _parse_boolean(self, value, default=False): try: if value is None or (PANDAS_AVAILABLE and pd.isna(value)): return default if isinstance(value, bool): return value try: import numpy as np if isinstance(value, (np.bool_,)): return bool(value) except Exception: pass if isinstance(value, (int, float)): return int(value) != 0 s = str(value).strip() if s == '': return default sl = s.lower() if CASE_INSENSITIVE else s true_set = {'true','t','yes','y','on','1','是','对','开','开启','启用'} false_set = {'false','f','no','n','off','0','否','错','关','关闭','禁用'} if (CASE_INSENSITIVE and sl in true_set) or (not CASE_INSENSITIVE and s in true_set): return True if (CASE_INSENSITIVE and sl in false_set) or (not CASE_INSENSITIVE and s in false_set): return False try: return float(s) != 0.0 except Exception: return default except Exception: return default def _get_sheet(self, all_sheets, candidates): if all_sheets is None: return None if CASE_INSENSITIVE: key_map = {str(k).strip().lower(): k for k in all_sheets.keys()} for name in candidates: key = str(name).strip().lower() if key in key_map: return all_sheets[key_map[key]] return None else: for name in candidates: if name in all_sheets: return all_sheets[name] return None def merge_configurations(self): """合并Excel配置和现有JSON配置""" print("开始合并配置...") # 创建敌人ID到JSON索引的映射 json_enemy_map = {} for i, enemy in enumerate(self.existing_json): json_enemy_map[enemy.get('id', '')] = i # 获取所有Excel中的敌人ID excel_enemy_ids = set() for config_type in ['basic', 'combat', 'movement', 'visual', 'audio', 'special', 'boss']: if config_type in self.excel_data: excel_enemy_ids.update(self.excel_data[config_type].keys()) # 复制现有JSON作为基础 self.merged_config = copy.deepcopy(self.existing_json) # 处理每个敌人 for enemy_id in excel_enemy_ids: if enemy_id in json_enemy_map: # 更新现有敌人 json_index = json_enemy_map[enemy_id] self._update_enemy_config(self.merged_config[json_index], enemy_id) print(f"更新敌人配置: {enemy_id}") else: # 创建新敌人(如果需要) new_enemy = self._create_new_enemy_config(enemy_id) if new_enemy: self.merged_config.append(new_enemy) print(f"创建新敌人配置: {enemy_id}") print(f"配置合并完成,共 {len(self.merged_config)} 个敌人") def _update_enemy_config(self, enemy_config, enemy_id): """更新单个敌人的配置""" # 更新基础配置 if 'basic' in self.excel_data and enemy_id in self.excel_data['basic']: basic_data = self.excel_data['basic'][enemy_id] if 'name' in basic_data: enemy_config['name'] = basic_data['name'] if 'type' in basic_data: enemy_config['type'] = basic_data['type'] # 更新stats部分 if 'stats' not in enemy_config: enemy_config['stats'] = {} stats_mapping = { 'health': 'health', 'maxHealth': 'maxHealth', 'defense': 'defense', 'speed': 'speed', 'dropEnergy': 'dropEnergy', 'dropCoins': 'dropCoins' } for excel_key, json_key in stats_mapping.items(): if excel_key in basic_data: enemy_config['stats'][json_key] = basic_data[excel_key] # 更新战斗配置 if 'combat' in self.excel_data and enemy_id in self.excel_data['combat']: combat_data = self.excel_data['combat'][enemy_id] if 'combat' not in enemy_config: enemy_config['combat'] = {} combat_mapping = { 'attackDamage': 'attackDamage', 'attackRange': 'attackRange', 'attackSpeed': 'attackSpeed', 'canBlock': 'canBlock', 'blockChance': 'blockChance', 'blockDamageReduction': 'blockDamageReduction', 'attackCooldown': 'attackCooldown', 'attackType': 'attackType', 'causesWallShake': 'causesWallShake', 'attackDelay': 'attackDelay', 'weaponType': 'weaponType', 'projectileType': 'projectileType', 'projectileSpeed': 'projectileSpeed' } for excel_key, json_key in combat_mapping.items(): if excel_key in combat_data: enemy_config['combat'][json_key] = combat_data[excel_key] # 更新移动配置 if 'movement' in self.excel_data and enemy_id in self.excel_data['movement']: movement_data = self.excel_data['movement'][enemy_id] if 'movement' not in enemy_config: enemy_config['movement'] = {} movement_mapping = { 'pattern': 'pattern', 'speed': 'speed', 'patrolRange': 'patrolRange', 'rotationSpeed': 'rotationSpeed', 'moveType': 'moveType', 'swingAmplitude': 'swingAmplitude', 'swingFrequency': 'swingFrequency', 'speedVariation': 'speedVariation' } for excel_key, json_key in movement_mapping.items(): if excel_key in movement_data: enemy_config['movement'][json_key] = movement_data[excel_key] # 更新视觉配置 if 'visual' in self.excel_data and enemy_id in self.excel_data['visual']: visual_data = self.excel_data['visual'][enemy_id] if 'visual' not in enemy_config: enemy_config['visual'] = {} # 直接映射视觉配置 for key, value in visual_data.items(): enemy_config['visual'][key] = value # 同步到 visualConfig(供引擎使用的驼峰命名结构) if 'visualConfig' not in enemy_config: enemy_config['visualConfig'] = {} vc = enemy_config['visualConfig'] v = enemy_config['visual'] # 基础字段映射:下划线 -> 驼峰 vc['spritePath'] = v.get('sprite_path', vc.get('spritePath', '')) vc['scale'] = v.get('scale', vc.get('scale', 1.0)) vc['animationSpeed'] = v.get('animation_speed', vc.get('animationSpeed', 1.0)) vc['flipX'] = v.get('flip_horizontal', vc.get('flipX', False)) # animations 映射,保留已有值作为默认 v_anims = v.get('animations', {}) if 'animations' not in vc: vc['animations'] = {} for key in ['idle', 'walk', 'attack', 'death']: if key in v_anims: vc['animations'][key] = v_anims[key] else: vc['animations'].setdefault(key, vc['animations'].get(key, key)) # weaponProp 映射(可选) if 'weapon_prop' in v: vc['weaponProp'] = v['weapon_prop'] # tint 保留已有 JSON 值,若无则设为默认白色 if 'tint' in v: vc['tint'] = v['tint'] else: vc['tint'] = vc.get('tint', '#FFFFFF') # 更新音频配置 if 'audio' in self.excel_data and enemy_id in self.excel_data['audio']: audio_data = self.excel_data['audio'][enemy_id] if 'audio' not in enemy_config: enemy_config['audio'] = {} # 直接映射音频配置 for key, value in audio_data.items(): enemy_config['audio'][key] = value # 更新特殊能力配置 if 'special' in self.excel_data and enemy_id in self.excel_data['special']: abilities_data = self.excel_data['special'][enemy_id] if 'special_abilities' not in enemy_config: enemy_config['special_abilities'] = [] # 替换特殊能力列表 enemy_config['special_abilities'] = abilities_data # 更新BOSS配置 if 'boss' in self.excel_data and enemy_id in self.excel_data['boss']: boss_data = self.excel_data['boss'][enemy_id] if 'boss' not in enemy_config: enemy_config['boss'] = {} # 直接映射BOSS配置 for key, value in boss_data.items(): enemy_config['boss'][key] = value def _create_new_enemy_config(self, enemy_id): """创建新的敌人配置(基于Excel数据)""" # 这里可以根据需要实现创建新敌人的逻辑 # 暂时返回None,只更新现有敌人 return None def backup_json(self): """备份原始JSON文件""" if not self.json_path.exists(): return True try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_path = self.json_path.parent / f"{self.json_path.stem}_backup_{timestamp}.json" with open(self.json_path, 'r', encoding='utf-8') as src: with open(backup_path, 'w', encoding='utf-8') as dst: dst.write(src.read()) print(f"备份文件已创建: {backup_path}") return True except Exception as e: print(f"备份失败: {e}") return False def save_merged_config(self): """保存合并后的配置到JSON文件""" try: # 确保目录存在 self.json_path.parent.mkdir(parents=True, exist_ok=True) with open(self.json_path, 'w', encoding='utf-8') as f: json.dump(self.merged_config, f, ensure_ascii=False, indent=2) print(f"配置已保存到: {self.json_path}") return True except Exception as e: print(f"保存配置失败: {e}") return False def import_config(self): """执行完整的配置导入流程""" print("=== 敌人配置导入开始 ===") try: # 1. 加载现有JSON配置 if not self.load_existing_json(): print("加载现有JSON配置失败") return False # 2. 读取Excel数据 if not self.read_excel_data(): print("读取Excel数据失败") return False # 3. 合并配置 self.merge_configurations() # 4. 备份原文件 if not self.backup_json(): print("备份失败,但继续执行") # 5. 保存新配置 if not self.save_merged_config(): print("保存配置失败") return False print("=== 敌人配置导入完成 ===") return True except Exception as e: print(f"配置导入过程中发生错误: {e}") return False # 测试函数 def test_enemy_config_manager(): """测试敌人配置管理器""" try: manager = EnemyConfigManager() success = manager.import_config() if success: print("敌人配置导入测试成功") else: print("敌人配置导入测试失败") return success except Exception as e: print(f"测试失败: {e}") return False if __name__ == "__main__": test_enemy_config_manager()