config_manager.py 139 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. BallController配置管理工具 - 简化版
  5. 图形界面版本,不依赖pandas,支持浏览文件并选择Excel/CSV表格进行配置导入
  6. 功能:
  7. 1. 图形界面浏览项目文件
  8. 2. 多选Excel/CSV配置表格
  9. 3. 预览配置内容
  10. 4. 一键导入配置到JSON
  11. 5. 配置备份和恢复
  12. 作者: AI Assistant
  13. 日期: 2024
  14. """
  15. import tkinter as tk
  16. from tkinter import ttk, filedialog, messagebox, scrolledtext
  17. import json
  18. import os
  19. import csv
  20. from datetime import datetime
  21. from pathlib import Path
  22. import threading
  23. # 尝试导入pandas,如果失败则使用纯CSV模式
  24. try:
  25. import pandas as pd
  26. PANDAS_AVAILABLE = True
  27. except ImportError:
  28. PANDAS_AVAILABLE = False
  29. print("警告: pandas未安装,将使用纯CSV模式(不支持Excel文件)")
  30. # 导入武器配置管理器
  31. try:
  32. from weapon_config_manager import WeaponConfigManager
  33. WEAPON_MANAGER_AVAILABLE = True
  34. except ImportError:
  35. WEAPON_MANAGER_AVAILABLE = False
  36. print("警告: 武器配置管理器未找到,武器配置功能将不可用")
  37. class SkillConfigImporter:
  38. """技能配置表导入工具类"""
  39. def __init__(self, excel_dir=None):
  40. # 获取当前脚本所在目录
  41. if excel_dir:
  42. self.script_dir = Path(excel_dir)
  43. else:
  44. self.script_dir = Path(__file__).parent
  45. self.excel_file = self.script_dir / "局外技能配置表.xlsx"
  46. self.json_file = self.script_dir.parent / "skill_config.json"
  47. # 默认配置结构
  48. self.default_config = {
  49. "skillTypes": [],
  50. "skillGroups": [],
  51. "totalGroups": 12,
  52. "skillsPerGroup": 3
  53. }
  54. def read_excel_config(self):
  55. """从Excel文件读取技能配置"""
  56. if not self.excel_file.exists():
  57. print(f"错误: Excel文件不存在: {self.excel_file}")
  58. return None
  59. try:
  60. # 读取所有工作表
  61. excel_data = pd.read_excel(self.excel_file, sheet_name=None)
  62. print(f"找到工作表: {list(excel_data.keys())}")
  63. config = self.default_config.copy()
  64. # 读取技能类型配置
  65. if "技能类型配置" in excel_data:
  66. skill_types = self.parse_skill_types(excel_data["技能类型配置"])
  67. config["skillTypes"] = skill_types
  68. print(f"读取到 {len(skill_types)} 个技能类型")
  69. elif "JSON技能类型配置" in excel_data:
  70. skill_types = self.parse_skill_types(excel_data["JSON技能类型配置"])
  71. config["skillTypes"] = skill_types
  72. print(f"读取到 {len(skill_types)} 个技能类型")
  73. # 读取技能组配置
  74. if "技能组配置" in excel_data:
  75. skill_groups = self.parse_skill_groups(excel_data["技能组配置"])
  76. config["skillGroups"] = skill_groups
  77. config["totalGroups"] = len(skill_groups)
  78. print(f"读取到 {len(skill_groups)} 个技能组")
  79. elif "JSON技能组配置" in excel_data:
  80. skill_groups = self.parse_skill_groups(excel_data["JSON技能组配置"])
  81. config["skillGroups"] = skill_groups
  82. config["totalGroups"] = len(skill_groups)
  83. print(f"读取到 {len(skill_groups)} 个技能组")
  84. # 读取基本配置
  85. if "基本配置" in excel_data:
  86. basic_config = self.parse_basic_config(excel_data["基本配置"])
  87. config.update(basic_config)
  88. elif "JSON基本配置" in excel_data:
  89. basic_config = self.parse_basic_config(excel_data["JSON基本配置"])
  90. config.update(basic_config)
  91. return config
  92. except Exception as e:
  93. print(f"读取Excel文件失败: {e}")
  94. return None
  95. def parse_skill_types(self, df):
  96. """解析技能类型配置"""
  97. skill_types = []
  98. # 读取所有数据行(pandas已经自动处理了标题行)
  99. for index, row in df.iterrows():
  100. # 检查是否为空行
  101. if pd.isna(row.iloc[0]):
  102. continue
  103. try:
  104. skill_type = {
  105. "id": int(row.iloc[0]) if not pd.isna(row.iloc[0]) else 0,
  106. "name": str(row.iloc[1]) if not pd.isna(row.iloc[1]) else "",
  107. "displayName": str(row.iloc[2]) if not pd.isna(row.iloc[2]) else "",
  108. "nameTemplate": str(row.iloc[3]) if not pd.isna(row.iloc[3]) else "",
  109. "type": str(row.iloc[4]) if not pd.isna(row.iloc[4]) else ""
  110. }
  111. skill_types.append(skill_type)
  112. except Exception as e:
  113. print(f"解析技能类型第 {index+1} 行失败: {e}")
  114. continue
  115. return skill_types
  116. def parse_skill_groups(self, df):
  117. """解析技能组配置"""
  118. skill_groups = []
  119. # 读取所有数据行(pandas已经自动处理了标题行)
  120. for index, row in df.iterrows():
  121. # 检查是否为空行
  122. if pd.isna(row.iloc[0]):
  123. continue
  124. try:
  125. skill_group = {
  126. "group": int(row.iloc[0]) if not pd.isna(row.iloc[0]) else 1,
  127. "effectPercent": int(row.iloc[1]) if not pd.isna(row.iloc[1]) else 0,
  128. "diamondCost": int(row.iloc[2]) if not pd.isna(row.iloc[2]) else 0
  129. }
  130. skill_groups.append(skill_group)
  131. except Exception as e:
  132. print(f"解析技能组第 {index+1} 行失败: {e}")
  133. continue
  134. return skill_groups
  135. def parse_basic_config(self, df):
  136. """解析基本配置"""
  137. basic_config = {}
  138. # 假设基本配置是纵向格式:配置项名称 | 数值
  139. for index, row in df.iterrows():
  140. if index == 0: # 跳过标题行
  141. continue
  142. # 检查是否为空行
  143. if pd.isna(row.iloc[0]):
  144. continue
  145. try:
  146. param_name = str(row.iloc[0]).strip()
  147. param_value = row.iloc[1]
  148. if param_name == "总组数" or param_name == "totalGroups":
  149. basic_config["totalGroups"] = int(param_value) if not pd.isna(param_value) else 12
  150. elif param_name == "每组技能数" or param_name == "skillsPerGroup":
  151. basic_config["skillsPerGroup"] = int(param_value) if not pd.isna(param_value) else 3
  152. except Exception as e:
  153. print(f"解析基本配置第 {index+1} 行失败: {e}")
  154. continue
  155. return basic_config
  156. def validate_config(self, config):
  157. """验证配置数据"""
  158. errors = []
  159. # 验证技能类型
  160. if not config.get("skillTypes"):
  161. errors.append("技能类型配置为空")
  162. else:
  163. for i, skill_type in enumerate(config["skillTypes"]):
  164. if not skill_type.get("name"):
  165. errors.append(f"技能类型 {i+1} 缺少名称")
  166. if not skill_type.get("type"):
  167. errors.append(f"技能类型 {i+1} 缺少类型标识")
  168. # 验证技能组
  169. if not config.get("skillGroups"):
  170. errors.append("技能组配置为空")
  171. else:
  172. groups = [sg["group"] for sg in config["skillGroups"]]
  173. if len(groups) != len(set(groups)):
  174. errors.append("技能组编号存在重复")
  175. # 验证基本配置
  176. if config.get("totalGroups", 0) <= 0:
  177. errors.append("总组数必须大于0")
  178. if config.get("skillsPerGroup", 0) <= 0:
  179. errors.append("每组技能数必须大于0")
  180. return errors
  181. def backup_json(self):
  182. """备份当前JSON文件(已禁用)"""
  183. # 不再创建备份文件,直接返回成功
  184. return True
  185. def save_json_config(self, config):
  186. """保存配置到JSON文件"""
  187. try:
  188. with open(self.json_file, 'w', encoding='utf-8') as f:
  189. json.dump(config, f, ensure_ascii=False, indent=2)
  190. print(f"配置已保存到: {self.json_file}")
  191. return True
  192. except Exception as e:
  193. print(f"保存JSON文件失败: {e}")
  194. return False
  195. def import_config(self):
  196. """执行配置导入"""
  197. print("=== 技能配置导入工具 ===")
  198. print(f"Excel文件: {self.excel_file}")
  199. print(f"JSON文件: {self.json_file}")
  200. print()
  201. # 读取Excel配置
  202. config = self.read_excel_config()
  203. if not config:
  204. print("读取Excel配置失败")
  205. return False
  206. # 验证配置
  207. errors = self.validate_config(config)
  208. if errors:
  209. print("配置验证失败:")
  210. for error in errors:
  211. print(f" - {error}")
  212. return False
  213. print("配置验证通过")
  214. # 显示配置摘要
  215. print(f"\n配置摘要:")
  216. print(f" 技能类型数量: {len(config['skillTypes'])}")
  217. print(f" 技能组数量: {len(config['skillGroups'])}")
  218. print(f" 总组数: {config['totalGroups']}")
  219. print(f" 每组技能数: {config['skillsPerGroup']}")
  220. # 备份原文件
  221. if not self.backup_json():
  222. print("备份失败,是否继续?(y/n): ", end="")
  223. # 在GUI环境中,我们默认继续
  224. print("y (自动继续)")
  225. # 保存新配置
  226. if self.save_json_config(config):
  227. print("\n配置导入成功!")
  228. return True
  229. else:
  230. print("\n配置导入失败!")
  231. return False
  232. class ConfigManagerGUI:
  233. def __init__(self):
  234. self.root = tk.Tk()
  235. self.root.title("游戏配置管理工具")
  236. self.root.geometry("1000x700")
  237. self.root.resizable(True, True)
  238. # 配置文件路径 - 动态获取项目根目录
  239. # 从当前脚本位置向上查找项目根目录
  240. current_dir = Path(__file__).parent
  241. self.project_root = current_dir.parent.parent.parent.parent # 从excel目录向上4级到项目根目录
  242. self.excel_dir = current_dir
  243. # 手动配置变量
  244. self.selected_json_path = None
  245. self.format_type = 'horizontal' # 默认为横向表格
  246. self.multi_sheet = False
  247. self.sheet_names = []
  248. self.param_types = {}
  249. # 当前选择的配置映射
  250. self.current_mapping = None
  251. # 移除json_config_path,现在使用selected_json_path
  252. self.param_types = {}
  253. # 默认配置值
  254. self.default_config = {
  255. 'baseSpeed': 60,
  256. 'maxReflectionRandomness': 0.2,
  257. 'antiTrapTimeWindow': 5.0,
  258. 'antiTrapHitThreshold': 5,
  259. 'deflectionAttemptThreshold': 3,
  260. 'antiTrapDeflectionMultiplier': 3.0,
  261. 'FIRE_COOLDOWN': 0.05,
  262. 'ballRadius': 10,
  263. 'gravityScale': 0,
  264. 'linearDamping': 0,
  265. 'angularDamping': 0,
  266. 'colliderGroup': 2,
  267. 'colliderTag': 1,
  268. 'friction': 0,
  269. 'restitution': 1,
  270. 'safeDistance': 50,
  271. 'edgeOffset': 20,
  272. 'sensor': False
  273. }
  274. self.selected_files = []
  275. self.config_data = {}
  276. self.setup_ui()
  277. self.load_current_config()
  278. def setup_ui(self):
  279. """设置用户界面"""
  280. # 主框架
  281. main_frame = ttk.Frame(self.root, padding="10")
  282. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  283. # 配置根窗口的网格权重
  284. self.root.columnconfigure(0, weight=1)
  285. self.root.rowconfigure(0, weight=1)
  286. main_frame.columnconfigure(1, weight=1)
  287. main_frame.rowconfigure(2, weight=1)
  288. # 标题
  289. title_label = ttk.Label(main_frame, text="游戏配置管理工具",
  290. font=("Arial", 16, "bold"))
  291. title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
  292. # 左侧面板 - 文件选择
  293. file_types_text = "Excel/CSV配置文件选择" if PANDAS_AVAILABLE else "CSV配置文件选择"
  294. left_frame = ttk.LabelFrame(main_frame, text=file_types_text, padding="10")
  295. left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
  296. left_frame.columnconfigure(0, weight=1)
  297. left_frame.rowconfigure(3, weight=1)
  298. # 文件浏览按钮
  299. browse_frame = ttk.Frame(left_frame)
  300. browse_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
  301. browse_frame.columnconfigure(0, weight=1)
  302. browse_text = "浏览Excel/CSV文件" if PANDAS_AVAILABLE else "浏览CSV文件"
  303. ttk.Button(browse_frame, text=browse_text,
  304. command=self.browse_files).grid(row=0, column=0, sticky=(tk.W, tk.E))
  305. ttk.Button(browse_frame, text="扫描项目目录",
  306. command=self.scan_project_files).grid(row=0, column=1, padx=(10, 0))
  307. # JSON文件选择区域
  308. json_frame = ttk.LabelFrame(left_frame, text="JSON输出路径选择", padding="5")
  309. json_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  310. json_frame.columnconfigure(1, weight=1)
  311. ttk.Label(json_frame, text="输出路径:").grid(row=0, column=0, sticky=tk.W)
  312. self.json_path_var = tk.StringVar()
  313. self.json_path_entry = ttk.Entry(json_frame, textvariable=self.json_path_var, state='readonly')
  314. self.json_path_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(5, 0))
  315. ttk.Button(json_frame, text="浏览", command=self.browse_json_path).grid(row=0, column=2, padx=(5, 0))
  316. # 配置选项
  317. config_frame = ttk.LabelFrame(left_frame, text="配置选项", padding="5")
  318. config_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  319. # 表格格式选择
  320. ttk.Label(config_frame, text="表格格式:").grid(row=0, column=0, sticky=tk.W)
  321. self.format_var = tk.StringVar(value="horizontal")
  322. format_frame = ttk.Frame(config_frame)
  323. format_frame.grid(row=0, column=1, sticky=tk.W, padx=(5, 0))
  324. ttk.Radiobutton(format_frame, text="横向", variable=self.format_var, value="horizontal").pack(side=tk.LEFT)
  325. ttk.Radiobutton(format_frame, text="纵向", variable=self.format_var, value="vertical").pack(side=tk.LEFT, padx=(10, 0))
  326. # 多工作表选项
  327. self.multi_sheet_var = tk.BooleanVar()
  328. ttk.Checkbutton(config_frame, text="多工作表文件", variable=self.multi_sheet_var,
  329. command=self.on_multi_sheet_change).grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
  330. # 单个JSON选项
  331. self.single_json_var = tk.BooleanVar(value=True)
  332. ttk.Checkbutton(config_frame, text="单个json", variable=self.single_json_var,
  333. command=self.on_single_json_change).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
  334. # 文件列表
  335. list_frame = ttk.Frame(left_frame)
  336. list_frame.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  337. list_frame.columnconfigure(0, weight=1)
  338. list_frame.rowconfigure(0, weight=1)
  339. self.file_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, height=15)
  340. scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
  341. self.file_listbox.configure(yscrollcommand=scrollbar.set)
  342. self.file_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  343. scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
  344. # 文件操作按钮
  345. file_btn_frame = ttk.Frame(left_frame)
  346. file_btn_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  347. ttk.Button(file_btn_frame, text="预览选中文件",
  348. command=self.preview_selected_files).pack(side=tk.LEFT)
  349. ttk.Button(file_btn_frame, text="清空选择",
  350. command=self.clear_selection).pack(side=tk.LEFT, padx=(10, 0))
  351. # 右侧面板 - 配置预览和操作
  352. right_frame = ttk.LabelFrame(main_frame, text="配置预览与操作", padding="10")
  353. right_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
  354. right_frame.columnconfigure(0, weight=1)
  355. right_frame.rowconfigure(0, weight=1)
  356. # 配置预览文本框
  357. self.preview_text = scrolledtext.ScrolledText(right_frame, height=20, width=50)
  358. self.preview_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
  359. # 操作按钮
  360. btn_frame = ttk.Frame(right_frame)
  361. btn_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
  362. ttk.Button(btn_frame, text="导入配置",
  363. command=self.import_config).pack(side=tk.LEFT)
  364. ttk.Button(btn_frame, text="导入技能配置",
  365. command=self.import_skill_config).pack(side=tk.LEFT, padx=(10, 0))
  366. ttk.Button(btn_frame, text="备份当前配置",
  367. command=self.backup_config).pack(side=tk.LEFT, padx=(10, 0))
  368. ttk.Button(btn_frame, text="恢复默认配置",
  369. command=self.restore_default_config).pack(side=tk.LEFT, padx=(10, 0))
  370. # 武器配置专用按钮
  371. weapon_btn_frame = ttk.Frame(right_frame)
  372. weapon_btn_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  373. if WEAPON_MANAGER_AVAILABLE:
  374. ttk.Button(weapon_btn_frame, text="导入武器配置",
  375. command=self.import_weapon_config).pack(side=tk.LEFT)
  376. else:
  377. ttk.Label(weapon_btn_frame, text="武器配置管理器不可用",
  378. foreground="red").pack(side=tk.LEFT)
  379. # 敌人配置专用按钮
  380. enemy_btn_frame = ttk.Frame(right_frame)
  381. enemy_btn_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
  382. ttk.Button(enemy_btn_frame, text="修复敌人JSON",
  383. command=self.fix_enemies_json).pack(side=tk.LEFT)
  384. ttk.Button(enemy_btn_frame, text="自动导入敌人配置",
  385. command=self.auto_import_enemies_from_excel).pack(side=tk.LEFT, padx=(10, 0))
  386. # 底部状态栏
  387. self.status_var = tk.StringVar()
  388. self.status_var.set("就绪")
  389. status_bar = ttk.Label(main_frame, textvariable=self.status_var,
  390. relief=tk.SUNKEN, anchor=tk.W)
  391. status_bar.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
  392. # 绑定事件
  393. self.file_listbox.bind('<<ListboxSelect>>', self.on_file_select)
  394. def browse_files(self):
  395. """浏览Excel/CSV文件"""
  396. if PANDAS_AVAILABLE:
  397. title = "选择Excel/CSV配置文件"
  398. filetypes = [("Excel文件", "*.xlsx *.xls"), ("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  399. else:
  400. title = "选择CSV配置文件"
  401. filetypes = [("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
  402. files = filedialog.askopenfilenames(
  403. title=title,
  404. filetypes=filetypes,
  405. initialdir=str(self.excel_dir)
  406. )
  407. if files:
  408. self.file_listbox.delete(0, tk.END)
  409. for file in files:
  410. self.file_listbox.insert(tk.END, file)
  411. self.status_var.set(f"已选择 {len(files)} 个文件")
  412. def scan_project_files(self):
  413. """扫描项目目录中的Excel/CSV文件"""
  414. self.status_var.set("正在扫描项目目录...")
  415. def scan_thread():
  416. config_files = []
  417. # 扫描常见的配置目录
  418. scan_dirs = [
  419. self.project_root / "assets/resources/data",
  420. self.project_root / "assets/resources/config",
  421. self.project_root / "assets/excel",
  422. self.project_root / "config",
  423. self.project_root / "data",
  424. self.project_root
  425. ]
  426. # 根据pandas可用性选择扫描的文件类型
  427. patterns = ['*.xlsx', '*.xls', '*.csv', '*.txt'] if PANDAS_AVAILABLE else ['*.csv', '*.txt']
  428. for scan_dir in scan_dirs:
  429. if scan_dir.exists():
  430. for pattern in patterns:
  431. config_files.extend(scan_dir.rglob(pattern))
  432. # 更新UI
  433. self.root.after(0, self.update_file_list, config_files)
  434. threading.Thread(target=scan_thread, daemon=True).start()
  435. def browse_json_path(self):
  436. """浏览JSON输出路径"""
  437. # 根据用户选择的选项组合决定选择文件还是目录
  438. multi_sheet = self.multi_sheet_var.get()
  439. single_json = self.single_json_var.get()
  440. if multi_sheet and not single_json:
  441. # 多工作表文件且不是单个JSON:输出到目录(每个sheet一个JSON文件)
  442. path = filedialog.askdirectory(
  443. title="选择JSON输出目录",
  444. initialdir=str(self.project_root / "assets/resources/data")
  445. )
  446. else:
  447. # 其他情况:输出单个JSON文件
  448. path = filedialog.askopenfilename(
  449. title="选择JSON输出文件",
  450. filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")],
  451. initialdir=str(self.project_root / "assets/resources/data")
  452. )
  453. if path:
  454. self.json_path_var.set(path)
  455. self.selected_json_path = Path(path)
  456. self.status_var.set(f"已选择输出路径: {Path(path).name}")
  457. def on_multi_sheet_change(self):
  458. """多工作表选项变化处理"""
  459. # 清空当前选择的JSON路径,让用户重新选择
  460. self.json_path_var.set("")
  461. def on_single_json_change(self):
  462. """单个JSON选项变化处理"""
  463. # 清空当前选择的JSON路径,让用户重新选择
  464. self.json_path_var.set("")
  465. self.selected_json_path = None
  466. if self.multi_sheet_var.get():
  467. self.status_var.set("多工作表模式:请选择输出目录")
  468. else:
  469. self.status_var.set("单工作表模式:请选择输出JSON文件")
  470. def update_file_list(self, files):
  471. """更新文件列表"""
  472. self.file_listbox.delete(0, tk.END)
  473. for file in files:
  474. self.file_listbox.insert(tk.END, str(file))
  475. file_type_text = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  476. self.status_var.set(f"找到 {len(files)} 个{file_type_text}")
  477. def on_file_select(self, event):
  478. """文件选择事件处理"""
  479. selection = self.file_listbox.curselection()
  480. if selection:
  481. self.selected_files = [self.file_listbox.get(i) for i in selection]
  482. # 根据选择的文件设置配置映射
  483. self._set_config_mapping_for_files()
  484. self.preview_selected_files()
  485. self.status_var.set(f"已选择 {len(selection)} 个文件")
  486. def _set_config_mapping_for_files(self):
  487. """根据选择的文件设置配置映射"""
  488. if not self.selected_files:
  489. return
  490. # 获取第一个文件名来判断配置类型
  491. first_file = Path(self.selected_files[0])
  492. filename = first_file.name.lower()
  493. # 根据文件名设置配置映射
  494. if '武器' in filename or 'weapon' in filename:
  495. self.current_mapping = {
  496. 'format_type': 'horizontal',
  497. 'param_types': {
  498. 'ID': str,
  499. '名称': str,
  500. '伤害': int,
  501. '射程': int,
  502. '射速': float,
  503. '弹药容量': int,
  504. '重装时间': float,
  505. '解锁等级': int,
  506. '价格': int,
  507. '描述': str,
  508. '最大等级': int,
  509. # 英文字段支持
  510. 'weaponId': str,
  511. 'name': str,
  512. 'damage': int,
  513. 'range': int,
  514. 'fireRate': float,
  515. 'ammoCapacity': int,
  516. 'reloadTime': float,
  517. 'unlockLevel': int,
  518. 'price': int,
  519. 'description': str,
  520. 'maxLevel': int
  521. }
  522. }
  523. elif '敌人' in filename or 'enemy' in filename:
  524. self.current_mapping = {
  525. 'format_type': 'multi_sheet',
  526. 'multi_sheet': True,
  527. 'param_types': {
  528. # 基础配置字段
  529. '敌人ID': ('id', str),
  530. '敌人名称': ('name', str),
  531. '敌人类型': ('type', str),
  532. '生命值': ('stats.health', int),
  533. '最大生命值': ('stats.maxHealth', int),
  534. '防御力': ('stats.defense', int),
  535. '移动速度': ('stats.speed', float),
  536. # 战斗配置字段
  537. '攻击伤害': ('combat.attackDamage', int),
  538. '攻击范围': ('combat.attackRange', int),
  539. '攻击速度': ('combat.attackSpeed', float),
  540. '能否格挡': ('combat.canBlock', bool),
  541. '格挡几率': ('combat.blockChance', float),
  542. '格挡伤害减免': ('combat.blockDamageReduction', float),
  543. '攻击冷却': ('combat.attackCooldown', float),
  544. '攻击类型': ('combat.attackType', str),
  545. '攻击延迟': ('combat.attackDelay', float),
  546. '武器类型': ('combat.weaponType', str),
  547. '投射物类型': ('combat.projectileType', str),
  548. '投射物速度': ('combat.projectileSpeed', float),
  549. # 移动配置字段
  550. '移动模式': ('movement.pattern', str),
  551. '巡逻范围': ('movement.patrolRange', int),
  552. '追击范围': ('movement.chaseRange', int),
  553. '旋转速度': ('movement.rotationSpeed', float),
  554. '移动类型': ('movement.moveType', str),
  555. '摆动幅度': ('movement.swingAmplitude', float),
  556. '摆动频率': ('movement.swingFrequency', float),
  557. '速度变化': ('movement.speedVariation', float),
  558. # 视觉配置字段
  559. '精灵路径': ('visualConfig.spritePath', str),
  560. '缩放比例': ('visualConfig.scale', float),
  561. '动画速度': ('visualConfig.animationSpeed', float),
  562. '水平翻转': ('visualConfig.flipX', bool),
  563. '待机动画': ('visualConfig.animations.idle', str),
  564. '行走动画': ('visualConfig.animations.walk', str),
  565. '攻击动画': ('visualConfig.animations.attack', str),
  566. '死亡动画': ('visualConfig.animations.death', str),
  567. '武器道具': ('visualConfig.weaponProp', str),
  568. # 音频配置字段
  569. '攻击音效': ('audioConfig.attackSound', str),
  570. '死亡音效': ('audioConfig.deathSound', str),
  571. '受击音效': ('audioConfig.hitSound', str),
  572. '行走音效': ('audioConfig.walkSound', str),
  573. '格挡音效': ('audioConfig.blockSound', str),
  574. '隐身音效': ('audioConfig.stealthSound', str),
  575. '护甲破碎音效': ('audioConfig.armorBreakSound', str),
  576. '引信音效': ('audioConfig.fuseSound', str),
  577. # 特殊能力配置字段
  578. '能力类型': ('specialAbilities.type', str),
  579. '伤害': ('specialAbilities.damage', int),
  580. '范围': ('specialAbilities.range', float),
  581. '冷却时间': ('specialAbilities.cooldown', float),
  582. # BOSS配置字段
  583. '是否BOSS': ('bossConfig.isBoss', bool),
  584. '阶段数': ('bossConfig.phases', int),
  585. '狂暴阈值': ('bossConfig.enrageThreshold', float),
  586. '狂暴伤害倍数': ('bossConfig.enrageDamageMultiplier', float),
  587. '狂暴速度倍数': ('bossConfig.enrageSpeedMultiplier', float),
  588. # 英文字段支持
  589. 'id': ('id', str),
  590. 'name': ('name', str),
  591. 'type': ('type', str),
  592. 'health': ('stats.health', int),
  593. 'maxHealth': ('stats.maxHealth', int),
  594. 'defense': ('stats.defense', int),
  595. 'speed': ('stats.speed', float),
  596. 'attackDamage': ('combat.attackDamage', int),
  597. 'attackRange': ('combat.attackRange', int),
  598. 'attackSpeed': ('combat.attackSpeed', float)
  599. }
  600. }
  601. elif '技能' in filename or 'skill' in filename:
  602. self.current_mapping = {
  603. 'format_type': 'horizontal',
  604. 'param_types': {
  605. 'ID': str,
  606. '名称': str,
  607. '伤害': int,
  608. '冷却时间': float,
  609. '消耗': int,
  610. '描述': str,
  611. # 英文字段支持
  612. 'skillId': str,
  613. 'skillName': str,
  614. 'damage': int,
  615. 'cooldown': float,
  616. 'cost': int,
  617. 'description': str
  618. }
  619. }
  620. elif '关卡' in filename or 'level' in filename:
  621. self.current_mapping = {
  622. 'format_type': 'horizontal',
  623. 'param_types': {
  624. 'ID': str,
  625. '名称': str,
  626. '敌人列表': str,
  627. '波数': int,
  628. '难度': int,
  629. # 英文字段支持
  630. 'levelId': str,
  631. 'name': str,
  632. 'enemies': str,
  633. 'waves': int,
  634. 'difficulty': int
  635. }
  636. }
  637. elif 'ballcontroller' in filename or '球控制' in filename or '标准配置表' in filename:
  638. # BallController配置映射(纵向格式:参数名-数值)
  639. self.current_mapping = {
  640. 'format_type': 'vertical',
  641. 'param_types': {
  642. 'baseSpeed': float,
  643. 'maxReflectionRandomness': float,
  644. 'antiTrapTimeWindow': float,
  645. 'antiTrapHitThreshold': int,
  646. 'deflectionAttemptThreshold': int,
  647. 'antiTrapDeflectionMultiplier': float,
  648. 'FIRE_COOLDOWN': float,
  649. 'ballRadius': float,
  650. 'gravityScale': float,
  651. 'linearDamping': float,
  652. 'angularDamping': float,
  653. 'colliderGroup': int,
  654. 'colliderTag': int,
  655. 'friction': float,
  656. 'restitution': float,
  657. 'safeDistance': float,
  658. 'edgeOffset': float,
  659. 'sensor': bool,
  660. 'maxAttempts': int
  661. }
  662. }
  663. else:
  664. # 默认配置映射
  665. self.current_mapping = {
  666. 'format_type': 'horizontal',
  667. 'param_types': {}
  668. }
  669. print(f"为文件 {filename} 设置配置映射: {self.current_mapping['format_type']} 格式")
  670. print(f"支持的参数类型: {list(self.current_mapping['param_types'].keys())}")
  671. def preview_selected_files(self):
  672. """预览选中的文件"""
  673. selection = self.file_listbox.curselection()
  674. if not selection:
  675. messagebox.showwarning("警告", "请先选择要预览的文件")
  676. return
  677. self.preview_text.delete(1.0, tk.END)
  678. self.config_data = {}
  679. for i in selection:
  680. file_path = Path(self.file_listbox.get(i))
  681. self.preview_text.insert(tk.END, f"=== {file_path.name} ===\n")
  682. try:
  683. # 根据文件扩展名和pandas可用性选择读取方法
  684. if file_path.suffix.lower() in ['.xlsx', '.xls']:
  685. if PANDAS_AVAILABLE:
  686. file_config = self.read_excel_config(file_path)
  687. else:
  688. self.preview_text.insert(tk.END, "Excel文件需要pandas库支持,请安装: pip install pandas openpyxl\n\n")
  689. continue
  690. elif file_path.suffix.lower() in ['.csv', '.txt']:
  691. file_config = self.read_csv_config(file_path)
  692. else:
  693. self.preview_text.insert(tk.END, "不支持的文件格式\n\n")
  694. continue
  695. if file_config:
  696. # 检查file_config的类型,如果是列表则特殊处理
  697. if isinstance(file_config, list):
  698. # 对于关卡配置等返回列表的情况
  699. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置项:\n")
  700. for i, item in enumerate(file_config):
  701. self.preview_text.insert(tk.END, f" 配置项 {i+1}: {item}\n")
  702. # 将列表数据存储到config_data中
  703. self.config_data[file_path.stem] = file_config
  704. else:
  705. # 原有的字典处理逻辑
  706. self.config_data.update(file_config)
  707. # 显示预览
  708. self.preview_text.insert(tk.END, f"找到 {len(file_config)} 个配置参数:\n")
  709. for key, value in file_config.items():
  710. self.preview_text.insert(tk.END, f" {key}: {value}\n")
  711. else:
  712. self.preview_text.insert(tk.END, "未找到有效的配置数据\n")
  713. except Exception as e:
  714. self.preview_text.insert(tk.END, f"读取失败: {str(e)}\n")
  715. self.preview_text.insert(tk.END, "\n")
  716. # 显示合并后的配置
  717. if self.config_data:
  718. self.preview_text.insert(tk.END, "=== 合并后的配置 ===\n")
  719. self.preview_text.insert(tk.END, json.dumps(self.config_data, indent=2, ensure_ascii=False))
  720. self.status_var.set(f"预览完成,共 {len(self.config_data)} 个配置参数")
  721. def read_excel_config(self, file_path):
  722. """读取Excel配置文件"""
  723. config = {}
  724. if not PANDAS_AVAILABLE:
  725. print("错误: pandas未安装,无法读取Excel文件")
  726. return config
  727. try:
  728. # 根据用户选择的"多工作表文件"选项决定处理方式
  729. if self.multi_sheet_var.get():
  730. # 多工作表模式:读取所有工作表
  731. print(f"多工作表模式处理文件: {file_path.name}")
  732. # 获取所有工作表名称(避免缓存问题)
  733. try:
  734. # 先尝试读取第一个工作表来获取所有工作表名称
  735. excel_file = pd.ExcelFile(file_path)
  736. sheet_names = excel_file.sheet_names
  737. excel_file.close() # 关闭文件句柄
  738. print(f"发现工作表: {sheet_names}")
  739. except Exception as e:
  740. print(f"获取工作表名称失败: {e}")
  741. return config
  742. all_sheets_data = {}
  743. for sheet_name in sheet_names:
  744. try:
  745. # 每次都重新读取文件,避免缓存
  746. df = pd.read_excel(file_path, sheet_name=sheet_name, engine='openpyxl')
  747. all_sheets_data[sheet_name] = df
  748. print(f"成功读取工作表: {sheet_name}, 数据行数: {len(df)}")
  749. # 打印第一行数据用于调试
  750. if len(df) > 0 and sheet_name == '敌人基础配置':
  751. print(f"敌人基础配置第一行数据: {df.iloc[0].to_dict()}")
  752. except Exception as e:
  753. print(f"读取工作表 {sheet_name} 时出错: {e}")
  754. # 根据工作表内容判断配置类型
  755. if any('关卡' in name for name in sheet_names):
  756. config = self.parse_level_multi_sheet_data(all_sheets_data, file_path.name)
  757. elif any('技能' in name for name in sheet_names):
  758. config = self.parse_skill_multi_sheet_data(all_sheets_data, file_path.name)
  759. elif any('敌人' in name for name in sheet_names):
  760. config = self.parse_enemy_multi_sheet_data(all_sheets_data, file_path.name)
  761. elif any('武器' in name for name in sheet_names):
  762. config = self.parse_weapon_multi_sheet_data(all_sheets_data, file_path.name)
  763. else:
  764. # 通用多工作表处理
  765. config = self.parse_multi_sheet_data(all_sheets_data, file_path.name)
  766. else:
  767. # 单工作表模式:读取第一个工作表或指定工作表
  768. print(f"单工作表模式处理文件: {file_path.name}")
  769. sheet_name = None
  770. if self.current_mapping and 'sheet_name' in self.current_mapping:
  771. sheet_name = self.current_mapping['sheet_name']
  772. # 读取Excel文件
  773. if sheet_name:
  774. df = pd.read_excel(file_path, sheet_name=sheet_name)
  775. print(f"读取指定工作表: {sheet_name}, 数据行数: {len(df)}")
  776. else:
  777. df = pd.read_excel(file_path)
  778. print(f"读取默认工作表, 数据行数: {len(df)}")
  779. config = self.parse_config_data(df, file_path.name)
  780. except Exception as e:
  781. print(f"读取Excel文件 {file_path.name} 时出错: {e}")
  782. return config
  783. def read_csv_config(self, file_path):
  784. """读取CSV配置文件"""
  785. config = {}
  786. try:
  787. # 如果pandas可用,优先使用pandas读取CSV(支持更多格式)
  788. if PANDAS_AVAILABLE:
  789. try:
  790. df = pd.read_csv(file_path)
  791. config = self.parse_config_data(df, file_path.name)
  792. except:
  793. # 如果pandas读取失败,回退到原始CSV读取方法
  794. config = self.read_csv_fallback(file_path)
  795. else:
  796. # 如果pandas不可用,直接使用原始CSV读取方法
  797. config = self.read_csv_fallback(file_path)
  798. except Exception as e:
  799. print(f"读取文件 {file_path.name} 时出错: {e}")
  800. return config
  801. def read_csv_fallback(self, file_path):
  802. """原始CSV读取方法(不依赖pandas)"""
  803. config = {}
  804. try:
  805. with open(file_path, 'r', encoding='utf-8') as f:
  806. reader = csv.reader(f)
  807. for row_num, row in enumerate(reader, 1):
  808. if len(row) < 2:
  809. continue
  810. param_name = row[0].strip()
  811. param_value = row[1].strip()
  812. # 跳过标题行
  813. if param_name in ['参数名', 'parameter', 'name']:
  814. continue
  815. # 检查参数是否有效
  816. if param_name in self.param_types:
  817. try:
  818. param_type = self.param_types[param_name]
  819. if param_type == bool:
  820. config[param_name] = param_value.lower() in ['true', '1', 'yes', 'on']
  821. else:
  822. config[param_name] = param_type(param_value)
  823. except (ValueError, TypeError):
  824. continue
  825. except Exception as e:
  826. print(f"读取CSV文件 {file_path.name} 时出错: {e}")
  827. return config
  828. def parse_level_multi_sheet_data(self, all_sheets_data, filename):
  829. """解析关卡配置的多工作表数据"""
  830. config = []
  831. try:
  832. # 获取各个工作表的数据(支持中英文工作表名)
  833. basic_config = None
  834. for sheet_name in ['关卡基础配置', 'Level Config', 'levels', '关卡配置']:
  835. if sheet_name in all_sheets_data:
  836. basic_config = all_sheets_data[sheet_name]
  837. break
  838. weapon_config = None
  839. for sheet_name in ['关卡武器配置', 'Level Weapon Config', 'level_weapons', '武器配置']:
  840. if sheet_name in all_sheets_data:
  841. weapon_config = all_sheets_data[sheet_name]
  842. break
  843. wave_config = None
  844. for sheet_name in ['关卡波次配置', 'Wave Config', 'waves', '波次配置']:
  845. if sheet_name in all_sheets_data:
  846. wave_config = all_sheets_data[sheet_name]
  847. break
  848. enemy_config = None
  849. for sheet_name in ['敌人详细配置', 'Enemy Detail Config', 'enemy_details', '敌人配置']:
  850. if sheet_name in all_sheets_data:
  851. enemy_config = all_sheets_data[sheet_name]
  852. break
  853. if basic_config is None:
  854. print("错误: 未找到关卡基础配置工作表(支持的工作表名: 关卡基础配置, Level Config, levels, 关卡配置)")
  855. return config
  856. # 处理每个关卡
  857. for _, basic_row in basic_config.iterrows():
  858. if pd.isna(basic_row['关卡ID']):
  859. continue
  860. level_id = str(basic_row['关卡ID'])
  861. # 获取武器配置
  862. available_weapons = []
  863. if weapon_config is not None:
  864. weapon_rows = weapon_config[weapon_config['关卡ID'] == level_id]
  865. for _, weapon_row in weapon_rows.iterrows():
  866. if pd.notna(weapon_row['可用武器']):
  867. weapons_str = str(weapon_row['可用武器'])
  868. # 支持多种分隔符
  869. import re
  870. weapons = re.split(r'[、,,;;]', weapons_str)
  871. available_weapons.extend([w.strip() for w in weapons if w.strip()])
  872. level_data = {
  873. 'levelId': level_id,
  874. 'name': str(basic_row['关卡名称']) if pd.notna(basic_row['关卡名称']) else '',
  875. 'scene': str(basic_row['场景']) if pd.notna(basic_row['场景']) else 'grassland',
  876. 'description': str(basic_row['描述']) if pd.notna(basic_row['描述']) else '',
  877. 'backgroundImage': str(basic_row['关卡背景图路径']) if pd.notna(basic_row['关卡背景图路径']) else 'images/LevelBackground/BG1',
  878. 'availableWeapons': available_weapons,
  879. 'coinReward': int(basic_row['钞票奖励']) if pd.notna(basic_row['钞票奖励']) else 100,
  880. 'diamondReward': int(basic_row['钻石奖励']) if pd.notna(basic_row['钻石奖励']) else 0,
  881. 'timeLimit': 300, # 默认值
  882. 'difficulty': 'normal', # 默认值
  883. 'healthMultiplier': float(basic_row['生命倍数']) if pd.notna(basic_row['生命倍数']) else 1.0, # 从Excel读取生命倍数
  884. 'waves': []
  885. }
  886. # 获取该关卡的波次配置
  887. if wave_config is not None:
  888. level_waves = wave_config[wave_config['关卡ID'] == level_id]
  889. for _, wave_row in level_waves.iterrows():
  890. wave_id = int(wave_row['波次ID']) if pd.notna(wave_row['波次ID']) else 1
  891. # 获取该波次的敌人配置
  892. wave_enemies = []
  893. if enemy_config is not None:
  894. wave_enemy_data = enemy_config[
  895. (enemy_config['关卡ID'] == level_id) &
  896. (enemy_config['波次ID'] == wave_id)
  897. ]
  898. for _, enemy_row in wave_enemy_data.iterrows():
  899. enemy_type = str(enemy_row['敌人类型']) if pd.notna(enemy_row['敌人类型']) else '普通僵尸'
  900. enemy_data = {
  901. 'enemyType': enemy_type,
  902. 'count': int(enemy_row['数量']) if pd.notna(enemy_row['数量']) else 1,
  903. 'spawnInterval': float(enemy_row['生成间隔']) if pd.notna(enemy_row['生成间隔']) else 2.0,
  904. 'spawnDelay': float(enemy_row['生成延迟']) if pd.notna(enemy_row['生成延迟']) else 0.0,
  905. 'characteristics': str(enemy_row['特征描述']) if pd.notna(enemy_row['特征描述']) else ''
  906. }
  907. wave_enemies.append(enemy_data)
  908. wave_data = {
  909. 'waveId': wave_id,
  910. 'enemies': wave_enemies
  911. }
  912. level_data['waves'].append(wave_data)
  913. config.append(level_data)
  914. print(f"处理关卡: {level_id}, 波次数: {len(level_data['waves'])}")
  915. print(f"成功解析关卡配置,共 {len(config)} 个关卡")
  916. except Exception as e:
  917. print(f"解析关卡多工作表数据时出错: {e}")
  918. import traceback
  919. traceback.print_exc()
  920. return config
  921. def parse_skill_multi_sheet_data(self, all_sheets_data, filename):
  922. """解析技能配置的多工作表数据"""
  923. skills_config = []
  924. try:
  925. # 检查是否为局外技能配置表格式(包含技能类型配置、技能组配置、基本配置)
  926. if '技能类型配置' in all_sheets_data and '技能组配置' in all_sheets_data and '基本配置' in all_sheets_data:
  927. print("检测到局外技能配置表格式,使用集成的技能配置导入工具处理")
  928. # 使用集成的SkillConfigImporter处理
  929. try:
  930. skill_importer = SkillConfigImporter(self.excel_dir)
  931. success = skill_importer.import_config()
  932. if success:
  933. print("技能配置导入成功!")
  934. # 返回成功标识,让调用者知道已经处理完成
  935. return [{'status': 'imported', 'message': '技能配置已成功导入到skill_config.json'}]
  936. else:
  937. print("技能配置导入失败")
  938. return [{'status': 'error', 'message': '技能配置导入失败'}]
  939. except Exception as e:
  940. print(f"技能配置导入过程中出错: {e}")
  941. return [{'status': 'error', 'message': f'技能配置导入出错: {e}'}]
  942. return skills_config
  943. # 获取技能信息表数据(支持中英文工作表名)
  944. skill_info = None
  945. for sheet_name in ['技能信息表', 'Skill Config', 'skills', '技能配置']:
  946. if sheet_name in all_sheets_data:
  947. skill_info = all_sheets_data[sheet_name]
  948. break
  949. if skill_info is None:
  950. print("错误: 未找到技能信息表工作表(支持的工作表名: 技能信息表, Skill Config, skills, 技能配置)")
  951. return skills_config
  952. # 处理每个技能(支持中英文字段名)
  953. for _, skill_row in skill_info.iterrows():
  954. # 获取技能ID(支持中英文字段名)
  955. skill_id_field = None
  956. for field in ['id', '技能ID', 'skill_id', 'ID']:
  957. if field in skill_row and pd.notna(skill_row[field]):
  958. skill_id_field = field
  959. break
  960. if skill_id_field is None:
  961. continue
  962. def get_skill_field_value(row, cn_field, en_fields, default_value, value_type=str):
  963. """获取技能字段值,支持中英文字段名"""
  964. for field in [cn_field] + (en_fields if isinstance(en_fields, list) else [en_fields]):
  965. if field in row and pd.notna(row[field]):
  966. try:
  967. return value_type(row[field])
  968. except (ValueError, TypeError):
  969. continue
  970. return default_value
  971. skill_data = {
  972. 'id': str(skill_row[skill_id_field]),
  973. 'name': get_skill_field_value(skill_row, '技能名称', ['name', 'skill_name'], ''),
  974. 'description': get_skill_field_value(skill_row, '技能描述', ['description', 'desc'], ''),
  975. 'iconPath': get_skill_field_value(skill_row, '图标路径', ['iconPath', 'icon_path', 'icon'], ''),
  976. 'maxLevel': get_skill_field_value(skill_row, '最大等级', ['maxLevel', 'max_level'], 5, int),
  977. 'currentLevel': get_skill_field_value(skill_row, '当前等级', ['currentLevel', 'current_level'], 0, int),
  978. 'priceReduction': get_skill_field_value(skill_row, '价格减少', ['priceReduction', 'price_reduction'], 0.0, float),
  979. 'critChanceIncrease': get_skill_field_value(skill_row, '暴击几率增加', ['critChanceIncrease', 'crit_chance_increase'], 0.0, float),
  980. 'critDamageBonus': get_skill_field_value(skill_row, '暴击伤害加成', ['critDamageBonus', 'crit_damage_bonus'], 0.0, float),
  981. 'healthIncrease': get_skill_field_value(skill_row, '生命值增加', ['healthIncrease', 'health_increase'], 0.0, float),
  982. 'multiShotChance': get_skill_field_value(skill_row, '多重射击几率', ['multiShotChance', 'multi_shot_chance'], 0.0, float),
  983. 'energyGainIncrease': get_skill_field_value(skill_row, '能量加成', ['energyGainIncrease', 'energy_gain_increase'], 0.0, float),
  984. 'ballSpeedIncrease': get_skill_field_value(skill_row, '速度提升', ['ballSpeedIncrease', 'ball_speed_increase'], 0.0, float)
  985. }
  986. skills_config.append(skill_data)
  987. print(f"处理技能: {skill_data['id']} - {skill_data['name']}")
  988. print(f"成功解析技能配置,共 {len(skills_config)} 个技能")
  989. except Exception as e:
  990. print(f"解析技能多工作表数据时出错: {e}")
  991. import traceback
  992. traceback.print_exc()
  993. return skills_config
  994. def parse_enemy_multi_sheet_data(self, all_sheets_data, filename):
  995. """解析敌人配置的多工作表数据"""
  996. enemies_config = []
  997. try:
  998. # 获取敌人基础配置数据(支持中英文工作表名)
  999. enemy_info = None
  1000. for sheet_name in ['敌人基础配置', 'Enemy Config', 'enemies', '敌人配置']:
  1001. if sheet_name in all_sheets_data:
  1002. enemy_info = all_sheets_data[sheet_name]
  1003. break
  1004. if enemy_info is None:
  1005. print("错误: 未找到敌人基础配置工作表(支持的工作表名: 敌人基础配置, Enemy Config, enemies, 敌人配置)")
  1006. return enemies_config
  1007. # 获取其他配置工作表
  1008. combat_sheet = all_sheets_data.get('战斗配置')
  1009. movement_sheet = all_sheets_data.get('移动配置')
  1010. visual_sheet = all_sheets_data.get('视觉配置')
  1011. audio_sheet = all_sheets_data.get('音频配置')
  1012. special_sheet = all_sheets_data.get('特殊能力配置')
  1013. boss_sheet = all_sheets_data.get('BOSS配置')
  1014. def get_field_value(row, cn_field, en_fields, default_value, value_type=str):
  1015. """获取字段值,支持中英文字段名"""
  1016. for field in [cn_field] + (en_fields if isinstance(en_fields, list) else [en_fields]):
  1017. if field in row and pd.notna(row[field]):
  1018. try:
  1019. return value_type(row[field])
  1020. except (ValueError, TypeError):
  1021. continue
  1022. return default_value
  1023. def find_row_by_id(sheet, enemy_id):
  1024. """根据敌人ID查找对应行"""
  1025. if sheet is None:
  1026. return None
  1027. for _, row in sheet.iterrows():
  1028. if str(row.get('敌人ID', '')) == enemy_id:
  1029. return row
  1030. return None
  1031. # 处理每个敌人(支持中英文字段名)
  1032. for _, enemy_row in enemy_info.iterrows():
  1033. # 获取敌人ID(支持中英文字段名)
  1034. enemy_id_field = None
  1035. for field in ['id', '敌人ID', 'enemy_id', 'ID']:
  1036. if field in enemy_row and pd.notna(enemy_row[field]):
  1037. enemy_id_field = field
  1038. break
  1039. if enemy_id_field is None:
  1040. continue
  1041. enemy_id = str(enemy_row[enemy_id_field])
  1042. # 基础配置(不包含攻击伤害,攻击伤害从战斗配置表获取)
  1043. enemy_data = {
  1044. 'id': enemy_id,
  1045. 'name': get_field_value(enemy_row, '敌人名称', ['name', 'enemy_name'], ''),
  1046. 'type': get_field_value(enemy_row, '敌人类型', ['type', 'enemy_type'], 'basic'),
  1047. 'health': get_field_value(enemy_row, '生命值', ['health', 'hp'], 100, int),
  1048. 'speed': get_field_value(enemy_row, '移动速度', ['speed', 'move_speed'], 1.0, float),
  1049. 'attackRange': get_field_value(enemy_row, '攻击范围', ['attackRange', 'attack_range', 'range'], 1.0, float),
  1050. 'attackSpeed': get_field_value(enemy_row, '攻击速度', ['attackSpeed', 'attack_speed'], 1.0, float),
  1051. 'defense': get_field_value(enemy_row, '防御力', ['defense'], 0, int),
  1052. 'attackDamage': 10 # 默认值,将从战斗配置表覆盖
  1053. }
  1054. # 获取战斗配置
  1055. combat_row = find_row_by_id(combat_sheet, enemy_id)
  1056. if combat_row is not None:
  1057. enemy_data.update({
  1058. 'attackDamage': get_field_value(combat_row, '攻击伤害', ['attackDamage', 'attack'], 10, int),
  1059. 'attackType': get_field_value(combat_row, '攻击类型', ['attackType'], 'melee'),
  1060. 'attackDelay': get_field_value(combat_row, '攻击延迟', ['attackDelay'], 0.5, float),
  1061. 'weaponType': get_field_value(combat_row, '武器类型', ['weaponType'], 'none'),
  1062. 'projectileType': get_field_value(combat_row, '投射物类型', ['projectileType'], 'none'),
  1063. 'projectileSpeed': get_field_value(combat_row, '投射物速度', ['projectileSpeed'], 100, float),
  1064. 'canBlock': get_field_value(combat_row, '能否格挡', ['canBlock'], False, bool),
  1065. 'blockChance': get_field_value(combat_row, '格挡几率', ['blockChance'], 0.0, float),
  1066. 'blockDamageReduction': get_field_value(combat_row, '格挡伤害减免', ['blockDamageReduction'], 0.5, float)
  1067. })
  1068. # 获取移动配置
  1069. movement_row = find_row_by_id(movement_sheet, enemy_id)
  1070. if movement_row is not None:
  1071. enemy_data.update({
  1072. 'movementPattern': get_field_value(movement_row, '移动模式', ['movementPattern'], 'walk_forward'),
  1073. 'patrolRange': get_field_value(movement_row, '巡逻范围', ['patrolRange'], 100, int),
  1074. 'chaseRange': get_field_value(movement_row, '追击范围', ['chaseRange'], 200, int),
  1075. 'rotationSpeed': get_field_value(movement_row, '旋转速度', ['rotationSpeed'], 180, float),
  1076. 'moveType': get_field_value(movement_row, '移动类型', ['moveType'], 'straight'),
  1077. 'swingAmplitude': get_field_value(movement_row, '摆动幅度', ['swingAmplitude'], 0.0, float),
  1078. 'swingFrequency': get_field_value(movement_row, '摆动频率', ['swingFrequency'], 0.0, float),
  1079. 'speedVariation': get_field_value(movement_row, '速度变化', ['speedVariation'], 0.1, float)
  1080. })
  1081. # 获取视觉配置
  1082. visual_row = find_row_by_id(visual_sheet, enemy_id)
  1083. if visual_row is not None:
  1084. enemy_data.update({
  1085. 'spritePath': get_field_value(visual_row, '精灵路径', ['spritePath'], f'enemies/{enemy_id}'),
  1086. 'scale': get_field_value(visual_row, '缩放比例', ['scale'], 1.0, float),
  1087. 'animationSpeed': get_field_value(visual_row, '动画速度', ['animationSpeed'], 1.0, float),
  1088. 'flipX': get_field_value(visual_row, '水平翻转', ['flipX'], False, bool),
  1089. 'idleAnimation': get_field_value(visual_row, '待机动画', ['idleAnimation'], 'idle'),
  1090. 'walkAnimation': get_field_value(visual_row, '行走动画', ['walkAnimation'], 'walk'),
  1091. 'attackAnimation': get_field_value(visual_row, '攻击动画', ['attackAnimation'], 'attack'),
  1092. 'deathAnimation': get_field_value(visual_row, '死亡动画', ['deathAnimation'], 'dead'),
  1093. 'weaponProp': get_field_value(visual_row, '武器道具', ['weaponProp'], '')
  1094. })
  1095. # 获取音频配置
  1096. audio_row = find_row_by_id(audio_sheet, enemy_id)
  1097. if audio_row is not None:
  1098. enemy_data.update({
  1099. 'attackSound': get_field_value(audio_row, '攻击音效', ['attackSound'], 'enemy_attack'),
  1100. 'deathSound': get_field_value(audio_row, '死亡音效', ['deathSound'], 'enemy_death'),
  1101. 'hitSound': get_field_value(audio_row, '受击音效', ['hitSound'], 'enemy_hit'),
  1102. 'walkSound': get_field_value(audio_row, '行走音效', ['walkSound'], ''),
  1103. 'blockSound': get_field_value(audio_row, '格挡音效', ['blockSound'], ''),
  1104. 'stealthSound': get_field_value(audio_row, '隐身音效', ['stealthSound'], ''),
  1105. 'armorBreakSound': get_field_value(audio_row, '护甲破碎音效', ['armorBreakSound'], ''),
  1106. 'fuseSound': get_field_value(audio_row, '引信音效', ['fuseSound'], '')
  1107. })
  1108. # 获取特殊能力配置
  1109. special_abilities = []
  1110. if special_sheet is not None:
  1111. for _, special_row in special_sheet.iterrows():
  1112. if str(special_row.get('敌人ID', '')) == enemy_id:
  1113. ability_type = get_field_value(special_row, '能力类型', ['abilityType'], '')
  1114. if ability_type: # 只添加有效的特殊能力
  1115. special_abilities.append({
  1116. 'type': ability_type,
  1117. 'damage': get_field_value(special_row, '伤害', ['damage'], 0, int),
  1118. 'range': get_field_value(special_row, '范围', ['range'], 0, float),
  1119. 'cooldown': get_field_value(special_row, '冷却时间', ['cooldown'], 0, float)
  1120. })
  1121. if special_abilities:
  1122. enemy_data['specialAbilities'] = special_abilities
  1123. # 获取BOSS配置
  1124. boss_row = find_row_by_id(boss_sheet, enemy_id)
  1125. if boss_row is not None:
  1126. is_boss = get_field_value(boss_row, '是否BOSS', ['isBoss'], False, bool)
  1127. if is_boss:
  1128. enemy_data['bossConfig'] = {
  1129. 'isBoss': True,
  1130. 'phases': get_field_value(boss_row, '阶段数', ['phases'], 1, int),
  1131. 'enrageThreshold': get_field_value(boss_row, '狂暴阈值', ['enrageThreshold'], 0.3, float),
  1132. 'enrageDamageMultiplier': get_field_value(boss_row, '狂暴伤害倍数', ['enrageDamageMultiplier'], 1.5, float),
  1133. 'enrageSpeedMultiplier': get_field_value(boss_row, '狂暴速度倍数', ['enrageSpeedMultiplier'], 1.3, float)
  1134. }
  1135. # 转换为嵌套结构
  1136. nested_enemy_data = {
  1137. 'id': enemy_data['id'],
  1138. 'name': enemy_data['name'],
  1139. 'type': enemy_data['type'],
  1140. 'stats': {
  1141. 'health': enemy_data.get('health', 100),
  1142. 'maxHealth': enemy_data.get('health', 100),
  1143. 'defense': enemy_data.get('defense', 0),
  1144. 'speed': enemy_data.get('speed', 50)
  1145. },
  1146. 'movement': {
  1147. 'pattern': enemy_data.get('movementPattern', 'direct'),
  1148. 'speed': enemy_data.get('speed', 50),
  1149. 'patrolRange': enemy_data.get('patrolRange', 100),
  1150. 'chaseRange': enemy_data.get('chaseRange', 200),
  1151. 'rotationSpeed': enemy_data.get('rotationSpeed', 180),
  1152. 'moveType': enemy_data.get('moveType', 'straight'),
  1153. 'swingAmplitude': enemy_data.get('swingAmplitude', 0.0),
  1154. 'swingFrequency': enemy_data.get('swingFrequency', 0.0),
  1155. 'speedVariation': enemy_data.get('speedVariation', 0.1)
  1156. },
  1157. 'combat': {
  1158. 'attackDamage': enemy_data.get('attackDamage', 10),
  1159. 'attackRange': enemy_data.get('attackRange', 100),
  1160. 'attackSpeed': enemy_data.get('attackSpeed', 1.0),
  1161. 'canBlock': enemy_data.get('canBlock', False),
  1162. 'blockChance': enemy_data.get('blockChance', 0.0),
  1163. 'blockDamageReduction': enemy_data.get('blockDamageReduction', 0.5),
  1164. 'attackCooldown': enemy_data.get('attackCooldown', 1.0),
  1165. 'attackType': enemy_data.get('attackType', 'melee'),
  1166. 'attackDelay': enemy_data.get('attackDelay', 0.5),
  1167. 'weaponType': enemy_data.get('weaponType', 'none'),
  1168. 'projectileType': enemy_data.get('projectileType', 'none'),
  1169. 'projectileSpeed': enemy_data.get('projectileSpeed', 100.0)
  1170. },
  1171. 'visualConfig': {
  1172. 'spritePath': enemy_data.get('spritePath', f'Animation/EnemyAni/001'),
  1173. 'scale': enemy_data.get('scale', 1.0),
  1174. 'animationSpeed': enemy_data.get('animationSpeed', 1.0),
  1175. 'flipX': enemy_data.get('flipX', False),
  1176. 'tint': enemy_data.get('tint', '#FFFFFF'),
  1177. 'animations': {
  1178. 'idle': 'idle',
  1179. 'walk': 'walk',
  1180. 'attack': 'attack',
  1181. 'death': 'dead'
  1182. },
  1183. 'weaponProp': enemy_data.get('weaponProp', '')
  1184. },
  1185. 'audioConfig': {
  1186. 'attackSound': enemy_data.get('attackSound', 'enemy_attack'),
  1187. 'deathSound': enemy_data.get('deathSound', 'enemy_death'),
  1188. 'hitSound': enemy_data.get('hitSound', 'enemy_hit'),
  1189. 'walkSound': enemy_data.get('walkSound', ''),
  1190. 'blockSound': enemy_data.get('blockSound', ''),
  1191. 'stealthSound': enemy_data.get('stealthSound', ''),
  1192. 'armorBreakSound': enemy_data.get('armorBreakSound', ''),
  1193. 'fuseSound': enemy_data.get('fuseSound', ''),
  1194. 'volume': enemy_data.get('volume', 1.0)
  1195. }
  1196. }
  1197. # 添加特殊能力配置
  1198. if 'specialAbilities' in enemy_data:
  1199. nested_enemy_data['specialAbilities'] = enemy_data['specialAbilities']
  1200. # 添加BOSS配置
  1201. if 'bossConfig' in enemy_data:
  1202. nested_enemy_data['bossConfig'] = enemy_data['bossConfig']
  1203. enemies_config.append(nested_enemy_data)
  1204. print(f"处理敌人: {enemy_data['id']} - {enemy_data['name']}")
  1205. print(f"敌人 {enemy_data['id']} 的生命值: {nested_enemy_data['stats']['health']}")
  1206. print(f"成功解析敌人配置,共 {len(enemies_config)} 个敌人")
  1207. except Exception as e:
  1208. print(f"解析敌人多工作表数据时出错: {e}")
  1209. import traceback
  1210. traceback.print_exc()
  1211. return enemies_config
  1212. def parse_multi_sheet_data(self, all_sheets_data, filename):
  1213. """解析通用多工作表数据"""
  1214. # 处理武器配置表的多工作表
  1215. if '方块武器配置表' in filename:
  1216. return self.parse_weapon_multi_sheet_data(all_sheets_data, filename)
  1217. # 处理敌人配置表的多工作表
  1218. if '敌人配置表' in filename or '敌人' in filename:
  1219. return self.parse_enemy_multi_sheet_data(all_sheets_data, filename)
  1220. # 这里可以添加其他多工作表文件的处理逻辑
  1221. # 目前只返回第一个工作表的数据
  1222. if all_sheets_data:
  1223. first_sheet_name = list(all_sheets_data.keys())[0]
  1224. first_sheet_data = all_sheets_data[first_sheet_name]
  1225. return self.parse_config_data(first_sheet_data, filename)
  1226. return []
  1227. def parse_config_data(self, df, filename):
  1228. """解析配置数据(支持多种格式,需要pandas)"""
  1229. config = {}
  1230. if not PANDAS_AVAILABLE or not self.current_mapping:
  1231. return config
  1232. format_type = self.current_mapping['format_type']
  1233. try:
  1234. if format_type == 'vertical':
  1235. # 纵向表格:参数名在第一列,值在第二/三列
  1236. for _, row in df.iterrows():
  1237. param_name = str(row.iloc[0]).strip()
  1238. # 跳过标题行和无效行
  1239. if param_name in ['参数名', 'parameter', 'name', 'nan', '球控制器参数'] or param_name == 'nan':
  1240. continue
  1241. # 检查参数是否有效
  1242. param_types = self.current_mapping.get('param_types', {})
  1243. if param_name in param_types:
  1244. try:
  1245. # 优先使用第3列(默认值),如果不存在则使用第2列
  1246. param_value = row.iloc[2] if len(row) > 2 and not pd.isna(row.iloc[2]) else row.iloc[1]
  1247. if pd.isna(param_value):
  1248. continue
  1249. param_type = param_types[param_name]
  1250. if param_type == bool:
  1251. config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  1252. else:
  1253. config[param_name] = param_type(param_value)
  1254. except (ValueError, TypeError, IndexError):
  1255. continue
  1256. elif format_type == 'horizontal':
  1257. # 横向表格:第一行是参数名(列名),数据从第0行开始
  1258. print(f"横向表格解析: 总行数={len(df)}")
  1259. print(f"表格列名: {list(df.columns)}")
  1260. # 打印前几行数据用于调试
  1261. for i in range(min(3, len(df))):
  1262. print(f"第{i}行数据: {df.iloc[i].to_dict()}")
  1263. # 检查第0行是否为有效数据行
  1264. # 如果第0行第一个单元格是描述性文字或空值,则跳过
  1265. data_start_row = 0
  1266. if len(df) > 0:
  1267. first_cell = str(df.iloc[0, 0]).strip().lower()
  1268. # 跳过描述行、空行或标题行
  1269. if (first_cell in ['唯一标识符', '描述', 'description', 'desc', 'nan', ''] or
  1270. first_cell == df.columns[0].lower()):
  1271. data_start_row = 1
  1272. print(f"跳过第0行(描述行或标题行): {first_cell}")
  1273. print(f"数据起始行: {data_start_row}")
  1274. # 解析多行数据(如敌人配置、武器配置、关卡配置等)
  1275. config_list = []
  1276. for i in range(data_start_row, len(df)):
  1277. row_config = {}
  1278. row_has_data = False
  1279. param_types = self.current_mapping.get('param_types', {})
  1280. print(f"可用的参数类型: {list(param_types.keys())}")
  1281. print(f"当前配置映射: {self.current_mapping}")
  1282. print(f"DataFrame列名: {list(df.columns)}")
  1283. # 如果参数类型为空,尝试从DataFrame列名自动推断
  1284. if not param_types:
  1285. print("参数类型为空,尝试自动推断...")
  1286. for col_name in df.columns:
  1287. col_str = str(col_name).strip()
  1288. if col_str and col_str != 'nan':
  1289. # 根据列名推断数据类型
  1290. if any(keyword in col_str for keyword in ['ID', 'id', '编号']):
  1291. param_types[col_str] = str
  1292. elif any(keyword in col_str for keyword in ['名称', 'name', '名字']):
  1293. param_types[col_str] = str
  1294. elif any(keyword in col_str for keyword in ['伤害', 'damage', '攻击', 'attack']):
  1295. param_types[col_str] = int
  1296. elif any(keyword in col_str for keyword in ['射程', 'range', '范围']):
  1297. param_types[col_str] = int
  1298. elif any(keyword in col_str for keyword in ['射速', 'fireRate', '频率']):
  1299. param_types[col_str] = float
  1300. elif any(keyword in col_str for keyword in ['价格', 'price', '金币', 'cost']):
  1301. param_types[col_str] = int
  1302. elif any(keyword in col_str for keyword in ['等级', 'level', 'Level']):
  1303. param_types[col_str] = int
  1304. elif any(keyword in col_str for keyword in ['时间', 'time', 'Time']):
  1305. param_types[col_str] = float
  1306. elif any(keyword in col_str for keyword in ['描述', 'description', '说明']):
  1307. param_types[col_str] = str
  1308. else:
  1309. # 默认为字符串类型
  1310. param_types[col_str] = str
  1311. # 更新配置映射
  1312. if param_types:
  1313. self.current_mapping['param_types'] = param_types
  1314. print(f"自动推断的参数类型: {param_types}")
  1315. for col_idx, col_name in enumerate(df.columns):
  1316. param_name = str(col_name).strip()
  1317. print(f"检查列名: '{param_name}' 是否在参数类型中")
  1318. if param_name in param_types:
  1319. try:
  1320. param_value = df.iloc[i, col_idx]
  1321. if pd.isna(param_value) or str(param_value).strip() == '':
  1322. print(f"跳过空值: {param_name} = {param_value}")
  1323. continue
  1324. param_type = param_types[param_name]
  1325. if param_type == bool:
  1326. row_config[param_name] = str(param_value).lower() in ['true', '1', 'yes', 'on']
  1327. else:
  1328. row_config[param_name] = param_type(param_value)
  1329. row_has_data = True
  1330. print(f"成功转换字段: {param_name} = {param_value} ({param_type})")
  1331. except (ValueError, TypeError, IndexError) as e:
  1332. print(f"转换字段 {param_name} 时出错: {e}")
  1333. continue
  1334. else:
  1335. print(f"字段 '{param_name}' 不在参数类型定义中,跳过")
  1336. if row_config and row_has_data: # 只添加非空且有有效数据的配置
  1337. config_list.append(row_config)
  1338. print(f"成功解析第{i}行,包含{len(row_config)}个字段")
  1339. else:
  1340. print(f"跳过第{i}行(无有效数据)")
  1341. print(f"总共解析出{len(config_list)}个有效配置项")
  1342. # 对于横向表格,返回配置列表
  1343. config = {'items': config_list}
  1344. except Exception as e:
  1345. print(f"解析文件 {filename} 时出错: {e}")
  1346. return config
  1347. def clear_selection(self):
  1348. """清空选择"""
  1349. self.file_listbox.selection_clear(0, tk.END)
  1350. self.preview_text.delete(1.0, tk.END)
  1351. self.config_data = {}
  1352. self.status_var.set("已清空选择")
  1353. self.load_current_config()
  1354. def load_current_config(self):
  1355. """加载当前配置"""
  1356. try:
  1357. if not self.selected_json_path:
  1358. self.preview_text.insert(tk.END, "请先选择JSON输出路径\n")
  1359. return
  1360. json_path = Path(self.selected_json_path)
  1361. if json_path.exists():
  1362. with open(json_path, 'r', encoding='utf-8') as f:
  1363. current_config = json.load(f)
  1364. self.preview_text.insert(tk.END, f"=== 当前配置 ({json_path.name}) ===\n")
  1365. # 根据配置类型显示不同的预览格式
  1366. if self.format_var.get() == 'horizontal':
  1367. # 横向表格配置(如敌人、武器)
  1368. if 'enemies' in current_config:
  1369. self.preview_text.insert(tk.END, f"敌人配置 ({len(current_config['enemies'])} 个):\n")
  1370. for i, enemy in enumerate(current_config['enemies'][:5]): # 只显示前5个
  1371. self.preview_text.insert(tk.END, f" {i+1}. {enemy.get('name', enemy.get('id', 'Unknown'))}\n")
  1372. if len(current_config['enemies']) > 5:
  1373. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['enemies']) - 5} 个\n")
  1374. if 'weapons' in current_config:
  1375. self.preview_text.insert(tk.END, f"\n武器配置 ({len(current_config['weapons'])} 个):\n")
  1376. for i, weapon in enumerate(current_config['weapons'][:5]): # 只显示前5个
  1377. self.preview_text.insert(tk.END, f" {i+1}. {weapon.get('name', weapon.get('id', 'Unknown'))}\n")
  1378. if len(current_config['weapons']) > 5:
  1379. self.preview_text.insert(tk.END, f" ... 还有 {len(current_config['weapons']) - 5} 个\n")
  1380. else:
  1381. # 纵向表格配置(如BallController)
  1382. self.preview_text.insert(tk.END, json.dumps(current_config, indent=2, ensure_ascii=False))
  1383. file_hint = "Excel/CSV文件" if PANDAS_AVAILABLE else "CSV文件"
  1384. self.preview_text.insert(tk.END, f"\n\n请选择{file_hint}进行配置导入...\n")
  1385. else:
  1386. self.preview_text.insert(tk.END, "配置文件不存在\n")
  1387. except Exception as e:
  1388. self.preview_text.insert(tk.END, f"加载当前配置失败: {e}\n")
  1389. def backup_config(self):
  1390. """备份当前配置"""
  1391. try:
  1392. if not self.selected_json_path:
  1393. messagebox.showwarning("警告", "请先选择JSON输出路径")
  1394. return
  1395. json_path = Path(self.selected_json_path)
  1396. if not json_path.exists():
  1397. messagebox.showwarning("警告", "配置文件不存在")
  1398. return
  1399. backup_path = json_path.parent / f"ballController_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  1400. with open(json_path, 'r', encoding='utf-8') as f:
  1401. content = f.read()
  1402. with open(backup_path, 'w', encoding='utf-8') as f:
  1403. f.write(content)
  1404. messagebox.showinfo("成功", f"配置已备份到:\n{backup_path}")
  1405. self.status_var.set(f"配置已备份")
  1406. except Exception as e:
  1407. messagebox.showerror("错误", f"备份失败: {e}")
  1408. def import_config(self):
  1409. """导入配置到JSON文件"""
  1410. if not self.config_data:
  1411. messagebox.showwarning("警告", "没有配置数据可导入")
  1412. return
  1413. if not self.selected_json_path:
  1414. messagebox.showwarning("警告", "请先选择JSON输出路径")
  1415. return
  1416. try:
  1417. print(f"开始导入配置...")
  1418. print(f"配置数据: {self.config_data}")
  1419. print(f"输出路径: {self.selected_json_path}")
  1420. print(f"选中文件: {self.selected_files}")
  1421. # 优先使用配置映射中的格式类型,如果没有则使用GUI设置
  1422. format_type = self.current_mapping.get('format_type', self.format_var.get()) if hasattr(self, 'current_mapping') and self.current_mapping else self.format_var.get()
  1423. print(f"格式类型: {format_type}")
  1424. print(f"配置映射格式: {self.current_mapping.get('format_type') if hasattr(self, 'current_mapping') and self.current_mapping else 'None'}")
  1425. print(f"GUI格式设置: {self.format_var.get()}")
  1426. if format_type == 'vertical':
  1427. # 处理纵向表格(如BallController)
  1428. print("开始处理纵向表格配置...")
  1429. self._import_vertical_config()
  1430. elif format_type == 'horizontal':
  1431. # 处理横向表格(如武器配置)
  1432. print("开始处理横向表格配置...")
  1433. self._import_horizontal_config()
  1434. elif format_type == 'multi_sheet':
  1435. # 处理多工作表格式(如敌人配置)
  1436. print("开始处理多工作表配置...")
  1437. self._import_multi_sheet_config() # 使用专门的多工作表处理逻辑
  1438. else:
  1439. raise ValueError(f"未知的格式类型: {format_type}")
  1440. except Exception as e:
  1441. import traceback
  1442. error_details = traceback.format_exc()
  1443. print(f"导入配置失败,详细错误信息:")
  1444. print(error_details)
  1445. messagebox.showerror("错误", f"导入配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1446. def import_skill_config(self):
  1447. """导入技能配置到skill_config.json"""
  1448. try:
  1449. # 查找局外技能配置表.xlsx文件
  1450. excel_dir = Path(self.excel_dir)
  1451. skill_excel_file = excel_dir / "局外技能配置表.xlsx"
  1452. if not skill_excel_file.exists():
  1453. messagebox.showwarning("警告", f"未找到技能配置文件: {skill_excel_file}")
  1454. return
  1455. # 使用SkillConfigImporter导入配置
  1456. importer = SkillConfigImporter(str(excel_dir))
  1457. success, message = importer.import_config()
  1458. if success:
  1459. messagebox.showinfo("成功", message)
  1460. self.status_var.set("技能配置导入成功")
  1461. # 更新预览文本
  1462. self.preview_text.delete(1.0, tk.END)
  1463. self.preview_text.insert(tk.END, f"技能配置导入成功\n\n{message}")
  1464. else:
  1465. messagebox.showerror("错误", f"技能配置导入失败: {message}")
  1466. self.status_var.set("技能配置导入失败")
  1467. except Exception as e:
  1468. import traceback
  1469. error_details = traceback.format_exc()
  1470. print(f"导入技能配置失败,详细错误信息:")
  1471. print(error_details)
  1472. messagebox.showerror("错误", f"导入技能配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1473. def import_weapon_config(self):
  1474. """导入武器配置"""
  1475. try:
  1476. if not WEAPON_MANAGER_AVAILABLE:
  1477. messagebox.showerror("错误", "武器配置管理器不可用,请检查weapon_config_manager.py文件是否存在")
  1478. return
  1479. # 创建武器配置管理器
  1480. excel_dir = Path(self.excel_dir)
  1481. weapon_excel_file = excel_dir / "方块武器配置" / "方块武器配置表.xlsx"
  1482. weapon_json_file = excel_dir.parent / "weapons.json"
  1483. if not weapon_excel_file.exists():
  1484. messagebox.showwarning("警告", f"未找到武器配置文件: {weapon_excel_file}")
  1485. return
  1486. # 使用WeaponConfigManager导入配置
  1487. weapon_manager = WeaponConfigManager(
  1488. excel_file_path=str(weapon_excel_file),
  1489. json_file_path=str(weapon_json_file)
  1490. )
  1491. # 在后台线程中执行导入
  1492. def import_thread():
  1493. try:
  1494. success = weapon_manager.import_weapon_config()
  1495. # 在主线程中更新UI
  1496. def update_ui():
  1497. if success:
  1498. messagebox.showinfo("成功", "武器配置导入成功!")
  1499. self.status_var.set("武器配置导入成功")
  1500. # 更新预览文本
  1501. self.preview_text.delete(1.0, tk.END)
  1502. self.preview_text.insert(tk.END, "武器配置导入成功\n\n配置已从Excel文件合并到JSON文件中")
  1503. else:
  1504. messagebox.showerror("错误", "武器配置导入失败,请查看控制台输出")
  1505. self.status_var.set("武器配置导入失败")
  1506. self.root.after(0, update_ui)
  1507. except Exception as e:
  1508. def show_error():
  1509. import traceback
  1510. error_details = traceback.format_exc()
  1511. print(f"导入武器配置失败,详细错误信息:")
  1512. print(error_details)
  1513. messagebox.showerror("错误", f"导入武器配置失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1514. self.status_var.set("武器配置导入失败")
  1515. self.root.after(0, show_error)
  1516. # 启动导入线程
  1517. self.status_var.set("正在导入武器配置...")
  1518. thread = threading.Thread(target=import_thread)
  1519. thread.daemon = True
  1520. thread.start()
  1521. except Exception as e:
  1522. import traceback
  1523. error_details = traceback.format_exc()
  1524. print(f"启动武器配置导入失败,详细错误信息:")
  1525. print(error_details)
  1526. messagebox.showerror("错误", f"启动武器配置导入失败: {str(e)}\n\n详细错误信息已输出到控制台,请查看。")
  1527. def _import_vertical_config(self):
  1528. """导入纵向表格配置"""
  1529. # 读取现有JSON配置
  1530. if self.selected_json_path.exists():
  1531. with open(self.selected_json_path, 'r', encoding='utf-8') as f:
  1532. current_config = json.load(f)
  1533. else:
  1534. current_config = self.default_config.copy()
  1535. # 合并配置
  1536. updated_count = 0
  1537. for key, value in self.config_data.items():
  1538. if key in current_config and current_config[key] != value:
  1539. current_config[key] = value
  1540. updated_count += 1
  1541. elif key not in current_config:
  1542. current_config[key] = value
  1543. updated_count += 1
  1544. # 写入更新后的配置
  1545. with open(self.selected_json_path, 'w', encoding='utf-8') as f:
  1546. json.dump(current_config, f, indent=2, ensure_ascii=False)
  1547. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {updated_count} 个参数")
  1548. self.status_var.set("配置导入成功")
  1549. # 刷新预览
  1550. self.clear_selection()
  1551. def _import_horizontal_config(self):
  1552. """导入横向表格配置"""
  1553. print(f"横向表格配置导入开始...")
  1554. print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
  1555. # 检查是否是关卡配置(目录类型)
  1556. is_level_config = str(self.selected_json_path).endswith('levels') or self.selected_json_path.is_dir()
  1557. # 对于关卡配置,支持多种数据格式
  1558. if is_level_config:
  1559. if isinstance(self.config_data, list):
  1560. # 多工作表数据格式:直接是关卡数据列表
  1561. print("检测到关卡配置的多工作表数据格式")
  1562. items = None # 不需要items字段
  1563. elif 'items' in self.config_data:
  1564. # 传统的items数据格式
  1565. print("检测到关卡配置的传统items数据格式")
  1566. items = self.config_data['items']
  1567. elif isinstance(self.config_data, dict) and len(self.config_data) == 1:
  1568. # 检查是否是包装在单个键中的配置数据
  1569. key = list(self.config_data.keys())[0]
  1570. wrapped_data = self.config_data[key]
  1571. print(f"检测到包装的关卡配置数据,键名: {key}")
  1572. if isinstance(wrapped_data, list):
  1573. print("提取到关卡配置的多工作表数据格式")
  1574. self.config_data = wrapped_data # 更新配置数据为实际内容
  1575. items = None
  1576. elif isinstance(wrapped_data, dict) and 'items' in wrapped_data:
  1577. print("提取到关卡配置的传统items数据格式")
  1578. self.config_data = wrapped_data
  1579. items = wrapped_data['items']
  1580. else:
  1581. print(f"错误: 包装数据格式不正确: {type(wrapped_data)}")
  1582. error_msg = f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n\n包装键: {key}\n数据类型: {type(wrapped_data)}"
  1583. messagebox.showwarning("警告", error_msg)
  1584. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  1585. self.preview_text.insert(tk.END, f"关卡配置数据格式错误:包装数据不是列表或包含items的字典\n")
  1586. self.preview_text.insert(tk.END, f"包装键: {key}\n数据类型: {type(wrapped_data)}\n")
  1587. return
  1588. else:
  1589. print(f"错误: 关卡配置数据格式不正确")
  1590. print(f"实际配置数据: {self.config_data}")
  1591. print(f"配置数据类型: {type(self.config_data)}")
  1592. print(f"配置数据键: {list(self.config_data.keys()) if isinstance(self.config_data, dict) else 'Not a dict'}")
  1593. error_msg = f"关卡配置数据格式错误:需要多工作表数据或items字段\n\n数据类型: {type(self.config_data)}\n数据内容: {str(self.config_data)[:200]}..."
  1594. messagebox.showwarning("警告", error_msg)
  1595. # 同时在GUI中显示错误信息
  1596. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  1597. self.preview_text.insert(tk.END, f"关卡配置数据格式错误:需要多工作表数据或items字段\n")
  1598. self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n")
  1599. self.preview_text.insert(tk.END, f"数据内容: {str(self.config_data)[:500]}...\n")
  1600. return
  1601. else:
  1602. # 其他配置类型:检查数据格式
  1603. if isinstance(self.config_data, dict):
  1604. # 检查是否是多工作表数据格式(如:{'技能配置表': [...]})
  1605. if len(self.config_data) == 1:
  1606. key = list(self.config_data.keys())[0]
  1607. data_content = self.config_data[key]
  1608. if isinstance(data_content, list):
  1609. print(f"检测到多工作表数据格式,键名: {key}")
  1610. items = data_content
  1611. elif isinstance(data_content, dict) and 'items' in data_content:
  1612. print(f"检测到包装的items数据格式,键名: {key}")
  1613. items = data_content['items']
  1614. else:
  1615. print(f"错误: 无法识别的数据格式")
  1616. error_msg = f"配置数据格式错误:无法识别的数据结构\n\n键名: {key}\n数据类型: {type(data_content)}"
  1617. messagebox.showwarning("警告", error_msg)
  1618. return
  1619. elif 'items' in self.config_data:
  1620. print("检测到传统items数据格式")
  1621. items = self.config_data['items']
  1622. else:
  1623. print(f"错误: 配置数据格式不正确")
  1624. print(f"实际配置数据: {self.config_data}")
  1625. print(f"配置数据类型: {type(self.config_data)}")
  1626. print(f"配置数据键: {list(self.config_data.keys())}")
  1627. error_msg = f"配置数据格式错误:需要items字段或多工作表数据格式\n\n数据类型: {type(self.config_data)}\n可用键: {list(self.config_data.keys())}"
  1628. messagebox.showwarning("警告", error_msg)
  1629. # 同时在GUI中显示错误信息
  1630. self.preview_text.insert(tk.END, f"\n=== 错误信息 ===\n")
  1631. self.preview_text.insert(tk.END, f"配置数据格式错误:需要items字段或多工作表数据格式\n")
  1632. self.preview_text.insert(tk.END, f"数据类型: {type(self.config_data)}\n")
  1633. self.preview_text.insert(tk.END, f"可用键: {list(self.config_data.keys())}\n")
  1634. return
  1635. elif isinstance(self.config_data, list):
  1636. print("检测到直接列表数据格式")
  1637. items = self.config_data
  1638. else:
  1639. print(f"错误: 不支持的数据类型: {type(self.config_data)}")
  1640. error_msg = f"不支持的数据类型: {type(self.config_data)}"
  1641. messagebox.showwarning("警告", error_msg)
  1642. return
  1643. # 验证数据有效性
  1644. if items is not None:
  1645. print(f"配置项数量: {len(items) if items else 0}")
  1646. if not items:
  1647. messagebox.showwarning("警告", "没有有效的配置项")
  1648. return
  1649. elif isinstance(self.config_data, list):
  1650. print(f"关卡配置数量: {len(self.config_data)}")
  1651. if not self.config_data:
  1652. messagebox.showwarning("警告", "没有有效的关卡配置")
  1653. return
  1654. # 打印前几个配置项用于调试
  1655. if items is not None:
  1656. for i, item in enumerate(items[:3]):
  1657. print(f"配置项{i}: {item}")
  1658. elif isinstance(self.config_data, list):
  1659. for i, item in enumerate(self.config_data[:3]):
  1660. print(f"关卡配置{i}: {item.get('levelId', 'Unknown')} - {item.get('name', 'Unknown')}")
  1661. # 读取现有JSON配置
  1662. print(f"JSON配置文件路径: {self.selected_json_path}")
  1663. # 检查是否是关卡配置(目录类型)
  1664. if is_level_config:
  1665. print("处理关卡配置目录...")
  1666. # 关卡配置是目录,确保目录存在
  1667. if not self.selected_json_path.exists():
  1668. print(f"创建关卡配置目录: {self.selected_json_path}")
  1669. self.selected_json_path.mkdir(parents=True, exist_ok=True)
  1670. current_config = {}
  1671. else:
  1672. # 普通JSON文件配置
  1673. if self.selected_json_path.exists():
  1674. print("读取现有JSON配置文件...")
  1675. with open(self.selected_json_path, 'r', encoding='utf-8') as f:
  1676. current_config = json.load(f)
  1677. else:
  1678. print("创建新的JSON配置...")
  1679. current_config = {}
  1680. # 根据用户选择的处理模式和数据类型处理
  1681. if not self.selected_files:
  1682. print("错误: 没有选中的文件")
  1683. messagebox.showerror("错误", "没有选中的文件")
  1684. return
  1685. filename = Path(self.selected_files[0]).name
  1686. print(f"处理文件: {filename}")
  1687. # 获取用户选择的处理模式
  1688. is_multi_sheet = self.multi_sheet_var.get()
  1689. is_single_json = self.single_json_var.get()
  1690. print(f"处理模式 - 多工作表: {is_multi_sheet}, 单个JSON: {is_single_json}")
  1691. # 根据数据类型和用户选择的模式进行处理
  1692. if is_level_config:
  1693. # 关卡配置:始终为目录模式,每个关卡一个JSON文件
  1694. print("处理关卡配置...")
  1695. # 关卡配置:为每个关卡创建单独的JSON文件
  1696. levels_dir = self.selected_json_path
  1697. print(f"关卡配置目录: {levels_dir}")
  1698. try:
  1699. # 检查并创建目录
  1700. if not levels_dir.exists():
  1701. print(f"创建关卡配置目录: {levels_dir}")
  1702. levels_dir.mkdir(parents=True, exist_ok=True)
  1703. else:
  1704. print(f"关卡配置目录已存在: {levels_dir}")
  1705. # 测试目录写入权限
  1706. test_file = levels_dir / "test_permission.tmp"
  1707. try:
  1708. with open(test_file, 'w', encoding='utf-8') as f:
  1709. f.write("test")
  1710. test_file.unlink() # 删除测试文件
  1711. except PermissionError:
  1712. messagebox.showerror("错误", f"没有写入权限到目录: {levels_dir}\n请检查目录权限或以管理员身份运行")
  1713. return
  1714. # 检查数据格式:多工作表数据 vs 传统items数据
  1715. if isinstance(self.config_data, list):
  1716. # 新的多工作表数据格式:直接是关卡数据列表
  1717. print("使用多工作表数据格式")
  1718. level_configs = self.config_data
  1719. elif 'items' in self.config_data:
  1720. # 传统的items数据格式:需要转换
  1721. print("使用传统items数据格式")
  1722. level_configs = []
  1723. for item in items:
  1724. level_data = self._convert_level_data(item)
  1725. if level_data:
  1726. level_configs.append(level_data)
  1727. else:
  1728. print(f"错误: 未知的关卡配置数据格式: {type(self.config_data)}")
  1729. messagebox.showerror("错误", "关卡配置数据格式错误")
  1730. return
  1731. # 保存关卡配置文件
  1732. updated_count = 0
  1733. failed_count = 0
  1734. for level_data in level_configs:
  1735. try:
  1736. if level_data and 'levelId' in level_data and level_data['levelId']:
  1737. level_id = level_data['levelId']
  1738. level_file = levels_dir / f"{level_id}.json"
  1739. print(f"保存关卡配置: {level_file}")
  1740. with open(level_file, 'w', encoding='utf-8') as f:
  1741. json.dump(level_data, f, indent=2, ensure_ascii=False)
  1742. updated_count += 1
  1743. else:
  1744. print(f"跳过无效的关卡数据: {level_data}")
  1745. failed_count += 1
  1746. except Exception as e:
  1747. print(f"保存关卡配置时出错: {e}")
  1748. failed_count += 1
  1749. continue
  1750. if updated_count > 0:
  1751. message = f"关卡配置导入成功!\n更新了 {updated_count} 个关卡文件"
  1752. if failed_count > 0:
  1753. message += f"\n跳过了 {failed_count} 个无效配置"
  1754. messagebox.showinfo("成功", message)
  1755. self.status_var.set("关卡配置导入成功")
  1756. else:
  1757. messagebox.showerror("错误", "没有成功导入任何关卡配置\n请检查Excel文件格式和数据")
  1758. self.status_var.set("关卡配置导入失败")
  1759. except Exception as e:
  1760. error_msg = f"关卡配置导入失败: {str(e)}"
  1761. print(error_msg)
  1762. messagebox.showerror("错误", error_msg)
  1763. self.status_var.set("关卡配置导入失败")
  1764. self.clear_selection()
  1765. return
  1766. else:
  1767. # 非关卡配置:根据用户选择的模式和数据内容进行处理
  1768. print("处理非关卡配置...")
  1769. # 根据数据类型自动识别配置类型
  1770. config_type = self._detect_config_type()
  1771. print(f"检测到配置类型: {config_type}")
  1772. if config_type == 'unknown':
  1773. print(f"警告: 无法识别的配置类型")
  1774. messagebox.showwarning("警告", f"无法识别配置类型\n请检查Excel文件的工作表名称或数据格式")
  1775. return
  1776. # 根据配置类型处理数据
  1777. if config_type == 'enemy':
  1778. print("处理敌人配置...")
  1779. if 'enemies' not in current_config:
  1780. current_config['enemies'] = []
  1781. updated_enemies = []
  1782. for i, item in enumerate(items):
  1783. print(f"转换敌人数据 {i+1}/{len(items)}: {item}")
  1784. enemy_data = self._convert_enemy_data(item)
  1785. if enemy_data:
  1786. updated_enemies.append(enemy_data)
  1787. print(f"成功转换敌人数据: {enemy_data['id']}")
  1788. else:
  1789. print(f"跳过无效的敌人数据: {item}")
  1790. print(f"总共转换了 {len(updated_enemies)} 个敌人配置")
  1791. current_config['enemies'] = updated_enemies
  1792. elif config_type == 'weapon':
  1793. print("处理武器配置...")
  1794. if 'weapons' not in current_config:
  1795. current_config['weapons'] = []
  1796. updated_weapons = []
  1797. for i, item in enumerate(items):
  1798. print(f"转换武器数据 {i+1}/{len(items)}: {item}")
  1799. weapon_data = self._convert_weapon_data(item)
  1800. if weapon_data:
  1801. updated_weapons.append(weapon_data)
  1802. print(f"成功转换武器数据: {weapon_data.get('id', 'Unknown')}")
  1803. else:
  1804. print(f"跳过无效的武器数据: {item}")
  1805. print(f"总共转换了 {len(updated_weapons)} 个武器配置")
  1806. current_config['weapons'] = updated_weapons
  1807. elif config_type == 'skill':
  1808. print("处理技能配置...")
  1809. if 'skills' not in current_config:
  1810. current_config['skills'] = []
  1811. updated_skills = []
  1812. for i, item in enumerate(items):
  1813. print(f"转换技能数据 {i+1}/{len(items)}: {item}")
  1814. skill_data = self._convert_skill_data(item)
  1815. if skill_data:
  1816. updated_skills.append(skill_data)
  1817. print(f"成功转换技能数据: {skill_data.get('id', 'Unknown')}")
  1818. else:
  1819. print(f"跳过无效的技能数据: {item}")
  1820. print(f"总共转换了 {len(updated_skills)} 个技能配置")
  1821. current_config['skills'] = updated_skills
  1822. else:
  1823. # 通用配置处理
  1824. print(f"处理通用配置类型: {config_type}")
  1825. # 直接使用items数据
  1826. current_config['items'] = items
  1827. # 写入更新后的配置(关卡配置已在前面处理,跳过)
  1828. if not is_level_config:
  1829. try:
  1830. print(f"写入配置文件: {self.selected_json_path}")
  1831. print(f"配置内容预览: {str(current_config)[:200]}...")
  1832. with open(self.selected_json_path, 'w', encoding='utf-8') as f:
  1833. json.dump(current_config, f, indent=2, ensure_ascii=False)
  1834. print("配置文件写入成功")
  1835. messagebox.showinfo("成功", f"配置导入成功!\n更新了 {len(items)} 个配置项")
  1836. self.status_var.set("配置导入成功")
  1837. except Exception as e:
  1838. print(f"写入配置文件失败: {e}")
  1839. messagebox.showerror("错误", f"写入配置文件失败: {str(e)}")
  1840. return
  1841. # 刷新预览
  1842. self.clear_selection()
  1843. def _import_multi_sheet_config(self):
  1844. """导入多工作表配置(直接使用解析后的数据,不再通过_convert_enemy_data转换)"""
  1845. print(f"多工作表配置导入开始...")
  1846. print(f"配置数据结构: {list(self.config_data.keys()) if self.config_data else 'None'}")
  1847. try:
  1848. # 读取现有JSON配置
  1849. if self.selected_json_path.exists():
  1850. with open(self.selected_json_path, 'r', encoding='utf-8') as f:
  1851. current_config = json.load(f)
  1852. else:
  1853. current_config = self.default_config.copy()
  1854. # 直接使用解析后的多工作表数据
  1855. if isinstance(self.config_data, list):
  1856. # 如果config_data是列表(如敌人配置),直接使用
  1857. print(f"使用解析后的敌人配置数据,共 {len(self.config_data)} 个敌人")
  1858. current_config['enemies'] = self.config_data
  1859. # 添加元数据
  1860. current_config['metadata'] = {
  1861. 'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  1862. 'totalEnemies': len(self.config_data),
  1863. 'conversionMethod': 'excel_multi_sheet_direct'
  1864. }
  1865. elif isinstance(self.config_data, dict):
  1866. # 如果config_data是字典,合并到current_config
  1867. print(f"合并字典格式的配置数据")
  1868. # 特殊处理武器配置,保留现有武器中表格没有的字段
  1869. if 'weapons' in self.config_data and 'weapons' in current_config:
  1870. print("检测到武器配置,执行智能合并以保留现有字段")
  1871. new_weapons = self.config_data['weapons']
  1872. existing_weapons = current_config['weapons']
  1873. # 创建现有武器的ID映射
  1874. existing_weapons_map = {}
  1875. for weapon in existing_weapons:
  1876. weapon_id = weapon.get('ID', '') or weapon.get('id', '')
  1877. if weapon_id:
  1878. existing_weapons_map[weapon_id] = weapon
  1879. # 合并武器配置,保留现有字段
  1880. merged_weapons = []
  1881. for new_weapon_data in new_weapons:
  1882. weapon_id = new_weapon_data.get('ID', '') or new_weapon_data.get('id', '')
  1883. if weapon_id and weapon_id in existing_weapons_map:
  1884. # 找到对应的现有武器,转换新数据时传入现有武器配置
  1885. existing_weapon = existing_weapons_map[weapon_id]
  1886. # 转换新武器数据,传入现有武器配置以保留特效字段
  1887. converted_weapon = self._convert_weapon_data(new_weapon_data, existing_weapon)
  1888. if converted_weapon:
  1889. merged_weapons.append(converted_weapon)
  1890. print(f"✓ 武器 {weapon_id} 已转换并保留了现有的特效字段")
  1891. else:
  1892. # 新武器,直接转换
  1893. converted_weapon = self._convert_weapon_data(new_weapon_data)
  1894. if converted_weapon:
  1895. merged_weapons.append(converted_weapon)
  1896. print(f"+ 新武器 {weapon_id} 已添加")
  1897. # 更新武器配置
  1898. self.config_data['weapons'] = merged_weapons
  1899. print(f"武器配置合并完成,共 {len(merged_weapons)} 个武器")
  1900. current_config.update(self.config_data)
  1901. # 写入更新后的配置
  1902. print(f"写入配置文件: {self.selected_json_path}")
  1903. print(f"配置内容预览: {str(current_config)[:200]}...")
  1904. with open(self.selected_json_path, 'w', encoding='utf-8') as f:
  1905. json.dump(current_config, f, indent=2, ensure_ascii=False)
  1906. print("多工作表配置文件写入成功")
  1907. messagebox.showinfo("成功", f"多工作表配置导入成功!\n更新了配置数据")
  1908. self.status_var.set("多工作表配置导入成功")
  1909. except Exception as e:
  1910. print(f"导入多工作表配置失败: {e}")
  1911. import traceback
  1912. traceback.print_exc()
  1913. messagebox.showerror("错误", f"导入多工作表配置失败: {str(e)}")
  1914. return
  1915. # 刷新预览
  1916. self.clear_selection()
  1917. def _deep_merge_weapon_config(self, existing_weapon, new_weapon):
  1918. """深度合并武器配置,保留现有武器中表格没有的字段"""
  1919. import copy
  1920. # 创建新武器的深拷贝作为基础
  1921. merged = copy.deepcopy(new_weapon)
  1922. # 递归合并函数,将现有武器中新武器没有的字段添加到合并结果中
  1923. def preserve_existing_fields(target, existing):
  1924. for key, value in existing.items():
  1925. if key not in target:
  1926. # 现有武器有但新武器没有的字段,保留
  1927. target[key] = copy.deepcopy(value)
  1928. elif isinstance(target[key], dict) and isinstance(value, dict):
  1929. # 如果两者都是字典,递归处理
  1930. preserve_existing_fields(target[key], value)
  1931. # 执行深度合并,保留现有武器中的额外字段
  1932. preserve_existing_fields(merged, existing_weapon)
  1933. return merged
  1934. def _detect_config_type(self):
  1935. """根据数据内容和工作表名称自动识别配置类型"""
  1936. try:
  1937. # 检查是否是多工作表数据
  1938. if self.multi_sheet_var.get() and hasattr(self, 'config_data') and isinstance(self.config_data, dict):
  1939. # 多工作表模式:根据工作表名称判断
  1940. if '敌人基础配置' in self.config_data or '敌人配置' in str(self.config_data.keys()):
  1941. return 'enemy'
  1942. elif '技能信息表' in self.config_data or '技能配置' in str(self.config_data.keys()):
  1943. return 'skill'
  1944. elif '武器配置' in str(self.config_data.keys()) or '武器信息' in self.config_data:
  1945. return 'weapon'
  1946. elif any('关卡' in str(key) for key in self.config_data.keys()):
  1947. return 'level'
  1948. # 检查数据内容中的字段来判断类型
  1949. if hasattr(self, 'config_data'):
  1950. sample_data = None
  1951. # 获取样本数据
  1952. if isinstance(self.config_data, dict) and 'items' in self.config_data:
  1953. items = self.config_data['items']
  1954. if items and len(items) > 0:
  1955. sample_data = items[0]
  1956. elif isinstance(self.config_data, list) and len(self.config_data) > 0:
  1957. sample_data = self.config_data[0]
  1958. if sample_data and isinstance(sample_data, dict):
  1959. # 根据字段名判断配置类型
  1960. fields = set(sample_data.keys())
  1961. # 敌人配置特征字段
  1962. enemy_fields = {'enemyId', 'name', 'hp', 'attack', 'defense', 'speed'}
  1963. if enemy_fields.intersection(fields):
  1964. return 'enemy'
  1965. # 技能配置特征字段
  1966. skill_fields = {'skillId', 'skillName', 'damage', 'cooldown', 'description'}
  1967. if skill_fields.intersection(fields):
  1968. return 'skill'
  1969. # 武器配置特征字段
  1970. weapon_fields = {'weaponId', 'weaponName', 'damage', 'range', 'fireRate'}
  1971. if weapon_fields.intersection(fields):
  1972. return 'weapon'
  1973. # 关卡配置特征字段
  1974. level_fields = {'levelId', 'name', 'enemies', 'waves'}
  1975. if level_fields.intersection(fields):
  1976. return 'level'
  1977. # 如果无法通过数据内容判断,尝试通过文件名判断(作为后备方案)
  1978. if hasattr(self, 'selected_files') and self.selected_files:
  1979. filename = Path(self.selected_files[0]).name.lower()
  1980. if '敌人' in filename:
  1981. return 'enemy'
  1982. elif '技能' in filename:
  1983. return 'skill'
  1984. elif '武器' in filename:
  1985. return 'weapon'
  1986. elif '关卡' in filename:
  1987. return 'level'
  1988. return 'unknown'
  1989. except Exception as e:
  1990. print(f"配置类型检测出错: {e}")
  1991. return 'unknown'
  1992. def _convert_enemy_data(self, item):
  1993. """转换敌人数据格式 - 支持嵌套结构"""
  1994. try:
  1995. print(f"开始转换敌人数据: {item}")
  1996. # 支持中英文字段名
  1997. enemy_id = item.get('id', item.get('敌人ID', ''))
  1998. enemy_name = item.get('name', item.get('敌人名称', ''))
  1999. enemy_type = item.get('type', item.get('敌人类型', ''))
  2000. print(f"敌人ID: {enemy_id}, 敌人名称: {enemy_name}, 类型: {enemy_type}")
  2001. # 检查必要字段
  2002. if not enemy_id:
  2003. print(f"跳过无效敌人数据: 缺少敌人ID - {item}")
  2004. return None
  2005. # 获取基础属性
  2006. health = item.get('health', item.get('生命值', 100))
  2007. speed = item.get('speed', item.get('移动速度', 50))
  2008. attack_damage = item.get('attackDamage', item.get('攻击伤害', 10))
  2009. attack_range = item.get('range', item.get('攻击范围', 100))
  2010. attack_speed = item.get('attackSpeed', item.get('攻击速度', 1.0))
  2011. defense = item.get('defense', item.get('防御力', 0))
  2012. # 调试信息
  2013. print(f"转换敌人 {enemy_id} 的属性: health={health}, speed={speed}, attackDamage={attack_damage}, defense={defense}")
  2014. print(f"原始数据中的字段: {list(item.keys())}")
  2015. if '生命值' in item:
  2016. print(f"找到生命值字段,值为: {item['生命值']}")
  2017. if 'health' in item:
  2018. print(f"找到health字段,值为: {item['health']}")
  2019. # 获取格挡相关属性
  2020. can_block = item.get('canBlock', item.get('可格挡', False))
  2021. block_chance = item.get('blockChance', item.get('格挡概率', 0.0))
  2022. block_damage_reduction = item.get('blockDamageReduction', item.get('格挡伤害减免', 0.5))
  2023. # 获取移动相关属性
  2024. movement_pattern = item.get('movementPattern', item.get('移动模式', 'direct'))
  2025. patrol_range = item.get('patrolRange', item.get('巡逻范围', 100))
  2026. chase_range = item.get('chaseRange', item.get('追击范围', 200))
  2027. # 获取视觉配置
  2028. # 敌人ID到动画编号的映射
  2029. enemy_to_ani = {
  2030. 'normal_zombie': '001',
  2031. 'roadblock_zombie': '002',
  2032. 'wandering_zombie': '003',
  2033. 'mage_zombie': '004',
  2034. 'archer_zombie': '005',
  2035. 'stealth_zombie': '006',
  2036. 'bucket_zombie': '007',
  2037. 'barrel_zombie': '008',
  2038. 'boss1_gatekeeper': '009',
  2039. 'boss2_gravedigger': '010',
  2040. 'boss3_cyborg': '011'
  2041. }
  2042. ani_num = enemy_to_ani.get(enemy_id, '001')
  2043. default_sprite_path = f'Animation/EnemyAni/{ani_num}'
  2044. sprite_path = item.get('spritePath', item.get('精灵路径', default_sprite_path))
  2045. scale = item.get('scale', item.get('缩放', 1.0))
  2046. # 获取音效配置
  2047. attack_sound = item.get('attackSound', item.get('攻击音效', 'enemy_attack'))
  2048. death_sound = item.get('deathSound', item.get('死亡音效', 'enemy_death'))
  2049. # 构建嵌套结构的配置
  2050. result = {
  2051. 'id': enemy_id,
  2052. 'name': enemy_name,
  2053. 'type': enemy_type,
  2054. 'stats': {
  2055. 'health': health,
  2056. 'maxHealth': health,
  2057. 'defense': defense,
  2058. 'speed': speed
  2059. },
  2060. 'movement': {
  2061. 'pattern': movement_pattern,
  2062. 'speed': speed,
  2063. 'patrolRange': patrol_range,
  2064. 'chaseRange': chase_range,
  2065. 'rotationSpeed': item.get('rotationSpeed', item.get('旋转速度', 180)),
  2066. 'moveType': item.get('moveType', item.get('移动类型', 'straight')),
  2067. 'swingAmplitude': item.get('swingAmplitude', item.get('摆动幅度', 0.0)),
  2068. 'swingFrequency': item.get('swingFrequency', item.get('摆动频率', 0.0)),
  2069. 'speedVariation': item.get('speedVariation', item.get('速度变化', 0.1))
  2070. },
  2071. 'combat': {
  2072. 'attackDamage': attack_damage,
  2073. 'attackRange': attack_range,
  2074. 'attackSpeed': attack_speed,
  2075. 'canBlock': can_block,
  2076. 'blockChance': block_chance,
  2077. 'blockDamageReduction': block_damage_reduction,
  2078. 'attackCooldown': 1.0 / attack_speed if attack_speed > 0 else 1.0,
  2079. 'attackType': item.get('attackType', item.get('攻击类型', 'melee')),
  2080. 'attackDelay': item.get('attackDelay', item.get('攻击延迟', 0.5)),
  2081. 'weaponType': item.get('weaponType', item.get('武器类型', 'none')),
  2082. 'projectileType': item.get('projectileType', item.get('投射物类型', 'none')),
  2083. 'projectileSpeed': item.get('projectileSpeed', item.get('投射物速度', 100))
  2084. },
  2085. 'visualConfig': {
  2086. 'spritePath': sprite_path,
  2087. 'scale': scale,
  2088. 'animationSpeed': item.get('animationSpeed', item.get('动画速度', 1.0)),
  2089. 'flipX': item.get('flipX', item.get('水平翻转', False)),
  2090. 'tint': item.get('tint', item.get('着色', '#FFFFFF')),
  2091. 'animations': {
  2092. 'idle': item.get('idleAnimation', item.get('待机动画', 'idle')),
  2093. 'walk': item.get('walkAnimation', item.get('行走动画', 'walk')),
  2094. 'attack': item.get('attackAnimation', item.get('攻击动画', 'attack')),
  2095. 'death': item.get('deathAnimation', item.get('死亡动画', 'dead'))
  2096. },
  2097. 'weaponProp': item.get('weaponProp', item.get('武器道具', ''))
  2098. },
  2099. 'audioConfig': {
  2100. 'attackSound': attack_sound,
  2101. 'deathSound': death_sound,
  2102. 'hitSound': item.get('hitSound', item.get('受击音效', 'enemy_hit')),
  2103. 'walkSound': item.get('walkSound', item.get('行走音效', '')),
  2104. 'blockSound': item.get('blockSound', item.get('格挡音效', '')),
  2105. 'stealthSound': item.get('stealthSound', item.get('隐身音效', '')),
  2106. 'armorBreakSound': item.get('armorBreakSound', item.get('护甲破碎音效', '')),
  2107. 'fuseSound': item.get('fuseSound', item.get('引信音效', '')),
  2108. 'volume': item.get('volume', item.get('音量', 1.0))
  2109. }
  2110. }
  2111. # 添加特殊能力配置
  2112. special_abilities = item.get('specialAbilities', [])
  2113. if special_abilities:
  2114. result['specialAbilities'] = special_abilities
  2115. # 添加BOSS配置
  2116. boss_config = item.get('bossConfig', {})
  2117. if boss_config and boss_config.get('isBoss', False):
  2118. result['bossConfig'] = boss_config
  2119. # 根据敌人类型添加特殊配置
  2120. result = self._add_enemy_special_config(result, enemy_type, item)
  2121. print(f"成功转换敌人数据: {enemy_id}")
  2122. return result
  2123. except Exception as e:
  2124. print(f"转换敌人数据失败: {e} - 数据: {item}")
  2125. return None
  2126. def _add_enemy_special_config(self, result, enemy_type, item):
  2127. """根据敌人类型添加特殊配置"""
  2128. try:
  2129. # 根据敌人类型添加特殊能力和配置
  2130. special_abilities = []
  2131. if enemy_type == 'stealth_zombie':
  2132. # 隐身僵尸特殊配置
  2133. special_abilities.append({
  2134. 'type': 'stealth',
  2135. 'duration': item.get('stealthDuration', item.get('隐身持续时间', 3.0)),
  2136. 'cooldown': item.get('stealthCooldown', item.get('隐身冷却时间', 8.0)),
  2137. 'alpha': item.get('stealthAlpha', item.get('隐身透明度', 0.3))
  2138. })
  2139. result['stealthConfig'] = {
  2140. 'canStealth': True,
  2141. 'stealthDuration': item.get('stealthDuration', item.get('隐身持续时间', 3.0)),
  2142. 'stealthCooldown': item.get('stealthCooldown', item.get('隐身冷却时间', 8.0)),
  2143. 'stealthAlpha': item.get('stealthAlpha', item.get('隐身透明度', 0.3))
  2144. }
  2145. elif enemy_type == 'bucket_zombie':
  2146. # 铁桶僵尸特殊配置
  2147. result['armorConfig'] = {
  2148. 'hasArmor': True,
  2149. 'armorHealth': item.get('armorHealth', item.get('护甲血量', 50)),
  2150. 'armorDefense': item.get('armorDefense', item.get('护甲防御', 5)),
  2151. 'armorSprite': item.get('armorSprite', item.get('护甲精灵', 'bucket_armor'))
  2152. }
  2153. elif enemy_type == 'explosive_zombie':
  2154. # 火药桶僵尸特殊配置
  2155. special_abilities.append({
  2156. 'type': 'explosion',
  2157. 'damage': item.get('explosionDamage', item.get('爆炸伤害', 30)),
  2158. 'radius': item.get('explosionRadius', item.get('爆炸半径', 80)),
  2159. 'triggerOnDeath': True
  2160. })
  2161. result['explosiveConfig'] = {
  2162. 'explosionDamage': item.get('explosionDamage', item.get('爆炸伤害', 30)),
  2163. 'explosionRadius': item.get('explosionRadius', item.get('爆炸半径', 80)),
  2164. 'fuseTime': item.get('fuseTime', item.get('引爆时间', 2.0)),
  2165. 'chainExplosion': item.get('chainExplosion', item.get('连锁爆炸', False))
  2166. }
  2167. elif enemy_type == 'ranged_enemy':
  2168. # 远程敌人特殊配置
  2169. result['rangedConfig'] = {
  2170. 'projectileType': item.get('projectileType', item.get('弹药类型', 'bullet')),
  2171. 'projectileSpeed': item.get('projectileSpeed', item.get('弹药速度', 100)),
  2172. 'projectileDamage': item.get('projectileDamage', item.get('弹药伤害', item.get('attack', item.get('攻击力', 10)))),
  2173. 'fireRate': item.get('fireRate', item.get('射速', 1.0)),
  2174. 'accuracy': item.get('accuracy', item.get('精度', 0.9))
  2175. }
  2176. elif 'boss' in enemy_type.lower():
  2177. # BOSS特殊配置
  2178. special_abilities.extend([
  2179. {
  2180. 'type': 'charge_attack',
  2181. 'damage': item.get('chargeDamage', item.get('冲锋伤害', item.get('attack', item.get('攻击力', 10)) * 2)),
  2182. 'range': item.get('chargeRange', item.get('冲锋范围', 150)),
  2183. 'cooldown': item.get('chargeCooldown', item.get('冲锋冷却', 8.0))
  2184. },
  2185. {
  2186. 'type': 'area_attack',
  2187. 'damage': item.get('areaDamage', item.get('范围伤害', item.get('attack', item.get('攻击力', 10)) * 1.5)),
  2188. 'radius': item.get('areaRadius', item.get('范围半径', 100)),
  2189. 'cooldown': item.get('areaCooldown', item.get('范围攻击冷却', 12.0))
  2190. }
  2191. ])
  2192. result['bossConfig'] = {
  2193. 'isBoss': True,
  2194. 'phases': item.get('phases', item.get('阶段数', 1)),
  2195. 'enrageThreshold': item.get('enrageThreshold', item.get('狂暴阈值', 0.3)),
  2196. 'enrageDamageMultiplier': item.get('enrageDamageMultiplier', item.get('狂暴伤害倍数', 1.5)),
  2197. 'enrageSpeedMultiplier': item.get('enrageSpeedMultiplier', item.get('狂暴速度倍数', 1.3))
  2198. }
  2199. # 添加特殊能力到结果中
  2200. if special_abilities:
  2201. result['specialAbilities'] = special_abilities
  2202. return result
  2203. except Exception as e:
  2204. print(f"添加特殊配置失败: {e}")
  2205. return result
  2206. def _convert_skill_data(self, item):
  2207. """转换技能数据格式"""
  2208. try:
  2209. # 支持多种字段名格式(中文和英文)
  2210. skill_id = item.get('id', item.get('技能ID', ''))
  2211. skill_name = item.get('name', item.get('技能名称', ''))
  2212. print(f"正在转换技能数据: {skill_id} - {skill_name}")
  2213. # 检查必要字段
  2214. if not skill_id:
  2215. print(f"跳过无效技能数据: 缺少技能ID - {item}")
  2216. return None
  2217. result = {
  2218. 'id': skill_id,
  2219. 'name': skill_name,
  2220. 'description': item.get('description', item.get('技能描述', '')),
  2221. 'iconPath': item.get('iconPath', item.get('图标路径', '')),
  2222. 'maxLevel': item.get('maxLevel', item.get('最大等级', 1)),
  2223. 'currentLevel': item.get('currentLevel', item.get('当前等级', 0)),
  2224. 'priceReduction': item.get('priceReduction', item.get('价格减少', 0.0)),
  2225. 'critChanceIncrease': item.get('critChanceIncrease', item.get('暴击几率增加', 0.0)),
  2226. 'critDamageBonus': item.get('critDamageBonus', item.get('暴击伤害加成', 0.0)),
  2227. 'healthIncrease': item.get('healthIncrease', item.get('生命值增加', 0.0)),
  2228. 'multiShotChance': item.get('multiShotChance', item.get('多重射击几率', 0.0)),
  2229. 'energyGainIncrease': item.get('energyGainIncrease', item.get('能量加成', 0.0)),
  2230. 'ballSpeedIncrease': item.get('ballSpeedIncrease', item.get('速度提升', 0.0))
  2231. }
  2232. print(f"成功转换技能数据: {result}")
  2233. return result
  2234. except Exception as e:
  2235. print(f"转换技能数据失败: {e} - 数据: {item}")
  2236. return None
  2237. def _convert_level_data(self, item):
  2238. """转换关卡数据格式"""
  2239. try:
  2240. # 如果item已经是完整的关卡数据结构(来自多工作表解析),直接返回
  2241. if isinstance(item, dict) and 'waves' in item:
  2242. return item
  2243. # 处理传统的单行数据格式
  2244. # 处理可用武器字符串,转换为数组(支持中英文字段名)
  2245. available_weapons = []
  2246. weapons_field = item.get('weapons', item.get('可用武器', ''))
  2247. if weapons_field:
  2248. weapons_str = str(weapons_field)
  2249. # 支持逗号、顿号、分号等多种分隔符
  2250. for separator in ['、', ',', ',', ';', ';']:
  2251. if separator in weapons_str:
  2252. available_weapons = [weapon.strip() for weapon in weapons_str.split(separator)]
  2253. break
  2254. # 如果没有找到分隔符,则作为单个武器处理
  2255. if not available_weapons:
  2256. available_weapons = [weapons_str.strip()]
  2257. # 获取关卡ID,用于读取现有配置(支持中英文字段名)
  2258. level_id = str(item.get('levelId', item.get('关卡ID', '')))
  2259. # 尝试读取现有的关卡配置文件,保留waves数据
  2260. existing_waves = []
  2261. existing_data = {}
  2262. if level_id:
  2263. try:
  2264. levels_dir = self.project_root / "assets/resources/data/levels"
  2265. level_file = levels_dir / f"{level_id}.json"
  2266. if level_file.exists():
  2267. with open(level_file, 'r', encoding='utf-8') as f:
  2268. existing_data = json.load(f)
  2269. existing_waves = existing_data.get('waves', [])
  2270. print(f"保留现有关卡 {level_id} 的 {len(existing_waves)} 个波次数据")
  2271. except Exception as e:
  2272. print(f"读取现有关卡配置时出错: {e}")
  2273. # 构建新的关卡数据,保留现有的waves(支持中英文字段名)
  2274. level_data = {
  2275. "levelId": level_id,
  2276. "name": str(item.get('name', item.get('关卡名称', existing_data.get('name', '')))),
  2277. "scene": str(item.get('scene', item.get('场景', existing_data.get('scene', '')))),
  2278. "description": str(item.get('description', item.get('描述', existing_data.get('description', '')))),
  2279. "backgroundImage": str(item.get('backgroundImage', item.get('关卡背景图路径', existing_data.get('backgroundImage', 'images/LevelBackground/BG1')))),
  2280. "weapons": available_weapons if available_weapons else existing_data.get('weapons', existing_data.get('availableWeapons', [])),
  2281. "timeLimit": int(item.get('timeLimit', item.get('时间限制', existing_data.get('timeLimit', 300)))),
  2282. "difficulty": str(item.get('difficulty', item.get('难度', existing_data.get('difficulty', 'normal')))),
  2283. "healthMultiplier": float(item.get('healthMultiplier', item.get('生命倍数', existing_data.get('healthMultiplier', 1.0)))),
  2284. "waves": existing_waves # 保留现有的waves数据
  2285. }
  2286. # 添加可选字段(如果存在,支持中英文字段名)
  2287. coin_reward = item.get('coinReward', item.get('钞票奖励'))
  2288. if coin_reward is not None:
  2289. level_data["coinReward"] = int(coin_reward)
  2290. elif 'coinReward' in existing_data:
  2291. level_data["coinReward"] = existing_data['coinReward']
  2292. diamond_reward = item.get('diamondReward', item.get('钻石奖励'))
  2293. if diamond_reward is not None:
  2294. level_data["diamondReward"] = int(diamond_reward)
  2295. elif 'diamondReward' in existing_data:
  2296. level_data["diamondReward"] = existing_data['diamondReward']
  2297. return level_data
  2298. except Exception as e:
  2299. print(f"转换关卡数据时出错: {e}")
  2300. return None
  2301. def fix_enemies_json(self):
  2302. """修复enemies.json文件 - 删除attack字段"""
  2303. try:
  2304. # 获取enemies.json路径
  2305. enemies_json_path = Path(self.json_path_var.get()) if self.json_path_var.get() else None
  2306. if not enemies_json_path or not enemies_json_path.exists():
  2307. # 尝试默认路径
  2308. enemies_json_path = Path(os.path.dirname(__file__)).parent / "enemies.json"
  2309. if not enemies_json_path.exists():
  2310. messagebox.showerror("错误", "未找到enemies.json文件")
  2311. return False
  2312. # 备份原文件
  2313. backup_path = f"{enemies_json_path}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  2314. with open(enemies_json_path, 'r', encoding='utf-8') as src:
  2315. with open(backup_path, 'w', encoding='utf-8') as dst:
  2316. dst.write(src.read())
  2317. # 读取JSON文件
  2318. with open(enemies_json_path, 'r', encoding='utf-8') as f:
  2319. data = json.load(f)
  2320. # 递归删除attack字段
  2321. def remove_attack_fields(obj):
  2322. if isinstance(obj, dict):
  2323. if 'attack' in obj:
  2324. del obj['attack']
  2325. for value in obj.values():
  2326. remove_attack_fields(value)
  2327. elif isinstance(obj, list):
  2328. for item in obj:
  2329. remove_attack_fields(item)
  2330. remove_attack_fields(data)
  2331. # 写回文件
  2332. with open(enemies_json_path, 'w', encoding='utf-8') as f:
  2333. json.dump(data, f, ensure_ascii=False, indent=2)
  2334. messagebox.showinfo("成功", f"已修复enemies.json文件\n备份文件: {backup_path}\n已删除所有attack字段")
  2335. return True
  2336. except Exception as e:
  2337. messagebox.showerror("错误", f"修复enemies.json失败: {str(e)}")
  2338. return False
  2339. def auto_import_enemies_from_excel(self):
  2340. """自动从Excel导入敌人配置"""
  2341. try:
  2342. # 查找敌人配置表.xlsx
  2343. excel_dir = Path(os.path.dirname(__file__))
  2344. excel_file = excel_dir / "敌人配置表.xlsx"
  2345. if not excel_file.exists():
  2346. messagebox.showerror("错误", f"未找到敌人配置表.xlsx文件: {excel_file}")
  2347. return False
  2348. # 读取Excel文件
  2349. if not PANDAS_AVAILABLE:
  2350. messagebox.showerror("错误", "pandas未安装,无法读取Excel文件")
  2351. return False
  2352. all_sheets = pd.read_excel(excel_file, sheet_name=None)
  2353. # 解析敌人配置
  2354. enemies_config = self.parse_enemy_multi_sheet_data(all_sheets, "敌人配置表.xlsx")
  2355. if not enemies_config:
  2356. messagebox.showerror("错误", "未能解析到任何敌人配置")
  2357. return False
  2358. # 获取输出路径
  2359. enemies_json_path = Path(self.json_path_var.get()) if self.json_path_var.get() else None
  2360. if not enemies_json_path:
  2361. enemies_json_path = excel_dir.parent / "enemies.json"
  2362. # 备份现有文件
  2363. if enemies_json_path.exists():
  2364. backup_path = f"{enemies_json_path}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
  2365. with open(enemies_json_path, 'r', encoding='utf-8') as src:
  2366. with open(backup_path, 'w', encoding='utf-8') as dst:
  2367. dst.write(src.read())
  2368. # 写入新配置
  2369. with open(enemies_json_path, 'w', encoding='utf-8') as f:
  2370. json.dump(enemies_config, f, ensure_ascii=False, indent=2)
  2371. messagebox.showinfo("成功", f"已成功导入{len(enemies_config)}个敌人配置到: {enemies_json_path}")
  2372. return True
  2373. except Exception as e:
  2374. messagebox.showerror("错误", f"自动导入敌人配置失败: {str(e)}")
  2375. return False
  2376. def restore_default_config(self):
  2377. """恢复默认配置"""
  2378. result = messagebox.askyesno("确认", "确定要恢复默认配置吗?\n当前配置将被覆盖!")
  2379. if not result:
  2380. return
  2381. try:
  2382. if not self.selected_json_path:
  2383. messagebox.showwarning("警告", "请先选择JSON输出路径")
  2384. return
  2385. json_path = Path(self.selected_json_path)
  2386. # 备份当前配置
  2387. if json_path.exists():
  2388. backup_path = json_path.parent / f"ballController_backup_before_default_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
  2389. with open(json_path, 'r', encoding='utf-8') as f:
  2390. content = f.read()
  2391. with open(backup_path, 'w', encoding='utf-8') as f:
  2392. f.write(content)
  2393. # 写入默认配置
  2394. with open(json_path, 'w', encoding='utf-8') as f:
  2395. json.dump(self.default_config, f, indent=2, ensure_ascii=False)
  2396. messagebox.showinfo("成功", "已恢复默认配置")
  2397. self.status_var.set("已恢复默认配置")
  2398. # 刷新预览
  2399. self.clear_selection()
  2400. except Exception as e:
  2401. messagebox.showerror("错误", f"恢复默认配置失败: {e}")
  2402. def run(self):
  2403. """运行应用"""
  2404. self.root.mainloop()
  2405. def main():
  2406. """主函数"""
  2407. try:
  2408. app = ConfigManagerGUI()
  2409. app.run()
  2410. except Exception as e:
  2411. print(f"启动应用失败: {e}")
  2412. input("按回车键退出...")
  2413. if __name__ == "__main__":
  2414. main()