CoolFace
Apppublic

MoebiusMeow/WCLA

sourceHugging Faceupdated 2y agoView on Hugging Face
1likes
app.py922 linesDownload Raw Back to root
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)