ango commited on
Commit
1cc60af
·
1 Parent(s): d26e0de

5.16 commit

Browse files
.github/workflows/space_sync.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Space
2
+ on:
3
+ push:
4
+ branches: [ "gradio" ]
5
+ workflow_dispatch:
6
+
7
+ jobs:
8
+ sync-to-hub:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v3
12
+ with:
13
+ fetch-depth: 0
14
+ lfs: true
15
+ - name: Push to HuggingFace
16
+ env:
17
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
18
+ run: git push -f https://AngoHF:[email protected]/spaces/AngoHF/Formulator gradio:main
19
+ # - name: Push to ModelScope
20
+ # env:
21
+ # MS_TOKEN: ${{ secrets.MS_TOKEN }}
22
+ # run: git push -f http://oauth2:[email protected]/studios/AngoMS/Formulator.git gradio:master
README.md CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  # Comprehensive Damage Formulator with JCL Parser
2
 
3
  ## User Guide
 
1
+ ---
2
+ title: Formulator
3
+ emoji: 🚀
4
+ colorFrom: yellow
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.31.0
8
+ python_version: 3.11
9
+ app_file: app.py
10
+ license: mit
11
+ ---
12
  # Comprehensive Damage Formulator with JCL Parser
13
 
14
  ## User Guide
base/buff.py CHANGED
@@ -59,7 +59,7 @@ class Buff(BaseBuff):
59
 
60
  @property
61
  def display_name(self):
62
- return f"{self.buff_name}#{self.buff_id}-{self.buff_level}-{self.buff_stack}"
63
 
64
  def value(self, values, stackable):
65
  if isinstance(values, list):
 
59
 
60
  @property
61
  def display_name(self):
62
+ return f"{self.buff_name}#{self.buff_id}-{self.buff_level}"
63
 
64
  def value(self, values, stackable):
65
  if isinstance(values, list):
base/skill.py CHANGED
@@ -221,7 +221,7 @@ class Skill(BaseSkill):
221
  for effect in self.pre_effects:
222
  effect(parser)
223
 
224
- def record(self, critical, parser):
225
  pass
226
 
227
  def post_record(self, parser):
@@ -234,18 +234,18 @@ class Skill(BaseSkill):
234
  for effect in self.post_effects:
235
  effect(parser)
236
 
237
- def parse(self, critical, parser):
238
  self.pre_record(parser)
239
- self.record(critical, parser)
240
  self.post_record(parser)
241
 
242
  def __call__(self, attribute: Attribute):
243
- return 0
244
 
245
 
246
  class DotSkill(Skill):
247
- def record(self, critical, parser):
248
- super().record(critical, parser)
249
  bind_skill = parser.current_school.skills[self.bind_skill]
250
  if not parser.current_dot_ticks[self.bind_skill]:
251
  parser.current_dot_stacks[self.bind_skill] = 0
@@ -253,7 +253,7 @@ class DotSkill(Skill):
253
  parser.current_dot_stacks[self.bind_skill] = min(
254
  parser.current_dot_stacks.get(self.bind_skill, 0) + 1, bind_skill.max_stack
255
  )
256
- parser.current_dot_snapshot[self.bind_skill] = parser.current_buff_stacks.copy()
257
 
258
 
259
  class DotConsumeSkill(Skill):
@@ -262,19 +262,15 @@ class DotConsumeSkill(Skill):
262
  parser.current_next_dot[self.bind_skill] = tick
263
 
264
  def consume_last(self, parser):
265
- if not (last_dot := parser.current_last_dot.pop(self.bind_skill, None)):
266
  return
267
- skill_tuple, status_tuple = last_dot
268
- skill_id, skill_level, skill_stack = skill_tuple
269
- parser.current_dot_ticks[skill_id] += 1
270
- tick = min(parser.current_dot_ticks[skill_id], self.tick)
271
- parser.current_records[(skill_id, skill_level, skill_stack * tick)][status_tuple].append(
272
- parser.current_records[skill_tuple][status_tuple].pop()
273
- )
274
- parser.current_dot_ticks[skill_id] -= tick
275
 
276
- def record(self, critical, parser):
277
- super().record(critical, parser)
278
  if self.last_dot:
279
  self.consume_last(parser)
280
  else:
@@ -282,29 +278,19 @@ class DotConsumeSkill(Skill):
282
 
283
 
284
  class Damage(Skill):
285
- def record(self, critical, parser):
286
- super().record(critical, parser)
287
- skill_stack = parser.current_dot_stacks[self.skill_id]
288
- skill_tuple = (self.skill_id, self.skill_level, skill_stack)
289
- status_tuple = parser.status(self.skill_id)
290
- parser.current_records[skill_tuple][status_tuple].append(
291
- (parser.current_frame - parser.start_frame, critical)
292
- )
293
- return skill_tuple, status_tuple
294
 
295
 
296
  class DotDamage(Damage):
297
- def record(self, critical, parser):
298
- skill_tuple, status_tuple = super().record(critical, parser)
299
-
 
300
  if tick := parser.current_next_dot.pop(self.skill_id, None):
301
- _, _, skill_stack = skill_tuple
302
- parser.current_records[(self.skill_id, self.skill_level, skill_stack * tick)][status_tuple].append(
303
- parser.current_records[skill_tuple][status_tuple].pop()
304
- )
305
  parser.current_dot_ticks[self.skill_id] -= tick
306
  else:
307
- parser.current_last_dot[self.skill_id] = (skill_tuple, status_tuple)
308
  parser.current_dot_ticks[self.skill_id] -= 1
309
 
310
 
@@ -344,7 +330,7 @@ class PetDamage(Damage):
344
 
345
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
346
 
347
- return damage, critical_damage, expected_damage, critical_strike
348
 
349
 
350
  class PureSkill(Skill):
@@ -354,7 +340,7 @@ class PureSkill(Skill):
354
  damage = level_reduction_result(damage, attribute.level_reduction)
355
  damage = vulnerable_result(damage, attribute.vulnerable)
356
 
357
- return damage, damage, damage, 0
358
 
359
 
360
  class PhysicalSkill(Skill):
@@ -390,7 +376,7 @@ class PhysicalSkill(Skill):
390
 
391
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
392
 
393
- return damage, critical_damage, expected_damage, critical_strike
394
 
395
 
396
  class MagicalSkill(Skill):
@@ -426,7 +412,7 @@ class MagicalSkill(Skill):
426
 
427
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
428
 
429
- return damage, critical_damage, expected_damage, critical_strike
430
 
431
 
432
  class AdaptiveSkill(Skill):
@@ -462,7 +448,7 @@ class AdaptiveSkill(Skill):
462
 
463
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
464
 
465
- return damage, critical_damage, expected_damage, critical_strike
466
 
467
 
468
  class PureDamage(PureSkill, Damage):
 
221
  for effect in self.pre_effects:
222
  effect(parser)
223
 
224
+ def record(self, parser):
225
  pass
226
 
227
  def post_record(self, parser):
 
234
  for effect in self.post_effects:
235
  effect(parser)
236
 
237
+ def parse(self, parser):
238
  self.pre_record(parser)
239
+ self.record(parser)
240
  self.post_record(parser)
241
 
242
  def __call__(self, attribute: Attribute):
243
+ return 0, 0, 0., 0
244
 
245
 
246
  class DotSkill(Skill):
247
+ def record(self, parser):
248
+ super().record(parser)
249
  bind_skill = parser.current_school.skills[self.bind_skill]
250
  if not parser.current_dot_ticks[self.bind_skill]:
251
  parser.current_dot_stacks[self.bind_skill] = 0
 
253
  parser.current_dot_stacks[self.bind_skill] = min(
254
  parser.current_dot_stacks.get(self.bind_skill, 0) + 1, bind_skill.max_stack
255
  )
256
+ parser.current_dot_buffs[self.bind_skill] = parser.current_index
257
 
258
 
259
  class DotConsumeSkill(Skill):
 
262
  parser.current_next_dot[self.bind_skill] = tick
263
 
264
  def consume_last(self, parser):
265
+ if not (last_dot_index := parser.current_last_dot.pop(self.bind_skill, None)):
266
  return
267
+ parser.current_dot_ticks[self.bind_skill] += 1
268
+ tick = min(parser.current_dot_ticks[self.bind_skill], self.tick)
269
+ parser.records.current_records[last_dot_index]['skill_stack'] *= tick
270
+ parser.current_dot_ticks[self.bind_skill] -= tick
 
 
 
 
271
 
272
+ def record(self, parser):
273
+ super().record(parser)
274
  if self.last_dot:
275
  self.consume_last(parser)
276
  else:
 
278
 
279
 
280
  class Damage(Skill):
281
+ pass
 
 
 
 
 
 
 
 
282
 
283
 
284
  class DotDamage(Damage):
285
+ def record(self, parser):
286
+ super().record(parser)
287
+ parser.current_record['skill_stack'] = parser.current_dot_stacks[self.skill_id]
288
+ parser.current_record['snapshot_index'] = parser.current_dot_buffs[self.skill_id]
289
  if tick := parser.current_next_dot.pop(self.skill_id, None):
290
+ parser.current_record['skill_stack'] *= tick
 
 
 
291
  parser.current_dot_ticks[self.skill_id] -= tick
292
  else:
293
+ parser.current_last_dot[self.skill_id] = parser.current_index
294
  parser.current_dot_ticks[self.skill_id] -= 1
295
 
296
 
 
330
 
331
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
332
 
333
+ return damage, critical_damage, critical_strike, expected_damage
334
 
335
 
336
  class PureSkill(Skill):
 
340
  damage = level_reduction_result(damage, attribute.level_reduction)
341
  damage = vulnerable_result(damage, attribute.vulnerable)
342
 
343
+ return damage, damage, 0, damage
344
 
345
 
346
  class PhysicalSkill(Skill):
 
376
 
377
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
378
 
379
+ return damage, critical_damage, critical_strike, expected_damage
380
 
381
 
382
  class MagicalSkill(Skill):
 
412
 
413
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
414
 
415
+ return damage, critical_damage, critical_strike, expected_damage
416
 
417
 
418
  class AdaptiveSkill(Skill):
 
448
 
449
  expected_damage = critical_strike * critical_damage + (1 - critical_strike) * damage
450
 
451
+ return damage, critical_damage, critical_strike, expected_damage
452
 
453
 
454
  class PureDamage(PureSkill, Damage):
gr/components/combat.py CHANGED
@@ -13,21 +13,18 @@ class CombatComponent:
13
 
14
  with gr.Tab("属性"):
15
  with gr.Row():
16
- self.init_attribute = gr.Textbox(label="初始属性")
17
- self.final_attribute = gr.Textbox(label="增益后属性")
18
  with gr.Tab("伤害总结"):
19
- self.details = gr.State({})
20
  self.skill_select = gr.Dropdown(label="选择技能")
21
- self.status_select = gr.Dropdown(label="选择增益")
22
  with gr.Row():
23
- self.damage_detail = gr.Textbox(label="伤害细节")
24
- self.damage_gradient = gr.Textbox(label="属性收益")
25
  with gr.Tab("战斗统计"):
26
  with gr.Row():
27
- self.summary = gr.DataFrame(label="战斗总结", headers=["技能/次数", "命中/%", "会心/%", "伤害/%"], scale=3)
 
28
  with gr.Column(scale=1):
29
  self.dps = gr.Textbox(label="每秒伤害")
30
  self.gradient = gr.Textbox(label="属性收益", lines=10)
31
-
32
-
33
-
 
13
 
14
  with gr.Tab("属性"):
15
  with gr.Row():
16
+ self.init_attribute = gr.Textbox(label="初始属性", lines=20)
17
+ self.final_attribute = gr.Textbox(label="增益后属性", lines=20)
18
  with gr.Tab("伤害总结"):
 
19
  self.skill_select = gr.Dropdown(label="选择技能")
20
+ self.status_select = gr.Dropdown(label="选择增益", multiselect=True)
21
  with gr.Row():
22
+ self.damage_detail = gr.Textbox(label="伤害细节", lines=10)
23
+ self.damage_timeline = gr.DataFrame(show_label=False, scale=4)
24
  with gr.Tab("战斗统计"):
25
  with gr.Row():
26
+ self.summary = gr.DataFrame(label="战斗总结", headers=["技能/次数", "命中/%", "会心/%", "伤害/%"],
27
+ show_label=False, scale=3)
28
  with gr.Column(scale=1):
29
  self.dps = gr.Textbox(label="每秒伤害")
30
  self.gradient = gr.Textbox(label="属性收益", lines=10)
 
 
 
gr/components/top.py CHANGED
@@ -4,10 +4,12 @@ import gradio as gr
4
  class TopComponent:
5
  def __init__(self):
6
  with gr.Row():
7
- self.upload_log = gr.UploadButton("上传JCL")
8
  with gr.Column():
9
- self.upload_json = gr.UploadButton("上传JSON")
10
- self.save_json = gr.DownloadButton("保存JSON", visible=False)
11
- with gr.Column(scale=2):
 
 
 
12
  self.player_select = gr.Dropdown(label="选择角色", visible=False)
13
  self.target_select = gr.Dropdown(label="选择目标", visible=False)
 
4
  class TopComponent:
5
  def __init__(self):
6
  with gr.Row():
 
7
  with gr.Column():
8
+ with gr.Row():
9
+ self.upload_log = gr.UploadButton("上传JCL")
10
+ self.upload_json = gr.UploadButton("上传JSON")
11
+ self.copy_json = gr.Textbox(label="复制JSON", show_copy_button=True, max_lines=2)
12
+
13
+ with gr.Column():
14
  self.player_select = gr.Dropdown(label="选择角色", visible=False)
15
  self.target_select = gr.Dropdown(label="选择目标", visible=False)
gr/scripts/combat.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Dict
2
 
3
  from gr.components.combat import CombatComponent
4
  from assets.constant import ATTR_TYPE_TRANSLATE
@@ -8,11 +8,43 @@ from gr.scripts.top import Parser
8
  from gr.scripts.equipments import Equipments
9
  from gr.scripts.recipes import Recipes
10
  from gr.scripts.talents import Talents
11
- from utils.analyzer import analyze_details, Detail
12
 
13
  import gradio as gr
14
 
15
  FULL_SPACE = "\u3000"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  def attribute_content(display_attrs, attribute):
@@ -22,47 +54,57 @@ def attribute_content(display_attrs, attribute):
22
  if isinstance(value, int):
23
  content.append(name.ljust(10, FULL_SPACE) + str(value))
24
  else:
25
- content.append(name.ljust(10, FULL_SPACE) + f"{round(value * 100, 2)}%")
26
  return "\n".join(content)
27
 
28
 
29
- def summary_content(summary: Dict[str, Detail], total_damage):
30
- content = []
31
- for skill in sorted(summary, key=lambda x: summary[x].expected_damage, reverse=True):
32
- detail = summary[skill]
33
- critical = round(detail.critical_count, 2)
34
- critical_rate = round(detail.critical_count / detail.count * 100, 2)
35
- hit = round(detail.count - critical, 2)
36
- hit_rate = round(100 - critical_rate, 2)
37
- damage = round(detail.expected_damage, 2)
38
- damage_rate = round(damage / total_damage * 100, 2)
39
- content.append(
40
- [f"{skill}/{detail.count}",
41
- f"{hit}/{hit_rate}%", f"{critical}/{critical_rate}%", f"{damage}/{damage_rate}%"]
 
 
42
  )
 
43
  return content
44
 
45
 
46
- def gradient_content(gradients, total_damage):
47
- return "\n".join(
48
- ATTR_TYPE_TRANSLATE[k].ljust(10, FULL_SPACE) + f"{round(v / total_damage * 100, 2)}%"
49
- for k, v in gradients.items()
50
- )
 
51
 
52
 
53
- def detail_content(detail: Detail):
54
  damage_detail = "\n".join([
55
- "命中伤害".ljust(10) + f"{round(detail.damage)}",
56
- "会心伤害".ljust(10) + f"{round(detail.critical_damage)}",
57
- "期望伤害".ljust(10) + f"{round(detail.expected_damage)}",
58
- "期望会心".ljust(10) + f"{round(detail.critical_strike * 100, 2)}%",
59
- "实际会心".ljust(10) + f"{round(detail.actual_critical_strike * 100, 2)}%",
60
- "统计数量".ljust(10) + f"{detail.count}"
 
61
  ])
62
 
63
- damage_gradient = gradient_content(detail.gradients, detail.expected_damage)
64
 
65
- return damage_detail, damage_gradient
 
 
 
 
 
66
 
67
 
68
  def combat_script(
@@ -71,9 +113,10 @@ def combat_script(
71
  # consumables: Consumables, bonuses: Bonuses
72
  combat_component: CombatComponent,
73
  ):
74
- def formulate(target_level, duration, skill, status):
 
 
75
  combat_update = {}
76
- record = parser.current_records
77
  school = parser.current_school
78
 
79
  attribute = school.attribute()
@@ -101,59 +144,64 @@ def combat_script(
101
  combat_update[combat_component.final_attribute] = gr.update(
102
  value=attribute_content(school.display_attrs, attribute)
103
  )
104
- total, summary, details = analyze_details(record, duration, attribute, school)
105
 
106
  for gain in gains:
107
  gain.sub(attribute, school.skills, school.buffs)
108
 
109
- combat_update[combat_component.dps] = gr.update(value=round(total.expected_damage / duration))
 
110
 
111
- combat_update[combat_component.gradient] = gradient_content(total.gradients, total.expected_damage)
112
 
113
- combat_update[combat_component.details] = details
114
- combat_update[combat_component.skill_select] = gr.update(choices=list(details))
115
- if skill_detail := details.get(skill, {}):
116
- combat_update[combat_component.status_select] = gr.update(choices=[""] + list(skill_detail))
117
- if detail := skill_detail.get(status):
118
- damage_detail, damage_gradient = detail_content(detail)
119
- combat_update[combat_component.damage_detail] = gr.update(value=damage_detail)
120
- combat_update[combat_component.damage_gradient] = gr.update(value=damage_gradient)
121
- else:
122
- combat_update[combat_component.damage_detail] = gr.update(value="")
123
- combat_update[combat_component.damage_gradient] = gr.update(value="")
 
124
 
125
- combat_update[combat_component.summary] = summary_content(summary, total.expected_damage)
126
  return combat_update
127
 
128
  combat_component.formulate.click(
129
  formulate,
130
- [combat_component.target_level, combat_component.combat_duration,
131
- combat_component.skill_select, combat_component.status_select],
132
  [combat_component.init_attribute, combat_component.final_attribute,
133
  combat_component.dps, combat_component.gradient, combat_component.summary,
134
- combat_component.details, combat_component.skill_select, combat_component.status_select,
135
- combat_component.damage_detail, combat_component.damage_gradient
136
- ]
137
  )
138
 
139
- def skill_changed(skill, details):
140
- if skill not in details:
141
- return None
142
- return gr.update(choices=[""] + list(details[skill]))
 
 
 
 
143
 
144
  combat_component.skill_select.change(
145
- skill_changed, [combat_component.skill_select, combat_component.details], combat_component.status_select
 
146
  )
147
 
148
- def status_changed(skill, status, details):
149
- if skill not in details:
150
- return None, None
151
- if status not in details[skill]:
152
- return None, None
153
- damage_detail, damage_gradient = detail_content(details[skill][status])
154
- return gr.update(value=damage_detail), gr.update(value=damage_gradient)
 
155
 
156
  combat_component.status_select.change(
157
- status_changed, [combat_component.skill_select, combat_component.status_select, combat_component.details],
158
- [combat_component.damage_detail, combat_component.damage_gradient]
159
  )
 
1
+ import pandas as pd
2
 
3
  from gr.components.combat import CombatComponent
4
  from assets.constant import ATTR_TYPE_TRANSLATE
 
8
  from gr.scripts.equipments import Equipments
9
  from gr.scripts.recipes import Recipes
10
  from gr.scripts.talents import Talents
11
+ from utils.analyzer import analyze_records
12
 
13
  import gradio as gr
14
 
15
  FULL_SPACE = "\u3000"
16
+ BUFF_CONCAT = ":"
17
+
18
+
19
+ class Detail:
20
+ records: pd.DataFrame = pd.DataFrame()
21
+
22
+ skill: str = ""
23
+ status_set: list = []
24
+
25
+ @property
26
+ def current_records(self):
27
+ records = self.records
28
+ if self.skill:
29
+ records = records[records.skill == self.skill]
30
+ if self.status_set:
31
+ for buff in self.status_set:
32
+ column, stack = buff.split(BUFF_CONCAT)
33
+ records = records[records[column] == int(stack)]
34
+ return records
35
+
36
+ @property
37
+ def available_status(self):
38
+ status_set = []
39
+ records = self.current_records
40
+ for column in records.columns:
41
+ if "#" not in column:
42
+ continue
43
+ for stack in records[column].unique():
44
+ if not stack:
45
+ continue
46
+ status_set.append(f"{column}{BUFF_CONCAT}{stack:.0f}")
47
+ return status_set
48
 
49
 
50
  def attribute_content(display_attrs, attribute):
 
54
  if isinstance(value, int):
55
  content.append(name.ljust(10, FULL_SPACE) + str(value))
56
  else:
57
+ content.append(name.ljust(10, FULL_SPACE) + f"{value * 100:.2f}%")
58
  return "\n".join(content)
59
 
60
 
61
+ def summary_content(records, total_damage):
62
+ content = {}
63
+ group_records = records.groupby("skill_name")
64
+ sum_expected_damage = group_records.expected_damage.sum()
65
+ mean_critical_strike = group_records.critical_strike.mean()
66
+ for skill_name, count in group_records.skill_name.count().items():
67
+ critical_strike = mean_critical_strike[skill_name]
68
+ critical_count = critical_strike * count
69
+ hit = 1 - critical_strike
70
+ hit_count = count - critical_strike
71
+ expected_damage = sum_expected_damage[skill_name]
72
+ content[expected_damage] = (
73
+ [f"{skill_name}/{count}",
74
+ f"{hit_count:.2f}/{hit * 100:.2f}%", f"{critical_count:.2f}/{critical_strike * 100:.2f}%",
75
+ f"{expected_damage:.2f}/{expected_damage / total_damage:.2f}%"]
76
  )
77
+ content = [content[k] for k in sorted(content, reverse=True)]
78
  return content
79
 
80
 
81
+ def gradient_content(records, attribute, total_damage):
82
+ content = []
83
+ for attr in attribute.grad_attrs:
84
+ gradient = records[attr].sum() / total_damage - 1
85
+ content.append(ATTR_TYPE_TRANSLATE[attr].ljust(10, FULL_SPACE) + f"{gradient * 100:.2f}")
86
+ return "\n".join(content)
87
 
88
 
89
+ def detail_content(records):
90
  damage_detail = "\n".join([
91
+ "统计数量".ljust(10) + f"{len(records)}",
92
+ "命中伤害".ljust(10) + f"{records.damage.mean():.0f}",
93
+ "会心伤害".ljust(10) + f"{records.critical_damage.mean():.0f}",
94
+ "期望伤害".ljust(10) + f"{records.expected_damage.mean():.0f}",
95
+ "实际伤害".ljust(10) + f"{records.actual_damage.mean():.0f}",
96
+ "期望会心".ljust(10) + f"{records.critical_strike.mean() * 100:.2f}%",
97
+ "实际会心".ljust(10) + f"{records.actual_critical_strike.mean() * 100:.2f}%"
98
  ])
99
 
100
+ # damage_gradient = gradient_content(records, attribute, records.expected_damage.sum())
101
 
102
+ return damage_detail
103
+
104
+
105
+ def timeline_content(records):
106
+ columns = [column for column in records.columns if "#" in column]
107
+ return records[["time", "skill"] + columns].rename(columns={"time": "时间", "skill": "技能"})
108
 
109
 
110
  def combat_script(
 
113
  # consumables: Consumables, bonuses: Bonuses
114
  combat_component: CombatComponent,
115
  ):
116
+ detail = Detail()
117
+
118
+ def formulate(target_level, duration):
119
  combat_update = {}
 
120
  school = parser.current_school
121
 
122
  attribute = school.attribute()
 
144
  combat_update[combat_component.final_attribute] = gr.update(
145
  value=attribute_content(school.display_attrs, attribute)
146
  )
147
+ records = analyze_records(parser, duration, attribute)
148
 
149
  for gain in gains:
150
  gain.sub(attribute, school.skills, school.buffs)
151
 
152
+ total_damage = records.expected_damage.sum()
153
+ combat_update[combat_component.dps] = gr.update(value=f"{total_damage / duration:.0f}")
154
 
155
+ combat_update[combat_component.gradient] = gradient_content(records, attribute, total_damage)
156
 
157
+ combat_update[combat_component.summary] = summary_content(records, total_damage)
158
+
159
+ detail.records = records
160
+
161
+ combat_update[combat_component.skill_select] = gr.update(choices=list(records.skill.unique()))
162
+ if detail.current_records.empty:
163
+ detail.status_set = []
164
+ records = detail.current_records
165
+
166
+ combat_update[combat_component.status_select] = gr.update(choices=detail.available_status)
167
+ combat_update[combat_component.damage_detail] = gr.update(value=detail_content(records), visible=True)
168
+ combat_update[combat_component.damage_timeline] = gr.update(value=timeline_content(records), visible=True)
169
 
 
170
  return combat_update
171
 
172
  combat_component.formulate.click(
173
  formulate,
174
+ [combat_component.target_level, combat_component.combat_duration],
 
175
  [combat_component.init_attribute, combat_component.final_attribute,
176
  combat_component.dps, combat_component.gradient, combat_component.summary,
177
+ combat_component.skill_select, combat_component.status_select,
178
+ combat_component.damage_detail, combat_component.damage_timeline]
 
179
  )
180
 
181
+ def skill_changed(skill):
182
+ detail.skill = skill
183
+
184
+ if detail.current_records.empty:
185
+ detail.status_set = []
186
+
187
+ records = detail.current_records
188
+ return gr.update(choices=detail.available_status), detail_content(records), timeline_content(records)
189
 
190
  combat_component.skill_select.change(
191
+ skill_changed, combat_component.skill_select,
192
+ [combat_component.status_select, combat_component.damage_detail, combat_component.damage_timeline]
193
  )
194
 
195
+ def status_changed(status_set):
196
+ detail.status_set = status_set
197
+ records = detail.current_records
198
+
199
+ if records.empty:
200
+ return gr.update(visible=False), gr.update(visible=False)
201
+ return (gr.update(value=detail_content(records), visible=True),
202
+ gr.update(value=timeline_content(records), visible=True))
203
 
204
  combat_component.status_select.change(
205
+ status_changed, combat_component.status_select,
206
+ [combat_component.damage_detail, combat_component.damage_timeline]
207
  )
gr/scripts/top.py CHANGED
@@ -1,5 +1,4 @@
1
  import json
2
- import os
3
 
4
  import gradio as gr
5
 
@@ -37,9 +36,8 @@ def top_script(
37
  select_talents=parser.select_talents,
38
  select_equipments=parser.select_equipments,
39
  )
40
- file_name = parser.file_name.split(".jcl")[0] + ".json"
41
- json.dump(result, open(file_name, "w", encoding="utf-8"), ensure_ascii=False)
42
- return file_name
43
 
44
  def upload_log(file_path):
45
  if not file_path:
@@ -47,38 +45,35 @@ def top_script(
47
  parser(file_path)
48
  players = [parser.id2name[player_id] for player_id in parser.players]
49
  player_select_update = gr.update(choices=players, value=players[0], visible=True)
50
- json_link = f"/file={save_json()}"
51
- return player_select_update, gr.update(visible=True), gr.update(visible=True), gr.update(value=json_link)
52
 
53
  top_component.upload_log.upload(
54
  upload_log, top_component.upload_log,
55
- [top_component.player_select, top_component.save_json, bottom_component, top_component.save_json]
56
  )
57
 
58
- def load_json(file_path):
59
- if not file_path:
60
- return [None] * 4
61
- result = json.load(open(file_path, encoding="utf-8"))
62
-
63
- file_name = os.path.basename(result['file_name']).split(".jcl")[0] + ".json"
64
- json.dump(result, open(file_name, "w", encoding="utf-8"), ensure_ascii=False)
65
-
66
- result['records'] = unserialize(result['records'])
67
- for player_id, school_id in result['players'].items():
68
- result['players'][player_id] = SUPPORT_SCHOOL[school_id]
69
- for k, v in result.items():
70
- setattr(parser, k, v)
71
-
72
- json_link = f"/file={save_json()}"
73
-
74
- players = [parser.id2name[player_id] for player_id in parser.players]
75
- player_select_update = gr.update(choices=players, value=players[0], visible=True)
76
- return player_select_update, gr.update(visible=True), gr.update(visible=True), gr.update(value=json_link)
77
-
78
- top_component.upload_json.upload(
79
- load_json, top_component.upload_json,
80
- [top_component.player_select, top_component.save_json, bottom_component, top_component.save_json]
81
- )
82
 
83
  def player_select(player_name):
84
  if not player_name:
@@ -89,9 +84,9 @@ def top_script(
89
 
90
  top_update = {
91
  top_component.target_select: gr.update(
92
- choices=[""] + [parser.id2name[target_id] for target_id in parser.current_targets],
93
  ),
94
- combat_component.combat_duration: gr.update(value=parser.duration)
95
  }
96
 
97
  # """ Update config """
@@ -161,11 +156,11 @@ def top_script(
161
  player_select, top_component.player_select,
162
  sum([[e.equipment, e.enchant, e.strength_level, *e.embed_levels] for e in equipments_component.values()], []) +
163
  talents_component.talents + recipes_component.recipes +
164
- [combat_component.combat_duration] + [top_component.target_select]
165
  )
166
 
167
  def target_select(target_name):
168
- target_id = parser.name2id.get(target_name, "")
169
  parser.current_target = target_id
170
 
171
  top_component.target_select.change(
 
1
  import json
 
2
 
3
  import gradio as gr
4
 
 
36
  select_talents=parser.select_talents,
37
  select_equipments=parser.select_equipments,
38
  )
39
+ result = json.dumps(result, ensure_ascii=False)
40
+ return result
 
41
 
42
  def upload_log(file_path):
43
  if not file_path:
 
45
  parser(file_path)
46
  players = [parser.id2name[player_id] for player_id in parser.players]
47
  player_select_update = gr.update(choices=players, value=players[0], visible=True)
48
+ json_copy_update = gr.update(value=save_json(), visible=True)
49
+ return player_select_update, json_copy_update, gr.update(visible=True)
50
 
51
  top_component.upload_log.upload(
52
  upload_log, top_component.upload_log,
53
+ [top_component.player_select, top_component.copy_json, bottom_component], show_progress="full"
54
  )
55
 
56
+ # def load_json(file_path):
57
+ # if not file_path:
58
+ # return [None] * 4
59
+ # result = json.load(open(file_path, encoding="utf-8"))
60
+ #
61
+ # result['records'] = unserialize(result['records'])
62
+ # for player_id, school_id in result['players'].items():
63
+ # result['players'][player_id] = SUPPORT_SCHOOL[school_id]
64
+ # for k, v in result.items():
65
+ # setattr(parser, k, v)
66
+ #
67
+ # json_link = f"/file={save_json()}"
68
+ #
69
+ # players = [parser.id2name[player_id] for player_id in parser.players]
70
+ # player_select_update = gr.update(choices=players, value=players[0], visible=True)
71
+ # return player_select_update, gr.update(visible=True), gr.update(visible=True), gr.update(value=json_link)
72
+ #
73
+ # top_component.upload_json.upload(
74
+ # load_json, top_component.upload_json,
75
+ # [top_component.player_select, top_component.save_json, bottom_component, top_component.save_json]
76
+ # )
 
 
 
77
 
78
  def player_select(player_name):
79
  if not player_name:
 
84
 
85
  top_update = {
86
  top_component.target_select: gr.update(
87
+ choices=[""] + [parser.id2name[target_id] for target_id in parser.current_targets], visible=True
88
  ),
89
+ combat_component.combat_duration: gr.update(value=parser.duration, maximum=parser.duration)
90
  }
91
 
92
  # """ Update config """
 
156
  player_select, top_component.player_select,
157
  sum([[e.equipment, e.enchant, e.strength_level, *e.embed_levels] for e in equipments_component.values()], []) +
158
  talents_component.talents + recipes_component.recipes +
159
+ [combat_component.combat_duration] + [top_component.target_select],
160
  )
161
 
162
  def target_select(target_name):
163
+ target_id = parser.name2id.get(target_name)
164
  parser.current_target = target_id
165
 
166
  top_component.target_select.change(
parse_new_school.py CHANGED
@@ -1,6 +1,6 @@
1
  import json
2
 
3
- from utils.lua import parse
4
 
5
 
6
  class Parser:
@@ -39,7 +39,7 @@ class Parser:
39
  for line in lines:
40
  row = line.split("\t")
41
  if row[4] == "4":
42
- detail = parse(row[-1])
43
  self.school_id = int(detail[3])
44
  if isinstance(detail, list):
45
  self.talents = self.parse_talents(detail[6])
 
1
  import json
2
 
3
+ from utils.lua import parse_player
4
 
5
 
6
  class Parser:
 
39
  for line in lines:
40
  row = line.split("\t")
41
  if row[4] == "4":
42
+ detail = parse_player(row[-1])
43
  self.school_id = int(detail[3])
44
  if isinstance(detail, list):
45
  self.talents = self.parse_talents(detail[6])
parse_skill.py CHANGED
@@ -1,8 +1,8 @@
1
- from utils.lua import parse
2
  lua = """
3
 
4
  """
5
- result = parse(lua)
6
  damage_base = [row['nDamageBase'] for row in result]
7
  print(f'"damage_base": {damage_base},')
8
  damage_rand = [row['nDamageRand'] for row in result]
 
1
+ from utils.lua import parse_player
2
  lua = """
3
 
4
  """
5
+ result = parse_player(lua)
6
  damage_base = [row['nDamageBase'] for row in result]
7
  print(f'"damage_base": {damage_base},')
8
  damage_rand = [row['nDamageRand'] for row in result]
schools/ao_xue_zhan_yi/__init__.py CHANGED
@@ -7,4 +7,4 @@ from schools.ao_xue_zhan_yi.attribute import AoXueZhanYi
7
 
8
 
9
  def prepare(self, player_id):
10
- self.buff_stacks[player_id][(-1, 1)] = 5
 
7
 
8
 
9
  def prepare(self, player_id):
10
+ self.buffs[player_id][(-1, 1)] = 5
schools/ao_xue_zhan_yi/skills.py CHANGED
@@ -9,7 +9,7 @@ class 战意判定(Skill):
9
  bind_buff = -1
10
 
11
  def record(self, critical, parser):
12
- if buff_level := parser.current_buff_stacks.get((self.bind_buff, 1)):
13
  parser.refresh_buff(self.final_buff, buff_level)
14
 
15
 
 
9
  bind_buff = -1
10
 
11
  def record(self, critical, parser):
12
+ if buff_level := parser.current_buffs.get((self.bind_buff, 1)):
13
  parser.refresh_buff(self.final_buff, buff_level)
14
 
15
 
schools/ao_xue_zhan_yi/talents.py CHANGED
@@ -64,7 +64,7 @@ class 骁勇(Gain):
64
  class 虎贲(Gain):
65
  @staticmethod
66
  def effect(parser):
67
- if parser.current_buff_stacks.get((-28169, 1)) == 3:
68
  parser.refresh_buff(-1, 1, 3)
69
  parser.refresh_buff(-28169, 1)
70
 
 
64
  class 虎贲(Gain):
65
  @staticmethod
66
  def effect(parser):
67
+ if parser.current_buffs.get((-28169, 1)) == 3:
68
  parser.refresh_buff(-1, 1, 3)
69
  parser.refresh_buff(-28169, 1)
70
 
schools/bing_xin_jue/__init__.py CHANGED
@@ -7,5 +7,5 @@ from schools.bing_xin_jue.attribute import BingXinJue
7
 
8
 
9
  def prepare(self, player_id):
10
- self.buff_stacks[player_id][(409, 21)] = 10
11
- self.buff_stacks[player_id][(17969, 1)] = 1
 
7
 
8
 
9
  def prepare(self, player_id):
10
+ self.buffs[player_id][(409, 21)] = 10
11
+ self.buffs[player_id][(17969, 1)] = 1
schools/du_jing/skills.py CHANGED
@@ -11,7 +11,7 @@ class 灵蛇引(Skill):
11
  def record(self, critical, parser):
12
  super().record(critical, parser)
13
  pet_buffs = {(bind_buff, 1): 1 for bind_buff in self.bind_buffs}
14
- parser.current_next_pet_buff_stacks.append(pet_buffs)
15
 
16
 
17
  SKILLS: Dict[int, Skill | dict] = {
 
11
  def record(self, critical, parser):
12
  super().record(critical, parser)
13
  pet_buffs = {(bind_buff, 1): 1 for bind_buff in self.bind_buffs}
14
+ parser.current_next_pet_buffs.append(pet_buffs)
15
 
16
 
17
  SKILLS: Dict[int, Skill | dict] = {
schools/gu_feng_jue/skills.py CHANGED
@@ -17,7 +17,7 @@ class 横刀断浪流血(Skill):
17
  parser.refresh_target_buff(bind_buff, self.stack, 1)
18
  parser.current_dot_ticks[self.bind_skill] = bind_skill.tick
19
  parser.current_dot_stacks[self.bind_skill] = self.stack
20
- parser.current_dot_snapshot[self.bind_skill] = parser.current_buff_stacks.copy()
21
 
22
 
23
  SKILLS: Dict[int, Skill | dict] = {
 
17
  parser.refresh_target_buff(bind_buff, self.stack, 1)
18
  parser.current_dot_ticks[self.bind_skill] = bind_skill.tick
19
  parser.current_dot_stacks[self.bind_skill] = self.stack
20
+ parser.current_dot_buffs[self.bind_skill] = parser.current_buffs.copy()
21
 
22
 
23
  SKILLS: Dict[int, Skill | dict] = {
schools/ling_hai_jue/buffs.py CHANGED
@@ -55,13 +55,13 @@ BUFFS = {
55
  }
56
  }
57
  },
58
- 14029: {
59
- "buff_name": "神降",
60
- "activate": False,
61
- "gain_attributes": {
62
- "all_damage_addition": 102
63
- }
64
- }
65
  }
66
 
67
  for buff_id, detail in BUFFS.items():
 
55
  }
56
  }
57
  },
58
+ # 14029: {
59
+ # "buff_name": "神降",
60
+ # "activate": False,
61
+ # "gain_attributes": {
62
+ # "all_damage_addition": 102
63
+ # }
64
+ # }
65
  }
66
 
67
  for buff_id, detail in BUFFS.items():
schools/tai_xu_jian_yi/__init__.py CHANGED
@@ -7,4 +7,4 @@ from schools.tai_xu_jian_yi.attribute import TaiXuJianYi
7
 
8
 
9
  def prepare(self, player_id):
10
- self.buff_stacks[player_id][(9949, 1)] = 3
 
7
 
8
 
9
  def prepare(self, player_id):
10
+ self.buffs[player_id][(9949, 1)] = 3
schools/yi_jin_jing/__init__.py CHANGED
@@ -7,4 +7,4 @@ from schools.yi_jin_jing.attribute import YiJinJing
7
 
8
 
9
  def prepare(self, player_id):
10
- self.buff_stacks[player_id][(10023, 1)] = 1
 
7
 
8
 
9
  def prepare(self, player_id):
10
+ self.buffs[player_id][(10023, 1)] = 1
schools/yi_jin_jing/skills.py CHANGED
@@ -9,8 +9,8 @@ class 明法判定(Skill):
9
  bind_buff = 890
10
 
11
  def record(self, critical, parser):
12
- if buff_level := parser.current_target_buff_stacks.get((self.bind_buff, 1)):
13
- parser.current_target_buff_stacks[(self.final_buff, buff_level)] = 1
14
 
15
 
16
  class 明法移除(Skill):
@@ -18,9 +18,9 @@ class 明法移除(Skill):
18
  bind_buff = 890
19
 
20
  def record(self, critical, parser):
21
- buff_level = parser.current_target_buff_stacks.get((self.bind_buff, 1), 0)
22
  for level in range(buff_level):
23
- parser.current_target_buff_stacks.pop((self.final_buff, level + 1), None)
24
 
25
 
26
  SKILLS: Dict[int, Skill | dict] = {
 
9
  bind_buff = 890
10
 
11
  def record(self, critical, parser):
12
+ if buff_level := parser.current_target_buffs.get((self.bind_buff, 1)):
13
+ parser.current_target_buffs[(self.final_buff, buff_level)] = 1
14
 
15
 
16
  class 明法移除(Skill):
 
18
  bind_buff = 890
19
 
20
  def record(self, critical, parser):
21
+ buff_level = parser.current_target_buffs.get((self.bind_buff, 1), 0)
22
  for level in range(buff_level):
23
+ parser.current_target_buffs.pop((self.final_buff, level + 1), None)
24
 
25
 
26
  SKILLS: Dict[int, Skill | dict] = {
schools/zi_xia_gong/__init__.py CHANGED
@@ -7,4 +7,4 @@ from schools.zi_xia_gong.attribute import ZiXiaGong
7
 
8
 
9
  def prepare(self, player_id):
10
- self.buff_stacks[player_id][(17918, 1)] = 1
 
7
 
8
 
9
  def prepare(self, player_id):
10
+ self.buffs[player_id][(17918, 1)] = 1
utils/analyzer.py CHANGED
@@ -1,76 +1,40 @@
1
- from dataclasses import dataclass
2
- from collections import defaultdict
3
- from typing import Dict
4
 
5
  from base.attribute import Attribute
6
- from base.constant import FRAME_PER_SECOND
7
  from base.skill import Skill, DotDamage, NpcDamage, PetDamage
8
- from utils.parser import School
9
 
 
10
 
11
- @dataclass
12
- class Detail:
13
- damage: int = 0
14
- critical_damage: int = 0
15
- expected_damage: float = 0.
16
- critical_strike: float = 0.
17
 
18
- gradients: Dict[str, float] = None
19
-
20
- critical_count: int = 0
21
- count: int = 0
22
-
23
- def __post_init__(self):
24
- self.gradients = defaultdict(float)
25
-
26
- @property
27
- def actual_critical_strike(self):
28
- if self.count:
29
- return self.critical_count / self.count
30
- return 0
31
-
32
-
33
- def filter_status(status, school: School, skill_id):
34
  buffs = []
35
  for buff_id, buff_level, buff_stack in status:
36
  buff = school.buffs[buff_id]
37
- if not buff.activate:
38
- continue
39
- buff.buff_level, buff.buff_stack = buff_level, buff_stack
40
- if buff.gain_attributes:
41
- buffs.append(buff)
42
- elif skill_id in buff.gain_skills:
43
  buffs.append(buff)
44
 
45
- return tuple(sorted(buffs, key=lambda x: x.buff_id))
46
 
47
 
48
  def add_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute, skill: Skill):
49
- final_buffs = []
50
  if not snapshot_buffs:
51
  for buff in current_buffs:
52
  buff.add_all(attribute, skill)
53
- final_buffs.append(buff)
54
  elif isinstance(skill, DotDamage):
55
  for buff in snapshot_buffs:
56
- if buff.add_dot(attribute, skill, True):
57
- final_buffs.append(buff)
58
  for buff in current_buffs:
59
- if buff.add_dot(attribute, skill, False):
60
- final_buffs.append(buff)
61
  elif isinstance(skill, NpcDamage):
62
  for buff in snapshot_buffs:
63
  buff.add_all(attribute, skill)
64
- final_buffs.append(buff)
65
  elif isinstance(skill, PetDamage):
66
  for buff in snapshot_buffs:
67
  buff.add_all(attribute, skill)
68
- final_buffs.append(buff)
69
  for buff in target_buffs:
70
  buff.add_all(attribute, skill)
71
 
72
- return final_buffs + list(target_buffs)
73
-
74
 
75
  def sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute, skill: Skill):
76
  if not snapshot_buffs:
@@ -91,85 +55,50 @@ def sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute,
91
  buff.sub_all(attribute, skill)
92
 
93
 
94
- def concat_buffs(buffs):
95
- buffs = ",".join(buff.display_name for buff in buffs)
96
- if not buffs:
97
- buffs = "~"
98
- return buffs
99
-
 
100
 
101
- def analyze_details(record, duration: int, attribute: Attribute, school: School):
102
- total = Detail()
103
- details = {}
104
- summary = {}
105
- duration *= FRAME_PER_SECOND
106
 
107
- for skill, status in record.items():
108
- skill_id, skill_level, skill_stack = skill
109
  skill: Skill = school.skills[skill_id]
110
- if not skill.activate:
111
- continue
112
  skill.skill_level, skill.skill_stack = skill_level, skill_stack
113
- skill_name = skill.skill_name
114
-
115
- skill_detail = details[skill.display_name] = {}
116
- if not (skill_summary := summary.get(skill_name)):
117
- skill_summary = summary[skill_name] = Detail()
118
- skill_total = skill_detail[""] = Detail()
119
- for (current_status, snapshot_status, target_status), timeline in status.items():
120
- if not (timeline := [t for t in timeline if t[0] < duration]):
121
- continue
122
- critical_timeline = [t for t in timeline if t[1]]
123
-
124
- current_buffs = filter_status(current_status, school, skill_id)
125
- snapshot_buffs = filter_status(snapshot_status, school, skill_id)
126
- target_buffs = filter_status(target_status, school, skill_id)
127
-
128
- buffs = add_buffs(current_buffs, snapshot_buffs, target_buffs, attribute, skill)
129
- buffs = concat_buffs(buffs)
130
- if buffs in skill_detail:
131
- detail = skill_detail[buffs]
132
- else:
133
- detail = skill_detail[buffs] = Detail(*skill(attribute))
134
- detail.gradients = analyze_gradients(skill, attribute)
135
- sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute, skill)
136
-
137
- detail.critical_count += len(critical_timeline)
138
- detail.count += len(timeline)
139
- skill_total.critical_count += len(critical_timeline)
140
- skill_total.count += len(timeline)
141
-
142
- skill_total.damage += detail.damage * len(timeline)
143
- skill_total.critical_damage += detail.critical_damage * len(timeline)
144
- skill_total.expected_damage += detail.expected_damage * len(timeline)
145
- skill_total.critical_strike += detail.critical_strike * len(timeline)
146
- for attr, residual_damage in detail.gradients.items():
147
- skill_total.gradients[attr] += residual_damage * len(timeline)
148
-
149
- if skill_total.count:
150
- total.expected_damage += skill_total.expected_damage
151
- skill_summary.expected_damage += skill_total.expected_damage
152
- skill_summary.critical_count += skill_total.critical_strike
153
- skill_summary.count += skill_total.count
154
- skill_total.damage /= skill_total.count
155
- skill_total.critical_damage /= skill_total.count
156
- skill_total.expected_damage /= skill_total.count
157
- skill_total.critical_strike /= skill_total.count
158
- for attr, residual_damage in skill_total.gradients.items():
159
- total.gradients[attr] += residual_damage
160
- skill_total.gradients[attr] /= skill_total.count
161
  else:
162
- details.pop(skill.display_name)
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- summary = {skill: detail for skill, detail in summary.items() if detail.count}
165
- return total, summary, details
166
 
167
 
168
  def analyze_gradients(skill, attribute):
169
- results = {}
170
  for attr, value in attribute.grad_attrs.items():
171
  origin_value = getattr(attribute, attr)
172
  setattr(attribute, attr, origin_value + value)
173
- _, _, results[attr], _ = skill(attribute)
 
174
  setattr(attribute, attr, origin_value)
175
  return results
 
1
+ import pandas as pd
 
 
2
 
3
  from base.attribute import Attribute
 
4
  from base.skill import Skill, DotDamage, NpcDamage, PetDamage
5
+ from utils.parser import School, Parser, CALCULATE_COLUMNS
6
 
7
+ DAMAGE_COLUMNS = ["damage", "critical_damage", "critical_strike", "expected_damage"]
8
 
 
 
 
 
 
 
9
 
10
+ def filter_status(status, school: School):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  buffs = []
12
  for buff_id, buff_level, buff_stack in status:
13
  buff = school.buffs[buff_id]
14
+ if buff.activate:
 
 
 
 
 
15
  buffs.append(buff)
16
 
17
+ return buffs
18
 
19
 
20
  def add_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute, skill: Skill):
 
21
  if not snapshot_buffs:
22
  for buff in current_buffs:
23
  buff.add_all(attribute, skill)
 
24
  elif isinstance(skill, DotDamage):
25
  for buff in snapshot_buffs:
26
+ buff.add_dot(attribute, skill, True)
 
27
  for buff in current_buffs:
28
+ buff.add_dot(attribute, skill, False)
 
29
  elif isinstance(skill, NpcDamage):
30
  for buff in snapshot_buffs:
31
  buff.add_all(attribute, skill)
 
32
  elif isinstance(skill, PetDamage):
33
  for buff in snapshot_buffs:
34
  buff.add_all(attribute, skill)
 
35
  for buff in target_buffs:
36
  buff.add_all(attribute, skill)
37
 
 
 
38
 
39
  def sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute: Attribute, skill: Skill):
40
  if not snapshot_buffs:
 
55
  buff.sub_all(attribute, skill)
56
 
57
 
58
+ def analyze_records(parser: Parser, duration: int, attribute: Attribute):
59
+ records: pd.DataFrame = parser.current_records
60
+ school = parser.current_school
61
+ condition = (records.player_id == parser.current_player) & (records.time < duration)
62
+ if parser.current_target:
63
+ condition = condition & (records.target_id == parser.current_target)
64
+ records = records[condition].copy()
65
 
66
+ damage_columns = [(0 for _ in DAMAGE_COLUMNS)] * len(records)
67
+ grad_attrs = list(attribute.grad_attrs)
68
+ gradient_columns = [(0 for _ in grad_attrs)] * len(records)
 
 
69
 
70
+ for row, indices in records.groupby(CALCULATE_COLUMNS).indices.items():
71
+ skill_id, skill_level, skill_stack, current_status, target_status, snapshot_index = row
72
  skill: Skill = school.skills[skill_id]
 
 
73
  skill.skill_level, skill.skill_stack = skill_level, skill_stack
74
+
75
+ current_buffs = filter_status(current_status, school)
76
+ target_buffs = filter_status(target_status, school)
77
+ if snapshot_index < 0:
78
+ snapshot_buffs = tuple()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  else:
80
+ snapshot_buffs = filter_status(records.loc[snapshot_index].current_status, school)
81
+
82
+ add_buffs(current_buffs, snapshot_buffs, target_buffs, attribute, skill)
83
+ damage_tuple = skill(attribute)
84
+ gradient_tuple = analyze_gradients(skill, attribute)
85
+ for index in indices:
86
+ damage_columns[index] = damage_tuple
87
+ gradient_columns[index] = gradient_tuple
88
+ sub_buffs(current_buffs, snapshot_buffs, target_buffs, attribute, skill)
89
+
90
+ records[DAMAGE_COLUMNS] = damage_columns
91
+ records[grad_attrs] = gradient_columns
92
 
93
+ return records
 
94
 
95
 
96
  def analyze_gradients(skill, attribute):
97
+ results = []
98
  for attr, value in attribute.grad_attrs.items():
99
  origin_value = getattr(attribute, attr)
100
  setattr(attribute, attr, origin_value + value)
101
+ _, _, _, expected_damage = skill(attribute)
102
+ results.append(expected_damage)
103
  setattr(attribute, attr, origin_value)
104
  return results
utils/io.py CHANGED
@@ -1,7 +1,6 @@
1
  import json
2
  from collections import defaultdict
3
-
4
- from base.constant import FRAME_PER_SECOND
5
 
6
  DELIMITER = "-"
7
  COMMA = ","
@@ -9,18 +8,9 @@ SEMICOLON = ";"
9
 
10
 
11
  def serialize(records):
12
- result = defaultdict(lambda: defaultdict(lambda: defaultdict(dict)))
13
  for player_id in records:
14
- for target_id in records[player_id]:
15
- for skill, status in records[player_id][target_id].items():
16
- skill = DELIMITER.join(str(e) for e in skill)
17
- for (current_status, snapshot_status, target_status), timeline in status.items():
18
- current_status = COMMA.join(DELIMITER.join(str(e) for e in buff) for buff in current_status)
19
- snapshot_status = COMMA.join(DELIMITER.join(str(e) for e in buff) for buff in snapshot_status)
20
- target_status = COMMA.join(DELIMITER.join(str(e) for e in buff) for buff in target_status)
21
- concat_status = SEMICOLON.join((current_status, snapshot_status, target_status))
22
-
23
- result[player_id][target_id][skill][concat_status] = timeline
24
 
25
  return result
26
 
 
1
  import json
2
  from collections import defaultdict
3
+ from utils.parser import COLUMNS
 
4
 
5
  DELIMITER = "-"
6
  COMMA = ","
 
8
 
9
 
10
  def serialize(records):
11
+ result = {}
12
  for player_id in records:
13
+ result[player_id] = records[player_id][COLUMNS].to_dict(orient="records")
 
 
 
 
 
 
 
 
 
14
 
15
  return result
16
 
utils/lua.py CHANGED
@@ -1,350 +1,16 @@
1
- def node_to_table(node):
2
- if len(node["entries"]) == node["lua_len"]:
3
- lst = []
4
- for kv in node["entries"]:
5
- lst.append(kv[1])
6
- return lst
7
- else:
8
- dct = {}
9
- for kv in node["entries"]:
10
- dct[kv[0]] = kv[1]
11
- return dct
12
 
 
13
 
14
- def sorter(kv):
15
- if isinstance(kv[0], int):
16
- return kv[0]
17
- return float("inf")
18
 
 
 
 
 
 
 
19
 
20
- def node_entries_append(node, key, val):
21
- node["entries"].append([key, val])
22
- node["entries"].sort(key=sorter)
23
- lua_len = 0
24
- for kv in node["entries"]:
25
- if kv[0] == lua_len + 1:
26
- lua_len = lua_len + 1
27
- node["lua_len"] = lua_len
28
 
29
-
30
- def parse(raw, encoding="utf-8", multi_val=False, verbose=False):
31
- sbins = raw.encode(encoding)
32
- root = {"entries": [], "lua_len": 0, "is_root": True}
33
- node = root
34
- stack = []
35
- state = "SEEK_CHILD"
36
- pos = 0
37
- slen = len(sbins)
38
- byte_quoting_char = None
39
- key = None
40
- escaping = False
41
- comment = None
42
- component_name = None
43
- errmsg = None
44
-
45
- while pos <= slen:
46
- byte_current = None
47
- byte_current_is_space = False
48
- if pos < slen:
49
- byte_current = sbins[pos: pos + 1]
50
- byte_current_is_space = (
51
- byte_current == b" "
52
- or byte_current == b"\r"
53
- or byte_current == b"\n"
54
- or byte_current == b"\t"
55
- )
56
- if verbose:
57
- print("[step] pos", pos, byte_current, state, comment, key, node)
58
-
59
- if comment == "MULTILINE":
60
- if byte_current == b"]" and sbins[pos: pos + 2] == b"]]":
61
- comment = None
62
- pos = pos + 1
63
- elif comment == "INLINE":
64
- if byte_current == b"\n":
65
- comment = None
66
- elif state == "SEEK_CHILD":
67
- if byte_current is None:
68
- break
69
- if byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
70
- comment = "MULTILINE"
71
- pos = pos + 3
72
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
73
- comment = "INLINE"
74
- pos = pos + 1
75
- elif not node["is_root"] and (
76
- (b"A" <= byte_current <= b"Z")
77
- or (b"a" <= byte_current <= b"z")
78
- or byte_current == b"_"
79
- ):
80
- state = "KEY_SIMPLE"
81
- pos1 = pos
82
- elif not node["is_root"] and byte_current == b"[":
83
- state = "KEY_EXPRESSION_OPEN"
84
- elif byte_current == b"}":
85
- if len(stack) == 0:
86
- errmsg = (
87
- "unexpected table closing, no matching opening braces found."
88
- )
89
- break
90
- prev_env = stack.pop()
91
- if prev_env["state"] == "KEY_EXPRESSION_OPEN":
92
- key = node_to_table(node)
93
- state = "KEY_END"
94
- elif prev_env["state"] == "VALUE":
95
- node_entries_append(
96
- prev_env["node"],
97
- prev_env["key"],
98
- node_to_table(node),
99
- )
100
- state = "VALUE_END"
101
- key = None
102
- node = prev_env["node"]
103
- elif not byte_current_is_space:
104
- key = node["lua_len"] + 1
105
- state = "VALUE"
106
- pos = pos - 1
107
- elif state == "VALUE":
108
- if byte_current is None:
109
- errmsg = "unexpected empty value."
110
- break
111
- if byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
112
- comment = "MULTILINE"
113
- pos = pos + 3
114
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
115
- comment = "INLINE"
116
- pos = pos + 1
117
- elif byte_current == b'"' or byte_current == b"'":
118
- state = "TEXT"
119
- component_name = "VALUE"
120
- pos1 = pos + 1
121
- byte_quoting_char = byte_current
122
- elif byte_current == b"-" or (
123
- b"0" <= byte_current <= b"9"
124
- ):
125
- state = "INT"
126
- component_name = "VALUE"
127
- pos1 = pos
128
- elif byte_current == b".":
129
- state = "FLOAT"
130
- component_name = "VALUE"
131
- pos1 = pos
132
- elif byte_current == b"t" and sbins[pos: pos + 4] == b"true":
133
- node_entries_append(node, key, True)
134
- state = "VALUE_END"
135
- key = None
136
- pos = pos + 3
137
- elif byte_current == b"f" and sbins[pos: pos + 5] == b"false":
138
- node_entries_append(node, key, False)
139
- state = "VALUE_END"
140
- key = None
141
- pos = pos + 4
142
- elif byte_current == b"{":
143
- stack.append({"node": node, "state": state, "key": key})
144
- state = "SEEK_CHILD"
145
- node = {"entries": [], "lua_len": 0, "is_root": False}
146
- elif state == "TEXT":
147
- if byte_current is None:
148
- errmsg = "unexpected string ending: missing close quote."
149
- break
150
- if escaping:
151
- escaping = False
152
- elif byte_current == b"\\":
153
- escaping = True
154
- elif byte_current == byte_quoting_char:
155
- data = (
156
- sbins[pos1:pos]
157
- .replace(b"\\\n", b"\n")
158
- .replace(b'\\"', b'"')
159
- .replace(b"\\\\", b"\\")
160
- .decode(encoding)
161
- )
162
- if component_name == "KEY":
163
- key = data
164
- state = "KEY_EXPRESSION_FINISH"
165
- elif component_name == "VALUE":
166
- node_entries_append(node, key, data)
167
- state = "VALUE_END"
168
- key = None
169
- data = None
170
- elif state == "INT":
171
- if byte_current == b".":
172
- state = "FLOAT"
173
- elif byte_current is None or byte_current < b"0" or byte_current > b"9":
174
- data = int(sbins[pos1:pos].decode(encoding))
175
- if component_name == "KEY":
176
- key = data
177
- state = "KEY_EXPRESSION_FINISH"
178
- pos = pos - 1
179
- elif component_name == "VALUE":
180
- node_entries_append(node, key, data)
181
- state = "VALUE_END"
182
- key = None
183
- pos = pos - 1
184
- data = None
185
- elif state == "FLOAT":
186
- if byte_current is None or byte_current < b"0" or byte_current > b"9":
187
- if pos == pos1 + 1 and sbins[pos1:pos] == b".":
188
- errmsg = "unexpected dot."
189
- break
190
- else:
191
- data = float(sbins[pos1:pos].decode(encoding))
192
- if component_name == "KEY":
193
- key = data
194
- state = "KEY_EXPRESSION_FINISH"
195
- pos = pos - 1
196
- elif component_name == "VALUE":
197
- node_entries_append(node, key, data)
198
- state = "VALUE_END"
199
- key = None
200
- pos = pos - 1
201
- data = None
202
- elif state == "VALUE_END":
203
- if byte_current is None:
204
- pass
205
- elif byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
206
- comment = "MULTILINE"
207
- pos = pos + 3
208
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
209
- comment = "INLINE"
210
- pos = pos + 1
211
- elif byte_current == b",":
212
- state = "SEEK_CHILD"
213
- elif byte_current == b"}":
214
- state = "SEEK_CHILD"
215
- pos = pos - 1
216
- elif not byte_current_is_space:
217
- errmsg = "unexpected character."
218
- break
219
- elif state == "KEY_EXPRESSION_OPEN":
220
- if byte_current is None:
221
- errmsg = "key expression expected."
222
- break
223
- if byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
224
- comment = "MULTILINE"
225
- pos = pos + 3
226
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
227
- comment = "INLINE"
228
- pos = pos + 1
229
- elif byte_current == b'"' or byte_current == b"'":
230
- state = "TEXT"
231
- component_name = "KEY"
232
- pos1 = pos + 1
233
- byte_quoting_char = byte_current
234
- elif byte_current == b"-" or (
235
- b"0" <= byte_current <= b"9"
236
- ):
237
- state = "INT"
238
- component_name = "KEY"
239
- pos1 = pos
240
- elif byte_current == b".":
241
- state = "FLOAT"
242
- component_name = "KEY"
243
- pos1 = pos
244
- elif byte_current == b"t" and sbins[pos: pos + 4] == b"true":
245
- errmsg = "python do not support bool as dict key."
246
- break
247
- key = True
248
- state = "KEY_EXPRESSION_FINISH"
249
- pos = pos + 3
250
- elif byte_current == b"f" and sbins[pos: pos + 5] == b"false":
251
- errmsg = "python do not support bool variable as dict key."
252
- break
253
- key = False
254
- state = "KEY_EXPRESSION_FINISH"
255
- pos = pos + 4
256
- elif byte_current == b"{":
257
- errmsg = "python do not support lua table variable as dict key."
258
- break
259
- state = "SEEK_CHILD"
260
- stack.push({"node": node, "state": state, "key": key})
261
- node = {"entries": [], "lua_len": 0}
262
- elif state == "KEY_EXPRESSION_FINISH":
263
- if byte_current is None:
264
- errmsg = 'unexpected end of table key expression, "]" expected.'
265
- break
266
- if byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
267
- comment = "MULTILINE"
268
- pos = pos + 3
269
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
270
- comment = "INLINE"
271
- pos = pos + 1
272
- elif byte_current == b"]":
273
- state = "KEY_EXPRESSION_CLOSE"
274
- elif not byte_current_is_space:
275
- errmsg = 'unexpected character, "]" expected.'
276
- break
277
- elif state == "KEY_EXPRESSION_CLOSE":
278
- if byte_current == b"=":
279
- state = "VALUE"
280
- elif byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
281
- comment = "MULTILINE"
282
- pos = pos + 3
283
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
284
- comment = "INLINE"
285
- pos = pos + 1
286
- elif not byte_current_is_space:
287
- errmsg = 'unexpected character, "=" expected.'
288
- break
289
- elif state == "KEY_SIMPLE":
290
- if not (
291
- (b"A" <= byte_current <= b"Z")
292
- or (b"a" <= byte_current <= b"z")
293
- or (b"0" <= byte_current <= b"9")
294
- or byte_current == b"_"
295
- ):
296
- key = sbins[pos1:pos].decode(encoding)
297
- state = "KEY_SIMPLE_END"
298
- pos = pos - 1
299
- elif state == "KEY_SIMPLE_END":
300
- if byte_current_is_space:
301
- pass
302
- elif byte_current == b"-" and sbins[pos: pos + 4] == b"--[[":
303
- comment = "MULTILINE"
304
- pos = pos + 3
305
- elif byte_current == b"-" and sbins[pos: pos + 2] == b"--":
306
- comment = "INLINE"
307
- pos = pos + 1
308
- elif byte_current == b"=":
309
- state = "VALUE"
310
- elif byte_current == b"," or byte_current == b"}":
311
- if key == "true":
312
- node_entries_append(node, node["lua_len"] + 1, True)
313
- state = "VALUE_END"
314
- key = None
315
- pos = pos - 1
316
- elif key == "false":
317
- node_entries_append(node, node["lua_len"] + 1, False)
318
- state = "VALUE_END"
319
- key = None
320
- pos = pos - 1
321
- else:
322
- key = None
323
- errmsg = "invalied table simple key character."
324
- break
325
- pos += 1
326
- if verbose:
327
- print(" ", pos, " ", state, comment, key, node)
328
-
329
- # check if there is any errors
330
- if errmsg is None and len(stack) != 0:
331
- errmsg = 'unexpected end of table, "}" expected.'
332
- if errmsg is None and root["lua_len"] == 0:
333
- errmsg = "nothing can be unserialized from input string."
334
- if errmsg is not None:
335
- pos = min(pos, slen)
336
- start_pos = max(0, pos - 4)
337
- end_pos = min(pos + 10, slen)
338
- err_parts = sbins[start_pos:end_pos].decode(encoding)
339
- err_indent = " " * (pos - start_pos)
340
- raise Exception(
341
- "Unserialize luadata failed on pos %d:\n %s\n %s^\n %s"
342
- % (pos, err_parts, err_indent, errmsg)
343
- )
344
-
345
- res = []
346
- for kv in root["entries"]:
347
- res.append(kv[1])
348
- if multi_val:
349
- return tuple(res)
350
- return res[0]
 
1
+ from ast import literal_eval
2
+ import re
 
 
 
 
 
 
 
 
 
3
 
4
+ DAMAGE_RESULT_PATTERN = re.compile(r"\[[0-4]\]=(\d+)")
5
 
 
 
 
 
6
 
7
+ def parse_player(lua_data):
8
+ try:
9
+ python_data = lua_data.strip().replace("{", "[").replace("}", "]")
10
+ return literal_eval(python_data)
11
+ except:
12
+ return None
13
 
 
 
 
 
 
 
 
 
14
 
15
+ def parse_damage(lua_data):
16
+ return sum(int(damage) for damage in DAMAGE_RESULT_PATTERN.findall(lua_data))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/parser.py CHANGED
@@ -1,23 +1,28 @@
1
- import os.path
 
2
  from collections import defaultdict
3
 
 
 
 
4
  from base.constant import FRAME_PER_SECOND
5
  from schools import *
6
- from utils.lua import parse
7
 
8
- FRAME_TYPE, SECOND_TYPE = int, int
9
- PLAYER_ID_TYPE, PLAYER_NAME_TYPE, TARGET_ID_TYPE, PET_ID_TYPE = str, str, str, str
10
  CASTER_ID_TYPE = PLAYER_ID_TYPE | PET_ID_TYPE
11
- SKILL_ID_TYPE, SKILL_LEVEL_TYPE, SKILL_STACK_TYPE, SKILL_CRITICAL_TYPE = int, int, int, bool
 
 
12
  SKILL_TYPE = Tuple[SKILL_ID_TYPE, SKILL_LEVEL_TYPE, SKILL_STACK_TYPE]
13
  BUFF_ID_TYPE, BUFF_LEVEL_TYPE, BUFF_STACK_TYPE = int, int, int
14
  BUFF_TYPE = Tuple[BUFF_ID_TYPE, BUFF_LEVEL_TYPE]
15
 
16
- CURRENT_STATUS_TYPE, SNAPSHOT_STATUS_TYPE, TARGET_STATUS_TYPE = tuple, tuple, tuple
17
- STATUS_TUPLE = Tuple[CURRENT_STATUS_TYPE, SNAPSHOT_STATUS_TYPE, TARGET_STATUS_TYPE]
18
- TIMELINE_TYPE = Tuple[FRAME_TYPE, SKILL_CRITICAL_TYPE]
19
- SUB_RECORD_TYPE = Dict[STATUS_TUPLE, List[TIMELINE_TYPE]]
20
- RECORD_TYPE = Dict[SKILL_TYPE, SUB_RECORD_TYPE]
21
 
22
  LABEL_MAPPING = {
23
  2: "远程武器",
@@ -43,29 +48,29 @@ class BaseParser:
43
  current_skill: SKILL_ID_TYPE
44
 
45
  current_frame: FRAME_TYPE
46
- current_second: SECOND_TYPE
47
 
48
- id2name: Dict[PLAYER_ID_TYPE | TARGET_ID_TYPE, PLAYER_NAME_TYPE]
49
- name2id: Dict[PLAYER_NAME_TYPE, PLAYER_ID_TYPE | TARGET_ID_TYPE]
50
  pet2employer: Dict[PET_ID_TYPE, PLAYER_ID_TYPE]
51
 
52
- records: Dict[PLAYER_ID_TYPE, Dict[TARGET_ID_TYPE, RECORD_TYPE]]
53
 
54
- frame_shift_buffs: Dict[FRAME_TYPE, Dict[PLAYER_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]
55
- second_shift_buffs: Dict[SECOND_TYPE, Dict[PLAYER_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]
56
 
57
- buff_stacks: Dict[CASTER_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]
58
  buff_intervals: Dict[CASTER_ID_TYPE, Dict[BUFF_TYPE, FRAME_TYPE]]
59
- target_buff_stacks: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]
60
  target_buff_intervals: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[BUFF_TYPE, FRAME_TYPE]]]
61
 
62
  dot_stacks: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, int]]]
63
  dot_ticks: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, int]]]
64
- dot_snapshot: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]]
65
 
66
- next_pet_buff_stacks: Dict[PLAYER_ID_TYPE, List[Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]
67
 
68
- last_dot: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, Tuple[SKILL_TYPE, Tuple[tuple, tuple]]]]]
69
  next_dot: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, int]]]
70
 
71
  start_frame: FRAME_TYPE
@@ -87,38 +92,35 @@ class BaseParser:
87
 
88
  @property
89
  def current_records(self):
90
- return self.records[self.current_player][self.current_target]
 
 
 
 
91
 
92
  @property
93
- def current_buff_stacks(self):
94
- return self.buff_stacks[self.current_player]
95
 
96
  @property
97
  def current_buff_intervals(self):
98
  return self.buff_intervals[self.current_player]
99
 
100
  @property
101
- def current_target_buff_stacks(self):
102
- return self.target_buff_stacks[self.current_target][self.current_player]
103
 
104
  @property
105
  def current_target_buff_intervals(self):
106
  return self.target_buff_intervals[self.current_target][self.current_player]
107
 
108
  @property
109
- def current_snapshot(self):
110
- if self.current_caster in self.pet2employer:
111
- return self.buff_stacks[self.current_caster]
112
- else:
113
- return self.dot_snapshot[self.current_target][self.current_player].get(self.current_skill, {})
114
-
115
- @property
116
- def current_next_pet_buff_stacks(self):
117
- return self.next_pet_buff_stacks[self.current_player]
118
 
119
  @property
120
- def current_dot_snapshot(self):
121
- return self.dot_snapshot[self.current_target][self.current_player]
122
 
123
  @property
124
  def current_dot_stacks(self):
@@ -138,27 +140,25 @@ class BaseParser:
138
 
139
  def reset(self):
140
  self.current_frame = 0
141
- self.current_second = 0
142
 
143
  self.id2name = {}
144
  self.name2id = {}
145
  self.pet2employer = {}
146
 
147
- self.records = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(list))))
148
 
149
- self.frame_shift_buffs = defaultdict(lambda: defaultdict(dict))
150
- self.second_shift_buffs = defaultdict(lambda: defaultdict(dict))
151
-
152
- self.buff_stacks = defaultdict(dict)
153
  self.buff_intervals = defaultdict(dict)
154
- self.target_buff_stacks = defaultdict(lambda: defaultdict(dict))
155
  self.target_buff_intervals = defaultdict(lambda: defaultdict(dict))
156
 
157
  self.dot_stacks = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 1)))
158
  self.dot_ticks = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0)))
159
 
160
- self.next_pet_buff_stacks = defaultdict(list)
161
- self.dot_snapshot = defaultdict(lambda: defaultdict(dict))
162
  self.last_dot = defaultdict(lambda: defaultdict(dict))
163
  self.next_dot = defaultdict(lambda: defaultdict(dict))
164
 
@@ -173,42 +173,61 @@ class BaseParser:
173
  def refresh_buff(self, buff_id, buff_level, buff_stack=1):
174
  buff = self.current_school.buffs[buff_id]
175
  buff_tuple = (buff_id, buff_level)
176
- stack = max(min(self.current_buff_stacks.get(buff_tuple, 0) + buff_stack, buff.max_stack), 0)
177
  if stack:
178
- self.current_buff_stacks[buff_tuple] = stack
179
  if buff.interval > 0:
180
  self.current_buff_intervals[buff_tuple] = self.current_frame + buff.interval + 1
181
  else:
182
- self.current_buff_stacks.pop(buff_tuple, None)
183
  self.current_buff_intervals.pop(buff_tuple, None)
184
 
185
  def refresh_target_buff(self, buff_id, buff_level, buff_stack=1):
186
  buff = self.current_school.buffs[buff_id]
187
  buff_tuple = (buff_id, buff_level)
188
- stack = max(min(self.current_target_buff_stacks.get(buff_tuple, 0) + buff_stack, buff.max_stack), 0)
189
  if stack:
190
- self.current_target_buff_stacks[buff_tuple] = stack
191
  if buff.interval > 0:
192
  self.current_target_buff_intervals[buff_tuple] = self.current_frame + buff.interval + 1
193
  else:
194
- self.current_target_buff_stacks.pop(buff_tuple, None)
195
  self.current_target_buff_intervals.pop(buff_tuple, None)
196
 
197
  def clear_buff(self, buff_id, buff_level):
198
  buff_tuple = (buff_id, buff_level)
199
- self.current_buff_stacks.pop(buff_tuple, None)
200
  self.current_buff_intervals.pop(buff_tuple, None)
201
 
202
  def clear_target_buff(self, buff_id, buff_level):
203
  buff_tuple = (buff_id, buff_level)
204
- self.current_target_buff_stacks.pop(buff_tuple, None)
205
  self.current_target_buff_intervals.pop(buff_tuple, None)
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  class Parser(BaseParser):
209
  @property
210
  def duration(self):
211
- return round((self.end_frame - self.start_frame) / FRAME_PER_SECOND, 3)
212
 
213
  @staticmethod
214
  def parse_equipments(detail):
@@ -236,8 +255,9 @@ class Parser(BaseParser):
236
  if player_id in self.id2name or school_id not in SUPPORT_SCHOOL:
237
  return
238
 
239
- if isinstance(detail := parse(row), list) and (school := SUPPORT_SCHOOL.get(detail[3])):
240
  player_name = detail[1]
 
241
  self.id2name[player_id] = player_name
242
  self.name2id[player_name] = player_id
243
  self.select_equipments[player_id] = self.parse_equipments(detail[5])
@@ -248,12 +268,8 @@ class Parser(BaseParser):
248
 
249
  def parse_npc(self, row):
250
  detail = row.strip("{}").split(",")
251
- npc_id, employer_id = detail[0], detail[3]
252
- if npc_id in self.id2name:
253
- return
254
-
255
- npc_name = detail[1].strip('"')
256
- if not npc_name:
257
  return
258
 
259
  self.id2name[npc_id] = npc_name
@@ -262,86 +278,29 @@ class Parser(BaseParser):
262
  self.pet2employer[npc_id] = employer_id
263
 
264
  def parse_pet(self, row):
265
- detail = row.strip().strip("{}")
266
- pet_id = detail[0]
267
  if player_id := self.pet2employer.get(pet_id):
268
- if self.next_pet_buff_stacks[player_id]:
269
- pet_buff_stacks = self.next_pet_buff_stacks[player_id].pop()
270
  else:
271
  pet_buff_stacks = {}
272
- self.buff_stacks[pet_id] = {**self.buff_stacks[player_id].copy(), **pet_buff_stacks}
273
-
274
- def parse_shift_buff(self, row):
275
- detail = row.strip("{}").split(",")
276
- player_id = detail[0]
277
- if player_id not in self.players:
278
- return
279
-
280
- buff_id, buff_stack, buff_level = int(detail[4]), int(detail[5]), int(detail[8])
281
- if buff_id not in self.players[player_id].buffs:
282
- return
283
- buff = self.players[player_id].buffs[buff_id]
284
- if frame_shift := buff.frame_shift:
285
- self.frame_shift_buffs[self.current_frame + frame_shift][player_id][(buff_id, buff_level)] = buff_stack
286
- # elif second_shift := buff.second_shift:
287
- # self.second_shift_buffs[self.current_second + second_shift][player_id][(buff_id, buff_level)] = buff_stack
288
-
289
- def parse_frame_shift_status(self):
290
- for frame in list(self.frame_shift_buffs):
291
- if frame > self.current_frame:
292
- break
293
- for player_id, shift_buffs in self.frame_shift_buffs.pop(frame).items():
294
- for buff, buff_stack in shift_buffs.items():
295
- if buff_stack:
296
- self.buff_stacks[player_id][buff] = buff_stack
297
- else:
298
- self.buff_stacks[player_id].pop(buff, None)
299
-
300
- def parse_second_shift_status(self):
301
- for second in list(self.second_shift_buffs):
302
- if second > self.current_second:
303
- break
304
- for player_id, shift_buffs in self.second_shift_buffs.pop(second).items():
305
- for buff, buff_stack in shift_buffs.items():
306
- if buff_stack:
307
- self.buff_stacks[player_id][buff] = buff_stack
308
- else:
309
- self.buff_stacks[player_id].pop(buff, None)
310
-
311
- def parse_buff_intervals(self):
312
- for caster_id, buffs in self.buff_intervals.items():
313
- pop_buffs = []
314
- for buff, end_frame in buffs.items():
315
- if end_frame < self.current_frame:
316
- self.buff_stacks[caster_id].pop(buff, None)
317
- pop_buffs.append(buff)
318
- for pop_buff in pop_buffs:
319
- buffs.pop(pop_buff)
320
- for target_id in self.target_buff_intervals:
321
- for caster_id, buffs in self.target_buff_intervals[target_id].items():
322
- pop_buffs = []
323
- for buff, end_frame in buffs.items():
324
- if end_frame < self.current_frame:
325
- self.target_buff_stacks[target_id][caster_id].pop(buff, None)
326
- pop_buffs.append(buff)
327
- for pop_buff in pop_buffs:
328
- buffs.pop(pop_buff)
329
 
330
  def parse_buff(self, row):
331
  detail = row.strip("{}").split(",")
332
  caster_id = detail[0]
333
  if caster_id in self.pet2employer:
334
  player_id = self.pet2employer[caster_id]
335
- if caster_id in self.buff_stacks:
336
- buff_stacks = self.buff_stacks[caster_id]
337
- elif self.next_pet_buff_stacks[player_id]:
338
- buff_stacks = self.next_pet_buff_stacks[player_id][0]
339
  else:
340
- buff_stacks = {}
341
- self.next_pet_buff_stacks[player_id].append(buff_stacks)
342
  else:
343
  player_id = caster_id
344
- buff_stacks = self.buff_stacks[player_id]
345
 
346
  if player_id not in self.players:
347
  return
@@ -350,15 +309,12 @@ class Parser(BaseParser):
350
  if buff_id not in self.players[player_id].buffs:
351
  return
352
 
353
- frame_shift = self.players[player_id].buffs[buff_id].frame_shift
354
- if frame_shift:
355
- return
356
-
357
  if buff_stack:
358
- buff_stacks[(buff_id, buff_level)] = buff_stack
359
  else:
360
- buff_stacks.pop((buff_id, buff_level), None)
361
 
 
362
  def parse_skill(self, row):
363
  detail = row.strip("{}").split(",")
364
  caster_id, target_id = detail[0], detail[1]
@@ -370,7 +326,8 @@ class Parser(BaseParser):
370
  if player_id not in self.players:
371
  return
372
 
373
- react, skill_id, skill_level, critical = int(detail[2]), int(detail[4]), int(detail[5]), detail[6] == "true"
 
374
  if react or skill_id not in self.players[player_id].skills:
375
  return
376
 
@@ -383,38 +340,65 @@ class Parser(BaseParser):
383
  self.current_targets.append(target_id)
384
  self.current_target = target_id
385
  self.current_skill = skill_id
 
386
  skill = self.players[player_id].skills[skill_id]
387
  skill.skill_level = skill_level
388
- skill.parse(critical, self)
389
-
390
- def status(self, skill_id):
 
 
 
 
 
 
 
 
 
391
  current_status = []
392
- for (buff_id, buff_level), buff_stack in self.current_buff_stacks.items():
393
  buff = self.current_school.buffs[buff_id]
394
  if buff.gain_attributes:
395
  current_status.append((buff_id, buff_level, buff_stack))
396
- elif buff.gain_skills and skill_id in buff.gain_skills:
397
  current_status.append((buff_id, buff_level, buff_stack))
398
-
399
- self.current_skill = skill_id
400
- snapshot_status = []
401
- for (buff_id, buff_level), buff_stack in self.current_snapshot.items():
402
- buff = self.current_school.buffs[buff_id]
403
- if buff.gain_attributes:
404
- snapshot_status.append((buff_id, buff_level, buff_stack))
405
- elif buff.gain_skills and skill_id in buff.gain_skills:
406
- snapshot_status.append((buff_id, buff_level, buff_stack))
407
 
408
  target_status = []
409
- for (buff_id, buff_level), buff_stack in self.current_target_buff_stacks.items():
410
  buff = self.current_school.buffs[buff_id]
411
  if buff.gain_attributes:
412
  target_status.append((buff_id, buff_level, buff_stack))
413
- elif buff.gain_skills and skill_id in buff.gain_skills:
414
  target_status.append((buff_id, buff_level, buff_stack))
415
-
416
- return tuple(current_status), tuple(snapshot_status), tuple(target_status)
417
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  def __call__(self, file_name):
419
  self.file_name = os.path.basename(file_name)
420
  self.reset()
@@ -433,27 +417,17 @@ class Parser(BaseParser):
433
  for talent_id in self.select_talents[player_id]:
434
  school.talent_gains[talent_id].add_skills(school.skills)
435
 
436
- for row in rows:
437
- self.current_frame = int(row[1])
438
- # self.current_second = int(row[3])
439
- if row[4] == "13":
440
- self.parse_shift_buff(row[-1])
441
-
442
  for row in rows:
443
  if (current_frame := int(row[1])) != self.current_frame:
444
  self.current_frame = current_frame
445
- self.parse_frame_shift_status()
446
- self.parse_buff_intervals()
447
- # if (current_second := int(row[3])) != self.current_second:
448
- # self.current_second = current_second
449
- # self.parse_frame_shift_status()
450
 
451
  if row[4] == "6":
452
- self.parse_pet(row[-1])
453
  elif row[4] == "13":
454
- self.parse_buff(row[-1])
455
  elif row[4] == "21":
456
- self.parse_skill(row[-1])
457
 
458
  self.end_frame = self.current_frame
459
 
@@ -461,10 +435,4 @@ class Parser(BaseParser):
461
  for talent_id in self.select_talents[player_id]:
462
  school.talent_gains[talent_id].sub_skills(school.skills)
463
 
464
- for player_id in self.records:
465
- player_record = defaultdict(lambda: defaultdict(list))
466
- for target_id, records in self.records[player_id].items():
467
- for skill_tuple, status in records.items():
468
- for status_tuple, timeline in status.items():
469
- player_record[skill_tuple][status_tuple] += timeline
470
- self.records[player_id][""] = player_record
 
1
+ import os
2
+ import time
3
  from collections import defaultdict
4
 
5
+ import pandas as pd
6
+ from line_profiler import profile
7
+
8
  from base.constant import FRAME_PER_SECOND
9
  from schools import *
10
+ from utils.lua import parse_player, parse_damage
11
 
12
+ FRAME_TYPE, INDEX_TYPE = int, int
13
+ PLAYER_ID_TYPE, TARGET_ID_TYPE, PET_ID_TYPE = str, str, str
14
  CASTER_ID_TYPE = PLAYER_ID_TYPE | PET_ID_TYPE
15
+ ENTITY_ID_TYPE, ENTITY_NAME_TYPE = CASTER_ID_TYPE | TARGET_ID_TYPE, str
16
+ SKILL_ID_TYPE, SKILL_LEVEL_TYPE, SKILL_STACK_TYPE = int, int, int
17
+ ACTUAL_DAMAGE, ACTUAL_CRITICAL_STRIKE = int, bool
18
  SKILL_TYPE = Tuple[SKILL_ID_TYPE, SKILL_LEVEL_TYPE, SKILL_STACK_TYPE]
19
  BUFF_ID_TYPE, BUFF_LEVEL_TYPE, BUFF_STACK_TYPE = int, int, int
20
  BUFF_TYPE = Tuple[BUFF_ID_TYPE, BUFF_LEVEL_TYPE]
21
 
22
+ TAG_COLUMNS = ["frame", "time", "player_id", "caster_id", "target_id", "skill", "skill_name"]
23
+ CALCULATE_COLUMNS = ["skill_id", "skill_level", "skill_stack", "current_status", "target_status", "snapshot_index"]
24
+ COMPARE_COLUMNS = ["actual_critical_strike", "actual_damage"]
25
+ COLUMNS = TAG_COLUMNS + CALCULATE_COLUMNS + COMPARE_COLUMNS
 
26
 
27
  LABEL_MAPPING = {
28
  2: "远程武器",
 
48
  current_skill: SKILL_ID_TYPE
49
 
50
  current_frame: FRAME_TYPE
51
+ current_index: INDEX_TYPE
52
 
53
+ id2name: Dict[ENTITY_ID_TYPE, ENTITY_NAME_TYPE]
54
+ name2id: Dict[ENTITY_NAME_TYPE, ENTITY_ID_TYPE]
55
  pet2employer: Dict[PET_ID_TYPE, PLAYER_ID_TYPE]
56
 
57
+ records: Dict[PLAYER_ID_TYPE, List[dict] | pd.DataFrame]
58
 
59
+ skill_display_names: Dict[tuple, str]
60
+ buff_display_names: Dict[tuple, str]
61
 
62
+ buffs: Dict[CASTER_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]
63
  buff_intervals: Dict[CASTER_ID_TYPE, Dict[BUFF_TYPE, FRAME_TYPE]]
64
+ target_buffs: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]
65
  target_buff_intervals: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[BUFF_TYPE, FRAME_TYPE]]]
66
 
67
  dot_stacks: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, int]]]
68
  dot_ticks: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, int]]]
69
+ dot_buffs: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, INDEX_TYPE]]]
70
 
71
+ next_pet_buffs: Dict[PLAYER_ID_TYPE, List[Dict[BUFF_TYPE, BUFF_STACK_TYPE]]]
72
 
73
+ last_dot: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, INDEX_TYPE]]]
74
  next_dot: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE, int]]]
75
 
76
  start_frame: FRAME_TYPE
 
92
 
93
  @property
94
  def current_records(self):
95
+ return self.records[self.current_player]
96
+
97
+ @property
98
+ def current_record(self):
99
+ return self.current_records[-1]
100
 
101
  @property
102
+ def current_buffs(self):
103
+ return self.buffs[self.current_caster]
104
 
105
  @property
106
  def current_buff_intervals(self):
107
  return self.buff_intervals[self.current_player]
108
 
109
  @property
110
+ def current_target_buffs(self):
111
+ return self.target_buffs[self.current_target][self.current_player]
112
 
113
  @property
114
  def current_target_buff_intervals(self):
115
  return self.target_buff_intervals[self.current_target][self.current_player]
116
 
117
  @property
118
+ def current_next_pet_buffs(self):
119
+ return self.next_pet_buffs[self.current_player]
 
 
 
 
 
 
 
120
 
121
  @property
122
+ def current_dot_buffs(self):
123
+ return self.dot_buffs[self.current_target][self.current_player]
124
 
125
  @property
126
  def current_dot_stacks(self):
 
140
 
141
  def reset(self):
142
  self.current_frame = 0
143
+ self.current_index = 0
144
 
145
  self.id2name = {}
146
  self.name2id = {}
147
  self.pet2employer = {}
148
 
149
+ self.records = defaultdict(list)
150
 
151
+ self.buff_display_names = {}
152
+ self.buffs = defaultdict(dict)
 
 
153
  self.buff_intervals = defaultdict(dict)
154
+ self.target_buffs = defaultdict(lambda: defaultdict(dict))
155
  self.target_buff_intervals = defaultdict(lambda: defaultdict(dict))
156
 
157
  self.dot_stacks = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 1)))
158
  self.dot_ticks = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0)))
159
 
160
+ self.next_pet_buffs = defaultdict(list)
161
+ self.dot_buffs = defaultdict(lambda: defaultdict(dict))
162
  self.last_dot = defaultdict(lambda: defaultdict(dict))
163
  self.next_dot = defaultdict(lambda: defaultdict(dict))
164
 
 
173
  def refresh_buff(self, buff_id, buff_level, buff_stack=1):
174
  buff = self.current_school.buffs[buff_id]
175
  buff_tuple = (buff_id, buff_level)
176
+ stack = max(min(self.current_buffs.get(buff_tuple, 0) + buff_stack, buff.max_stack), 0)
177
  if stack:
178
+ self.current_buffs[buff_tuple] = stack
179
  if buff.interval > 0:
180
  self.current_buff_intervals[buff_tuple] = self.current_frame + buff.interval + 1
181
  else:
182
+ self.current_buffs.pop(buff_tuple, None)
183
  self.current_buff_intervals.pop(buff_tuple, None)
184
 
185
  def refresh_target_buff(self, buff_id, buff_level, buff_stack=1):
186
  buff = self.current_school.buffs[buff_id]
187
  buff_tuple = (buff_id, buff_level)
188
+ stack = max(min(self.current_target_buffs.get(buff_tuple, 0) + buff_stack, buff.max_stack), 0)
189
  if stack:
190
+ self.current_target_buffs[buff_tuple] = stack
191
  if buff.interval > 0:
192
  self.current_target_buff_intervals[buff_tuple] = self.current_frame + buff.interval + 1
193
  else:
194
+ self.current_target_buffs.pop(buff_tuple, None)
195
  self.current_target_buff_intervals.pop(buff_tuple, None)
196
 
197
  def clear_buff(self, buff_id, buff_level):
198
  buff_tuple = (buff_id, buff_level)
199
+ self.current_buffs.pop(buff_tuple, None)
200
  self.current_buff_intervals.pop(buff_tuple, None)
201
 
202
  def clear_target_buff(self, buff_id, buff_level):
203
  buff_tuple = (buff_id, buff_level)
204
+ self.current_target_buffs.pop(buff_tuple, None)
205
  self.current_target_buff_intervals.pop(buff_tuple, None)
206
 
207
+ def buff_timer(self):
208
+ for caster_id, buffs in self.buff_intervals.items():
209
+ pop_buffs = []
210
+ for buff, end_frame in buffs.items():
211
+ if end_frame < self.current_frame:
212
+ self.buffs[caster_id].pop(buff, None)
213
+ pop_buffs.append(buff)
214
+ for pop_buff in pop_buffs:
215
+ buffs.pop(pop_buff)
216
+ for target_id in self.target_buff_intervals:
217
+ for caster_id, buffs in self.target_buff_intervals[target_id].items():
218
+ pop_buffs = []
219
+ for buff, end_frame in buffs.items():
220
+ if end_frame < self.current_frame:
221
+ self.target_buffs[target_id][caster_id].pop(buff, None)
222
+ pop_buffs.append(buff)
223
+ for pop_buff in pop_buffs:
224
+ buffs.pop(pop_buff)
225
+
226
 
227
  class Parser(BaseParser):
228
  @property
229
  def duration(self):
230
+ return round((self.end_frame - self.start_frame) / FRAME_PER_SECOND, 2)
231
 
232
  @staticmethod
233
  def parse_equipments(detail):
 
255
  if player_id in self.id2name or school_id not in SUPPORT_SCHOOL:
256
  return
257
 
258
+ if detail := parse_player(row):
259
  player_name = detail[1]
260
+ school = SUPPORT_SCHOOL[school_id]
261
  self.id2name[player_id] = player_name
262
  self.name2id[player_name] = player_id
263
  self.select_equipments[player_id] = self.parse_equipments(detail[5])
 
268
 
269
  def parse_npc(self, row):
270
  detail = row.strip("{}").split(",")
271
+ npc_id, npc_name, employer_id = detail[0], detail[1].strip('"'), detail[3]
272
+ if npc_id in self.id2name or not npc_name:
 
 
 
 
273
  return
274
 
275
  self.id2name[npc_id] = npc_name
 
278
  self.pet2employer[npc_id] = employer_id
279
 
280
  def parse_pet(self, row):
281
+ pet_id = row.strip("{}")
 
282
  if player_id := self.pet2employer.get(pet_id):
283
+ if self.next_pet_buffs[player_id]:
284
+ pet_buff_stacks = self.next_pet_buffs[player_id].pop()
285
  else:
286
  pet_buff_stacks = {}
287
+ self.buffs[pet_id] = {**self.buffs[player_id].copy(), **pet_buff_stacks}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  def parse_buff(self, row):
290
  detail = row.strip("{}").split(",")
291
  caster_id = detail[0]
292
  if caster_id in self.pet2employer:
293
  player_id = self.pet2employer[caster_id]
294
+ if caster_id in self.buffs:
295
+ buffs = self.buffs[caster_id]
296
+ elif self.next_pet_buffs[player_id]:
297
+ buffs = self.next_pet_buffs[player_id][0]
298
  else:
299
+ buffs = {}
300
+ self.next_pet_buffs[player_id].append(buffs)
301
  else:
302
  player_id = caster_id
303
+ buffs = self.buffs[player_id]
304
 
305
  if player_id not in self.players:
306
  return
 
309
  if buff_id not in self.players[player_id].buffs:
310
  return
311
 
 
 
 
 
312
  if buff_stack:
313
+ buffs[(buff_id, buff_level)] = buff_stack
314
  else:
315
+ buffs.pop((buff_id, buff_level), None)
316
 
317
+ @profile
318
  def parse_skill(self, row):
319
  detail = row.strip("{}").split(",")
320
  caster_id, target_id = detail[0], detail[1]
 
326
  if player_id not in self.players:
327
  return
328
 
329
+ react, skill_id, skill_level = int(detail[2]), int(detail[4]), int(detail[5])
330
+ actual_critical_strike, actual_damage = detail[6] == "true", parse_damage(row)
331
  if react or skill_id not in self.players[player_id].skills:
332
  return
333
 
 
340
  self.current_targets.append(target_id)
341
  self.current_target = target_id
342
  self.current_skill = skill_id
343
+
344
  skill = self.players[player_id].skills[skill_id]
345
  skill.skill_level = skill_level
346
+ self.current_records.append({
347
+ "frame": self.current_frame, **self.current_buffs, **self.current_target_buffs,
348
+ "player_id": self.current_player, "caster_id": self.current_caster, "target_id": self.current_target,
349
+ "skill_id": skill_id, "skill_level": skill_level, "skill_stack": 1, "skill_name": skill.skill_name,
350
+ "actual_critical_strike": actual_critical_strike, "actual_damage": actual_damage,
351
+ })
352
+ self.set_status()
353
+ skill.parse(self)
354
+ self.current_index += 1
355
+
356
+ @profile
357
+ def set_status(self):
358
  current_status = []
359
+ for (buff_id, buff_level), buff_stack in self.current_buffs.items():
360
  buff = self.current_school.buffs[buff_id]
361
  if buff.gain_attributes:
362
  current_status.append((buff_id, buff_level, buff_stack))
363
+ elif buff.gain_skills and self.current_skill in buff.gain_skills:
364
  current_status.append((buff_id, buff_level, buff_stack))
365
+ self.current_record['current_status'] = tuple(sorted(current_status))
 
 
 
 
 
 
 
 
366
 
367
  target_status = []
368
+ for (buff_id, buff_level), buff_stack in self.current_target_buffs.items():
369
  buff = self.current_school.buffs[buff_id]
370
  if buff.gain_attributes:
371
  target_status.append((buff_id, buff_level, buff_stack))
372
+ elif buff.gain_skills and self.current_skill in buff.gain_skills:
373
  target_status.append((buff_id, buff_level, buff_stack))
374
+ self.current_record['target_status'] = tuple(sorted(target_status))
375
+
376
+ def convert_records(self):
377
+ for player_id, records in self.records.items():
378
+ records = pd.DataFrame(records)
379
+ records.snapshot_index = records.snapshot_index.fillna(-1)
380
+ records = records.fillna(0)
381
+ records['time'] = (records.frame - self.start_frame) / FRAME_PER_SECOND
382
+ skills = [""] * len(records)
383
+ for skill_tuple, indices in records.groupby(["skill_id", "skill_level", "skill_stack"]).indices.items():
384
+ skill_id, skill_level, skill_stack = skill_tuple
385
+ skill = self.players[player_id].skills[skill_id]
386
+ skill.skill_level, skill.skill_stack = skill_level, skill_stack
387
+ for index in indices:
388
+ skills[index] = skill.display_name
389
+ records["skill"] = skills
390
+ buffs = {}
391
+ for buff_tuple in [column for column in records.columns if isinstance(column, tuple)]:
392
+ buff_id, buff_level = buff_tuple
393
+ buff = self.players[player_id].buffs[buff_id]
394
+ buff.buff_level = buff_level
395
+ buffs[buff_tuple] = buff.display_name
396
+ records = records.rename(columns=buffs)
397
+ columns = [c for c in COLUMNS if c in records.columns] + [c for c in records.columns if c not in COLUMNS]
398
+ records = records[columns]
399
+ self.records[player_id] = records
400
+
401
+ @profile
402
  def __call__(self, file_name):
403
  self.file_name = os.path.basename(file_name)
404
  self.reset()
 
417
  for talent_id in self.select_talents[player_id]:
418
  school.talent_gains[talent_id].add_skills(school.skills)
419
 
 
 
 
 
 
 
420
  for row in rows:
421
  if (current_frame := int(row[1])) != self.current_frame:
422
  self.current_frame = current_frame
423
+ self.buff_timer()
 
 
 
 
424
 
425
  if row[4] == "6":
426
+ self.parse_pet(row[-1].strip())
427
  elif row[4] == "13":
428
+ self.parse_buff(row[-1].strip())
429
  elif row[4] == "21":
430
+ self.parse_skill(row[-1].strip())
431
 
432
  self.end_frame = self.current_frame
433
 
 
435
  for talent_id in self.select_talents[player_id]:
436
  school.talent_gains[talent_id].sub_skills(school.skills)
437
 
438
+ self.convert_records()