MoebiusMeow/WCLA
1
1from functools import partial, reduce
2from matplotlib import pyplot as plt
3import numpy as np
4import pandas as pd
5import gradio as gr
6import os
7import re
8import random
9from PIL import Image as PILImage
10
11from langs import *
12from rules import *
13
14version = "0.2.2.hf (2025 Jan 11)"
15# TODO
16
17theme = gr.themes.Default()
18
19def load_theme(is_dark=False):
20 global theme
21 path = "./theme/theme{}.json".format("_dark" if is_dark else "")
22 if os.path.exists(path):
23 theme = theme.load(path)
24
25def load_asset():
26 for i in range(0, 20):
27 path = "./asset/breed/{}.tga".format(i * 10 + 1 + (0 if i <= 17 else -10) if i > 0 else -10)
28 path_png = "./asset/breed/{}.png".format(i)
29 if not os.path.exists(path_png):
30 with PILImage.open(path) as img:
31 img.save(path_png)
32 continue
33
34def get_default_logs_path():
35 result = "/SteamLibrary/steamapps/common/Wakfu/preferences/logs"
36 for i in range(0, 26):
37 disk = chr(ord("A") + i)
38 if os.path.isdir(disk + ":" + result):
39 return disk + ":" + result
40 result = "/Users/{}/AppData/Roaming/zaap/gamesLogs/wakfu/logs".format(os.getlogin())
41 for i in range(0, 26):
42 disk = chr(ord("A") + i)
43 if os.path.isdir(disk + ":" + result):
44 return disk + ":" + result
45 return None
46
47class CombatData():
48 def __init__(self):
49 self.players = []
50 self.players_pos = []
51 self.players_summon = []
52 self.players_summon_pos = []
53 self.players_breed = {}
54 self.mobs = []
55 self.mobs_pos = []
56 self.mobs_summon = []
57 self.mobs_summon_pos = []
58 self.combat_id = "Unknown"
59 self.data = []
60 self.data_header = ["角色", "目标", "伤害", "元素", "暴击", "格挡", "描述", "其他"]
61 self.lazy_result = {}
62
63 def is_team_member(self, name, is_player):
64 if name == "Unknown":
65 return True
66 return (name in self.players or name in self.players_summon) == is_player
67
68 def add_entry(self, caster, target, damage=0, element="Neutral", crit=False, block=False, desc="", extra=""):
69 self.data.append([caster, target, damage, element, crit, block, desc, extra])
70
71 def get_raw_df(self):
72 result = {}
73 for i, k in enumerate(self.data_header):
74 result[k] = []
75 for line in self.data:
76 result[k].append(line[i])
77 return pd.DataFrame(result)
78
79 def df_aggregation( self,
80 fn=lambda x, y: x+y,
81 fn0=0,
82 select=lambda v: int(v[2].replace("HP", "").replace("Armor", "").strip()),
83 filter_fn=None,
84 groupby=None ):
85 key = (fn, fn0, select, filter_fn, groupby)
86 if key in self.lazy_result:
87 return self.lazy_result
88 groups = {}
89 for line in (self.data if not filter_fn else filter(filter_fn, self.data)):
90 group_name = "-" if not groupby else groupby(line)
91 groups.setdefault(group_name, fn0)
92 groups[group_name] = fn(groups[group_name], select(line))
93 result = groups["-"] if not groupby else groups
94 self.lazy_result[key] = result
95 return result
96
97 def get_start_scene(self, select=None):
98 result = {}
99 result["Name"] = []
100 result["x"] = []
101 result["y"] = []
102 players = ["{} ({})".format(x, zh_dict.get("breed." + self.players_breed.get(x, "-"), "-")) for x in self.players]
103 desc = ""
104 for i, player in enumerate(players):
105 if select and select != self.players[i]:
106 continue
107 for k in range(0, len(self.players_pos[i]), 3):
108 result["Name"].append(player)
109 desc = player
110 result["x"].append(self.players_pos[i][0 + k])
111 result["y"].append(self.players_pos[i][1 + k])
112 for mobs, mobs_pos in zip([self.mobs, self.players_summon, self.mobs_summon], [self.mobs_pos, self.players_summon_pos, self.mobs_summon_pos]):
113 for i, mob in enumerate(mobs):
114 if select and select != mob:
115 continue
116 for k in range(0, len(mobs_pos[i]), 3):
117 result["Name"].append(mob)
118 result["x"].append(mobs_pos[i][0 + k])
119 result["y"].append(mobs_pos[i][1 + k])
120
121 for i, player in enumerate(players):
122 if not select or select == self.players[i]:
123 continue
124 for k in range(0, len(self.players_pos[i]), 3):
125 result["Name"].append("其他玩家")
126 result["x"].append(self.players_pos[i][0 + k])
127 result["y"].append(self.players_pos[i][1 + k])
128 for i, mob in enumerate(self.mobs):
129 if not select or select == self.mobs[i]:
130 continue
131 for k in range(0, len(self.mobs_pos[i]), 3):
132 result["Name"].append("其他NPC")
133 result["x"].append(self.mobs_pos[i][0 + k])
134 result["y"].append(self.mobs_pos[i][1 + k])
135 if len(result["Name"]) == 0:
136 x_lim = [-1, 1]
137 y_lim = [-1, 1]
138 result["Name"].append("无")
139 result["x"].append(0)
140 result["y"].append(0)
141 else:
142 x_lim = [-1 + np.min(np.array(result["x"])), np.max(np.array(result["x"])) + 1]
143 y_lim = [-1 + np.min(np.array(result["y"])), np.max(np.array(result["y"])) + 1]
144 if False:
145 x_mean = (x_lim[0] + x_lim[1]) / 2
146 y_mean = (y_lim[0] + y_lim[1]) / 2
147 result["x"].extend([x_lim[0], x_lim[0], x_lim[1], x_lim[1]])
148 result["y"].extend([y_lim[0], y_lim[1], y_lim[0], y_lim[1]])
149 result["Name"].extend(["(边界标记)"] * 4)
150 result["x"] = [x - x_mean for x in result["x"]]
151 result["y"] = [y - y_mean for y in result["y"]]
152 return gr.ScatterPlot(pd.DataFrame(result), "x", "y", color="Name", title="起始站位", visible=True, color_map=None if not select else
153 {desc: "red", "(边界标记)": "white", "其他玩家": "deepskyblue", "其他NPC": "orange"},
154 x_lim = x_lim, y_lim = y_lim)
155
156 def as_example(self):
157 players = [x for x in self.players]
158 mobs = [x for x in self.mobs]
159 players += [""] * max(len(mobs) - len(players), 0)
160 mobs += [""] * max(len(players) - len(mobs), 0)
161 return [self.combat_id, [players, mobs]]
162
163combat_data_dict : "dict[str, CombatData]" = {}
164
165def parse_combat_data(full_text : str):
166 global combat_data_dict
167 combat_data_dict = {}
168
169 lines = full_text.split("\n")
170
171 current = CombatData()
172 recent_summoner = ""
173 recent_caster = ""
174 recent_dest = ""
175 recent_named_effect_apply = {}
176 recent_named_effect_crit = {}
177 recent_named_summon_apply = {}
178 recent_breed_player = {}
179 lines_to_fill_caster = []
180 spell = ""
181
182 is_critic = False
183
184 is_creating = False
185
186 for line_id, line in enumerate(lines):
187 if line.count("CREATION DU COMBAT") > 0:
188 is_creating = True
189 continue
190 if line.count("[_FL_]") > 0:
191 if not is_creating:
192 continue
193
194 res = re.search(r"fightId=([0-9]+) ([^:]+) breed : ([0-9]+) .+ isControlledByAI=(true|false) .+ join the fight at \{Point3 : \(([^,]+), ([^,]+), ([^,]+)\)\}", line)
195 if not res:
196 print("未识别的[_FL_]记录:", line)
197 continue
198 res = res.groups()
199 current.combat_id = res[0]
200 fighter_name = res[1].strip()
201 fighter_breed = res[2]
202 fighter_ismob = res[3] == "true"
203 fighter_position = [int(res[4]), -int(res[5]), int(res[6])]
204
205 is_masque_double = False
206
207 if not fighter_ismob:
208 if fighter_breed not in recent_breed_player.keys():
209 recent_breed_player[fighter_breed] = fighter_name
210 elif recent_breed_player[fighter_breed] != fighter_name:
211 print("另一玩家 {} 具有相同种族({}),无法区分种族被动:".format(recent_breed_player[fighter_breed], fighter_breed), fighter_name)
212 recent_breed_player[fighter_breed] = "Unknown"
213
214 if fighter_ismob and recent_summoner != "":
215
216 if fighter_name == recent_summoner:
217 print("检测到分身/重生类召唤:", recent_summoner)
218 is_masque_double = True
219 else:
220 if fighter_name not in recent_named_summon_apply.keys():
221 recent_named_summon_apply[fighter_name] = recent_summoner
222 elif recent_named_summon_apply[fighter_name] != recent_summoner:
223 print("另一角色 {} 已有同名召唤物,无法区分:".format(recent_named_summon_apply[fighter_name]), recent_summoner, fighter_name)
224 recent_named_summon_apply[fighter_name] = "Unknown"
225
226 desc = "{} ({})".format(fighter_name, recent_named_summon_apply[fighter_name])
227 if recent_summoner in current.players and desc not in current.players_summon:
228 current.players_summon.append(desc)
229 current.players_summon_pos.append(fighter_position)
230 if recent_summoner in current.mobs and desc not in current.mobs_summon:
231 current.mobs_summon.append(desc)
232 current.mobs_summon_pos.append(fighter_position)
233 else:
234 if fighter_ismob:
235 if fighter_name not in current.mobs:
236 current.mobs.append(fighter_name)
237 current.mobs_pos.append(fighter_position)
238 else:
239 index = current.mobs.index(fighter_name)
240 current.mobs_pos[index].extend(fighter_position)
241 else:
242 if fighter_name not in current.players:
243 current.players.append(fighter_name)
244 current.players_pos.append(fighter_position)
245
246 if not is_masque_double:
247 current.players_breed[fighter_name] = fighter_breed
248
249 if line.count("End fight with id") > 0:
250 res = re.search(r"End fight with id ([0-9]+)", line).groups()
251 end_id = res[0]
252
253 if end_id == current.combat_id:
254 is_creating = False
255 combat_data_dict[current.combat_id] = current
256 has_data = lambda name: any(line[0] == name for line in current.data)
257 # 删掉吃干饭的召唤物
258 current.players_summon = [v for v in current.players_summon if has_data(v)]
259 current.mobs_summon = [v for v in current.mobs_summon if has_data(v)]
260
261 current = CombatData()
262 recent_summoner = ""
263 recent_caster = ""
264 recent_named_effect_apply = {}
265 recent_named_effect_crit = {}
266 recent_named_summon_apply = {}
267 recent_breed_player = {}
268 lines_to_fill_caster = []
269 spell = ""
270
271 if not is_creating:
272 continue
273 if contain_with_multi_lang_key(line, "chat.pipeName.gameInformation"):
274 # gamelog分类的聊天消息
275 recent_summoner = ""
276 res = re.search(r".*\[.+\] (.+)", line).groups()
277 line = res[0]
278
279 if contain_any(line, cast_langs):
280 # A cast B型
281 res = re.search(r"(.+)({})(.+)".format("|".join(cast_langs)), line).groups()
282 caster = res[0].strip()
283 spell = res[2].strip().replace("“", "").replace("”", "").replace("」", "").replace("「", "").replace(".", "").strip()
284 is_critic = False
285
286 if caster in recent_named_summon_apply.keys():
287 # 是召唤物施法
288 caster = "{} ({})".format(caster, recent_named_summon_apply[caster])
289
290 for line_to_fill in range(len(lines_to_fill_caster)):
291 lines_to_fill_caster[line_to_fill][0] = caster
292 lines_to_fill_caster = []
293
294 if contain_with_multi_lang_key(spell, "critical"):
295 spell = re.sub(r"\(({})\)".format(get_joined_multi_lang("critical")), "", spell).strip()
296 # print(caster, "释放了暴击的", spell)
297 is_critic = True
298 recent_caster = caster
299 if True or spell in delayed_modifier_langs:
300 # print("施放具名效果:", caster, spell)
301 if spell not in recent_named_effect_apply.keys():
302 recent_named_effect_apply[spell] = caster
303 recent_named_effect_crit[spell] = is_critic
304 elif recent_named_effect_apply[spell] != caster:
305 # print("另一角色 {} 已有具名效果,无法区分:".format(recent_named_effect_apply[spell]), caster, spell)
306 recent_named_effect_apply[spell] = "Unknown"
307 recent_named_effect_crit[spell] = is_critic
308
309 elif contain_any(line, [":"]):
310 # A: B型
311
312 res = re.search(r"(.+)(:)(.+)".format("|".join(cast_langs)), line).groups()
313 caster = res[0].strip()
314 behavior = res[2].strip().replace("“", "").replace("”", "")
315
316 if caster in recent_named_summon_apply.keys():
317 # 主体是召唤物
318 caster = "{} ({})".format(caster, recent_named_summon_apply[caster])
319
320 if contain_any(behavior, summons_langs):
321 while caster in recent_named_summon_apply.keys() and caster != recent_named_summon_apply[caster]:
322 caster = recent_named_summon_apply[caster]
323 # 行动是召唤类
324 recent_summoner = caster
325 for bomb in bomb_langs:
326 if behavior.count(bomb):
327 spell = bomb
328 if spell not in recent_named_effect_apply.keys():
329 recent_named_effect_apply[spell] = caster
330 elif recent_named_effect_apply[spell] != caster:
331 # print("另一角色 {} 已有具名效果,无法区分:".format(recent_named_effect_apply[spell]), caster, spell)
332 recent_named_effect_apply[spell] = "Unknown"
333
334 if contain_any(behavior, addedvalue_langs):
335 # 触发增值时 修正上一个伤害条目
336 if current.data[-1][0] != caster:
337 print("通过增值效果修正间接伤害来源:", caster)
338 current.data[-1][0] = caster
339 current.data[-1][6] = "未知间接伤害"
340
341 if re.match(r".*([0-9]|,)+.*({}).*".format("|".join(hp_langs + armor_langs)), behavior):
342 # 是伤害/治疗/护甲条目
343 modifiers = []
344 is_armor = contain_any(behavior, armor_langs)
345 while True:
346 res = re.search(r"(\(|()([^()()]*)(\)|))", behavior)
347 if not res:
348 break
349 modifiers.append(res.groups()[1])
350 behavior = behavior.replace("".join(res.groups()), "").strip()
351 behavior = re.sub(r"({})".format("|".join(hp_langs)), "", behavior.strip().replace(",", "")).strip()
352 behavior = re.sub(r"({})".format("|".join(armor_langs)), "", behavior.strip().replace(",", "")).strip()
353 damage_element = "Neutral"
354 is_block = False
355 for elem in ["FIRE", "WATER", "EARTH", "AIR"]:
356 if list_contain_exact_multi_lang_key(modifiers, elem):
357 modifiers = [x for x in modifiers if not list_contain_exact_multi_lang_key([x], elem)]
358 damage_element = en_dict[elem]
359 if list_contain_exact_multi_lang_key(modifiers, "exec.block.chat"):
360 is_block = True
361 modifiers = [x for x in modifiers if not list_contain_exact_multi_lang_key([x], "exec.block.chat")]
362 modifiers = [x for x in modifiers if not contain_any(x, light_langs) and not x in neutral_langs and x.strip() != ""]
363
364 value = 0
365 try:
366 value = str(int(behavior)) + " " + ("Armor" if is_armor else "HP")
367 desc = ""
368 known_count = 0
369 source = "Unknown"
370 for x in modifiers:
371 known_count += 1 if x in known_modifier_langs else 0
372
373 if x in pair_lookup.keys():
374 # 异名效果
375 for s in pair_lookup[x]:
376 tmp_source = recent_named_effect_apply.get(s, "Unknown")
377 if tmp_source != "Unknown":
378 source = tmp_source
379 desc = x
380 is_critic = recent_named_effect_crit.get(s, False)
381 break
382 elif x in breed_lookup.keys():
383 # 种族被动
384 for s in breed_lookup[x]:
385 tmp_source = recent_breed_player.get(str(s), "Unknown")
386 if tmp_source != "Unknown":
387 source = tmp_source
388 desc = x
389 is_critic = False
390 break
391 else:
392 # 并非异名效果,直接查找同名技能的施放者
393 processed_source = x
394 tmp_source = recent_named_effect_apply.get(processed_source, "Unknown")
395 if tmp_source != "Unknown":
396 source = tmp_source
397 desc = x
398 is_critic = recent_named_effect_crit.get(processed_source, False)
399
400 if x in direct_modifier_langs:
401 source = recent_caster
402 desc = x
403
404 if x in self_modifier_langs:
405 source = caster
406 desc = x
407
408 if x in burning_modifier_langs:
409 source = "[Burning]"
410 desc = x
411 is_critic = False
412
413 if x in counterattack_modifier_langs:
414 source = recent_dest
415 desc = x
416 is_critic = False
417
418 if x in ignore_modifier_langs:
419 source = "[Ignored]"
420 desc = x
421 is_critic = False
422
423 # if x not in known_modifier_langs: print("未处理的标记", value, source, caster, x)
424 if len(modifiers) == 0:
425 source = recent_caster
426 desc = spell
427 current.add_entry(source, caster, value, damage_element, is_critic, is_block, desc, ",".join(modifiers))
428 if source == "[Burning]":
429 lines_to_fill_caster.append(current.data[-1])
430
431 except Exception as e:
432 #print("伤害转换失败: ", behavior, damage_element, modifiers, e)
433 pass
434 recent_dest = caster
435
436 pass
437 else:
438 pass
439 # print("未识别的记录", line)
440
441def get_samples():
442 result = []
443 for k, v in combat_data_dict.items():
444 result.append(v.as_example())
445
446 result.sort(key=lambda p: float(p[0]), reverse=True)
447 return result
448
449css = '''
450.plr_btn {
451 background: none;
452 min-width: 250px;
453 max-width: 280px;
454 justify-content: left;
455 box-shadow: none;
456 flex-wrap: nowrap;
457}
458
459.plr_btn .button-icon {
460 width: 40px;
461 height: 40px;
462}
463
464.plr_btn_big {
465 background: white;
466 box-shadow: none;
467 flex-wrap: nowrap;
468 justify-content: left;
469 border: 1px solid var(--block-label-border-color);
470}
471
472.plr_btn_big .button-icon {
473 width: 40px;
474 height: 40px;
475}
476
477.sml_label {
478 flex-wrap: nowrap;
479}
480
481.fire_text h2 {
482 --body-text-color: orange;
483}
484.water_text h2 {
485 --body-text-color: deepskyblue;
486}
487.air_text h2 {
488 --body-text-color: violet;
489}
490.earth_text h2 {
491 --body-text-color: yellowgreen;
492}
493.heal_text h2 {
494 --body-text-color: orangered;
495}
496.armor_text h2 {
497 --body-text-color: darkgreen;
498}
499'''
500
501load_theme(False)
502load_asset()
503
504global_current_combat_id = "-"
505
506with gr.Blocks(theme=theme, css=css, title="Logs Analysis -w-") as demo:
507
508 header = gr.Markdown("""
509 # 沃土战斗日志分析 Wakfu Combat Logs Analysis
510
511 version {}
512
513 声明:
514 * 本程序仅根据正常获取的日志内容统计和推测战斗数据,不涉及内存扫描等侵入性探测。
515 * 因为上述原因,聊天记录和游戏日志无法体现的信息无法被统计,如:Buff、同名技能、怪物之间的差异、甚至背刺与否。
516 * 统计数据仅供优化参考,研究归研究,请勿使用该记录出警其他人。
517 <div style="text-align: right;">
518 by 王大喵
519 </div>
520 """.format(version))
521
522 with gr.Row():
523 with gr.Column(scale=2):
524
525
526 with gr.Row(variant="panel"):
527
528 with gr.Tab("从文件上传"):
529 def upload_single_file(file_paths):
530 print(f"Selected directory: {file_paths}")
531 full_text = ""
532 try:
533 file_paths.sort(key=lambda p: os.path.basename(p), reverse=True)
534 print(f"sorted: {file_paths}")
535 for file_path in file_paths:
536 with open(file_path, "r", encoding="utf-8") as f:
537 full_text += f.read() + "\n"
538 parse_combat_data(full_text)
539 return gr.Dataset(samples=get_samples())
540 except Exception as e:
541 full_text = ""
542 print("处理失败", e)
543 return gr.Dataset(samples=[])
544 single_file_comp = gr.File(file_count="multiple", label="日志文件wakfu.log(包含wakfu.log.1、wakfu.log.2)", render=False)
545 gr.Interface(None, single_file_comp, None, clear_btn=None, submit_btn=gr.Button(visible=False))
546 single_file_submit = gr.Button("开始分析", variant="primary", render=True)
547
548 with gr.Column(variant="panel"):
549 md_text = "请选择一场战斗进行分析(底部可以翻页):\n+ 当前选中:{}\n+ 参战玩家:{}\n+ 参战NPC: {}"
550 combat_id_display = gr.Markdown(md_text.format("无", "", ""))
551 table_inputs = [
552 gr.Textbox(label="战斗ID", render=True, interactive=True, max_lines=1, show_label=False, visible=False),
553 gr.Dataframe(label="参战者", visible=False, render=True)
554 ]
555
556 # combat_submit_btn = gr.Button("开始分析", render=True, variant="primary", interactive=True)
557 # combat_seletor = gr.Interface(None, table_inputs, None, clear_btn=None, submit_btn=gr.Button(visible=False), examples=None)
558 test_fight = CombatData()
559 test_fight.players.extend(["玩家1", "玩家2", "玩家3", "玩家4", "玩家5", "玩家6"])
560 test_fight.mobs.extend(["敌人1", "敌人2", "敌人3"])
561 combat_ex = gr.Examples([test_fight.as_example()], table_inputs, None, label="战斗列表")
562
563 # 确认上传
564 single_file_submit.click(upload_single_file, single_file_comp, combat_ex.dataset)
565
566 def update_combat_id_display(combat_id):
567 id_found = "未找到此战斗"
568 players = []
569 mobs = []
570 if combat_id in combat_data_dict.keys():
571 players = combat_data_dict[combat_id].players
572 players = ["{} ({})".format(x, zh_dict.get("breed." + combat_data_dict[combat_id].players_breed.get(x, "-"), "-")) for x in players]
573 mobs = [m if len(p) <= 3 else "{} ({}x)".format(m, len(p) // 3) for m, p in zip(combat_data_dict[combat_id].mobs, combat_data_dict[combat_id].mobs_pos)]
574 id_found = combat_id
575 return gr.Markdown(md_text.format(id_found, ", ".join([x for x in players if len(x) > 0]), ", ".join([x for x in mobs if len(x) > 0])))
576 table_inputs[0].change(update_combat_id_display, table_inputs[0], combat_id_display)
577 single_file_submit.click(update_combat_id_display, table_inputs[0], combat_id_display)
578 # gr.Dataframe(value=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], label="Dataframe", scale=None)
579
580 with gr.Column(scale=4, variant="panel"):
581 player_button_list = []
582 player_tab_list = []
583 npc_button_list = []
584 npc_tab_list = []
585
586 player_overall_label_list = []
587 npc_overall_label_list = []
588
589 player_detail_list = []
590 npc_detail_list = []
591
592 # 标记是否加载了具体页面
593 state = gr.State({})
594 player_detail_loaded = {}
595 npc_detail_loaded = {}
596 last_selected_index = [-1, -1]
597
598 with gr.Tabs() as base_tabs:
599 with gr.Tab("总览"):
600
601 with gr.Column():
602 with gr.Tab("玩家"):
603 with gr.Row():
604 for i in range(18):
605 player_button_list.append(gr.Button(icon="asset/breed/1.png", variant="stop", elem_classes="plr_btn", visible=False))
606 with gr.Column():
607 with gr.Tab("NPC"):
608 with gr.Row():
609 for i in range(18):
610 npc_button_list.append(gr.Button(icon="asset/breed/0.png", variant="stop", elem_classes="plr_btn", visible=False))
611 # gallery_64 = gr.Gallery(value=[asset_breed[1]] * 8, type="pil", label="64", allow_preview=False, elem_id="gallery_64")
612 position_scene = [(None, None, CombatData().get_start_scene())]
613
614 with gr.Row():
615 player_overall_label_list.append(gr.Label(value={}, label="玩家输出 (不含自伤)"))
616 player_overall_label_list.append(gr.Label(value={}, label="玩家承伤"))
617 player_overall_label_list.append(gr.Label(value={}, label="玩家治疗/护盾"))
618
619 with gr.Row():
620 npc_overall_label_list.append(gr.Label(value={}, label="NPC输出"))
621 npc_overall_label_list.append(gr.Label(value={}, label="NPC承伤"))
622 npc_overall_label_list.append(gr.Label(value={}, label="NPC治疗/护盾"))
623
624
625 for current_detail_list in [player_detail_list, npc_detail_list]:
626 with gr.Tab("单角色统计 ({})".format("玩家" if current_detail_list == player_detail_list else "NPC")):
627 with gr.Tabs() as detail_tabs:
628 for i in range(18):
629 gen_fn = lambda i: (lambda: gr.update(selected=i))
630 (player_detail_loaded if current_detail_list == player_detail_list else npc_detail_loaded)[i] = gr.Textbox(value="-", visible=False)
631 player_button_list[i].click(gen_fn(i), None, detail_tabs)
632 player_button_list[i].click(lambda: gr.update(selected=1), None, base_tabs)
633 with gr.Tab("PlayerName", id=i, visible=False) as tab:
634 gen_detail = lambda: {
635 "tab": tab, "btn": None, "Fire": None, "Water": None, "Air": None, "Earth": None,
636 "heal": None, "armor": None, "elements": None, "spells": None,
637 "dd_chart": None, "dd_chart2": None, "heal_chart": None, "heal_chart2": None, "dr_chart2": None, "scene": None
638 }
639 flatten_detail = lambda v: [v["tab"], v["btn"], v["Fire"], v["Water"], v["Air"], v["Earth"], v["heal"], v["armor"], v["elements"], v["spells"],
640 v["dd_chart"], v["dd_chart2"], v["heal_chart"], v["heal_chart2"], v["dr_chart2"], v["scene"]]
641 player_detail = gen_detail()
642 with gr.Row():
643 with gr.Column(scale=2, min_width=180):
644 player_detail["btn"] = gr.Button(icon="asset/breed/1.png", variant="stop", elem_classes="plr_btn_big", visible=True)
645 with gr.Row():
646 with gr.Column(scale=2, min_width=90):
647 player_detail["Fire"] = gr.Label(value=100, label="火系伤害", elem_classes=["sml_label", "fire_text"])
648 player_detail["Air"] = gr.Label(value=100, label="风系伤害", elem_classes=["sml_label", "air_text"])
649 with gr.Column(scale=2, min_width=90):
650 player_detail["Water"] = gr.Label(value=100, label="水系伤害", elem_classes=["sml_label", "water_text"])
651 player_detail["Earth"] = gr.Label(value=100, label="土系伤害", elem_classes=["sml_label", "earth_text"])
652 player_detail["elements"] = gr.Label(value={"Fire": 1, "Water": 0.5, "Air": 0, "Earth": 0.1, "Neutral": 0}, label="元素输出占比", scale=2, elem_classes="sml_label")
653 player_detail["spells"] = gr.Label(value={"Fire": 1, "Water": 0.5, "Air": 0, "Earth": 0.1, "Neutral": 0}, label="技能/效果输出占比", scale=2, elem_classes="sml_label", num_top_classes=5)
654
655 with gr.Row():
656 player_detail["heal"] = gr.Label(value=100, label="输出治疗(含生命偷取)", elem_classes=["sml_label", "heal_text"])
657 player_detail["armor"] = gr.Label(value=100, label="输出护盾", elem_classes=["sml_label", "armor_text"])
658 position_scene.append((i, current_detail_list == player_detail_list, CombatData().get_start_scene()))
659 player_detail["scene"] = position_scene[-1][2]
660
661 color_map = {
662 "Fire": "orange", "Water": "deepskyblue", "Air": "violet", "Earth": "yellowgreen", "Neutral": "grey", "Armor": "darkseagreen", "Heal": "pink",
663 "Fire!": "orangered", "Water!": "cornflowerblue", "Air!": "darkviolet", "Earth!": "green", "Neutral!": "darkgrey",
664 }
665 with gr.Accordion("输出详情"):
666 player_detail["dd_chart"] = gr.BarPlot(value=pd.DataFrame({}), x_title=" ", y_title=" ", x_label_angle=-15,
667 x="技能/效果", y="总伤害", title="技能/效果元素输出", color="元素", color_map=color_map)
668 player_detail["dd_chart2"] = gr.BarPlot(value=pd.DataFrame({}), x_title=" ", y_title=" ", x_label_angle=-15,
669 x="目标", y="总伤害", title="输出目标分配", color="元素", color_map=color_map)
670 # gr.Markdown("+ 暴击使用深色标记(元素类型标记为“!”)\n+ 可能存在部分伤害无法推断伤害来源(标记为Unknown),统计结果并非完全准确\n+ 在“完整伤害记录”一栏可以找到场上来源为Unknown的伤害记录")
671 pass
672 with gr.Accordion("治疗/护盾详情"):
673 player_detail["heal_chart"] = gr.BarPlot(value=pd.DataFrame({}), x_title=" ", y_title=" ", x_label_angle=-15,
674 x="技能/效果", y="总伤害", title="治疗/护盾输出", color="元素", color_map=color_map)
675 player_detail["heal_chart2"] = gr.BarPlot(value=pd.DataFrame({}), x_title=" ", y_title=" ", x_label_angle=-15,
676 x="目标", y="总伤害", title="治疗/护盾目标分配", color="元素", color_map=color_map)
677 pass
678 with gr.Accordion("承伤详情"):
679 player_detail["dr_chart2"] = gr.BarPlot(value=pd.DataFrame({}), x_title=" ", y_title=" ", x_label_angle=-15,
680 x="来源", y="总伤害", title="承受伤害/治疗/护甲来源", color="元素", color_map=color_map)
681 pass
682
683 current_detail_list.append(player_detail)
684
685 with gr.Tab("完整伤害记录"):
686 # gr.Radio(["1", "2"], label="我是…")
687 combat_data_raw = gr.Dataframe(CombatData().get_raw_df(), label="伤害信息", height=1000)
688
689 def combat_submit(id):
690 global global_current_combat_id
691 combat = combat_data_dict.get(id, None)
692 if not combat:
693 combat = CombatData()
694 global_current_combat_id = id
695 return [gr.Dataframe(combat.get_raw_df())] + [combat.get_start_scene()]
696
697 def combat_submit_player_btns(player_button_list, is_player, id):
698 combat = combat_data_dict.get(id, None)
699 if not combat:
700 combat = CombatData()
701 result = []
702 for i in range(len(player_button_list)):
703 result.append(gr.update(visible=False))
704 for i, player in enumerate(combat.players if is_player else combat.mobs):
705 if i >= len(player_button_list):
706 break
707 count = len(combat.players_pos[i] if is_player else combat.mobs_pos[i]) // 3
708 # breed_name = zh_dict.get("breed." + combat.players_breed.get(player, "-"), "")
709 desc = player + (" ({}x)".format(count) if count > 1 else "")
710 icon = "asset/breed/{}.png".format(combat.players_breed.get(player, "0") if is_player else "0")
711 if not os.path.exists(icon):
712 icon = "asset/breed/0.png"
713 result[i] = gr.update(icon=icon, value=desc, visible=True)
714 return result
715
716 def combat_submit_overall_label(label_list, is_player, id):
717 combat = combat_data_dict.get(id, None)
718 if not combat:
719 combat = CombatData()
720 result = []
721 for i in range(len(label_list)):
722 result.append(gr.update(visible=False))
723 df = combat.get_raw_df()
724 total_dd = {}
725 total_dr = {}
726 total_heal = {}
727 for i in range(len(df["伤害"])):
728 A = df["角色"][i]
729 B = df["目标"][i]
730 D = df["伤害"][i].replace("HP", "").replace("Armor", "").strip()
731 D = int(D)
732 if D > 0 and combat.is_team_member(A, is_player):
733 total_heal.setdefault(A, 0)
734 if is_player and A in combat.players_summon:
735 summoner = A.split("(")[1].split(")")[0]
736 total_heal.setdefault(summoner, 0)
737 total_heal[summoner] += D
738 else:
739 total_heal[A] += D
740 if D < 0 and combat.is_team_member(B, is_player):
741 total_dr.setdefault(B, 0)
742 total_dr[B] -= D
743 if D < 0 and combat.is_team_member(A, is_player):
744 total_dd.setdefault(A, 0)
745 if is_player and A in combat.players_summon:
746 summoner = A.split("(")[1].split(")")[0]
747 total_dd.setdefault(summoner, 0)
748 total_dd[summoner] -= D if summoner != B else 0
749 else:
750 total_dd[A] -= D if B != A else 0
751 for i, dict in enumerate([total_dd, total_dr, total_heal]):
752 total_list = [(v, k) for k, v in dict.items()]
753 total_list.sort(reverse=True)
754 total_value = reduce(lambda x, y: x + y, dict.values(), 0)
755 label_dict = {}
756 for k in range(min(5, len(total_list))):
757 desc = total_list[k][1]
758 breed_name = zh_dict.get("breed." + combat.players_breed.get(desc, "-"), "")
759 desc = desc + (" ({})".format(breed_name) if breed_name else "")
760 label_dict[desc] = total_list[k][0] / total_value
761 result[i] = gr.update(value=label_dict)
762 return result
763
764 def select_detail_page(_):
765 return gr.update(value=global_current_combat_id)
766
767 def combat_submit_detail_page(index, is_player, selected, id):
768 global global_current_combat_id
769 if selected:
770 last_selected_index[1 if is_player else 0] = index
771
772 combat = combat_data_dict.get(id, None)
773 if not combat:
774 combat = CombatData()
775 result = gen_detail()
776 players = combat.players if is_player else combat.mobs
777 players_pos = combat.players_pos if is_player else combat.mobs_pos
778 summons = combat.players_summon if is_player else combat.mobs_summon
779 summons_pos = combat.players_summon_pos if is_player else combat.mobs_summon_pos
780 if index >= len(players) + len(summons):
781 return [gr.update(visible=False)] + [gr.update()] * (len(flatten_detail(gen_detail())) - 1)
782 if index >= len(players):
783 index -= len(players)
784 player = summons[index]
785 # if index > 0 and summons[index - 1] == summons[index]: return [gr.update(visible=False)] + [gr.update()] * (len(flatten_detail(gen_detail())) - 1)
786 count = len(summons_pos[index]) // 3
787 # while index + count < len(summons) and summons[index + count] == summons[index]: count += 1
788 desc = player + (" ({}x)".format(count) if count > 1 else "")
789 icon = "asset/breed/0.png"
790 else:
791 player = players[index]
792 # if index > 0 and not is_player and players[index - 1] == players[index]: return [gr.update(visible=False)] + [gr.update()] * (len(flatten_detail(gen_detail())) - 1)
793 count = len(players_pos[index]) // 3
794 # while index + count < len(players) and players[index + count] == players[index]: count += 1
795 desc = player
796 breed_name = zh_dict.get("breed." + combat.players_breed.get(desc, "-"), "")
797 desc = desc + (" ({})".format(breed_name) if breed_name else "")
798 icon = "asset/breed/{}.png".format(combat.players_breed.get(player, "0") if is_player else "0")
799 desc = desc + (" ({}x)".format(count) if count > 1 else "")
800
801 if "btn&tab":
802 if not os.path.exists(icon):
803 icon = "asset/breed/0.png"
804 result["btn"] = gr.update(icon=icon, value=desc, visible=True)
805 result["tab"] = gr.update(visible=True, label=desc)
806
807 if not selected:
808 last = last_selected_index[1 if is_player else 0]
809 if last > len(players) + len(summons):
810 last = 0
811 if index != last:
812 icon = "asset/breed/0.png"
813 result["btn"] = gr.update(icon=icon, value="分析中 (首次加载个人数据)", visible=True)
814 combat = CombatData()
815 # return [result["tab"], result["btn"]] + [gr.update()] * (len(flatten_detail(gen_detail())) - 2)
816
817 sum_damage = lambda x, y: x + max(-y, 0)
818 sum_heal = lambda x, y: x + max(y, 0)
819 sum_dorh = lambda x, y: x + y
820 filter_fn = lambda v: v[0] == player
821 filter_not_self = lambda v: v[0] == player and v[1] != player
822 filter_self = lambda v: v[1] == player
823 if "elements":
824 sum_elements = combat.df_aggregation(fn=sum_damage, groupby=lambda v: v[3], filter_fn=filter_fn)
825 for e in ["Fire", "Water", "Air", "Earth", "Neutral"]:
826 if e in result.keys():
827 result[e] = gr.update(value=sum_elements.get(e, 0))
828 sum_elements = {k: v for k, v in sum_elements.items() if v > 0}
829 total_dmg = max(1, reduce(lambda x, y: x+y, sum_elements.values(), 0))
830 for k in sum_elements.keys():
831 sum_elements[k] /= total_dmg
832 result["elements"] = gr.update(value=sum_elements)
833
834 sum_armor_heal = combat.df_aggregation(fn=sum_heal, groupby=lambda v: v[2].count("HP"), filter_fn=filter_fn)
835 result["armor"] = gr.update(value="+{}".format(sum_armor_heal.get(0, 0)))
836 result["heal"] = gr.update(value="+{}".format(sum_armor_heal.get(1, 0)))
837
838 sum_spells = combat.df_aggregation(fn=sum_damage, groupby=lambda v: v[6], filter_fn=filter_fn)
839 sum_spells = {k: v for k, v in sum_spells.items() if v > 0}
840 for k in sum_spells.keys():
841 sum_spells[k] /= total_dmg
842 result["spells"] = gr.update(value=sum_spells)
843
844 if "dd_chart":
845 sum_elements = combat.df_aggregation(fn=sum_damage, groupby=lambda v: (v[1], v[3], v[4], v[6]), filter_fn=filter_not_self)
846 sum_elements = {k: v for k, v in sum_elements.items() if v > 0}
847 df = {"元素": [], "总伤害": [], "技能/效果": [], "目标": []}
848 for (t, e, c, n), d in sum_elements.items():
849 df["总伤害"].append(d)
850 df["元素"].append(e if not c else e + "!")
851 df["技能/效果"].append(n)
852 df["目标"].append(t)
853 if len(df["元素"]) == 0:
854 df = {"元素": ["Neutral"], "总伤害": [0], "技能/效果": ["无"], "目标": ["无"]}
855 result["dd_chart"] = gr.update(visible=False)
856 result["dd_chart2"] = gr.update(visible=False)
857 else:
858 result["dd_chart"] = gr.update(value=pd.DataFrame(df), visible=True)
859 result["dd_chart2"] = gr.update(value=pd.DataFrame(df), visible=True)
860
861 if "heal_chart":
862 sum_elements = combat.df_aggregation(fn=sum_heal, groupby=lambda v: (v[1], v[3], v[4], v[6], v[2].count("HP")), filter_fn=filter_fn)
863 sum_elements = {k: v for k, v in sum_elements.items() if v > 0}
864 df = {"元素": [], "总伤害": [], "技能/效果": [], "目标": []}
865 for (t, e, c, n, is_heal), d in sum_elements.items():
866 df["总伤害"].append(d)
867 df["元素"].append("Armor" if not is_heal else e if not c else e + "!")
868 df["技能/效果"].append(n)
869 df["目标"].append(t)
870 if len(df["元素"]) == 0:
871 df = {"元素": ["Neutral"], "总伤害": [0], "技能/效果": ["无"], "目标": ["无"]}
872 result["heal_chart"] = gr.update(visible=False)
873 result["heal_chart2"] = gr.update(visible=False)
874 else:
875 result["heal_chart"] = gr.update(value=pd.DataFrame(df), visible=True)
876 result["heal_chart2"] = gr.update(value=pd.DataFrame(df), visible=True)
877
878 if "dr_chart":
879 sum_elements = combat.df_aggregation(fn=sum_dorh, groupby=lambda v: (v[0], v[3], v[4], v[2].count("HP")), filter_fn=filter_self)
880 sum_elements = {k: v for k, v in sum_elements.items() if v != 0}
881 df = {"元素": [], "总伤害": [], "来源": []}
882 for (t, e, c, is_hp), d in sum_elements.items():
883 df["总伤害"].append(abs(d))
884 df["元素"].append("Armor" if d > 0 and not is_hp else "Heal" if d > 0 and is_hp else e if not c else e + "!")
885 df["来源"].append(t)
886 if len(df["元素"]) == 0:
887 df = {"元素": ["Neutral"], "总伤害": [0], "来源": ["无"]}
888 result["dr_chart2"] = gr.update(visible=False)
889 else:
890 result["dr_chart2"] = gr.update(value=pd.DataFrame(df), visible=True)
891
892 def get_selected(i, is_player, combat):
893 if i is None:
894 return None
895 if is_player:
896 return combat.players[i] if i < len(combat.players) else combat.players_summon[i - len(combat.players)] if i < len(combat.players + combat.players_summon) else None
897 else:
898 return combat.mobs[i] if i < len(combat.mobs) else combat.mobs_summon[i - len(combat.mobs)] if i < len(combat.mobs + combat.mobs_summon) else None
899
900 result["scene"] = combat.get_start_scene(select=get_selected(index, is_player, combat)) if combat.combat_id != "Unknown" else gr.update(visible=False)
901
902 return flatten_detail(result)
903
904 # combat_submit_btn.click(combat_submit, table_inputs[0], [combat_data_raw, position_scene])
905 table_inputs[0].change(combat_submit, table_inputs[0], [combat_data_raw] + [position_scene[0][2]])
906 table_inputs[0].change(partial(combat_submit_player_btns, player_button_list, True), table_inputs[0], player_button_list)
907 table_inputs[0].change(partial(combat_submit_player_btns, npc_button_list, False), table_inputs[0], npc_button_list)
908 table_inputs[0].change(partial(combat_submit_overall_label, player_overall_label_list, True), table_inputs[0], player_overall_label_list)
909 table_inputs[0].change(partial(combat_submit_overall_label, npc_overall_label_list, False), table_inputs[0], npc_overall_label_list)
910 for i, detail in enumerate(player_detail_list):
911 table_inputs[0].change(partial(combat_submit_detail_page, i, True, False), table_inputs[0], flatten_detail(detail))
912 detail["tab"].select(select_detail_page, state, player_detail_loaded[i])
913 player_detail_loaded[i].change(partial(combat_submit_detail_page, i, True, True), player_detail_loaded[i], flatten_detail(detail))
914 for i, detail in enumerate(npc_detail_list):
915 table_inputs[0].change(partial(combat_submit_detail_page, i, False, False), table_inputs[0], flatten_detail(detail))
916 detail["tab"].select(select_detail_page, state, npc_detail_loaded[i])
917 npc_detail_loaded[i].change(partial(combat_submit_detail_page, i, False, True), npc_detail_loaded[i], flatten_detail(detail))
918 # detail["tab"].select(partial(combat_submit_detail_page, i, False, True), state, flatten_detail(detail), show_progress="hidden")
919
920
921if __name__ == '__main__':
922 demo.launch(inbrowser=True, share=False, debug=True, max_threads=2)