Formulator / utils /parser.py
AngoHF's picture
04.23 commit
4079f21
raw
history blame
13 kB
from dataclasses import dataclass
from typing import Dict, List, Type, Union, Tuple
from collections import defaultdict
from base.attribute import Attribute
from base.buff import Buff
from base.gain import Gain
from base.skill import Skill, DotSkill, DotConsumeSkill, Damage, DotDamage
from schools import bei_ao_jue, shan_hai_xin_jue
from utils.lua import parse
SKILL_TYPE = Tuple[int, int, int]
BUFF_TYPE = Tuple[int, int, int]
TIMELINE_TYPE = List[Tuple[int, bool]]
SUB_RECORD_TYPE = Dict[Tuple[tuple, tuple], TIMELINE_TYPE]
RECORD_TYPE = Dict[SKILL_TYPE, SUB_RECORD_TYPE]
STATUS_TYPE = Dict[Tuple[int, int], int]
SNAPSHOT_TYPE = Dict[int, STATUS_TYPE]
@dataclass
class School:
school: str
major: str
kind: str
attribute: Type[Attribute]
formation: str
skills: Dict[int, Skill]
buffs: Dict[int, Buff]
talent_gains: Dict[int, Gain]
talents: List[List[int]]
talent_decoder: Dict[int, str]
talent_encoder: Dict[str, int]
recipe_gains: Dict[str, Dict[str, Gain]]
recipes: Dict[str, List[str]]
gains: Dict[Union[Tuple[int, int], int], Gain]
display_attrs: Dict[str, str]
def attr_content(self, attribute):
content = []
for attr, name in self.display_attrs.items():
value = getattr(attribute, attr)
if isinstance(value, int):
content.append([name, f"{value}"])
else:
content.append([name, f"{round(value * 100, 2)}%"])
return content
SUPPORT_SCHOOL = {
10464: School(
school="霸刀",
major="力道",
kind="外功",
attribute=bei_ao_jue.BeiAoJue,
formation="霜岚洗锋阵",
skills=bei_ao_jue.SKILLS,
buffs=bei_ao_jue.BUFFS,
talent_gains=bei_ao_jue.TALENT_GAINS,
talents=bei_ao_jue.TALENTS,
talent_decoder=bei_ao_jue.TALENT_DECODER,
talent_encoder=bei_ao_jue.TALENT_ENCODER,
recipe_gains=bei_ao_jue.RECIPE_GAINS,
recipes=bei_ao_jue.RECIPES,
gains=bei_ao_jue.GAINS,
display_attrs={
"strength": "力道",
"base_physical_attack_power": "基础攻击",
"physical_attack_power": "攻击",
"base_physical_critical_strike": "会心等级",
"physical_critical_strike": "会心",
"physical_critical_power_base": "会效等级",
"physical_critical_power": "会效",
"base_physical_overcome": "基础破防",
"final_physical_overcome": "最终破防",
"physical_overcome": "破防",
"weapon_damage_base": "基础武器伤害",
"weapon_damage_rand": "浮动武器伤害",
"strain_base": "无双等级",
"strain": "无双",
"surplus": "破招",
}
),
10756: School(
school="万灵",
major="身法",
kind="外功",
attribute=shan_hai_xin_jue.ShanHaiXinJue,
formation="苍梧引灵阵",
skills=shan_hai_xin_jue.SKILLS,
buffs=shan_hai_xin_jue.BUFFS,
talent_gains=shan_hai_xin_jue.TALENT_GAINS,
talents=shan_hai_xin_jue.TALENTS,
talent_decoder=shan_hai_xin_jue.TALENT_DECODER,
talent_encoder=shan_hai_xin_jue.TALENT_ENCODER,
recipe_gains=shan_hai_xin_jue.RECIPE_GAINS,
recipes=shan_hai_xin_jue.RECIPES,
gains=shan_hai_xin_jue.GAINS,
display_attrs={
"agility": "身法",
"base_physical_attack_power": "基础攻击",
"physical_attack_power": "攻击",
"base_physical_critical_strike": "会心等级",
"physical_critical_strike": "会心",
"physical_critical_power_base": "会效等级",
"physical_critical_power": "会效",
"base_physical_overcome": "基础破防",
"final_physical_overcome": "最终破防",
"physical_overcome": "破防",
"weapon_damage_base": "基础武器伤害",
"weapon_damage_rand": "浮动武器伤害",
"strain_base": "无双等级",
"strain": "无双",
"surplus": "破招",
}
)
}
LABEL_MAPPING = {
2: "远程武器",
3: "上衣",
4: "帽子",
5: "项链",
6: "戒指1",
7: "戒指2",
8: "腰带",
9: "腰坠",
10: "下装",
11: "鞋子",
12: "护腕",
0: "近战武器"
}
EMBED_MAPPING = {(5, 24449 - i): 8 - i for i in range(8)}
class Parser:
current_player: int
id2name: Dict[int, str]
name2id: Dict[str, int]
records: Dict[int, List[RECORD_TYPE]]
status: Dict[int, STATUS_TYPE]
snapshot: Dict[int, SNAPSHOT_TYPE]
last_dot: Dict[int, Dict[int, Tuple[Tuple[int, int, int], Tuple[tuple, tuple]]]]
stacks: Dict[int, Dict[int, int]]
ticks: Dict[int, Dict[int, int]]
pets: Dict[int, int]
fight_flag: Dict[int, bool]
start_time: Dict[int, List[int]]
end_time: Dict[int, List[int]]
record_index: Dict[int, Dict[str, int]]
select_talents: Dict[int, List[int]]
select_equipments: Dict[int, Dict[int, Dict[str, int | list]]]
school: Dict[int, School]
def duration(self, player_id, i):
return round((self.end_time[player_id][i] - self.start_time[player_id][i]) / 1000, 3)
def available_status(self, player_id, skill_id):
current_status = []
for (buff_id, buff_level), buff_stack in self.status[player_id].items():
buff = self.school[player_id].buffs[buff_id]
if buff.gain_attributes:
current_status.append((buff_id, buff_level, buff_stack))
elif buff.gain_skills and skill_id in buff.gain_skills:
current_status.append((buff_id, buff_level, buff_stack))
snapshot_status = []
for (buff_id, buff_level), buff_stack in self.snapshot[player_id].get(skill_id, {}).items():
buff = self.school[player_id].buffs[buff_id]
if buff.gain_attributes:
snapshot_status.append((buff_id, buff_level, buff_stack))
elif buff.gain_skills and skill_id in buff.gain_skills:
snapshot_status.append((buff_id, buff_level, buff_stack))
return tuple(current_status), tuple(snapshot_status)
def reset(self):
self.id2name = {}
self.name2id = {}
self.records = defaultdict(list)
self.status = defaultdict(dict)
self.snapshot = defaultdict(dict)
self.last_dot = defaultdict(dict)
self.stacks = defaultdict(lambda: defaultdict(lambda: 1))
self.ticks = defaultdict(lambda: defaultdict(int))
self.pets = {}
self.fight_flag = defaultdict(bool)
self.start_time = defaultdict(list)
self.end_time = defaultdict(list)
self.select_talents = {}
self.select_equipments = {}
self.school = {}
@staticmethod
def parse_equipments(detail):
select_equipments = {}
for row in detail:
if not (label := LABEL_MAPPING.get(row[0])):
continue
select_equipment = select_equipments[label] = {}
select_equipment['equipment'] = row[2]
select_equipment['strength_level'] = row[3]
select_equipment['embed_levels'] = [EMBED_MAPPING.get(tuple(e), 0) for e in row[4]]
select_equipment['enchant'] = row[5]
return select_equipments
@staticmethod
def parse_talents(detail):
return [row[1] for row in detail]
def parse_info(self, row):
detail = row.strip("{}").split(",")
player_id, school_id = int(detail[0]), int(detail[3])
if player_id in self.id2name or school_id not in SUPPORT_SCHOOL:
return
if isinstance(detail := parse(row), list):
player_name = detail[1]
self.id2name[player_id] = player_name
self.name2id[player_name] = player_id
if school := SUPPORT_SCHOOL.get(detail[3]):
self.school[player_id] = school
self.select_equipments[player_id] = self.parse_equipments(detail[5])
self.select_talents[player_id] = self.parse_talents(detail[6])
def parse_pet(self, row):
detail = row.strip("{}").split(",")
pet_id, player_id = int(detail[0]), int(detail[3])
if player_id in self.school:
self.pets[pet_id] = player_id
def parse_time(self, row, timestamp):
detail = row.strip("{}").split(",")
player_id = int(detail[0])
if player_id not in self.school:
return
if detail[1] == "true":
self.start_time[player_id].append(int(timestamp))
self.records[player_id].append(defaultdict(lambda: defaultdict(list)))
self.fight_flag[player_id] = True
else:
self.end_time[player_id].append(int(timestamp))
self.fight_flag[player_id] = False
def parse_buff(self, row):
detail = row.strip("{}").split(",")
player_id = int(detail[0])
if player_id not in self.school:
return
buff_id, buff_stack, buff_level = int(detail[4]), int(detail[5]), int(detail[8])
if buff_id not in self.school[player_id].buffs:
return
if not buff_stack:
self.status[player_id].pop((buff_id, buff_level), None)
else:
self.status[player_id][(buff_id, buff_level)] = buff_stack
def parse_skill(self, row, timestamp):
detail = row.strip("{}").split(",")
caster_id = int(detail[0])
if caster_id in self.pets:
player_id = self.pets[caster_id]
else:
player_id = caster_id
if not self.fight_flag[player_id] or player_id not in self.school:
return
skill_id, skill_level, critical = int(detail[4]), int(detail[5]), detail[6] == "true"
if skill_id not in self.school[player_id].skills:
return
timestamp = int(timestamp) - self.start_time[player_id][-1]
skill_stack = self.stacks[player_id][skill_id]
skill = self.school[player_id].skills[skill_id]
if isinstance(skill, DotSkill):
bind_skill = skill.bind_skill
if not self.ticks[player_id][bind_skill]:
self.stacks[player_id][bind_skill] = 0
self.ticks[player_id][bind_skill] = skill.tick
self.stacks[player_id][bind_skill] = min(self.stacks[player_id][bind_skill] + 1, skill.max_stack)
self.snapshot[player_id][bind_skill] = self.status[player_id].copy()
elif isinstance(skill, DotConsumeSkill):
bind_skill = skill.bind_skill
skill_tuple, status_tuple = self.last_dot[player_id][bind_skill]
skill_id, skill_level, skill_stack = skill_tuple
self.ticks[player_id][skill_id] += 1
tick = min(self.ticks[player_id][skill_id], skill.tick)
current_record = self.records[player_id][len(self.start_time) - 1]
current_record[(skill_id, skill_level, skill_stack * tick)][status_tuple].append(
current_record[skill_tuple][status_tuple].pop()
)
self.ticks[player_id][skill_id] -= tick
elif isinstance(skill, Damage):
skill_tuple = (skill_id, skill_level, skill_stack)
status_tuple = self.available_status(player_id, skill_id)
current_record = self.records[player_id][len(self.start_time) - 1]
current_record[skill_tuple][status_tuple].append((timestamp, critical))
if isinstance(skill, DotDamage):
self.last_dot[player_id][skill_id] = (skill_tuple, status_tuple)
self.ticks[player_id][skill_id] -= 1
def __call__(self, file_name):
self.reset()
lines = open(file_name).readlines()
for line in lines:
row = line.split("\t")
if row[4] == "4":
self.parse_info(row[-1])
for player_id, school in self.school.items():
for talent_id in self.select_talents[player_id]:
school.talent_gains[talent_id].add_skills(school.skills)
for line in lines:
row = line.split("\t")
if row[4] == "5":
self.parse_time(row[-1], row[3])
if row[4] == "8":
self.parse_pet(row[-1])
elif row[4] == "13":
self.parse_buff(row[-1])
elif row[4] == "21":
self.parse_skill(row[-1], row[3])
for player_id, school in self.school.items():
for talent_id in self.select_talents[player_id]:
school.talent_gains[talent_id].sub_skills(school.skills)
self.record_index = {
player_id: {
f"{i + 1}:{round((end_time - self.start_time[player_id][i]) / 1000, 3)}": i
for i, end_time in enumerate(self.end_time[player_id])
}
for player_id in self.end_time
}