level_config_manager.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. # -*- coding: utf-8 -*-
  2. """
  3. 关卡配置管理器
  4. 负责从Excel读取关卡配置数据,并智能合并到现有JSON文件中
  5. 只修改或新增策划表中配置的字段,不会完全覆盖JSON文件
  6. """
  7. import pandas as pd
  8. import json
  9. import os
  10. from datetime import datetime
  11. from pathlib import Path
  12. import re
  13. # 是否启用大小写不敏感解析(由启动脚本设置 CFG_CASE_INSENSITIVE=1)
  14. CASE_INSENSITIVE = str(os.environ.get('CFG_CASE_INSENSITIVE', '0')).strip().lower() in ('1', 'true', 'yes', 'y')
  15. class LevelConfigManager:
  16. def __init__(self, excel_path=None, levels_dir=None):
  17. """
  18. 初始化关卡配置管理器
  19. Args:
  20. excel_path: Excel文件路径
  21. levels_dir: 关卡JSON文件目录
  22. """
  23. self.excel_path = excel_path
  24. self.levels_dir = levels_dir or "d:/CocosGame/Pong/assets/data/levels"
  25. self.backup_dir = os.path.join(os.path.dirname(self.levels_dir), "backups", "levels")
  26. # 确保备份目录存在
  27. os.makedirs(self.backup_dir, exist_ok=True)
  28. # 配置数据存储
  29. self.basic_config = None
  30. self.weapon_config = None
  31. self.wave_config = None
  32. self.enemy_config = None
  33. self.energy_exp_config = None
  34. self.energy_upgrade_config = None
  35. # 大小写不敏感的辅助方法
  36. def _normalize_key(self, s):
  37. if s is None:
  38. return ""
  39. return str(s).strip().lower()
  40. # 统一关卡ID的规范形式,例如 level12 -> Level12
  41. def _canonical_level_id(self, s):
  42. if s is None:
  43. return ""
  44. v = str(s).strip()
  45. m = re.match(r"^\s*level\s*(\d+)\s*$", v, flags=re.IGNORECASE)
  46. if m:
  47. return f"Level{m.group(1)}"
  48. return v
  49. def _get_sheet(self, all_sheets, candidate_names):
  50. if not CASE_INSENSITIVE:
  51. for name in candidate_names:
  52. if name in all_sheets:
  53. return all_sheets[name], name
  54. return None, None
  55. lower_map = {self._normalize_key(k): k for k in all_sheets.keys()}
  56. for name in candidate_names:
  57. key = self._normalize_key(name)
  58. if key in lower_map:
  59. resolved = lower_map[key]
  60. return all_sheets[resolved], resolved
  61. return None, None
  62. def _get_row_value(self, row, possible_names, default=None):
  63. index_map = {self._normalize_key(col): col for col in row.index}
  64. for name in possible_names:
  65. norm = self._normalize_key(name)
  66. if norm in index_map:
  67. val = row[index_map[norm]]
  68. return val if pd.notna(val) else default
  69. return default
  70. def _to_int(self, v, default=None):
  71. try:
  72. if v is None or (isinstance(v, float) and pd.isna(v)):
  73. return default
  74. return int(float(str(v).strip()))
  75. except Exception:
  76. return default
  77. def _to_float(self, v, default=None):
  78. try:
  79. if v is None or (isinstance(v, float) and pd.isna(v)):
  80. return default
  81. return float(str(v).strip())
  82. except Exception:
  83. return default
  84. def read_excel_data(self, excel_path=None):
  85. """
  86. 读取Excel文件中的所有工作表数据
  87. Args:
  88. excel_path: Excel文件路径,如果不提供则使用初始化时的路径
  89. Returns:
  90. bool: 是否成功读取数据
  91. """
  92. if excel_path:
  93. self.excel_path = excel_path
  94. if not self.excel_path or not os.path.exists(self.excel_path):
  95. print(f"错误: Excel文件不存在: {self.excel_path}")
  96. return False
  97. try:
  98. # 读取所有工作表
  99. all_sheets = pd.read_excel(self.excel_path, sheet_name=None)
  100. # 解析各个工作表
  101. self._parse_basic_config(all_sheets)
  102. self._parse_weapon_config(all_sheets)
  103. self._parse_wave_config(all_sheets)
  104. self._parse_enemy_config(all_sheets)
  105. self._parse_energy_exp_config(all_sheets)
  106. self._parse_energy_upgrade_config(all_sheets)
  107. print(f"成功读取Excel数据: {self.excel_path}")
  108. return True
  109. except Exception as e:
  110. print(f"读取Excel文件失败: {e}")
  111. return False
  112. def _parse_basic_config(self, all_sheets):
  113. """
  114. 解析关卡基础配置工作表
  115. """
  116. sheet_aliases = ['关卡基础配置', '关卡配置', 'Level Config', 'levels', 'level config']
  117. sheet, resolved = self._get_sheet(all_sheets, sheet_aliases)
  118. if sheet is not None:
  119. self.basic_config = sheet
  120. print(f"找到关卡基础配置工作表: {resolved}")
  121. return
  122. print("警告: 未找到关卡基础配置工作表")
  123. def _parse_weapon_config(self, all_sheets):
  124. """
  125. 解析关卡武器配置工作表
  126. """
  127. sheet_aliases = ['关卡武器配置', '武器配置', 'Level Weapon Config', 'level_weapons', 'level weapons']
  128. sheet, resolved = self._get_sheet(all_sheets, sheet_aliases)
  129. if sheet is not None:
  130. self.weapon_config = sheet
  131. print(f"找到关卡武器配置工作表: {resolved}")
  132. return
  133. print("警告: 未找到关卡武器配置工作表")
  134. def _parse_wave_config(self, all_sheets):
  135. """
  136. 解析关卡波次配置工作表
  137. """
  138. sheet_aliases = ['关卡波次配置', '波次配置', 'Wave Config', 'waves', 'wave config']
  139. sheet, resolved = self._get_sheet(all_sheets, sheet_aliases)
  140. if sheet is not None:
  141. self.wave_config = sheet
  142. print(f"找到关卡波次配置工作表: {resolved}")
  143. return
  144. print("警告: 未找到关卡波次配置工作表")
  145. def _parse_enemy_config(self, all_sheets):
  146. """
  147. 解析敌人详细配置工作表
  148. """
  149. sheet_aliases = ['敌人详细配置', '敌人配置', 'Enemy Detail Config', 'enemy_details', 'enemy details']
  150. sheet, resolved = self._get_sheet(all_sheets, sheet_aliases)
  151. if sheet is not None:
  152. self.enemy_config = sheet
  153. print(f"找到敌人详细配置工作表: {resolved}")
  154. return
  155. print("警告: 未找到敌人详细配置工作表")
  156. def _parse_energy_exp_config(self, all_sheets):
  157. """
  158. 解析能量条经验值配置工作表
  159. """
  160. sheet_aliases = ['能量经验配置', '能量经验', 'Energy Exp Config', 'energy_exp', 'energy exp']
  161. sheet, resolved = self._get_sheet(all_sheets, sheet_aliases)
  162. if sheet is not None:
  163. self.energy_exp_config = sheet
  164. print(f"找到能量条经验值配置工作表: {resolved}")
  165. return
  166. print("警告: 未找到能量条经验值配置工作表")
  167. def _parse_energy_upgrade_config(self, all_sheets):
  168. """
  169. 解析能量条升级配置工作表
  170. """
  171. sheet_aliases = ['能量最大值升级', '能量升级', 'Energy Upgrade Config', 'energy_upgrade', 'energy upgrade']
  172. sheet, resolved = self._get_sheet(all_sheets, sheet_aliases)
  173. if sheet is not None:
  174. self.energy_upgrade_config = sheet
  175. print(f"找到能量条升级配置工作表: {resolved}")
  176. return
  177. print("警告: 未找到能量条升级配置工作表")
  178. def get_all_level_ids(self):
  179. """
  180. 获取所有关卡ID(支持大小写不敏感与英文别名)
  181. Returns:
  182. set: 所有关卡ID的集合(规范化形式,如 Level10)
  183. """
  184. level_ids = set()
  185. id_aliases = ['关卡ID', '关卡Id', '关卡id', 'LevelID', 'Level ID', 'level_id', 'level id', 'id']
  186. def add_id(val):
  187. if val is not None:
  188. level_ids.add(self._canonical_level_id(val))
  189. # 从基础配置中获取关卡ID
  190. if self.basic_config is not None:
  191. for _, row in self.basic_config.iterrows():
  192. add_id(self._get_row_value(row, id_aliases))
  193. # 从武器配置中获取关卡ID
  194. if self.weapon_config is not None:
  195. for _, row in self.weapon_config.iterrows():
  196. add_id(self._get_row_value(row, id_aliases))
  197. # 从波次配置中获取关卡ID
  198. if self.wave_config is not None:
  199. for _, row in self.wave_config.iterrows():
  200. add_id(self._get_row_value(row, id_aliases))
  201. return level_ids
  202. def merge_configurations(self):
  203. """
  204. 合并Excel配置到现有JSON文件
  205. Returns:
  206. dict: 合并结果统计
  207. """
  208. if not any([self.basic_config is not None, self.weapon_config is not None,
  209. self.wave_config is not None, self.enemy_config is not None]):
  210. print("错误: 没有可用的配置数据")
  211. return {"success": False, "message": "没有可用的配置数据"}
  212. level_ids = self.get_all_level_ids()
  213. if not level_ids:
  214. print("错误: 没有找到任何关卡ID")
  215. return {"success": False, "message": "没有找到任何关卡ID"}
  216. results = {
  217. "success": True,
  218. "processed_levels": [],
  219. "created_levels": [],
  220. "updated_levels": [],
  221. "errors": []
  222. }
  223. for level_id in level_ids:
  224. try:
  225. result = self._update_level_config(level_id)
  226. results["processed_levels"].append(level_id)
  227. if result["created"]:
  228. results["created_levels"].append(level_id)
  229. else:
  230. results["updated_levels"].append(level_id)
  231. except Exception as e:
  232. error_msg = f"处理关卡 {level_id} 时出错: {str(e)}"
  233. print(error_msg)
  234. results["errors"].append(error_msg)
  235. return results
  236. def _update_level_config(self, level_id):
  237. """
  238. 更新单个关卡的配置
  239. """
  240. canonical_id = self._canonical_level_id(level_id)
  241. cmp_id_lower = canonical_id.strip().lower()
  242. # 选择规范化的文件路径,并修正历史上小写文件名
  243. json_path = os.path.join(self.levels_dir, f"{canonical_id}.json")
  244. lower_path = os.path.join(self.levels_dir, f"{canonical_id.lower()}.json")
  245. if not os.path.exists(json_path) and os.path.exists(lower_path):
  246. try:
  247. os.replace(lower_path, json_path)
  248. print(f"已规范化文件名: {lower_path} -> {json_path}")
  249. except Exception as e:
  250. print(f"文件名规范化失败: {e}")
  251. # 读取现有配置或创建新配置
  252. if os.path.exists(json_path):
  253. with open(json_path, 'r', encoding='utf-8') as f:
  254. existing_config = json.load(f)
  255. created = False
  256. else:
  257. existing_config = {
  258. "levelId": canonical_id,
  259. "name": "",
  260. "scene": "grassland",
  261. "description": "",
  262. "backgroundImage": "images/LevelBackground/BG1",
  263. "availableWeapons": [],
  264. "coinReward": 100,
  265. "diamondReward": 0,
  266. "initialCoins": 50,
  267. "timeLimit": 300,
  268. "difficulty": "normal",
  269. "healthMultiplier": 1.0,
  270. "levelSettings": {
  271. "energyMax": 5
  272. },
  273. "waves": []
  274. }
  275. created = True
  276. # 保证levelId写入规范化形式
  277. existing_config['levelId'] = canonical_id
  278. # 备份现有文件
  279. if not created:
  280. self._backup_level_file(json_path, canonical_id)
  281. # 公共列别名
  282. id_aliases = ['关卡ID', '关卡Id', '关卡id', 'LevelID', 'Level ID', 'level_id', 'level id', 'id']
  283. # 更新基础配置
  284. if self.basic_config is not None:
  285. basic_row = None
  286. for _, row in self.basic_config.iterrows():
  287. rid = self._get_row_value(row, id_aliases)
  288. if rid is not None and str(rid).strip().lower() == cmp_id_lower:
  289. basic_row = row
  290. break
  291. if basic_row is not None:
  292. name = self._get_row_value(basic_row, ['关卡名称','关卡名','Level Name','name'])
  293. scene = self._get_row_value(basic_row, ['场景','Scene','scene'])
  294. desc = self._get_row_value(basic_row, ['描述','Description','desc'])
  295. bg = self._get_row_value(basic_row, ['关卡背景图路径','背景图','背景','Background Image','backgroundImage'])
  296. coins = self._get_row_value(basic_row, ['钞票奖励','Coin Reward','coins','coinReward'])
  297. diamonds = self._get_row_value(basic_row, ['钻石奖励','Diamond Reward','diamonds','diamondReward'])
  298. health_mul = self._get_row_value(basic_row, ['生命倍数','Health Multiplier','healthMultiplier'])
  299. difficulty = self._get_row_value(basic_row, ['难度','Difficulty','difficulty'])
  300. initial = self._get_row_value(basic_row, ['初始金币','Initial Coins','initialCoins'])
  301. energy_max = self._get_row_value(basic_row, ['能量最大值','Energy Max','energyMax'])
  302. if name is not None:
  303. existing_config['name'] = str(name)
  304. if scene is not None:
  305. existing_config['scene'] = str(scene)
  306. if desc is not None:
  307. existing_config['description'] = str(desc)
  308. if bg is not None:
  309. existing_config['backgroundImage'] = str(bg)
  310. if coins is not None:
  311. existing_config['coinReward'] = self._to_int(coins, existing_config.get('coinReward', 100))
  312. if diamonds is not None:
  313. existing_config['diamondReward'] = self._to_int(diamonds, existing_config.get('diamondReward', 0))
  314. if health_mul is not None:
  315. existing_config['healthMultiplier'] = self._to_float(health_mul, existing_config.get('healthMultiplier', 1.0))
  316. if difficulty is not None:
  317. existing_config['difficulty'] = str(difficulty)
  318. if initial is not None:
  319. existing_config['initialCoins'] = self._to_int(initial, existing_config.get('initialCoins', 50))
  320. if energy_max is not None:
  321. existing_config.setdefault('levelSettings', {})['energyMax'] = self._to_int(energy_max, existing_config.get('levelSettings', {}).get('energyMax', 5))
  322. if 'levelSettings' not in existing_config:
  323. existing_config['levelSettings'] = {}
  324. # 能量条升级配置
  325. if self.energy_upgrade_config is not None:
  326. energy_upgrade_row = None
  327. for _, row in self.energy_upgrade_config.iterrows():
  328. rid = self._get_row_value(row, id_aliases)
  329. if rid is not None and str(rid).strip().lower() == cmp_id_lower:
  330. energy_upgrade_row = row
  331. break
  332. if energy_upgrade_row is not None:
  333. if 'levelSettings' not in existing_config:
  334. existing_config['levelSettings'] = {}
  335. energy_max_values = []
  336. for i in range(1, 21):
  337. cn = f'第{i}次升级后最大值'
  338. en1 = f'Energy Max After Upgrade {i}'
  339. en2 = f'EnergyMaxUpgrade{i}'
  340. en3 = f'EnergyMax{i}'
  341. val = self._get_row_value(energy_upgrade_row, [cn, en1, en2, en3])
  342. if val is not None:
  343. energy_max_values.append(self._to_int(val, None))
  344. else:
  345. base_max = 6
  346. energy_max_values.append(base_max + (i - 1))
  347. existing_config['levelSettings']['energyMaxUpgrades'] = energy_max_values
  348. # 更新武器配置
  349. if self.weapon_config is not None:
  350. available_weapons = []
  351. for _, weapon_row in self.weapon_config.iterrows():
  352. rid = self._get_row_value(weapon_row, id_aliases)
  353. if rid is None or str(rid).strip().lower() != cmp_id_lower:
  354. continue
  355. weapons_str = self._get_row_value(weapon_row, ['可用武器','Available Weapons','weapons'])
  356. if weapons_str:
  357. weapons = re.split(r'[、,,;;]', str(weapons_str))
  358. available_weapons.extend([w.strip() for w in weapons if w.strip()])
  359. if available_weapons:
  360. existing_config['availableWeapons'] = available_weapons
  361. # 更新波次和敌人配置
  362. if self.wave_config is not None and self.enemy_config is not None:
  363. waves_data = []
  364. # collect rows for this level
  365. level_waves = []
  366. for _, wave_row in self.wave_config.iterrows():
  367. rid = self._get_row_value(wave_row, id_aliases)
  368. if rid is not None and str(rid).strip().lower() == cmp_id_lower:
  369. level_waves.append(wave_row)
  370. for wave_row in level_waves:
  371. wave_id_val = self._get_row_value(wave_row, ['波次ID','波次','Wave ID','waveId','wave id'])
  372. wave_id = self._to_int(wave_id_val, 1)
  373. wave_enemies = []
  374. wave_enemy_data = []
  375. for _, enemy_row in self.enemy_config.iterrows():
  376. rid = self._get_row_value(enemy_row, id_aliases)
  377. wid = self._get_row_value(enemy_row, ['波次ID','波次','Wave ID','waveId','wave id'])
  378. if rid is not None and str(rid).strip().lower() == cmp_id_lower and self._to_int(wid, None) == wave_id:
  379. wave_enemy_data.append(enemy_row)
  380. wave_health_multiplier = 1.0
  381. for enemy_row in wave_enemy_data:
  382. enemy_health_multiplier = self._to_float(
  383. self._get_row_value(enemy_row, ['血量系数','Health Multiplier','healthMultiplier']), 1.0
  384. )
  385. wave_health_multiplier = max(wave_health_multiplier, enemy_health_multiplier)
  386. enemy_type = self._get_row_value(enemy_row, ['敌人类型','Enemy Type','enemyType'])
  387. count = self._to_int(self._get_row_value(enemy_row, ['数量','Count','count']), 1)
  388. spawn_interval = self._to_float(self._get_row_value(enemy_row, ['生成间隔','Spawn Interval','spawnInterval']), 2.0)
  389. spawn_delay = self._to_float(self._get_row_value(enemy_row, ['生成延迟','Spawn Delay','spawnDelay']), 0.0)
  390. characteristics = self._get_row_value(enemy_row, ['特征描述','Characteristics','characteristics'])
  391. enemy_data = {
  392. 'enemyType': str(enemy_type) if enemy_type is not None else 'normal_zombie',
  393. 'count': count,
  394. 'spawnInterval': spawn_interval,
  395. 'spawnDelay': spawn_delay,
  396. 'characteristics': str(characteristics) if characteristics is not None else '',
  397. 'healthMultiplier': enemy_health_multiplier
  398. }
  399. wave_enemies.append(enemy_data)
  400. wave_data = {
  401. 'waveId': wave_id,
  402. 'healthMultiplier': wave_health_multiplier,
  403. 'enemies': wave_enemies
  404. }
  405. waves_data.append(wave_data)
  406. if waves_data:
  407. waves_data.sort(key=lambda x: x['waveId'])
  408. existing_config['waves'] = waves_data
  409. # 保存更新后的配置
  410. with open(json_path, 'w', encoding='utf-8') as f:
  411. json.dump(existing_config, f, ensure_ascii=False, indent=2)
  412. print(f"{'创建' if created else '更新'}关卡配置: {canonical_id}")
  413. return {"created": created, "updated": not created}
  414. def _backup_level_file(self, json_path, level_id):
  415. """
  416. 备份关卡JSON文件
  417. Args:
  418. json_path: JSON文件路径
  419. level_id: 关卡ID
  420. """
  421. try:
  422. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  423. backup_filename = f"{level_id}_{timestamp}.json"
  424. backup_path = os.path.join(self.backup_dir, backup_filename)
  425. with open(json_path, 'r', encoding='utf-8') as src:
  426. with open(backup_path, 'w', encoding='utf-8') as dst:
  427. dst.write(src.read())
  428. print(f"备份关卡文件: {backup_path}")
  429. except Exception as e:
  430. print(f"备份关卡文件失败: {e}")
  431. def import_from_excel(self, excel_path=None, levels_dir=None):
  432. """
  433. 从Excel导入关卡配置的完整流程
  434. Args:
  435. excel_path: Excel文件路径
  436. levels_dir: 关卡JSON文件目录
  437. Returns:
  438. dict: 导入结果
  439. """
  440. if excel_path:
  441. self.excel_path = excel_path
  442. if levels_dir:
  443. self.levels_dir = levels_dir
  444. print("开始导入关卡配置...")
  445. # 读取Excel数据
  446. if not self.read_excel_data():
  447. return {"success": False, "message": "读取Excel数据失败"}
  448. # 合并配置
  449. results = self.merge_configurations()
  450. if results["success"]:
  451. print(f"关卡配置导入完成:")
  452. print(f" 处理关卡数: {len(results['processed_levels'])}")
  453. print(f" 新建关卡数: {len(results['created_levels'])}")
  454. print(f" 更新关卡数: {len(results['updated_levels'])}")
  455. if results["errors"]:
  456. print(f" 错误数: {len(results['errors'])}")
  457. for error in results["errors"]:
  458. print(f" {error}")
  459. return results
  460. # 使用示例
  461. if __name__ == "__main__":
  462. # 创建关卡配置管理器
  463. manager = LevelConfigManager(
  464. excel_path="d:/CocosGame/Pong/assets/data/excel/关卡配置/关卡配置表.xlsx",
  465. levels_dir="d:/CocosGame/Pong/assets/data/levels"
  466. )
  467. # 导入配置
  468. result = manager.import_from_excel()
  469. if result["success"]:
  470. print("关卡配置导入成功!")
  471. else:
  472. print(f"关卡配置导入失败: {result['message']}")