enemy_config_manager.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 敌人配置管理器
  5. 负责从Excel文件读取敌人配置并与现有JSON配置合并
  6. 只修改策划配置的值,保持JSON中其他字段不变
  7. 作者: AI Assistant
  8. 日期: 2024
  9. """
  10. import json
  11. import os
  12. from pathlib import Path
  13. from datetime import datetime
  14. import copy
  15. try:
  16. import pandas as pd
  17. PANDAS_AVAILABLE = True
  18. except ImportError:
  19. PANDAS_AVAILABLE = False
  20. print("警告: pandas未安装,敌人配置管理器将不可用")
  21. class EnemyConfigManager:
  22. """敌人配置管理器类"""
  23. def __init__(self, excel_path=None, json_path=None):
  24. """初始化敌人配置管理器"""
  25. if not PANDAS_AVAILABLE:
  26. raise ImportError("pandas未安装,无法使用敌人配置管理器")
  27. # 设置文件路径
  28. self.script_dir = Path(__file__).parent
  29. self.excel_path = Path(excel_path) if excel_path else self.script_dir / "敌人配置表.xlsx"
  30. self.json_path = Path(json_path) if json_path else self.script_dir.parent / "enemies.json"
  31. # 存储配置数据
  32. self.excel_data = {}
  33. self.existing_json = []
  34. self.merged_config = []
  35. print(f"敌人配置管理器初始化完成")
  36. print(f"Excel文件: {self.excel_path}")
  37. print(f"JSON文件: {self.json_path}")
  38. def load_existing_json(self):
  39. """加载现有的JSON配置"""
  40. if self.json_path.exists():
  41. try:
  42. with open(self.json_path, 'r', encoding='utf-8') as f:
  43. self.existing_json = json.load(f)
  44. print(f"成功加载现有JSON配置,包含 {len(self.existing_json)} 个敌人")
  45. return True
  46. except Exception as e:
  47. print(f"加载JSON配置失败: {e}")
  48. self.existing_json = []
  49. return False
  50. else:
  51. print("JSON文件不存在,将创建新配置")
  52. self.existing_json = []
  53. return True
  54. def read_excel_data(self):
  55. """从Excel文件读取敌人配置数据"""
  56. if not self.excel_path.exists():
  57. raise FileNotFoundError(f"Excel文件不存在: {self.excel_path}")
  58. try:
  59. # 读取所有工作表,不指定header以保留所有行
  60. excel_data = pd.read_excel(self.excel_path, sheet_name=None, header=None)
  61. print(f"找到工作表: {list(excel_data.keys())}")
  62. # 解析各个工作表
  63. self.excel_data = {
  64. 'basic': self._parse_basic_config(excel_data.get('敌人基础配置')),
  65. 'combat': self._parse_combat_config(excel_data.get('战斗配置')),
  66. 'movement': self._parse_movement_config(excel_data.get('移动配置')),
  67. 'visual': self._parse_visual_config(excel_data.get('视觉配置')),
  68. 'audio': self._parse_audio_config(excel_data.get('音频配置')),
  69. 'special': self._parse_special_abilities(excel_data.get('特殊能力配置')),
  70. 'boss': self._parse_boss_config(excel_data.get('BOSS配置'))
  71. }
  72. print("Excel数据解析完成")
  73. return True
  74. except Exception as e:
  75. print(f"读取Excel文件失败: {e}")
  76. return False
  77. def _parse_basic_config(self, df):
  78. """解析敌人基础配置"""
  79. if df is None:
  80. return {}
  81. config = {}
  82. # 从第1行开始处理(跳过第0行标题)
  83. for i in range(1, len(df)):
  84. row = df.iloc[i]
  85. enemy_id = str(row.iloc[0]).strip()
  86. if pd.isna(row.iloc[0]) or enemy_id == '':
  87. continue
  88. config[enemy_id] = {
  89. 'name': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else '',
  90. 'type': str(row.iloc[2]) if not pd.isna(row.iloc[2]) else '',
  91. 'health': int(row.iloc[3]) if not pd.isna(row.iloc[3]) else 100,
  92. 'maxHealth': int(row.iloc[4]) if not pd.isna(row.iloc[4]) else 100,
  93. 'defense': int(row.iloc[5]) if not pd.isna(row.iloc[5]) else 0,
  94. 'speed': float(row.iloc[6]) if not pd.isna(row.iloc[6]) else 30.0,
  95. 'dropEnergy': int(row.iloc[7]) if not pd.isna(row.iloc[7]) else 1,
  96. 'dropCoins': int(row.iloc[8]) if not pd.isna(row.iloc[8]) else 5
  97. }
  98. print(f"解析基础配置: {len(config)} 个敌人")
  99. return config
  100. def _parse_combat_config(self, df):
  101. """解析战斗配置"""
  102. if df is None:
  103. return {}
  104. config = {}
  105. for index, row in df.iterrows():
  106. if index == 0: # 跳过标题行
  107. continue
  108. enemy_id = str(row.iloc[0]).strip()
  109. if pd.isna(row.iloc[0]) or enemy_id == '':
  110. continue
  111. config[enemy_id] = {
  112. 'attackDamage': int(row.iloc[1]) if not pd.isna(row.iloc[1]) else 1,
  113. 'attackRange': float(row.iloc[2]) if not pd.isna(row.iloc[2]) else 1.0,
  114. 'attackSpeed': float(row.iloc[3]) if not pd.isna(row.iloc[3]) else 1.0,
  115. 'canBlock': bool(row.iloc[4]) if not pd.isna(row.iloc[4]) else False,
  116. 'blockChance': float(row.iloc[5]) if not pd.isna(row.iloc[5]) else 0.0,
  117. 'blockDamageReduction': float(row.iloc[6]) if not pd.isna(row.iloc[6]) else 0.5,
  118. 'attackCooldown': float(row.iloc[7]) if not pd.isna(row.iloc[7]) else 1.0,
  119. 'attackType': str(row.iloc[8]) if not pd.isna(row.iloc[8]) else 'melee',
  120. 'attackDelay': float(row.iloc[9]) if not pd.isna(row.iloc[9]) else 1.0,
  121. 'weaponType': str(row.iloc[10]) if not pd.isna(row.iloc[10]) else 'none',
  122. 'projectileType': str(row.iloc[11]) if not pd.isna(row.iloc[11]) else 'none',
  123. 'projectileSpeed': float(row.iloc[12]) if not pd.isna(row.iloc[12]) else 100.0
  124. }
  125. print(f"解析战斗配置: {len(config)} 个敌人")
  126. return config
  127. def _parse_movement_config(self, df):
  128. """解析移动配置"""
  129. if df is None:
  130. return {}
  131. config = {}
  132. for index, row in df.iterrows():
  133. if index == 0: # 跳过标题行
  134. continue
  135. enemy_id = str(row.iloc[0]).strip()
  136. if pd.isna(row.iloc[0]) or enemy_id == '':
  137. continue
  138. config[enemy_id] = {
  139. 'pattern': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else 'direct',
  140. 'speed': float(row.iloc[2]) if not pd.isna(row.iloc[2]) else 30.0,
  141. 'patrolRange': int(row.iloc[3]) if not pd.isna(row.iloc[3]) else 100,
  142. 'rotationSpeed': float(row.iloc[4]) if not pd.isna(row.iloc[4]) else 180.0,
  143. 'moveType': str(row.iloc[5]) if not pd.isna(row.iloc[5]) else 'straight',
  144. 'swingAmplitude': float(row.iloc[6]) if not pd.isna(row.iloc[6]) else 0.0,
  145. 'swingFrequency': float(row.iloc[7]) if not pd.isna(row.iloc[7]) else 0.0,
  146. 'speedVariation': float(row.iloc[8]) if not pd.isna(row.iloc[8]) else 0.1
  147. }
  148. print(f"解析移动配置: {len(config)} 个敌人")
  149. return config
  150. def _parse_visual_config(self, df):
  151. """解析视觉配置"""
  152. if df is None:
  153. return {}
  154. visual_config = {}
  155. for index, row in df.iterrows():
  156. if index == 0: # 跳过标题行
  157. continue
  158. enemy_id = str(row.iloc[0]).strip()
  159. if pd.isna(row.iloc[0]) or enemy_id == '':
  160. continue
  161. config = {
  162. 'sprite_path': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else '',
  163. 'scale': float(row.iloc[2]) if not pd.isna(row.iloc[2]) else 1.0,
  164. 'animation_speed': float(row.iloc[3]) if not pd.isna(row.iloc[3]) else 1.0,
  165. 'flip_horizontal': bool(row.iloc[4]) if not pd.isna(row.iloc[4]) else False,
  166. 'animations': {
  167. 'idle': str(row.iloc[5]) if not pd.isna(row.iloc[5]) else 'idle',
  168. 'walk': str(row.iloc[6]) if not pd.isna(row.iloc[6]) else 'walk',
  169. 'attack': str(row.iloc[7]) if not pd.isna(row.iloc[7]) else 'attack',
  170. 'death': str(row.iloc[8]) if not pd.isna(row.iloc[8]) else 'dead'
  171. }
  172. }
  173. # 武器道具(可选)
  174. if len(row) > 9 and not pd.isna(row.iloc[9]):
  175. weapon_prop = str(row.iloc[9]).strip()
  176. if weapon_prop:
  177. config['weapon_prop'] = weapon_prop
  178. # 色调(可选),第10列
  179. if len(row) > 10 and not pd.isna(row.iloc[10]):
  180. tint_val = str(row.iloc[10]).strip()
  181. if tint_val:
  182. config['tint'] = tint_val
  183. visual_config[enemy_id] = config
  184. print(f"解析视觉配置: {len(visual_config)} 个敌人")
  185. return visual_config
  186. def _parse_audio_config(self, df):
  187. """解析音频配置"""
  188. if df is None:
  189. return {}
  190. audio_config = {}
  191. for index, row in df.iterrows():
  192. if index == 0: # 跳过标题行
  193. continue
  194. enemy_id = str(row.iloc[0]).strip()
  195. if pd.isna(row.iloc[0]) or enemy_id == '':
  196. continue
  197. config = {
  198. 'attack_sound': str(row.iloc[1]) if not pd.isna(row.iloc[1]) else '',
  199. 'death_sound': str(row.iloc[2]) if not pd.isna(row.iloc[2]) else '',
  200. 'hit_sound': str(row.iloc[3]) if not pd.isna(row.iloc[3]) else '',
  201. 'walk_sound': str(row.iloc[4]) if not pd.isna(row.iloc[4]) else ''
  202. }
  203. audio_config[enemy_id] = config
  204. print(f"解析音频配置: {len(audio_config)} 个敌人")
  205. return audio_config
  206. def _parse_special_abilities(self, df):
  207. """解析特殊能力配置"""
  208. if df is None:
  209. return {}
  210. abilities_config = {}
  211. for index, row in df.iterrows():
  212. if index == 0: # 跳过标题行
  213. continue
  214. enemy_id = str(row.iloc[0]).strip()
  215. if pd.isna(row.iloc[0]) or enemy_id == '':
  216. continue
  217. ability_type = str(row.iloc[1]).strip() if not pd.isna(row.iloc[1]) else ''
  218. if not ability_type:
  219. # 没有特殊能力的敌人
  220. if enemy_id not in abilities_config:
  221. abilities_config[enemy_id] = []
  222. continue
  223. ability = {
  224. 'type': ability_type,
  225. 'damage': int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 0,
  226. 'range': int(row.iloc[3]) if not pd.isna(row.iloc[3]) else 0,
  227. 'cooldown': int(row.iloc[4]) if not pd.isna(row.iloc[4]) else 0
  228. }
  229. if enemy_id not in abilities_config:
  230. abilities_config[enemy_id] = []
  231. abilities_config[enemy_id].append(ability)
  232. print(f"解析特殊能力配置: {len(abilities_config)} 个敌人")
  233. return abilities_config
  234. def _parse_boss_config(self, df):
  235. """解析BOSS配置"""
  236. if df is None:
  237. return {}
  238. boss_config = {}
  239. for index, row in df.iterrows():
  240. if index == 0: # 跳过标题行
  241. continue
  242. enemy_id = str(row.iloc[0]).strip()
  243. if pd.isna(row.iloc[0]) or enemy_id == '':
  244. continue
  245. config = {
  246. 'is_boss': bool(row.iloc[1]) if not pd.isna(row.iloc[1]) else False,
  247. 'phases': int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 1,
  248. 'rage_threshold': float(row.iloc[3]) if not pd.isna(row.iloc[3]) else 0.3,
  249. 'rage_damage_multiplier': float(row.iloc[4]) if not pd.isna(row.iloc[4]) else 1.0,
  250. 'rage_speed_multiplier': float(row.iloc[5]) if not pd.isna(row.iloc[5]) else 1.0
  251. }
  252. boss_config[enemy_id] = config
  253. print(f"解析BOSS配置: {len(boss_config)} 个敌人")
  254. return boss_config
  255. def merge_configurations(self):
  256. """合并Excel配置和现有JSON配置"""
  257. print("开始合并配置...")
  258. # 创建敌人ID到JSON索引的映射
  259. json_enemy_map = {}
  260. for i, enemy in enumerate(self.existing_json):
  261. json_enemy_map[enemy.get('id', '')] = i
  262. # 获取所有Excel中的敌人ID
  263. excel_enemy_ids = set()
  264. for config_type in ['basic', 'combat', 'movement', 'visual', 'audio', 'special', 'boss']:
  265. if config_type in self.excel_data:
  266. excel_enemy_ids.update(self.excel_data[config_type].keys())
  267. # 复制现有JSON作为基础
  268. self.merged_config = copy.deepcopy(self.existing_json)
  269. # 处理每个敌人
  270. for enemy_id in excel_enemy_ids:
  271. if enemy_id in json_enemy_map:
  272. # 更新现有敌人
  273. json_index = json_enemy_map[enemy_id]
  274. self._update_enemy_config(self.merged_config[json_index], enemy_id)
  275. print(f"更新敌人配置: {enemy_id}")
  276. else:
  277. # 创建新敌人(如果需要)
  278. new_enemy = self._create_new_enemy_config(enemy_id)
  279. if new_enemy:
  280. self.merged_config.append(new_enemy)
  281. print(f"创建新敌人配置: {enemy_id}")
  282. print(f"配置合并完成,共 {len(self.merged_config)} 个敌人")
  283. def _update_enemy_config(self, enemy_config, enemy_id):
  284. """更新单个敌人的配置"""
  285. # 更新基础配置
  286. if 'basic' in self.excel_data and enemy_id in self.excel_data['basic']:
  287. basic_data = self.excel_data['basic'][enemy_id]
  288. if 'name' in basic_data:
  289. enemy_config['name'] = basic_data['name']
  290. if 'type' in basic_data:
  291. enemy_config['type'] = basic_data['type']
  292. # 更新stats部分
  293. if 'stats' not in enemy_config:
  294. enemy_config['stats'] = {}
  295. stats_mapping = {
  296. 'health': 'health',
  297. 'maxHealth': 'maxHealth',
  298. 'defense': 'defense',
  299. 'speed': 'speed',
  300. 'dropEnergy': 'dropEnergy',
  301. 'dropCoins': 'dropCoins'
  302. }
  303. for excel_key, json_key in stats_mapping.items():
  304. if excel_key in basic_data:
  305. enemy_config['stats'][json_key] = basic_data[excel_key]
  306. # 更新战斗配置
  307. if 'combat' in self.excel_data and enemy_id in self.excel_data['combat']:
  308. combat_data = self.excel_data['combat'][enemy_id]
  309. if 'combat' not in enemy_config:
  310. enemy_config['combat'] = {}
  311. combat_mapping = {
  312. 'attackDamage': 'attackDamage',
  313. 'attackRange': 'attackRange',
  314. 'attackSpeed': 'attackSpeed',
  315. 'canBlock': 'canBlock',
  316. 'blockChance': 'blockChance',
  317. 'blockDamageReduction': 'blockDamageReduction',
  318. 'attackCooldown': 'attackCooldown',
  319. 'attackType': 'attackType',
  320. 'attackDelay': 'attackDelay',
  321. 'weaponType': 'weaponType',
  322. 'projectileType': 'projectileType',
  323. 'projectileSpeed': 'projectileSpeed'
  324. }
  325. for excel_key, json_key in combat_mapping.items():
  326. if excel_key in combat_data:
  327. enemy_config['combat'][json_key] = combat_data[excel_key]
  328. # 更新移动配置
  329. if 'movement' in self.excel_data and enemy_id in self.excel_data['movement']:
  330. movement_data = self.excel_data['movement'][enemy_id]
  331. if 'movement' not in enemy_config:
  332. enemy_config['movement'] = {}
  333. movement_mapping = {
  334. 'pattern': 'pattern',
  335. 'speed': 'speed',
  336. 'patrolRange': 'patrolRange',
  337. 'rotationSpeed': 'rotationSpeed',
  338. 'moveType': 'moveType',
  339. 'swingAmplitude': 'swingAmplitude',
  340. 'swingFrequency': 'swingFrequency',
  341. 'speedVariation': 'speedVariation'
  342. }
  343. for excel_key, json_key in movement_mapping.items():
  344. if excel_key in movement_data:
  345. enemy_config['movement'][json_key] = movement_data[excel_key]
  346. # 更新视觉配置
  347. if 'visual' in self.excel_data and enemy_id in self.excel_data['visual']:
  348. visual_data = self.excel_data['visual'][enemy_id]
  349. if 'visual' not in enemy_config:
  350. enemy_config['visual'] = {}
  351. # 直接映射视觉配置
  352. for key, value in visual_data.items():
  353. enemy_config['visual'][key] = value
  354. # 同步到 visualConfig(供引擎使用的驼峰命名结构)
  355. if 'visualConfig' not in enemy_config:
  356. enemy_config['visualConfig'] = {}
  357. vc = enemy_config['visualConfig']
  358. v = enemy_config['visual']
  359. # 基础字段映射:下划线 -> 驼峰
  360. vc['spritePath'] = v.get('sprite_path', vc.get('spritePath', ''))
  361. vc['scale'] = v.get('scale', vc.get('scale', 1.0))
  362. vc['animationSpeed'] = v.get('animation_speed', vc.get('animationSpeed', 1.0))
  363. vc['flipX'] = v.get('flip_horizontal', vc.get('flipX', False))
  364. # animations 映射,保留已有值作为默认
  365. v_anims = v.get('animations', {})
  366. if 'animations' not in vc:
  367. vc['animations'] = {}
  368. for key in ['idle', 'walk', 'attack', 'death']:
  369. if key in v_anims:
  370. vc['animations'][key] = v_anims[key]
  371. else:
  372. vc['animations'].setdefault(key, vc['animations'].get(key, key))
  373. # weaponProp 映射(可选)
  374. if 'weapon_prop' in v:
  375. vc['weaponProp'] = v['weapon_prop']
  376. # tint 保留已有 JSON 值,若无则设为默认白色
  377. if 'tint' in v:
  378. vc['tint'] = v['tint']
  379. else:
  380. vc['tint'] = vc.get('tint', '#FFFFFF')
  381. # 更新音频配置
  382. if 'audio' in self.excel_data and enemy_id in self.excel_data['audio']:
  383. audio_data = self.excel_data['audio'][enemy_id]
  384. if 'audio' not in enemy_config:
  385. enemy_config['audio'] = {}
  386. # 直接映射音频配置
  387. for key, value in audio_data.items():
  388. enemy_config['audio'][key] = value
  389. # 更新特殊能力配置
  390. if 'special' in self.excel_data and enemy_id in self.excel_data['special']:
  391. abilities_data = self.excel_data['special'][enemy_id]
  392. if 'special_abilities' not in enemy_config:
  393. enemy_config['special_abilities'] = []
  394. # 替换特殊能力列表
  395. enemy_config['special_abilities'] = abilities_data
  396. # 更新BOSS配置
  397. if 'boss' in self.excel_data and enemy_id in self.excel_data['boss']:
  398. boss_data = self.excel_data['boss'][enemy_id]
  399. if 'boss' not in enemy_config:
  400. enemy_config['boss'] = {}
  401. # 直接映射BOSS配置
  402. for key, value in boss_data.items():
  403. enemy_config['boss'][key] = value
  404. def _create_new_enemy_config(self, enemy_id):
  405. """创建新的敌人配置(基于Excel数据)"""
  406. # 这里可以根据需要实现创建新敌人的逻辑
  407. # 暂时返回None,只更新现有敌人
  408. return None
  409. def backup_json(self):
  410. """备份原始JSON文件"""
  411. if not self.json_path.exists():
  412. return True
  413. try:
  414. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  415. backup_path = self.json_path.parent / f"{self.json_path.stem}_backup_{timestamp}.json"
  416. with open(self.json_path, 'r', encoding='utf-8') as src:
  417. with open(backup_path, 'w', encoding='utf-8') as dst:
  418. dst.write(src.read())
  419. print(f"备份文件已创建: {backup_path}")
  420. return True
  421. except Exception as e:
  422. print(f"备份失败: {e}")
  423. return False
  424. def save_merged_config(self):
  425. """保存合并后的配置到JSON文件"""
  426. try:
  427. # 确保目录存在
  428. self.json_path.parent.mkdir(parents=True, exist_ok=True)
  429. with open(self.json_path, 'w', encoding='utf-8') as f:
  430. json.dump(self.merged_config, f, ensure_ascii=False, indent=2)
  431. print(f"配置已保存到: {self.json_path}")
  432. return True
  433. except Exception as e:
  434. print(f"保存配置失败: {e}")
  435. return False
  436. def import_config(self):
  437. """执行完整的配置导入流程"""
  438. print("=== 敌人配置导入开始 ===")
  439. try:
  440. # 1. 加载现有JSON配置
  441. if not self.load_existing_json():
  442. print("加载现有JSON配置失败")
  443. return False
  444. # 2. 读取Excel数据
  445. if not self.read_excel_data():
  446. print("读取Excel数据失败")
  447. return False
  448. # 3. 合并配置
  449. self.merge_configurations()
  450. # 4. 备份原文件
  451. if not self.backup_json():
  452. print("备份失败,但继续执行")
  453. # 5. 保存新配置
  454. if not self.save_merged_config():
  455. print("保存配置失败")
  456. return False
  457. print("=== 敌人配置导入完成 ===")
  458. return True
  459. except Exception as e:
  460. print(f"配置导入过程中发生错误: {e}")
  461. return False
  462. # 测试函数
  463. def test_enemy_config_manager():
  464. """测试敌人配置管理器"""
  465. try:
  466. manager = EnemyConfigManager()
  467. success = manager.import_config()
  468. if success:
  469. print("敌人配置导入测试成功")
  470. else:
  471. print("敌人配置导入测试失败")
  472. return success
  473. except Exception as e:
  474. print(f"测试失败: {e}")
  475. return False
  476. if __name__ == "__main__":
  477. test_enemy_config_manager()