Spaces:
Runtime error
Runtime error
ango
commited on
Commit
·
1cc60af
1
Parent(s):
d26e0de
5.16 commit
Browse files- .github/workflows/space_sync.yml +22 -0
- README.md +11 -0
- base/buff.py +1 -1
- base/skill.py +26 -40
- gr/components/combat.py +7 -10
- gr/components/top.py +6 -4
- gr/scripts/combat.py +114 -66
- gr/scripts/top.py +30 -35
- parse_new_school.py +2 -2
- parse_skill.py +2 -2
- schools/ao_xue_zhan_yi/__init__.py +1 -1
- schools/ao_xue_zhan_yi/skills.py +1 -1
- schools/ao_xue_zhan_yi/talents.py +1 -1
- schools/bing_xin_jue/__init__.py +2 -2
- schools/du_jing/skills.py +1 -1
- schools/gu_feng_jue/skills.py +1 -1
- schools/ling_hai_jue/buffs.py +7 -7
- schools/tai_xu_jian_yi/__init__.py +1 -1
- schools/yi_jin_jing/__init__.py +1 -1
- schools/yi_jin_jing/skills.py +4 -4
- schools/zi_xia_gong/__init__.py +1 -1
- utils/analyzer.py +41 -112
- utils/io.py +3 -13
- utils/lua.py +11 -345
- utils/parser.py +145 -177
.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}
|
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,
|
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,
|
238 |
self.pre_record(parser)
|
239 |
-
self.record(
|
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,
|
248 |
-
super().record(
|
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.
|
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 (
|
266 |
return
|
267 |
-
|
268 |
-
|
269 |
-
parser.
|
270 |
-
|
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,
|
277 |
-
super().record(
|
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 |
-
|
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,
|
298 |
-
|
299 |
-
|
|
|
300 |
if tick := parser.current_next_dot.pop(self.skill_id, None):
|
301 |
-
|
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] =
|
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,
|
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,
|
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,
|
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,
|
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,
|
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.
|
25 |
with gr.Tab("战斗统计"):
|
26 |
with gr.Row():
|
27 |
-
self.summary = gr.DataFrame(label="战斗总结", headers=["技能/次数", "命中/%", "会心/%", "伤害/%"],
|
|
|
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 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
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 |
-
|
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
|
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"{
|
26 |
return "\n".join(content)
|
27 |
|
28 |
|
29 |
-
def summary_content(
|
30 |
-
content =
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
42 |
)
|
|
|
43 |
return content
|
44 |
|
45 |
|
46 |
-
def gradient_content(
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
51 |
|
52 |
|
53 |
-
def detail_content(
|
54 |
damage_detail = "\n".join([
|
55 |
-
"
|
56 |
-
"
|
57 |
-
"
|
58 |
-
"
|
59 |
-
"
|
60 |
-
"
|
|
|
61 |
])
|
62 |
|
63 |
-
damage_gradient = gradient_content(
|
64 |
|
65 |
-
return damage_detail
|
|
|
|
|
|
|
|
|
|
|
66 |
|
67 |
|
68 |
def combat_script(
|
@@ -71,9 +113,10 @@ def combat_script(
|
|
71 |
# consumables: Consumables, bonuses: Bonuses
|
72 |
combat_component: CombatComponent,
|
73 |
):
|
74 |
-
|
|
|
|
|
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 |
-
|
105 |
|
106 |
for gain in gains:
|
107 |
gain.sub(attribute, school.skills, school.buffs)
|
108 |
|
109 |
-
|
|
|
110 |
|
111 |
-
combat_update[combat_component.gradient] = gradient_content(
|
112 |
|
113 |
-
combat_update[combat_component.
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
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.
|
135 |
-
combat_component.damage_detail, combat_component.
|
136 |
-
]
|
137 |
)
|
138 |
|
139 |
-
def skill_changed(skill
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
143 |
|
144 |
combat_component.skill_select.change(
|
145 |
-
skill_changed,
|
|
|
146 |
)
|
147 |
|
148 |
-
def status_changed(
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
return gr.update(value=
|
|
|
155 |
|
156 |
combat_component.status_select.change(
|
157 |
-
status_changed,
|
158 |
-
[combat_component.damage_detail, combat_component.
|
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 |
-
|
41 |
-
|
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 |
-
|
51 |
-
return player_select_update,
|
52 |
|
53 |
top_component.upload_log.upload(
|
54 |
upload_log, top_component.upload_log,
|
55 |
-
[top_component.player_select, top_component.
|
56 |
)
|
57 |
|
58 |
-
def load_json(file_path):
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
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
|
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 =
|
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
|
2 |
lua = """
|
3 |
|
4 |
"""
|
5 |
-
result =
|
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.
|
|
|
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.
|
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.
|
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.
|
11 |
-
self.
|
|
|
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.
|
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.
|
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 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
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.
|
|
|
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.
|
|
|
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.
|
13 |
-
parser.
|
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.
|
22 |
for level in range(buff_level):
|
23 |
-
parser.
|
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.
|
|
|
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 |
-
|
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 |
-
|
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
|
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
|
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 |
-
|
57 |
-
final_buffs.append(buff)
|
58 |
for buff in current_buffs:
|
59 |
-
|
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
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
|
|
100 |
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
summary = {}
|
105 |
-
duration *= FRAME_PER_SECOND
|
106 |
|
107 |
-
for
|
108 |
-
skill_id, skill_level, skill_stack =
|
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 |
-
|
114 |
-
|
115 |
-
|
116 |
-
if
|
117 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
|
164 |
-
|
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 |
-
_, _,
|
|
|
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 =
|
13 |
for player_id in records:
|
14 |
-
|
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 |
-
|
2 |
-
|
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 |
-
|
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
|
|
|
2 |
from collections import defaultdict
|
3 |
|
|
|
|
|
|
|
4 |
from base.constant import FRAME_PER_SECOND
|
5 |
from schools import *
|
6 |
-
from utils.lua import
|
7 |
|
8 |
-
FRAME_TYPE,
|
9 |
-
PLAYER_ID_TYPE,
|
10 |
CASTER_ID_TYPE = PLAYER_ID_TYPE | PET_ID_TYPE
|
11 |
-
|
|
|
|
|
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 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
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 |
-
|
47 |
|
48 |
-
id2name: Dict[
|
49 |
-
name2id: Dict[
|
50 |
pet2employer: Dict[PET_ID_TYPE, PLAYER_ID_TYPE]
|
51 |
|
52 |
-
records: Dict[PLAYER_ID_TYPE,
|
53 |
|
54 |
-
|
55 |
-
|
56 |
|
57 |
-
|
58 |
buff_intervals: Dict[CASTER_ID_TYPE, Dict[BUFF_TYPE, FRAME_TYPE]]
|
59 |
-
|
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 |
-
|
65 |
|
66 |
-
|
67 |
|
68 |
-
last_dot: Dict[TARGET_ID_TYPE, Dict[PLAYER_ID_TYPE, Dict[SKILL_ID_TYPE,
|
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]
|
|
|
|
|
|
|
|
|
91 |
|
92 |
@property
|
93 |
-
def
|
94 |
-
return self.
|
95 |
|
96 |
@property
|
97 |
def current_buff_intervals(self):
|
98 |
return self.buff_intervals[self.current_player]
|
99 |
|
100 |
@property
|
101 |
-
def
|
102 |
-
return self.
|
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
|
110 |
-
|
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
|
121 |
-
return self.
|
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.
|
142 |
|
143 |
self.id2name = {}
|
144 |
self.name2id = {}
|
145 |
self.pet2employer = {}
|
146 |
|
147 |
-
self.records = defaultdict(
|
148 |
|
149 |
-
self.
|
150 |
-
self.
|
151 |
-
|
152 |
-
self.buff_stacks = defaultdict(dict)
|
153 |
self.buff_intervals = defaultdict(dict)
|
154 |
-
self.
|
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.
|
161 |
-
self.
|
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.
|
177 |
if stack:
|
178 |
-
self.
|
179 |
if buff.interval > 0:
|
180 |
self.current_buff_intervals[buff_tuple] = self.current_frame + buff.interval + 1
|
181 |
else:
|
182 |
-
self.
|
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.
|
189 |
if stack:
|
190 |
-
self.
|
191 |
if buff.interval > 0:
|
192 |
self.current_target_buff_intervals[buff_tuple] = self.current_frame + buff.interval + 1
|
193 |
else:
|
194 |
-
self.
|
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.
|
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.
|
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,
|
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
|
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 |
-
|
266 |
-
pet_id = detail[0]
|
267 |
if player_id := self.pet2employer.get(pet_id):
|
268 |
-
if self.
|
269 |
-
pet_buff_stacks = self.
|
270 |
else:
|
271 |
pet_buff_stacks = {}
|
272 |
-
self.
|
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.
|
336 |
-
|
337 |
-
elif self.
|
338 |
-
|
339 |
else:
|
340 |
-
|
341 |
-
self.
|
342 |
else:
|
343 |
player_id = caster_id
|
344 |
-
|
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 |
-
|
359 |
else:
|
360 |
-
|
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
|
|
|
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 |
-
|
389 |
-
|
390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
391 |
current_status = []
|
392 |
-
for (buff_id, buff_level), buff_stack in self.
|
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
|
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.
|
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
|
414 |
target_status.append((buff_id, buff_level, buff_stack))
|
415 |
-
|
416 |
-
|
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.
|
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 |
-
|
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()
|
|
|
|
|
|
|
|
|
|
|
|