ango commited on
Commit
3393f9d
·
1 Parent(s): 0f5c288

04.20 commit

Browse files
base/buff.py CHANGED
@@ -29,7 +29,7 @@ class Buff:
29
 
30
  @property
31
  def display_name(self):
32
- return f"{self.buff_name}/{self.buff_id}-{self.buff_level}-{self.buff_stack}"
33
 
34
  def level_value(self, value):
35
  if isinstance(value, list):
 
29
 
30
  @property
31
  def display_name(self):
32
+ return f"{self.buff_name}#{self.buff_id}-{self.buff_level}-{self.buff_stack}"
33
 
34
  def level_value(self, value):
35
  if isinstance(value, list):
base/skill.py CHANGED
@@ -40,7 +40,7 @@ class Skill:
40
 
41
  @property
42
  def display_name(self):
43
- return f"{self.skill_name}/{self.skill_id}-{self.skill_level}-{self.skill_stack}"
44
 
45
  @property
46
  def damage_base(self):
 
40
 
41
  @property
42
  def display_name(self):
43
+ return f"{self.skill_name}#{self.skill_id}-{self.skill_level}-{self.skill_stack}"
44
 
45
  @property
46
  def damage_base(self):
qt/app.py CHANGED
@@ -73,7 +73,8 @@ class MainWindow(QMainWindow):
73
  self.equipments_widget, self.consumable_widget, self.bonus_widget
74
  )
75
  config_script(
76
- parser, self.config_widget, self.talents_widget, self.recipes_widget,
 
77
  self.equipments_widget, self.consumable_widget, self.bonus_widget
78
  )
79
  talents = talents_script(self.talents_widget)
@@ -81,7 +82,8 @@ class MainWindow(QMainWindow):
81
  equipments = equipments_script(self.equipments_widget)
82
  consumables = consumables_script(self.consumable_widget)
83
  bonuses = bonuses_script(parser, self.bonus_widget)
84
- dashboard_script(parser, self.dashboard_widget, talents, recipes, equipments, consumables, bonuses)
 
85
 
86
 
87
  if __name__ == "__main__":
 
73
  self.equipments_widget, self.consumable_widget, self.bonus_widget
74
  )
75
  config_script(
76
+ parser, self.config_widget,
77
+ self.talents_widget, self.recipes_widget,
78
  self.equipments_widget, self.consumable_widget, self.bonus_widget
79
  )
80
  talents = talents_script(self.talents_widget)
 
82
  equipments = equipments_script(self.equipments_widget)
83
  consumables = consumables_script(self.consumable_widget)
84
  bonuses = bonuses_script(parser, self.bonus_widget)
85
+ dashboard_script(parser, self.top_widget, self.dashboard_widget,
86
+ talents, recipes, equipments, consumables, bonuses)
87
 
88
 
89
  if __name__ == "__main__":
qt/components/dashboard.py CHANGED
@@ -9,9 +9,9 @@ class DetailWidget(QWidget):
9
  super().__init__()
10
  layout = QVBoxLayout(self)
11
  self.details = {}
12
- self.skill_combo = ComboWithLabel("选择技能", info="技能名字/技能ID-技能等级-技能层数")
13
  layout.addWidget(self.skill_combo)
14
- self.status_combo = ComboWithLabel("选择增益", info="增益名字/增益ID-增益等级-增益层数")
15
  layout.addWidget(self.status_combo)
16
  detail_table = QWidget()
17
  detail_table_layout = QHBoxLayout(detail_table)
 
9
  super().__init__()
10
  layout = QVBoxLayout(self)
11
  self.details = {}
12
+ self.skill_combo = ComboWithLabel("选择技能", info="技能名字#技能ID-技能等级-技能层数")
13
  layout.addWidget(self.skill_combo)
14
+ self.status_combo = ComboWithLabel("选择增益", info="增益名字#增益ID-增益等级-增益层数")
15
  layout.addWidget(self.status_combo)
16
  detail_table = QWidget()
17
  detail_table_layout = QHBoxLayout(detail_table)
qt/components/top.py CHANGED
@@ -1,12 +1,18 @@
1
- from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton
 
 
 
2
 
3
 
4
  class TopWidget(QWidget):
5
  def __init__(self):
6
  super().__init__()
7
- layout = QVBoxLayout()
8
  self.setLayout(layout)
9
 
10
  self.upload_button = QPushButton("请上传JCL")
11
-
12
  layout.addWidget(self.upload_button)
 
 
 
 
 
1
+ from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton
2
+ from PySide6.QtCore import Qt
3
+
4
+ from qt.components import ComboWithLabel
5
 
6
 
7
  class TopWidget(QWidget):
8
  def __init__(self):
9
  super().__init__()
10
+ layout = QHBoxLayout()
11
  self.setLayout(layout)
12
 
13
  self.upload_button = QPushButton("请上传JCL")
 
14
  layout.addWidget(self.upload_button)
15
+ self.player_select = ComboWithLabel("请选择角色")
16
+ layout.addWidget(self.player_select)
17
+ self.player_select.hide()
18
+ layout.setAlignment(Qt.AlignmentFlag.AlignTop)
qt/scripts/bonuses.py CHANGED
@@ -20,13 +20,13 @@ class Bonuses(dict):
20
  def bonuses_script(parser: Parser, bonuses_widget: BonusesWidget):
21
  bonuses = Bonuses()
22
 
23
- def formation_update(arg):
24
  widget = bonuses_widget.formation
25
  formation = widget.formation.combo_box.currentText()
26
  core_rate = widget.core_rate.spin_box.value()
27
  formation_rate = widget.rate.spin_box.value()
28
 
29
- if formation == parser.school.formation:
30
  widget.core_rate.show()
31
  else:
32
  core_rate = 0
 
20
  def bonuses_script(parser: Parser, bonuses_widget: BonusesWidget):
21
  bonuses = Bonuses()
22
 
23
+ def formation_update(_):
24
  widget = bonuses_widget.formation
25
  formation = widget.formation.combo_box.currentText()
26
  core_rate = widget.core_rate.spin_box.value()
27
  formation_rate = widget.rate.spin_box.value()
28
 
29
+ if formation == parser.school[parser.current_player].formation:
30
  widget.core_rate.show()
31
  else:
32
  core_rate = 0
qt/scripts/config.py CHANGED
@@ -8,6 +8,7 @@ from qt.components.equipments import EquipmentsWidget
8
  from qt.components.consumables import ConsumablesWidget
9
  from qt.components.recipes import RecipesWidget
10
  from qt.components.talents import TalentsWidget
 
11
  from utils.parser import Parser
12
 
13
  if not os.path.exists("config"):
@@ -78,7 +79,8 @@ def config_script(
78
 
79
  def load_config():
80
  config_name = config_widget.config_select.combo_box.currentText()
81
- config = CONFIG.get(parser.school.school, {}).get(config_name, {})
 
82
  if not config:
83
  return
84
  category = config_widget.config_category.combo_box.currentText()
@@ -149,10 +151,12 @@ def config_script(
149
 
150
  def save_config():
151
  config_name = config_widget.config_name.text_browser.text()
152
- if parser.school.school not in CONFIG:
153
- CONFIG[parser.school.school] = {}
154
- if config_name not in CONFIG[parser.school.school]:
155
- CONFIG[parser.school.school][config_name] = {
 
 
156
  "equipments": {},
157
  "consumables": {},
158
  "bonuses": {"formation": {}, "team_gains": {}},
@@ -160,28 +164,30 @@ def config_script(
160
  "recipes": []
161
  }
162
 
163
- save_equipments(CONFIG[parser.school.school][config_name]['equipments'])
164
- save_consumables(CONFIG[parser.school.school][config_name]['consumables'])
165
- save_bonuses(CONFIG[parser.school.school][config_name]['bonuses'])
166
  # save_recipes(CONFIG[parser.school.school][config_name]['recipes'])
167
  json.dump(CONFIG, open("config", "w", encoding="utf-8"), ensure_ascii=False)
168
 
169
  config_widget.config_select.set_items(
170
- list(CONFIG.get(parser.school.school, {})), keep_index=True, default_index=-1
171
  )
172
 
173
  config_widget.save_config.clicked.connect(save_config)
174
 
175
  def delete_config():
176
  config_name = config_widget.config_name.text_browser.text()
177
- if config_name not in CONFIG.get(parser.school.school, {}):
 
 
178
  return
179
 
180
- CONFIG[parser.school.school].pop(config_name)
181
 
182
  json.dump(CONFIG, open("config", "w", encoding="utf-8"), ensure_ascii=False)
183
 
184
- config_widget.config_select.set_items(list(CONFIG.get(parser.school.school, {})), default_index=-1)
185
  config_widget.config_name.text_browser.clear()
186
 
187
  config_widget.delete_config.clicked.connect(delete_config)
 
8
  from qt.components.consumables import ConsumablesWidget
9
  from qt.components.recipes import RecipesWidget
10
  from qt.components.talents import TalentsWidget
11
+ from qt.components.top import TopWidget
12
  from utils.parser import Parser
13
 
14
  if not os.path.exists("config"):
 
79
 
80
  def load_config():
81
  config_name = config_widget.config_select.combo_box.currentText()
82
+ player_id = parser.current_player
83
+ config = CONFIG.get(parser.school[player_id].school, {}).get(config_name, {})
84
  if not config:
85
  return
86
  category = config_widget.config_category.combo_box.currentText()
 
151
 
152
  def save_config():
153
  config_name = config_widget.config_name.text_browser.text()
154
+ player_id = parser.current_player
155
+ school = parser.school[player_id].school
156
+ if school not in CONFIG:
157
+ CONFIG[school] = {}
158
+ if config_name not in CONFIG[school]:
159
+ CONFIG[school][config_name] = {
160
  "equipments": {},
161
  "consumables": {},
162
  "bonuses": {"formation": {}, "team_gains": {}},
 
164
  "recipes": []
165
  }
166
 
167
+ save_equipments(CONFIG[school][config_name]['equipments'])
168
+ save_consumables(CONFIG[school][config_name]['consumables'])
169
+ save_bonuses(CONFIG[school][config_name]['bonuses'])
170
  # save_recipes(CONFIG[parser.school.school][config_name]['recipes'])
171
  json.dump(CONFIG, open("config", "w", encoding="utf-8"), ensure_ascii=False)
172
 
173
  config_widget.config_select.set_items(
174
+ list(CONFIG.get(school, {})), keep_index=True, default_index=-1
175
  )
176
 
177
  config_widget.save_config.clicked.connect(save_config)
178
 
179
  def delete_config():
180
  config_name = config_widget.config_name.text_browser.text()
181
+ player_id = parser.current_player
182
+ school = parser.school[player_id].school
183
+ if config_name not in CONFIG.get(school, {}):
184
  return
185
 
186
+ CONFIG[school].pop(config_name)
187
 
188
  json.dump(CONFIG, open("config", "w", encoding="utf-8"), ensure_ascii=False)
189
 
190
+ config_widget.config_select.set_items(list(CONFIG.get(school, {})), default_index=-1)
191
  config_widget.config_name.text_browser.clear()
192
 
193
  config_widget.delete_config.clicked.connect(delete_config)
qt/scripts/dashboard.py CHANGED
@@ -1,6 +1,7 @@
1
  from typing import Dict
2
 
3
  from qt.components.dashboard import DashboardWidget
 
4
  from qt.constant import ATTR_TYPE_TRANSLATE
5
  from qt.scripts.bonuses import Bonuses
6
  from qt.scripts.consumables import Consumables
@@ -45,21 +46,24 @@ def detail_content(detail: Detail):
45
  return damage_content, gradient_content
46
 
47
 
48
- def dashboard_script(parser: Parser,
49
  dashboard_widget: DashboardWidget, talents: Talents, recipes: Recipes,
50
  equipments: Equipments, consumables: Consumables, bonuses: Bonuses):
51
 
52
  def select_fight(text):
53
- index = parser.record_index[text]
54
- dashboard_widget.duration.set_value(parser.duration(index))
 
55
 
56
  dashboard_widget.fight_select.combo_box.currentTextChanged.connect(select_fight)
57
 
58
  def formulate():
59
  duration = dashboard_widget.duration.spin_box.value()
60
- record = parser.records[parser.record_index[dashboard_widget.fight_select.combo_box.currentText()]]
 
 
61
 
62
- school = parser.school
63
  attribute = school.attribute()
64
  attribute.target_level = int(dashboard_widget.target_level.combo_box.currentText())
65
  for attr, value in equipments.attrs.items():
 
1
  from typing import Dict
2
 
3
  from qt.components.dashboard import DashboardWidget
4
+ from qt.components.top import TopWidget
5
  from qt.constant import ATTR_TYPE_TRANSLATE
6
  from qt.scripts.bonuses import Bonuses
7
  from qt.scripts.consumables import Consumables
 
46
  return damage_content, gradient_content
47
 
48
 
49
+ def dashboard_script(parser: Parser, top_widget: TopWidget,
50
  dashboard_widget: DashboardWidget, talents: Talents, recipes: Recipes,
51
  equipments: Equipments, consumables: Consumables, bonuses: Bonuses):
52
 
53
  def select_fight(text):
54
+ player_id = parser.current_player
55
+ index = parser.record_index[player_id][text]
56
+ dashboard_widget.duration.set_value(parser.duration(player_id, index))
57
 
58
  dashboard_widget.fight_select.combo_box.currentTextChanged.connect(select_fight)
59
 
60
  def formulate():
61
  duration = dashboard_widget.duration.spin_box.value()
62
+ player_id = parser.current_player
63
+ record_index = dashboard_widget.fight_select.combo_box.currentText()
64
+ record = parser.records[player_id][parser.record_index[player_id][record_index]]
65
 
66
+ school = parser.school[player_id]
67
  attribute = school.attribute()
68
  attribute.target_level = int(dashboard_widget.target_level.combo_box.currentText())
69
  for attr, value in equipments.attrs.items():
qt/scripts/top.py CHANGED
@@ -27,22 +27,35 @@ def top_script(
27
  if not file_name[0]:
28
  return
29
  parser(file_name[0])
30
- school = parser.school
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  """ Update config """
32
  config_choices = list(CONFIG.get(school.school, {}))
33
  config_widget.config_select.set_items(config_choices, default_index=-1)
34
  """ Update dashboard """
35
- record_index = list(parser.record_index)
36
- dashboard_widget.fight_select.set_items(record_index)
37
- dashboard_widget.duration.set_value(parser.duration(parser.record_index[record_index[0]]))
38
 
39
  """ Update talent options """
40
  for i, talent_widget in enumerate(talents_widget.values()):
41
  talents = school.talents[i]
42
- default_index = talents.index(parser.select_talents[i]) + 1
43
  talent_widget.set_items(
44
- [""] + [school.talent_decoder[talent] for talent in talents],
45
- keep_index=True, default_index=default_index
46
  )
47
 
48
  """ Update recipe options """
@@ -71,7 +84,7 @@ def top_script(
71
  if not (current_index := equipment_widget.stone_level.combo_box.currentIndex()):
72
  current_index = MAX_STONE_LEVEL
73
  equipment_widget.stone_level.combo_box.setCurrentIndex(current_index)
74
- if select_equipment := parser.select_equipments.get(label, {}):
75
  if equipment := equipment_widget.equipment_mapping.get(select_equipment['equipment']):
76
  if equipment in equipment_widget.equipment.items:
77
  equipment_widget.equipment.combo_box.setCurrentText(equipment)
@@ -97,6 +110,6 @@ def top_script(
97
  config_widget.show()
98
  bottom_widget.show()
99
 
100
- top_widget.upload_button.clicked.connect(upload_logs)
101
 
102
  return parser
 
27
  if not file_name[0]:
28
  return
29
  parser(file_name[0])
30
+ top_widget.player_select.set_items(
31
+ [parser.id2name[player_id] for player_id in parser.school], keep_index=True, default_index=0
32
+ )
33
+ top_widget.player_select.show()
34
+ select_player(None)
35
+
36
+ top_widget.upload_button.clicked.connect(upload_logs)
37
+
38
+ def select_player(_):
39
+ player_name = top_widget.player_select.combo_box.currentText()
40
+ if not player_name:
41
+ return
42
+ player_id = parser.name2id[player_name]
43
+ parser.current_player = player_id
44
+ school = parser.school[player_id]
45
  """ Update config """
46
  config_choices = list(CONFIG.get(school.school, {}))
47
  config_widget.config_select.set_items(config_choices, default_index=-1)
48
  """ Update dashboard """
49
+ record_index = list(parser.record_index[player_id])
50
+ dashboard_widget.fight_select.set_items(record_index, default_index=0)
51
+ dashboard_widget.duration.set_value(parser.duration(player_id, parser.record_index[player_id][record_index[0]]))
52
 
53
  """ Update talent options """
54
  for i, talent_widget in enumerate(talents_widget.values()):
55
  talents = school.talents[i]
56
+ default_index = talents.index(parser.select_talents[player_id][i]) + 1
57
  talent_widget.set_items(
58
+ [""] + [school.talent_decoder[talent] for talent in talents], default_index=default_index
 
59
  )
60
 
61
  """ Update recipe options """
 
84
  if not (current_index := equipment_widget.stone_level.combo_box.currentIndex()):
85
  current_index = MAX_STONE_LEVEL
86
  equipment_widget.stone_level.combo_box.setCurrentIndex(current_index)
87
+ if select_equipment := parser.select_equipments[player_id].get(label, {}):
88
  if equipment := equipment_widget.equipment_mapping.get(select_equipment['equipment']):
89
  if equipment in equipment_widget.equipment.items:
90
  equipment_widget.equipment.combo_box.setCurrentText(equipment)
 
110
  config_widget.show()
111
  bottom_widget.show()
112
 
113
+ top_widget.player_select.combo_box.currentTextChanged.connect(select_player)
114
 
115
  return parser
utils/parser.py CHANGED
@@ -9,6 +9,14 @@ from base.skill import Skill
9
  from schools import bei_ao_jue
10
  from utils.lua import parse
11
 
 
 
 
 
 
 
 
 
12
 
13
  @dataclass
14
  class School:
@@ -75,7 +83,6 @@ SUPPORT_SCHOOL = {
75
  )
76
  }
77
 
78
-
79
  LABEL_MAPPING = {
80
  2: "远程武器",
81
  3: "上衣",
@@ -94,42 +101,40 @@ EMBED_MAPPING = {(5, 24449 - i): 8 - i for i in range(8)}
94
 
95
 
96
  class Parser:
97
- records: list
98
- status: dict
99
- snapshot: dict
100
- stacks: dict
101
- ticks: dict
102
-
103
- start_time: list
104
- end_time: list
105
- record_index: Dict[str, int]
106
-
107
- fight_flag: bool
108
 
109
- select_talents: List[int]
110
- select_equipments: Dict[int, Dict[str, int | list]]
 
 
111
 
112
- school: School | None
 
113
 
114
- def duration(self, i):
115
- return round((self.end_time[i] - self.start_time[i]) / 1000, 3)
116
 
117
- @property
118
- def current_record(self):
119
- return self.records[len(self.start_time) - 1]
120
 
121
- def available_status(self, skill_id):
122
  current_status = []
123
- for (buff_id, buff_level), buff_stack in self.status.items():
124
- buff = self.school.buffs[buff_id]
125
  if buff.gain_attributes:
126
  current_status.append((buff_id, buff_level, buff_stack))
127
  elif buff.gain_skills and skill_id in buff.gain_skills:
128
  current_status.append((buff_id, buff_level, buff_stack))
129
 
130
  snapshot_status = []
131
- for (buff_id, buff_level), buff_stack in self.snapshot.get(skill_id, {}).items():
132
- buff = self.school.buffs[buff_id]
133
  if buff.gain_attributes:
134
  snapshot_status.append((buff_id, buff_level, buff_stack))
135
  elif buff.gain_skills and skill_id in buff.gain_skills:
@@ -138,103 +143,125 @@ class Parser:
138
  return tuple(current_status), tuple(snapshot_status)
139
 
140
  def reset(self):
141
- self.fight_flag = False
142
-
143
- self.records = []
144
- self.status = {}
145
- self.snapshot = {}
146
- self.stacks = defaultdict(int)
147
- self.ticks = defaultdict(int)
148
-
149
- self.start_time = []
150
- self.end_time = []
151
-
152
- self.record_index = {}
153
-
154
- self.school = None
155
-
156
- def parse_equipments(self, detail):
157
  self.select_equipments = {}
 
 
 
 
 
158
  for row in detail:
159
  if not (label := LABEL_MAPPING.get(row[0])):
160
  continue
161
- select_equipment = self.select_equipments[label] = {}
162
  select_equipment['equipment'] = row[2]
163
  select_equipment['strength_level'] = row[3]
164
  select_equipment['embed_levels'] = [EMBED_MAPPING.get(tuple(e), 0) for e in row[4]]
165
  select_equipment['enchant'] = row[5]
 
166
 
167
- def parse_info(self, detail):
168
- if isinstance(detail, list):
169
- self.school = SUPPORT_SCHOOL.get(detail[3])
170
- if not self.school:
171
- raise AttributeError(f"Cannot support {detail[3]} now")
172
- self.parse_equipments(detail[5])
173
- self.select_talents = [row[1] for row in detail[6]]
174
- return self.school
175
-
176
- def parse_time(self, detail, timestamp):
177
- if detail[1]:
178
- self.start_time.append(int(timestamp))
179
- self.records.append({})
180
- self.fight_flag = True
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  else:
182
- self.end_time.append(int(timestamp))
183
- self.fight_flag = False
184
 
185
  def parse_buff(self, row):
186
- detail = row.split(",")
 
 
 
187
  buff_id, buff_stack, buff_level = int(detail[4]), int(detail[5]), int(detail[8])
188
- if buff_id not in self.school.buffs:
189
  return
190
  if not buff_stack:
191
- self.status.pop((buff_id, buff_level))
192
  else:
193
- self.status[(buff_id, buff_level)] = buff_stack
194
 
195
  def parse_skill(self, row, timestamp):
196
- detail = row.split(",")
 
 
 
197
  skill_id, skill_level, critical = int(detail[4]), int(detail[5]), detail[6] == "true"
198
- if skill_id not in self.school.skills:
199
  return
200
- timestamp = int(timestamp) - self.start_time[-1]
201
- skill_stack = max(1, self.stacks[skill_id])
202
- if self.ticks[skill_id]:
203
- self.ticks[skill_id] -= 1
204
- if not self.ticks[skill_id]:
205
- self.stacks[skill_id] = 0
206
 
207
  skill_tuple = (skill_id, skill_level, skill_stack)
208
- skill = self.school.skills[skill_id]
209
  if bind_skill := skill.bind_skill:
210
- self.stacks[bind_skill] = min(self.stacks[bind_skill] + 1, skill.max_stack)
211
- self.ticks[bind_skill] = skill.tick if not self.ticks[bind_skill] else skill.tick - 1
212
- self.snapshot[bind_skill] = self.status.copy()
213
  else:
214
- if skill_tuple not in self.current_record:
215
- self.current_record[skill_tuple] = {}
216
- status_tuple = self.available_status(skill_id)
217
- if status_tuple not in self.current_record[skill_tuple]:
218
- self.current_record[skill_tuple][status_tuple] = []
219
- self.current_record[skill_tuple][status_tuple].append((timestamp, critical))
220
 
221
  def __call__(self, file_name):
222
  self.reset()
223
  lines = open(file_name).readlines()
224
  for line in lines:
225
  row = line.split("\t")
226
- if row[4] == "4" and self.parse_info(parse(row[-1])):
227
- break
228
-
229
  for line in lines:
230
  row = line.split("\t")
231
  if row[4] == "5":
232
- self.parse_time(parse(row[-1]), row[3])
233
  elif row[4] == "13":
234
  self.parse_buff(row[-1])
235
- elif row[4] == "21" and self.fight_flag:
236
  self.parse_skill(row[-1], row[3])
237
 
238
  self.record_index = {
239
- f"{i + 1}:{round((end_time - self.start_time[i]) / 1000, 3)}": i for i, end_time in enumerate(self.end_time)
 
 
 
 
240
  }
 
9
  from schools import bei_ao_jue
10
  from utils.lua import parse
11
 
12
+ SKILL_TYPE = Tuple[int, int, int]
13
+ BUFF_TYPE = Tuple[int, int, int]
14
+ TIMELINE_TYPE = List[Tuple[int, bool]]
15
+ SUB_RECORD_TYPE = Dict[Tuple[tuple, tuple], TIMELINE_TYPE]
16
+ RECORD_TYPE = Dict[SKILL_TYPE, SUB_RECORD_TYPE]
17
+ STATUS_TYPE = Dict[Tuple[int, int], int]
18
+ SNAPSHOT_TYPE = Dict[int, STATUS_TYPE]
19
+
20
 
21
  @dataclass
22
  class School:
 
83
  )
84
  }
85
 
 
86
  LABEL_MAPPING = {
87
  2: "远程武器",
88
  3: "上衣",
 
101
 
102
 
103
  class Parser:
104
+ current_player: int
105
+ id2name: Dict[int, str]
106
+ name2id: Dict[str, int]
107
+ records: Dict[int, List[RECORD_TYPE]]
108
+ status: Dict[int, STATUS_TYPE]
109
+ snapshot: Dict[int, SNAPSHOT_TYPE]
110
+ stacks: Dict[int, Dict[int, int]]
111
+ ticks: Dict[int, Dict[int, int]]
 
 
 
112
 
113
+ fight_flag: Dict[int, bool]
114
+ start_time: Dict[int, List[int]]
115
+ end_time: Dict[int, List[int]]
116
+ record_index: Dict[int, Dict[str, int]]
117
 
118
+ select_talents: Dict[int, List[int]]
119
+ select_equipments: Dict[int, Dict[int, Dict[str, int | list]]]
120
 
121
+ school: Dict[int, School]
 
122
 
123
+ def duration(self, player_id, i):
124
+ return round((self.end_time[player_id][i] - self.start_time[player_id][i]) / 1000, 3)
 
125
 
126
+ def available_status(self, player_id, skill_id):
127
  current_status = []
128
+ for (buff_id, buff_level), buff_stack in self.status[player_id].items():
129
+ buff = self.school[player_id].buffs[buff_id]
130
  if buff.gain_attributes:
131
  current_status.append((buff_id, buff_level, buff_stack))
132
  elif buff.gain_skills and skill_id in buff.gain_skills:
133
  current_status.append((buff_id, buff_level, buff_stack))
134
 
135
  snapshot_status = []
136
+ for (buff_id, buff_level), buff_stack in self.snapshot[player_id].get(skill_id, {}).items():
137
+ buff = self.school[player_id].buffs[buff_id]
138
  if buff.gain_attributes:
139
  snapshot_status.append((buff_id, buff_level, buff_stack))
140
  elif buff.gain_skills and skill_id in buff.gain_skills:
 
143
  return tuple(current_status), tuple(snapshot_status)
144
 
145
  def reset(self):
146
+ self.id2name = {}
147
+ self.name2id = {}
148
+ self.records = defaultdict(list)
149
+ self.status = defaultdict(dict)
150
+ self.snapshot = defaultdict(dict)
151
+ self.stacks = defaultdict(lambda: defaultdict(lambda: 1))
152
+ self.ticks = defaultdict(lambda: defaultdict(int))
153
+
154
+ self.fight_flag = defaultdict(bool)
155
+ self.start_time = defaultdict(list)
156
+ self.end_time = defaultdict(list)
157
+
158
+ self.select_talents = {}
 
 
 
159
  self.select_equipments = {}
160
+ self.school = {}
161
+
162
+ @staticmethod
163
+ def parse_equipments(detail):
164
+ select_equipments = {}
165
  for row in detail:
166
  if not (label := LABEL_MAPPING.get(row[0])):
167
  continue
168
+ select_equipment = select_equipments[label] = {}
169
  select_equipment['equipment'] = row[2]
170
  select_equipment['strength_level'] = row[3]
171
  select_equipment['embed_levels'] = [EMBED_MAPPING.get(tuple(e), 0) for e in row[4]]
172
  select_equipment['enchant'] = row[5]
173
+ return select_equipments
174
 
175
+ @staticmethod
176
+ def parse_talents(detail):
177
+ return [row[1] for row in detail]
178
+
179
+ def parse_info(self, row):
180
+ detail = row.strip("{}").split(",")
181
+ player_id, school_id = int(detail[0]), int(detail[3])
182
+ if player_id in self.id2name or school_id not in SUPPORT_SCHOOL:
183
+ return
184
+ if isinstance(detail := parse(row), list):
185
+ player_name = detail[1]
186
+ self.id2name[player_id] = player_name
187
+ self.name2id[player_name] = player_id
188
+ if school := SUPPORT_SCHOOL.get(detail[3]):
189
+ self.school[player_id] = school
190
+ self.select_equipments[player_id] = self.parse_equipments(detail[5])
191
+ self.select_talents[player_id] = self.parse_talents(detail[6])
192
+
193
+ def parse_time(self, row, timestamp):
194
+ detail = row.strip("{}").split(",")
195
+ player_id = int(detail[0])
196
+ if player_id not in self.school:
197
+ return
198
+ if detail[1] == "true":
199
+ self.start_time[player_id].append(int(timestamp))
200
+ self.records[player_id].append(defaultdict(lambda: defaultdict(list)))
201
+ self.fight_flag[player_id] = True
202
  else:
203
+ self.end_time[player_id].append(int(timestamp))
204
+ self.fight_flag[player_id] = False
205
 
206
  def parse_buff(self, row):
207
+ detail = row.strip("{}").split(",")
208
+ player_id = int(detail[0])
209
+ if player_id not in self.school:
210
+ return
211
  buff_id, buff_stack, buff_level = int(detail[4]), int(detail[5]), int(detail[8])
212
+ if buff_id not in self.school[player_id].buffs:
213
  return
214
  if not buff_stack:
215
+ self.status[player_id].pop((buff_id, buff_level), None)
216
  else:
217
+ self.status[player_id][(buff_id, buff_level)] = buff_stack
218
 
219
  def parse_skill(self, row, timestamp):
220
+ detail = row.strip("{}").split(",")
221
+ player_id = int(detail[0])
222
+ if not self.fight_flag[player_id] or player_id not in self.school:
223
+ return
224
  skill_id, skill_level, critical = int(detail[4]), int(detail[5]), detail[6] == "true"
225
+ if skill_id not in self.school[player_id].skills:
226
  return
227
+ timestamp = int(timestamp) - self.start_time[player_id][-1]
228
+ skill_stack = self.stacks[player_id][skill_id]
229
+ if self.ticks[player_id][skill_id]:
230
+ self.ticks[player_id][skill_id] -= 1
231
+ if not self.ticks[player_id][skill_id]:
232
+ self.stacks[player_id].pop(skill_id)
233
 
234
  skill_tuple = (skill_id, skill_level, skill_stack)
235
+ skill = self.school[player_id].skills[skill_id]
236
  if bind_skill := skill.bind_skill:
237
+ self.stacks[player_id][bind_skill] = min(self.stacks[player_id][bind_skill] + 1, skill.max_stack)
238
+ self.ticks[player_id][bind_skill] = skill.tick if not self.ticks[player_id][bind_skill] else skill.tick - 1
239
+ self.snapshot[player_id][bind_skill] = self.status[player_id].copy()
240
  else:
241
+ current_record = self.records[player_id][len(self.start_time) - 1]
242
+ status_tuple = self.available_status(player_id, skill_id)
243
+ current_record[skill_tuple][status_tuple].append((timestamp, critical))
 
 
 
244
 
245
  def __call__(self, file_name):
246
  self.reset()
247
  lines = open(file_name).readlines()
248
  for line in lines:
249
  row = line.split("\t")
250
+ if row[4] == "4":
251
+ self.parse_info(row[-1])
 
252
  for line in lines:
253
  row = line.split("\t")
254
  if row[4] == "5":
255
+ self.parse_time(row[-1], row[3])
256
  elif row[4] == "13":
257
  self.parse_buff(row[-1])
258
+ elif row[4] == "21":
259
  self.parse_skill(row[-1], row[3])
260
 
261
  self.record_index = {
262
+ player_id: {
263
+ f"{i + 1}:{round((end_time - self.start_time[player_id][i]) / 1000, 3)}": i
264
+ for i, end_time in enumerate(self.end_time[player_id])
265
+ }
266
+ for player_id in self.end_time
267
  }