enemy_config_manager.py 23 KB

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