game_skill_config_manager.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 游戏内技能配置管理器
  5. 处理技能配置表.xlsx中的技能信息,与现有skill.json配置合并
  6. 支持技能参数配置和技能信息表的读取与处理
  7. """
  8. import json
  9. import os
  10. from pathlib import Path
  11. from datetime import datetime
  12. try:
  13. import pandas as pd
  14. PANDAS_AVAILABLE = True
  15. except ImportError:
  16. PANDAS_AVAILABLE = False
  17. print("警告: pandas未安装,无法处理Excel文件")
  18. class GameSkillConfigManager:
  19. """游戏内技能配置管理器"""
  20. def __init__(self, excel_file_path=None, json_file_path=None):
  21. """初始化游戏内技能配置管理器
  22. Args:
  23. excel_file_path: Excel配置文件路径
  24. json_file_path: JSON配置文件路径
  25. """
  26. self.script_dir = Path(__file__).parent
  27. # 设置默认路径
  28. if excel_file_path is None:
  29. self.excel_file = self.script_dir / "技能配置表.xlsx"
  30. else:
  31. self.excel_file = Path(excel_file_path)
  32. if json_file_path is None:
  33. self.json_file = self.script_dir.parent / "skill.json"
  34. else:
  35. self.json_file = Path(json_file_path)
  36. print(f"Excel文件路径: {self.excel_file}")
  37. print(f"JSON文件路径: {self.json_file}")
  38. # 技能参数映射
  39. self.skill_param_mapping = {
  40. 'skillId': 'id',
  41. 'skillName': 'name',
  42. 'description': 'description',
  43. 'icon': 'iconPath',
  44. 'maxLevel': 'maxLevel',
  45. 'currentLevel': 'currentLevel',
  46. 'priceReduction': 'priceReduction',
  47. 'critChanceIncrease': 'critChanceIncrease',
  48. 'critDamageBonus': 'critDamageBonus',
  49. 'healthIncrease': 'healthIncrease',
  50. 'multiShotChance': 'multiShotChance',
  51. 'energyBonus': 'energyBonus',
  52. 'speedIncrease': 'speedIncrease'
  53. }
  54. # 默认配置结构
  55. self.default_config = {
  56. "skills": []
  57. }
  58. def load_existing_json_config(self):
  59. """加载现有的JSON配置文件"""
  60. try:
  61. if self.json_file.exists():
  62. with open(self.json_file, 'r', encoding='utf-8') as f:
  63. config = json.load(f)
  64. print(f"成功加载现有JSON配置")
  65. return config
  66. else:
  67. print(f"JSON文件不存在,将创建新配置: {self.json_file}")
  68. return self.default_config.copy()
  69. except Exception as e:
  70. print(f"加载JSON配置失败: {e}")
  71. return self.default_config.copy()
  72. def read_excel_config(self):
  73. """读取Excel配置文件"""
  74. if not PANDAS_AVAILABLE:
  75. raise Exception("pandas未安装,无法读取Excel文件")
  76. if not self.excel_file.exists():
  77. raise Exception(f"Excel文件不存在: {self.excel_file}")
  78. try:
  79. # 读取所有工作表
  80. excel_data = pd.read_excel(self.excel_file, sheet_name=None)
  81. print(f"成功读取Excel文件,包含工作表: {list(excel_data.keys())}")
  82. return excel_data
  83. except Exception as e:
  84. raise Exception(f"读取Excel文件失败: {e}")
  85. def parse_skill_params(self, df):
  86. """解析技能参数配置"""
  87. skill_params = {}
  88. try:
  89. for index, row in df.iterrows():
  90. # 跳过表头,但允许有效的参数名
  91. if index == 0 and str(row.iloc[0]).strip() in ['技能ID', 'skillId', '参数名', 'paramName']:
  92. continue
  93. # 检查是否为有效数据行
  94. if pd.isna(row.iloc[0]) or str(row.iloc[0]).strip() == '':
  95. continue
  96. param_name = str(row.iloc[0]).strip()
  97. data_type = str(row.iloc[1]).strip() if pd.notna(row.iloc[1]) else 'string'
  98. default_value = row.iloc[2] if pd.notna(row.iloc[2]) else ''
  99. valid_range = str(row.iloc[3]).strip() if pd.notna(row.iloc[3]) else ''
  100. description = str(row.iloc[4]).strip() if pd.notna(row.iloc[4]) else ''
  101. skill_params[param_name] = {
  102. 'dataType': data_type,
  103. 'defaultValue': default_value,
  104. 'validRange': valid_range,
  105. 'description': description
  106. }
  107. print(f"解析到 {len(skill_params)} 个技能参数")
  108. return skill_params
  109. except Exception as e:
  110. print(f"解析技能参数失败: {e}")
  111. return {}
  112. def parse_skill_info(self, df):
  113. """解析技能信息表"""
  114. skills = []
  115. try:
  116. for index, row in df.iterrows():
  117. # 跳过表头,但允许有效的技能ID
  118. if index == 0 and str(row.iloc[0]).strip() in ['技能ID', 'skillId', 'ID']:
  119. continue
  120. # 检查是否为有效数据行
  121. if pd.isna(row.iloc[0]) or str(row.iloc[0]).strip() == '':
  122. continue
  123. skill = {
  124. 'id': str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else '',
  125. 'name': str(row.iloc[1]).strip() if pd.notna(row.iloc[1]) else '',
  126. 'description': str(row.iloc[2]).strip() if pd.notna(row.iloc[2]) else '',
  127. 'iconPath': str(row.iloc[3]).strip() if pd.notna(row.iloc[3]) else '',
  128. 'maxLevel': int(row.iloc[4]) if pd.notna(row.iloc[4]) else 5,
  129. 'currentLevel': int(row.iloc[5]) if pd.notna(row.iloc[5]) else 0,
  130. 'levelEffects': {}
  131. }
  132. # 解析技能效果参数,保持原有JSON格式
  133. effect_arrays = {}
  134. descriptions = []
  135. # 获取基础效果值
  136. base_effects = {}
  137. if pd.notna(row.iloc[6]) and float(row.iloc[6]) != 0: # 价格减少
  138. base_effects['priceReduction'] = float(row.iloc[6])
  139. if pd.notna(row.iloc[7]) and float(row.iloc[7]) != 0: # 暴击几率增加
  140. base_effects['critChanceIncrease'] = float(row.iloc[7])
  141. if pd.notna(row.iloc[8]) and float(row.iloc[8]) != 0: # 暴击伤害加成
  142. base_effects['critDamageBonus'] = float(row.iloc[8])
  143. if pd.notna(row.iloc[9]) and float(row.iloc[9]) != 0: # 生命值增加
  144. base_effects['healthIncrease'] = float(row.iloc[9])
  145. if pd.notna(row.iloc[10]) and float(row.iloc[10]) != 0: # 多重射击几率
  146. base_effects['multiShotChance'] = float(row.iloc[10])
  147. if pd.notna(row.iloc[11]) and float(row.iloc[11]) != 0: # 能量加成
  148. base_effects['energyBonus'] = float(row.iloc[11])
  149. if pd.notna(row.iloc[12]) and float(row.iloc[12]) != 0: # 速度提升
  150. base_effects['speedIncrease'] = float(row.iloc[12])
  151. # 生成每个效果的数组格式(包含0级)
  152. for effect_name, base_value in base_effects.items():
  153. effect_array = [0] # 0级效果为0
  154. for level in range(1, skill['maxLevel'] + 1):
  155. effect_array.append(base_value * level)
  156. effect_arrays[effect_name] = effect_array
  157. # 生成描述数组
  158. for level in range(1, skill['maxLevel'] + 1):
  159. desc = skill['description']
  160. # 替换描述中的占位符,根据实际效果值计算
  161. if '{value}' in desc:
  162. # 找到第一个有效的效果值来替换占位符
  163. replacement_value = None
  164. for effect_name, effect_array in effect_arrays.items():
  165. if len(effect_array) > level and effect_array[level] > 0:
  166. replacement_value = int(effect_array[level] * 100)
  167. break
  168. if replacement_value is not None:
  169. desc = desc.replace('{value}', str(replacement_value))
  170. else:
  171. # 如果没有找到有效值,使用基础效果值
  172. for effect_name, base_value in base_effects.items():
  173. replacement_value = int(base_value * level * 100)
  174. desc = desc.replace('{value}', str(replacement_value))
  175. break
  176. descriptions.append(desc)
  177. # 设置levelEffects为对象格式
  178. skill['levelEffects'] = effect_arrays.copy()
  179. if descriptions:
  180. skill['levelEffects']['descriptions'] = descriptions
  181. skills.append(skill)
  182. print(f"解析到 {len(skills)} 个技能")
  183. return skills
  184. except Exception as e:
  185. print(f"解析技能信息失败: {e}")
  186. return []
  187. def parse_game_skill_data(self, excel_data):
  188. """解析游戏技能配置数据"""
  189. try:
  190. config = self.default_config.copy()
  191. skill_params = {}
  192. # 读取技能参数配置
  193. if "技能参数配置" in excel_data:
  194. skill_params = self.parse_skill_params(excel_data["技能参数配置"])
  195. # 读取技能信息表
  196. if "技能信息表" in excel_data:
  197. skills = self.parse_skill_info(excel_data["技能信息表"])
  198. config["skills"] = skills
  199. return config, skill_params
  200. except Exception as e:
  201. print(f"解析游戏技能配置数据失败: {e}")
  202. return self.default_config.copy(), {}
  203. def validate_config(self, config):
  204. """验证配置数据"""
  205. try:
  206. # 检查必要字段
  207. if 'skills' not in config:
  208. print("错误: 缺少 skills 字段")
  209. return False
  210. if not isinstance(config['skills'], list):
  211. print("错误: skills 必须是列表")
  212. return False
  213. # 检查每个技能的必要字段
  214. required_skill_fields = ['id', 'name', 'description', 'iconPath', 'maxLevel', 'currentLevel', 'levelEffects']
  215. for i, skill in enumerate(config['skills']):
  216. for field in required_skill_fields:
  217. if field not in skill:
  218. print(f"警告: 技能 {i} 缺少字段 {field}")
  219. return False
  220. print("配置验证通过")
  221. return True
  222. except Exception as e:
  223. print(f"配置验证失败: {e}")
  224. return False
  225. def merge_skill_configs(self, existing_config, excel_config):
  226. """合并现有配置和Excel配置"""
  227. try:
  228. # 创建合并后的配置
  229. merged_config = existing_config.copy()
  230. if 'skills' not in merged_config:
  231. merged_config['skills'] = []
  232. # 创建现有技能的ID映射
  233. existing_skills_map = {}
  234. for skill in merged_config['skills']:
  235. if 'id' in skill:
  236. existing_skills_map[skill['id']] = skill
  237. # 更新或添加技能
  238. updated_count = 0
  239. added_count = 0
  240. for excel_skill in excel_config.get('skills', []):
  241. skill_id = excel_skill.get('id')
  242. if not skill_id:
  243. continue
  244. if skill_id in existing_skills_map:
  245. # 更新现有技能,保留currentLevel等用户数据
  246. existing_skill = existing_skills_map[skill_id]
  247. # 保存原有的currentLevel
  248. original_current_level = existing_skill.get('currentLevel', 0)
  249. # 更新基本信息
  250. existing_skill['name'] = excel_skill.get('name', existing_skill.get('name', ''))
  251. existing_skill['description'] = excel_skill.get('description', existing_skill.get('description', ''))
  252. existing_skill['iconPath'] = excel_skill.get('iconPath', existing_skill.get('iconPath', ''))
  253. existing_skill['maxLevel'] = excel_skill.get('maxLevel', existing_skill.get('maxLevel', 5))
  254. # 更新levelEffects,保持对象格式
  255. if 'levelEffects' in excel_skill:
  256. existing_skill['levelEffects'] = excel_skill['levelEffects']
  257. # 恢复原有的currentLevel(用户进度)
  258. existing_skill['currentLevel'] = original_current_level
  259. updated_count += 1
  260. else:
  261. # 添加新技能
  262. merged_config['skills'].append(excel_skill)
  263. added_count += 1
  264. print(f"技能配置合并完成: 更新 {updated_count} 个技能,新增 {added_count} 个技能")
  265. return merged_config
  266. except Exception as e:
  267. print(f"合并配置失败: {e}")
  268. return existing_config
  269. def backup_json_config(self):
  270. """备份现有JSON配置"""
  271. try:
  272. if self.json_file.exists():
  273. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  274. backup_file = self.json_file.parent / f"{self.json_file.stem}_backup_{timestamp}.json"
  275. with open(self.json_file, 'r', encoding='utf-8') as src:
  276. with open(backup_file, 'w', encoding='utf-8') as dst:
  277. dst.write(src.read())
  278. print(f"配置已备份到: {backup_file}")
  279. return backup_file
  280. else:
  281. print("JSON文件不存在,无需备份")
  282. return None
  283. except Exception as e:
  284. print(f"备份配置失败: {e}")
  285. return None
  286. def save_json_config(self, config):
  287. """保存配置到JSON文件"""
  288. try:
  289. # 确保目录存在
  290. self.json_file.parent.mkdir(parents=True, exist_ok=True)
  291. with open(self.json_file, 'w', encoding='utf-8') as f:
  292. json.dump(config, f, ensure_ascii=False, indent=2)
  293. print(f"配置已保存到: {self.json_file}")
  294. return True
  295. except Exception as e:
  296. print(f"保存JSON文件失败: {e}")
  297. return False
  298. def import_game_skill_config(self):
  299. """导入游戏技能配置的主方法"""
  300. try:
  301. print("开始导入游戏技能配置...")
  302. # 1. 加载现有JSON配置
  303. existing_config = self.load_existing_json_config()
  304. # 2. 读取Excel配置
  305. excel_data = self.read_excel_config()
  306. # 3. 解析Excel数据
  307. excel_config, skill_params = self.parse_game_skill_data(excel_data)
  308. # 4. 验证配置
  309. if not self.validate_config(excel_config):
  310. print("配置验证失败,使用默认配置")
  311. excel_config = self.default_config.copy()
  312. # 5. 合并配置
  313. merged_config = self.merge_skill_configs(existing_config, excel_config)
  314. # 6. 备份现有配置
  315. self.backup_json_config()
  316. # 7. 保存新配置
  317. if self.save_json_config(merged_config):
  318. print("游戏技能配置导入成功!")
  319. return True, "游戏技能配置导入成功"
  320. else:
  321. print("游戏技能配置保存失败!")
  322. return False, "游戏技能配置保存失败"
  323. except Exception as e:
  324. error_msg = f"导入游戏技能配置失败: {e}"
  325. print(error_msg)
  326. return False, error_msg
  327. def main():
  328. """主函数"""
  329. print("游戏内技能配置管理器")
  330. print("=" * 50)
  331. # 创建游戏技能配置管理器
  332. manager = GameSkillConfigManager()
  333. # 导入配置
  334. success, message = manager.import_game_skill_config()
  335. if success:
  336. print(f"\n✓ {message}")
  337. else:
  338. print(f"\n✗ {message}")
  339. if __name__ == "__main__":
  340. main()