mirror of
https://github.com/PaiGramTeam/PaiGram.git
synced 2024-12-29 12:22:36 +00:00
406 lines
17 KiB
Python
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,
|
|
)
|