Formulator / utils /parser.py
ango
04.26 commit
60d173b
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, ling_hai_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
PHYSICAL_DISPLAY_ATTRS = {
"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": "破招",
}
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": "力道", **PHYSICAL_DISPLAY_ATTRS}
),
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": "身法", **PHYSICAL_DISPLAY_ATTRS}
),
10533: School(
school="蓬莱", major="身法", kind="外功", attribute=ling_hai_jue.LingHaiJue, formation="墟海引归阵",
skills=ling_hai_jue.SKILLS, buffs=ling_hai_jue.BUFFS,
talent_gains=ling_hai_jue.TALENT_GAINS, talents=ling_hai_jue.TALENTS,
talent_decoder=ling_hai_jue.TALENT_DECODER, talent_encoder=ling_hai_jue.TALENT_ENCODER,
recipe_gains=ling_hai_jue.RECIPE_GAINS, recipes=ling_hai_jue.RECIPES,
gains=ling_hai_jue.GAINS, display_attrs={"agility": "身法", **PHYSICAL_DISPLAY_ATTRS}
),
}
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]
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.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]
if isinstance(row[4], list):
select_equipment['embed_levels'] = [EMBED_MAPPING.get(tuple(e), 0) for e in row[4]]
else:
select_equipment['embed_levels'] = []
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" and len(self.start_time[player_id]) == len(self.end_time[player_id]):
self.start_time[player_id].append(int(timestamp))
self.records[player_id].append(defaultdict(lambda: defaultdict(list)))
elif detail[1] == "false" and len(self.start_time[player_id]) - len(self.end_time[player_id]) == 1:
self.end_time[player_id].append(int(timestamp))
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 player_id not in self.school:
return
react, skill_id, skill_level, critical = int(detail[2]), int(detail[4]), int(detail[5]), detail[6] == "true"
if react or skill_id not in self.school[player_id].skills:
return
if len(self.start_time[player_id]) == len(self.end_time[player_id]):
self.start_time[player_id].append(int(timestamp))
self.records[player_id].append(defaultdict(lambda: defaultdict(list)))
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][-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][-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
}