MibooGram/plugins/genshin/model/converters/gcsim.py

406 lines
17 KiB
Python

import re
from collections import Counter
from decimal import Decimal
from functools import lru_cache
from typing import List, Optional, Tuple, Dict
from gcsim_pypi.aliases import CHARACTER_ALIASES, WEAPON_ALIASES, ARTIFACT_ALIASES
from pydantic import ValidationError
from plugins.genshin.model import (
Set,
Weapon,
DigitType,
WeaponInfo,
Artifact,
ArtifactAttributeType,
Character,
CharacterInfo,
GCSim,
GCSimTarget,
GCSimWeapon,
GCSimWeaponInfo,
GCSimSet,
GCSimSetInfo,
GCSimCharacter,
GCSimEnergySettings,
GCSimCharacterInfo,
GCSimCharacterStats,
)
from plugins.genshin.model.metadata import Metadata
from utils.log import logger
metadata = Metadata()
def remove_non_words(text: str) -> str:
return text.replace("'", "").replace('"', "").replace("-", "").replace(" ", "")
def from_character_gcsim_character(character: Character) -> GCSimCharacter:
if character == "Raiden Shogun":
return GCSimCharacter("raiden")
if character == "Yae Miko":
return GCSimCharacter("yaemiko")
if character == "Hu Tao":
return GCSimCharacter("hutao")
if character == "Yun Jin":
return GCSimCharacter("yunjin")
if character == "Kuki Shinobu":
return GCSimCharacter("kuki")
if "Traveler" in character:
s = character.split(" ")
traveler_name = "aether" if s[-1] == "Boy" else "lumine"
return GCSimCharacter(f"{traveler_name}{s[0].lower()}")
return GCSimCharacter(character.split(" ")[-1].lower())
GCSIM_CHARACTER_TO_CHARACTER: Dict[GCSimCharacter, Tuple[int, Character]] = {}
for char in metadata.characters_metadata.values():
GCSIM_CHARACTER_TO_CHARACTER[from_character_gcsim_character(char["route"])] = (char["id"], char["route"])
for alias, char in CHARACTER_ALIASES.items():
if alias not in GCSIM_CHARACTER_TO_CHARACTER:
if char in GCSIM_CHARACTER_TO_CHARACTER:
GCSIM_CHARACTER_TO_CHARACTER[alias] = GCSIM_CHARACTER_TO_CHARACTER[char]
elif alias.startswith("traveler") or alias.startswith("aether") or alias.startswith("lumine"):
continue
else:
logger.warning("Character alias %s not found in GCSIM", alias)
GCSIM_WEAPON_TO_WEAPON: Dict[GCSimWeapon, Tuple[int, Weapon]] = {}
for _weapon in metadata.weapon_metadata.values():
GCSIM_WEAPON_TO_WEAPON[remove_non_words(_weapon["route"].lower())] = (_weapon["id"], _weapon["route"])
for alias, _weapon in WEAPON_ALIASES.items():
if alias not in GCSIM_WEAPON_TO_WEAPON:
if _weapon in GCSIM_WEAPON_TO_WEAPON:
GCSIM_WEAPON_TO_WEAPON[alias] = GCSIM_WEAPON_TO_WEAPON[_weapon]
else:
logger.warning("Weapon alias %s not found in GCSIM", alias)
GCSIM_ARTIFACT_TO_ARTIFACT: Dict[GCSimSet, Tuple[int, Set]] = {}
for _artifact in metadata.artifacts_metadata.values():
GCSIM_ARTIFACT_TO_ARTIFACT[remove_non_words(_artifact["route"].lower())] = (_artifact["id"], _artifact["route"])
for alias, _artifact in ARTIFACT_ALIASES.items():
if alias not in GCSIM_ARTIFACT_TO_ARTIFACT:
if _artifact in GCSIM_ARTIFACT_TO_ARTIFACT:
GCSIM_ARTIFACT_TO_ARTIFACT[alias] = GCSIM_ARTIFACT_TO_ARTIFACT[_artifact]
else:
logger.warning("Artifact alias %s not found in GCSIM", alias)
class GCSimConverter:
literal_keys_numeric_values_regex = re.compile(
r"([\w_%]+)=(\d+ *, *\d+ *, *\d+|[\d*\.*\d+]+ *, *[\d*\.*\d+]+|\d+/\d+|\d*\.*\d+|\d+)"
)
@classmethod
def to_character(cls, character: GCSimCharacter) -> Tuple[int, Character]:
return GCSIM_CHARACTER_TO_CHARACTER[character]
@classmethod
def from_character(cls, character: Character) -> GCSimCharacter:
return from_character_gcsim_character(character)
@classmethod
def to_weapon(cls, weapon: GCSimWeapon) -> Tuple[int, Weapon]:
return GCSIM_WEAPON_TO_WEAPON[weapon]
@classmethod
def from_weapon(cls, weapon: Weapon) -> GCSimWeapon:
return GCSimWeapon(remove_non_words(weapon).lower())
@classmethod
def from_weapon_info(cls, weapon_info: Optional[WeaponInfo]) -> GCSimWeaponInfo:
if weapon_info is None:
return GCSimWeaponInfo(weapon=GCSimWeapon("dullblade"), refinement=1, level=1, max_level=20)
return GCSimWeaponInfo(
weapon=cls.from_weapon(weapon_info.weapon),
refinement=weapon_info.refinement,
level=weapon_info.level,
max_level=weapon_info.max_level,
)
@classmethod
def to_set(cls, set_name: GCSimSet) -> Tuple[int, Set]:
return GCSIM_ARTIFACT_TO_ARTIFACT[set_name]
@classmethod
def from_set(cls, set_name: Set) -> GCSimSet:
return GCSimSet(remove_non_words(set_name).lower())
@classmethod
def from_artifacts(cls, artifacts: List[Artifact]) -> List[GCSimSetInfo]:
c = Counter()
for art in artifacts:
c[cls.from_set(art.set)] += 1
return [GCSimSetInfo(set=set_name, count=count) for set_name, count in c.items()]
@classmethod
@lru_cache
def from_attribute_type(cls, attribute_type: ArtifactAttributeType) -> str: # skipcq: PY-R1000
if attribute_type == ArtifactAttributeType.HP:
return "HP"
if attribute_type == ArtifactAttributeType.HP_PERCENT:
return "HP_PERCENT"
if attribute_type == ArtifactAttributeType.ATK:
return "ATK"
if attribute_type == ArtifactAttributeType.ATK_PERCENT:
return "ATK_PERCENT"
if attribute_type == ArtifactAttributeType.DEF:
return "DEF"
if attribute_type == ArtifactAttributeType.DEF_PERCENT:
return "DEF_PERCENT"
if attribute_type == ArtifactAttributeType.ELEMENTAL_MASTERY:
return "EM"
if attribute_type == ArtifactAttributeType.ENERGY_RECHARGE:
return "ER"
if attribute_type == ArtifactAttributeType.CRIT_RATE:
return "CR"
if attribute_type == ArtifactAttributeType.CRIT_DMG:
return "CD"
if attribute_type == ArtifactAttributeType.HEALING_BONUS:
return "HEAL"
if attribute_type == ArtifactAttributeType.PYRO_DMG_BONUS:
return "PYRO_PERCENT"
if attribute_type == ArtifactAttributeType.HYDRO_DMG_BONUS:
return "HYDRO_PERCENT"
if attribute_type == ArtifactAttributeType.DENDRO_DMG_BONUS:
return "DENDRO_PERCENT"
if attribute_type == ArtifactAttributeType.ELECTRO_DMG_BONUS:
return "ELECTRO_PERCENT"
if attribute_type == ArtifactAttributeType.ANEMO_DMG_BONUS:
return "ANEMO_PERCENT"
if attribute_type == ArtifactAttributeType.CRYO_DMG_BONUS:
return "CRYO_PERCENT"
if attribute_type == ArtifactAttributeType.GEO_DMG_BONUS:
return "GEO_PERCENT"
if attribute_type == ArtifactAttributeType.PHYSICAL_DMG_BONUS:
return "PHYS_PERCENT"
raise ValueError(f"Unknown attribute type: {attribute_type}")
@classmethod
def from_artifacts_stats(cls, artifacts: List[Artifact]) -> GCSimCharacterStats:
gcsim_stats = GCSimCharacterStats()
for art in artifacts:
main_attr_name = cls.from_attribute_type(art.main_attribute.type)
setattr(
gcsim_stats,
main_attr_name,
getattr(gcsim_stats, main_attr_name)
+ (
Decimal(art.main_attribute.digit.value) / Decimal(100)
if art.main_attribute.digit.type == DigitType.PERCENT
else Decimal(art.main_attribute.digit.value)
),
)
for sub_attr in art.sub_attributes:
attr_name = cls.from_attribute_type(sub_attr.type)
setattr(
gcsim_stats,
attr_name,
getattr(gcsim_stats, attr_name)
+ (
Decimal(sub_attr.digit.value) / Decimal(100)
if sub_attr.digit.type == DigitType.PERCENT
else Decimal(sub_attr.digit.value)
),
)
return gcsim_stats
@classmethod
def from_character_info(cls, character: CharacterInfo) -> GCSimCharacterInfo:
return GCSimCharacterInfo(
character=cls.from_character(character.character),
level=character.level,
max_level=character.max_level,
constellation=character.constellation,
talent=character.skills,
weapon_info=cls.from_weapon_info(character.weapon_info),
set_info=cls.from_artifacts(character.artifacts),
# NOTE: Only stats from arifacts are needed
stats=cls.from_artifacts_stats(character.artifacts),
)
@classmethod
def merge_character_infos(cls, gcsim: GCSim, character_infos: List[CharacterInfo]) -> GCSim:
gcsim_characters = {ch.character: ch for ch in gcsim.characters}
for character_info in character_infos:
try:
gcsim_character = cls.from_character_info(character_info)
if gcsim_character.character in gcsim_characters:
gcsim_characters[gcsim_character.character] = gcsim_character
except ValidationError as e:
errors = e.errors()
if errors and errors[0].get("msg").startswith("Not supported"):
# Something is not supported, skip
continue
logger.warning("Failed to convert character info: %s", character_info)
gcsim.characters = list(gcsim_characters.values())
return gcsim
@classmethod
def prepend_scripts(cls, gcsim: GCSim, scripts: List[str]) -> GCSim:
gcsim.scripts = scripts + gcsim.scripts
return gcsim
@classmethod
def append_scripts(cls, gcsim: GCSim, scripts: List[str]) -> GCSim:
gcsim.scripts = gcsim.scripts + scripts
return gcsim
@classmethod
def from_gcsim_energy(cls, line: str) -> GCSimEnergySettings:
energy_settings = GCSimEnergySettings()
matches = cls.literal_keys_numeric_values_regex.findall(line)
for key, value in matches:
if key == "interval":
energy_settings.intervals = list(map(int, value.split(",")))
elif key == "amount":
energy_settings.amount = int(value)
else:
logger.warning("Unknown energy setting: %s=%s", key, value)
return energy_settings
@classmethod
def from_gcsim_target(cls, line: str) -> GCSimTarget:
target = GCSimTarget()
matches = cls.literal_keys_numeric_values_regex.findall(line)
for key, value in matches:
if key == "lvl":
target.level = int(value)
elif key == "hp":
target.hp = int(value)
elif key == "amount":
target.amount = int(value)
elif key == "resist":
target.resist = float(value)
elif key == "pos":
target.position = tuple(p for p in value.split(","))
elif key == "interval":
target.interval = list(map(int, value.split(",")))
elif key == "radius":
target.radius = float(value)
elif key == "particle_threshold":
target.particle_threshold = float(value)
elif key == "particle_drop_count":
target.particle_drop_count = float(value)
elif key in ("pyro", "hydro", "dendro", "electro", "anemo", "cryo", "geo", "physical"):
target.others[key] = float(value)
else:
logger.warning("Unknown target setting: %s=%s", key, value)
return target
@classmethod
def from_gcsim_char_line(cls, line: str, character: GCSimCharacterInfo) -> GCSimCharacterInfo:
matches = cls.literal_keys_numeric_values_regex.findall(line)
for key, value in matches:
if key == "lvl":
character.level, character.max_level = map(int, value.split("/"))
elif key == "cons":
character.constellation = int(value)
elif key == "talent":
character.talent = list(map(int, value.split(",")))
elif key == "start_hp":
character.start_hp = int(value)
elif key == "breakthrough":
character.params.append(f"{key}={value}")
else:
logger.warning("Unknown character setting: %s=%s", key, value)
return character
@classmethod
def from_gcsim_weapon_line(cls, line: str, weapon_info: GCSimWeaponInfo) -> GCSimWeaponInfo:
weapon_name = re.search(r"weapon= *\"(.*)\"", line).group(1)
if weapon_name not in WEAPON_ALIASES:
raise ValueError(f"Unknown weapon: {weapon_name}")
weapon_info.weapon = WEAPON_ALIASES[weapon_name]
for key, value in cls.literal_keys_numeric_values_regex.findall(line):
if key == "refine":
weapon_info.refinement = int(value)
elif key == "lvl":
weapon_info.level, weapon_info.max_level = map(int, value.split("/"))
elif key.startswith("stack"):
weapon_info.params.append(f"stacks={value}")
elif key in ("pickup_delay", "breakthrough"):
weapon_info.params.append(f"{key}={value}")
else:
logger.warning("Unknown weapon setting: %s=%s", key, value)
return weapon_info
@classmethod
def from_gcsim_set_line(cls, line: str) -> GCSimSetInfo:
gcsim_set = re.search(r"set= *\"(.*)\"", line).group(1)
if gcsim_set not in ARTIFACT_ALIASES:
raise ValueError(f"Unknown set: {gcsim_set}")
gcsim_set = ARTIFACT_ALIASES[gcsim_set]
set_info = GCSimSetInfo(set=gcsim_set)
for key, value in cls.literal_keys_numeric_values_regex.findall(line):
if key == "count":
set_info.count = int(value)
elif key.startswith("stack"):
set_info.params.append(f"stacks={value}")
else:
logger.warning("Unknown set info: %s=%s", key, value)
return set_info
@classmethod
def from_gcsim_stats_line(cls, line: str, stats: GCSimCharacterStats) -> GCSimCharacterStats:
matches = re.findall(r"(\w+%?)=(\d*\.*\d+)", line)
for stat, value in matches:
attr = stat.replace("%", "_percent").upper()
setattr(stats, attr, getattr(stats, attr) + Decimal(value))
return stats
@classmethod
def from_gcsim_script(cls, script: str) -> GCSim: # skipcq: PY-R1000
options = ""
characters = {}
character_aliases = {}
active_character = None
targets = []
energy_settings = GCSimEnergySettings()
script_lines = []
for line in script.strip().split("\n"):
line = line.split("#")[0].strip()
if not line or line.startswith("#"):
continue
if line.startswith("options"):
options = line.strip(";")
elif line.startswith("target"):
targets.append(cls.from_gcsim_target(line))
elif line.startswith("energy"):
energy_settings = cls.from_gcsim_energy(line)
elif line.startswith("active"):
active_character = line.strip(";").split(" ")[1]
elif m := re.match(r"(\w+) +(char|add weapon|add set|add stats)\W", line):
if m.group(1) not in CHARACTER_ALIASES:
raise ValueError(f"Unknown character: {m.group(1)}")
c = CHARACTER_ALIASES[m.group(1)]
if c not in characters:
characters[c] = GCSimCharacterInfo(character=c)
if m.group(1) != c:
character_aliases[m.group(1)] = c
if m.group(2) == "char":
characters[c] = cls.from_gcsim_char_line(line, characters[c])
elif m.group(2) == "add weapon":
characters[c].weapon_info = cls.from_gcsim_weapon_line(line, characters[c].weapon_info)
elif m.group(2) == "add set":
characters[c].set_info.append(cls.from_gcsim_set_line(line))
elif m.group(2) == "add stats":
characters[c].stats = cls.from_gcsim_stats_line(line, characters[c].stats)
else:
for key, value in character_aliases.items():
line = line.replace(f"{key} ", f"{value} ")
line = line.replace(f".{key}.", f".{value}.")
script_lines.append(line)
return GCSim(
options=options,
characters=list(characters.values()),
targets=targets,
energy_settings=energy_settings,
active_character=active_character,
script_lines=script_lines,
)