Refactor genshin artifact core

Co-authored-by: kotoriのねこ <minamiktr@outlook.com>
This commit is contained in:
luoshuijs 2023-11-08 00:42:44 +08:00 committed by GitHub
parent 5f7033d331
commit 6847ba685a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1718 additions and 460 deletions

View File

@ -67,21 +67,19 @@ jobs:
shell: bash shell: bash
run: | run: |
source activate_env.sh source activate_env.sh
cd python_genshin_artifact_core
maturin develop maturin develop
- name: Export install file - name: Export install file
shell: bash shell: bash
run: | run: |
source activate_env.sh source activate_env.sh
cd python_genshin_artifact_core
maturin build --out ./dist maturin build --out ./dist
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: genshin-artifact-core name: genshin-artifact-core
path: python_genshin_artifact_core/dist path: ./dist
upload-core: upload-core:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -110,38 +108,3 @@ jobs:
env: env:
TWINE_USERNAME: __token__ TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
build-artifact:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: 3.11
- name: Install Poetry and Twine
run: |
pip install poetry twine
- name: Install dependencies
run: poetry install
- name: Build wheel
run: poetry build
- name: upload to pypi
if: startsWith(github.ref, 'refs/tags/')
run: twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
- name: upload to artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v3
with:
name: Python-Genshin-Artifact
path: dist/*.whl

38
Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "python_genshin_artifact"
version = "0.1.4"
edition = "2021"
license = "MIT"
include = [
"/pyproject.toml",
"/README.md",
"/LICENSE",
"/Makefile",
"/src",
"/watchfiles",
"/tests",
"/requirements",
"/.cargo",
"!__pycache__",
"!tests/.mypy_cache",
"!tests/.pytest_cache",
"!*.so",
]
[lib]
name = "_python_genshin_artifact"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.19.2", features = ["anyhow"] }
mona_wasm = { path = "genshin_artifact/mona_wasm" }
mona = { path = "genshin_artifact/mona_core" }
mona_generate = { path = "genshin_artifact/mona_generate" }
num = "0.4"
serde="1.0"
serde_json = "1.0"
anyhow = "1.0.75"
pythonize = "0.19.0"
[features]
default = ["pyo3/extension-module"]

2
dev-requirements.txt Normal file
View File

@ -0,0 +1,2 @@
black~=23.10.1
pytest~=4.1.0

View File

@ -1,32 +1,55 @@
[tool.poetry] [project]
name = "Python-Genshin-Artifact" name = "python_genshin_artifact"
requires-python = ">=3.8"
version = "0.1.4" version = "0.1.4"
description = "A Python library that binds to Genshin Artifact damage calculation and analysis engine." authors = [
authors = ["luoshuijs"] {name = "luoshuijs", email = "luoshuijs@outlook.com"},
license = "MIT license" {name = "kotori", email = "minamiktr@outlook.com"}
readme = "README.md" ]
packages = [ classifiers = [
{ include = "python_genshin_artifact" }, "Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Filesystems",
"Framework :: AnyIO",
]
dynamic = [
"license",
"readme",
"version"
] ]
[tool.poetry.dependencies] [tool.maturin]
python = "^3.8" module-name = "python_genshin_artifact._python_genshin_artifact"
pydantic = "^1.10.7" bindings = "pyo3"
#python-source = "python"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["maturin>=1.0,<2.0"]
build-backend = "poetry.core.masonry.api" build-backend = "maturin"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto"
log_cli = true log_cli = true
log_cli_level = "INFO" log_cli_level = "INFO"
log_cli_format = "%(message)s" log_cli_format = "%(message)s"
log_cli_date_format = "%Y-%m-%d %H:%M:%S" log_cli_date_format = "%Y-%m-%d %H:%M:%S"
[tool.black] [tool.black]
include = '\.pyi?$' include = '\.pyi?$'
line-length = 120 line-length = 120
target-version = ['py38'] target-version = ["py38"]

View File

@ -0,0 +1,37 @@
from ._python_genshin_artifact import (
get_damage_analysis,
get_transformative_damage,
gen_character_meta_as_json,
gen_weapon_meta_as_json,
gen_artifact_meta_as_json,
gen_generate_locale_as_json,
TransformativeDamage,
CharacterInterface,
WeaponInterface,
BuffInterface,
Artifact,
SkillInterface,
EnemyInterface,
CalculatorConfig,
DamageResult,
DamageAnalysis,
)
__all__ = (
"get_damage_analysis",
"get_transformative_damage",
"gen_character_meta_as_json",
"gen_weapon_meta_as_json",
"gen_artifact_meta_as_json",
"gen_generate_locale_as_json",
"TransformativeDamage",
"CharacterInterface",
"WeaponInterface",
"BuffInterface",
"Artifact",
"SkillInterface",
"EnemyInterface",
"CalculatorConfig",
"DamageResult",
"DamageAnalysis",
)

View File

@ -0,0 +1,234 @@
import sys
from typing import List, Optional, Tuple, TYPE_CHECKING, Dict, final
if sys.version_info < (3, 11):
from typing_extensions import Literal
else:
from typing import Literal
if TYPE_CHECKING:
StatName = Literal[
"ATKFixed",
"ATKPercentage",
"HealingBonus",
"HPFixed",
"HPPercentage",
"DEFFixed",
"DEFPercentage",
"CriticalRate",
"CriticalDamage",
"ElementalMastery",
"Recharge",
"ElectroBonus",
"PyroBonus",
"HydroBonus",
"CryoBonus",
"AnemoBonus",
"GeoBonus",
"DendroBonus",
"PhysicalBonus",
]
else:
StatName = str
def get_damage_analysis(calculator_config: "CalculatorConfig") -> "DamageAnalysis": ...
def get_transformative_damage(calculator_config: "CalculatorConfig") -> "TransformativeDamage": ...
def gen_character_meta_as_json() -> str: ...
def gen_weapon_meta_as_json() -> str: ...
def gen_artifact_meta_as_json() -> str: ...
def gen_generate_locale_as_json(loc: str) -> str: ...
@final
class TransformativeDamage:
swirl_cryo: float
swirl_hydro: float
swirl_pyro: float
swirl_electro: float
overload: float
electro_charged: float
shatter: float
super_conduct: float
bloom: float
hyper_bloom: float
burgeon: float
burning: float
crystallize: float
def __new__(
cls,
swirl_cryo: float,
swirl_hydro: float,
swirl_pyro: float,
swirl_electro: float,
overload: float,
electro_charged: float,
shatter: float,
super_conduct: float,
bloom: float,
hyper_bloom: float,
burgeon: float,
burning: float,
crystallize: float,
) -> "TransformativeDamage": ...
@final
class DamageResult:
critical: float
non_critical: float
expectation: float
is_heal: bool
is_shield: bool
def __new__(
cls, critical: float, non_critical: float, expectation: float, is_heal: bool, is_shield: bool
) -> "DamageResult": ...
@final
class DamageAnalysis:
atk: Dict[str, float]
atk_ratio: Dict[str, float]
hp: Dict[str, float]
hp_ratio: Dict[str, float]
defense: Dict[str, float]
def_ratio: Dict[str, float]
em: Dict[str, float]
em_ratio: Dict[str, float]
extra_damage: Dict[str, float]
bonus: Dict[str, float]
critical: Dict[str, float]
critical_damage: Dict[str, float]
melt_enhance: Dict[str, float]
vaporize_enhance: Dict[str, float]
healing_bonus: Dict[str, float]
shield_strength: Dict[str, float]
spread_compose: Dict[str, float]
aggravate_compose: Dict[str, float]
def_minus: Dict[str, float]
def_penetration: Dict[str, float]
res_minus: Dict[str, float]
element: str
is_heal: bool
is_shield: bool
normal: DamageResult
melt: Optional[DamageResult]
vaporize: Optional[DamageResult]
spread: Optional[DamageResult]
aggravate: Optional[DamageResult]
@final
class CharacterInterface:
name: str
level: int
ascend: bool
constellation: int
skill1: int
skill2: int
skill3: int
params: Optional[dict] = None
def __new__(
cls,
name: str,
level: int,
ascend: bool,
constellation: int,
skill1: int,
skill2: int,
skill3: int,
params: Optional[dict] = None,
) -> "CharacterInterface": ...
@final
class WeaponInterface:
name: str
level: int
ascend: bool
refine: int
params: Optional[dict] = None
def __new__(
cls, name: str, level: int, ascend: bool, refine: int, params: Optional[dict] = None
) -> "WeaponInterface": ...
@final
class BuffInterface:
name: str
config: Optional[dict] = None
def __new__(cls, name: str, config: Optional[dict] = None) -> "BuffInterface": ...
@final
class Artifact:
set_name: str
slot: str
level: int
star: int
sub_stats: List[Tuple[StatName, float]]
main_stat: Tuple[StatName, float]
id: int
def __new__(
cls,
set_name: str,
slot: str,
level: int,
star: int,
sub_stats: List[Tuple[StatName, float]],
main_stat: Tuple[StatName, float],
id: int,
) -> "Artifact": ...
@final
class SkillInterface:
index: int
config: Optional[dict] = None
def __new__(cls, index: int, config: Optional[dict] = None) -> "SkillInterface": ...
@final
class EnemyInterface:
level: int
electro_res: float
pyro_res: float
hydro_res: float
cryo_res: float
geo_res: float
anemo_res: float
dendro_res: float
physical_res: float
def __new__(
cls,
level: int,
electro_res: float,
pyro_res: float,
hydro_res: float,
cryo_res: float,
geo_res: float,
anemo_res: float,
dendro_res: float,
physical_res: float,
) -> "EnemyInterface": ...
@final
class CalculatorConfig:
character: CharacterInterface
weapon: WeaponInterface
buffs: List[BuffInterface] = []
artifacts: List[Artifact] = []
artifact_config: Optional[dict] = None
skill: SkillInterface
enemy: Optional[EnemyInterface] = None
def __new__(
cls,
character: CharacterInterface,
weapon: WeaponInterface,
skill: SkillInterface,
buffs: Optional[List[BuffInterface]] = None,
artifacts: Optional[List[Artifact]] = None,
artifact_config: Optional[dict] = None,
enemy: Optional[EnemyInterface] = None,
) -> "CalculatorConfig": ...

View File

@ -1,7 +1,7 @@
import json import json
from typing import Dict, Tuple, List from typing import Dict, Tuple, List
from genshin_artifact_core import ( from python_genshin_artifact import (
gen_character_meta_as_json, gen_character_meta_as_json,
gen_weapon_meta_as_json, gen_weapon_meta_as_json,
gen_artifact_meta_as_json, gen_artifact_meta_as_json,

View File

@ -1,26 +0,0 @@
from json import JSONDecodeError
from genshin_artifact_core import (
get_damage_analysis as _get_damage_analysis,
get_transformative_damage as _get_transformative_damage,
)
from python_genshin_artifact.error import JsonParseException
from python_genshin_artifact.models.calculator import CalculatorConfig
from python_genshin_artifact.models.damage.analysis import DamageAnalysis, TransformativeDamage
def get_damage_analysis(value: CalculatorConfig) -> DamageAnalysis:
try:
ret = _get_damage_analysis(value.json())
except JSONDecodeError as exc:
raise JsonParseException from exc
return DamageAnalysis.parse_raw(ret)
def get_transformative_damage(value: CalculatorConfig) -> TransformativeDamage:
try:
ret = _get_transformative_damage(value.json())
except JSONDecodeError as exc:
raise JsonParseException from exc
return TransformativeDamage.parse_raw(ret)

View File

@ -4,12 +4,10 @@ from typing import Tuple, List, Optional
from python_genshin_artifact.enka.artifacts import artifacts_name_map, equip_type_map from python_genshin_artifact.enka.artifacts import artifacts_name_map, equip_type_map
from python_genshin_artifact.enka.assets import Assets from python_genshin_artifact.enka.assets import Assets
from python_genshin_artifact.enka.characters import characters_map from python_genshin_artifact.enka.characters import characters_map
from python_genshin_artifact.enka.fight import fight_map, toFloat from python_genshin_artifact.enka.fight import fight_map, to_float
from python_genshin_artifact.enka.weapon import weapon_name_map from python_genshin_artifact.enka.weapon import weapon_name_map
from python_genshin_artifact.error import EnkaParseException from python_genshin_artifact.error import EnkaParseException
from python_genshin_artifact.models.artifact import ArtifactInfo from python_genshin_artifact import Artifact, CharacterInterface, WeaponInterface
from python_genshin_artifact.models.characterInfo import CharacterInfo
from python_genshin_artifact.models.weapon import WeaponInfo
assets = Assets() assets = Assets()
@ -23,7 +21,7 @@ def is_ascend(level: int, promote_level: int) -> bool:
return promote_level >= expected_promote_level return promote_level >= expected_promote_level
def enka_parser(data: dict, avatar_id: int) -> Tuple[CharacterInfo, WeaponInfo, List[ArtifactInfo]]: def enka_parser(data: dict, avatar_id: int) -> Tuple[CharacterInterface, WeaponInterface, List[Artifact]]:
character_info = assets.get_character(avatar_id) character_info = assets.get_character(avatar_id)
if character_info is None: if character_info is None:
raise EnkaParseException(f"avatarId={avatar_id} is not found in assets") raise EnkaParseException(f"avatarId={avatar_id} is not found in assets")
@ -51,7 +49,7 @@ def enka_parser(data: dict, avatar_id: int) -> Tuple[CharacterInfo, WeaponInfo,
if _value.endswith("02"): if _value.endswith("02"):
skill_info["skill3"] += 3 skill_info["skill3"] += 3
character_name = characters_map.get(avatar_id) character_name = characters_map.get(avatar_id)
character = CharacterInfo( character = CharacterInterface(
name=character_name, name=character_name,
level=level, level=level,
constellation=len(talent_id_list), constellation=len(talent_id_list),
@ -66,9 +64,9 @@ def enka_parser(data: dict, avatar_id: int) -> Tuple[CharacterInfo, WeaponInfo,
return character, weapon, artifacts return character, weapon, artifacts
def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInfo, List[ArtifactInfo]]: def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInterface, List[Artifact]]:
weapon: Optional[WeaponInfo] = None weapon: Optional[WeaponInterface] = None
artifacts: List[ArtifactInfo] = [] artifacts: List[Artifact] = []
for _equip in equip_list: for _equip in equip_list:
_weapon = _equip.get("weapon") _weapon = _equip.get("weapon")
_reliquary = _equip.get("reliquary") _reliquary = _equip.get("reliquary")
@ -86,18 +84,18 @@ def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInfo, List[ArtifactInfo
_reliquary_main_stat = _flat["reliquaryMainstat"] _reliquary_main_stat = _flat["reliquaryMainstat"]
_main_prop_id = _reliquary_main_stat["mainPropId"] _main_prop_id = _reliquary_main_stat["mainPropId"]
stat_name = fight_map[_main_prop_id] stat_name = fight_map[_main_prop_id]
stat_value = toFloat(_main_prop_id, _reliquary_main_stat["statValue"]) stat_value = to_float(_main_prop_id, _reliquary_main_stat["statValue"])
_main_stat = (stat_name, stat_value) _main_stat = (stat_name, stat_value)
for _reliquary_sub_stats in _flat["reliquarySubstats"]: for _reliquary_sub_stats in _flat["reliquarySubstats"]:
_append_prop_id = _reliquary_sub_stats["appendPropId"] _append_prop_id = _reliquary_sub_stats["appendPropId"]
stat_name = fight_map[_append_prop_id] stat_name = fight_map[_append_prop_id]
stat_value = toFloat(_append_prop_id, _reliquary_sub_stats["statValue"]) stat_value = to_float(_append_prop_id, _reliquary_sub_stats["statValue"])
_sub_stats = (stat_name, stat_value) _sub_stats = (stat_name, stat_value)
sub_stats.append(_sub_stats) sub_stats.append(_sub_stats)
slot = equip_type_map[_flat["equipType"]] slot = equip_type_map[_flat["equipType"]]
star = _flat["rankLevel"] star = _flat["rankLevel"]
artifacts.append( artifacts.append(
ArtifactInfo( Artifact(
set_name=set_name, set_name=set_name,
id=artifact_id, id=artifact_id,
level=_level, level=_level,
@ -118,5 +116,5 @@ def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInfo, List[ArtifactInfo
ascend = False ascend = False
if _promote_level is not None: if _promote_level is not None:
ascend = is_ascend(_level, _promote_level) ascend = is_ascend(_level, _promote_level)
weapon = WeaponInfo(level=_level, refine=refinement_level, ascend=ascend, name=weapon_name) weapon = WeaponInterface(level=_level, refine=refinement_level, ascend=ascend, name=weapon_name)
return weapon, artifacts return weapon, artifacts

View File

@ -1,4 +1,4 @@
from typing import Dict, List from typing import Dict, Set
fight_map: Dict[str, str] = { fight_map: Dict[str, str] = {
"FIGHT_PROP_ATTACK": "ATKFixed", "FIGHT_PROP_ATTACK": "ATKFixed",
@ -22,7 +22,7 @@ fight_map: Dict[str, str] = {
"FIGHT_PROP_GRASS_ADD_HURT": "DendroBonus", "FIGHT_PROP_GRASS_ADD_HURT": "DendroBonus",
} }
fixed: List[str] = { fixed: Set[str] = {
"FIGHT_PROP_ATTACK", "FIGHT_PROP_ATTACK",
"FIGHT_PROP_DEFENSE", "FIGHT_PROP_DEFENSE",
"FIGHT_PROP_HP", "FIGHT_PROP_HP",
@ -30,5 +30,5 @@ fixed: List[str] = {
} }
def toFloat(prop_id: str, pc: float) -> float: def to_float(prop_id: str, pc: float) -> float:
return pc if prop_id in fixed else (pc / 100) return pc if prop_id in fixed else (pc / 100)

View File

@ -1,13 +0,0 @@
from typing import List, Tuple
from pydantic import BaseModel
class ArtifactInfo(BaseModel):
set_name: str
slot: str
level: int
star: int
sub_stats: List[Tuple[str, float]]
main_stat: Tuple[str, float]
id: int

View File

@ -1,7 +0,0 @@
from pydantic import BaseModel
class BuffInfo(BaseModel):
name: str
config: str
star: int

View File

@ -1,20 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel
from python_genshin_artifact.models.artifact import ArtifactInfo
from python_genshin_artifact.models.buff import BuffInfo
from python_genshin_artifact.models.characterInfo import CharacterInfo
from python_genshin_artifact.models.enemy import EnemyInfo
from python_genshin_artifact.models.skill import SkillInfo
from python_genshin_artifact.models.weapon import WeaponInfo
class CalculatorConfig(BaseModel):
character: CharacterInfo
weapon: WeaponInfo
buffs: List[BuffInfo] = []
artifacts: List[ArtifactInfo] = []
artifact_config: Optional[dict] = None
skill: SkillInfo
enemy: Optional[EnemyInfo] = None

View File

@ -1,14 +0,0 @@
from typing import Union
from pydantic import BaseModel
class CharacterInfo(BaseModel):
name: str
level: int
ascend: bool
constellation: int
skill1: int
skill2: int
skill3: int
params: Union[str, dict] = "NoConfig"

View File

@ -1,56 +0,0 @@
from typing import Dict, Optional
from pydantic import BaseModel, Field
from python_genshin_artifact.models.damage.result import DamageResult
class DamageAnalysis(BaseModel):
atk: Dict[str, float]
atk_ratio: Dict[str, float]
hp: Dict[str, float]
hp_ratio: Dict[str, float]
def_: Dict[str, float] = Field(alias="def")
def_ratio: Dict[str, float]
em: Dict[str, float]
em_ratio: Dict[str, float]
extra_damage: Dict[str, float]
bonus: Dict[str, float]
critical: Dict[str, float]
critical_damage: Dict[str, float]
melt_enhance: Dict[str, float]
vaporize_enhance: Dict[str, float]
healing_bonus: Dict[str, float]
shield_strength: Dict[str, float]
spread_compose: Dict[str, float]
aggravate_compose: Dict[str, float]
def_minus: Dict[str, float]
def_penetration: Dict[str, float]
res_minus: Dict[str, float]
element: str
is_heal: bool
is_shield: bool
normal: DamageResult
melt: Optional[DamageResult]
vaporize: Optional[DamageResult]
spread: Optional[DamageResult]
aggravate: Optional[DamageResult]
class TransformativeDamage(BaseModel):
swirl_cryo: float
swirl_hydro: float
swirl_pyro: float
swirl_electro: float
overload: float
electro_charged: float
shatter: float
super_conduct: float = Field(alias="superconduct")
bloom: float
hyper_bloom: float = Field(alias="hyperbloom")
burgeon: float
burning: float
crystallize: float

View File

@ -1,10 +0,0 @@
from pydantic import BaseModel
class DamageResult(BaseModel):
critical: float
non_critical: float
expectation: float
is_heal: bool
is_shield: bool

View File

@ -1,13 +0,0 @@
from pydantic import BaseModel
class EnemyInfo(BaseModel):
level: int
electro_res: float
pyro_res: float
hydro_res: float
cryo_res: float
geo_res: float
anemo_res: float
dendro_res: float
physical_res: float

View File

@ -1,7 +0,0 @@
from typing import Union
from pydantic import BaseModel
class SkillInfo(BaseModel):
index: int
config: Union[str, dict] = "NoConfig"

View File

@ -1,11 +0,0 @@
from typing import Union
from pydantic import BaseModel
class WeaponInfo(BaseModel):
name: str
level: int
ascend: bool
refine: int
params: Union[str, dict] = "NoConfig"

View File

@ -1,18 +0,0 @@
import json
from json import JSONDecodeError
from pathlib import Path
import pytest
from python_genshin_artifact.calculator import get_damage_analysis
from python_genshin_artifact.models.calculator import CalculatorConfig
TEST_DATA_DIR = Path(__file__).resolve().parent / "input"
def test_damage_analysis_exception():
"""Test damage analysis raises expected exception on invalid input"""
with open(TEST_DATA_DIR / "invalid_enka_name.json") as f:
config = CalculatorConfig(**json.load(f))
with pytest.raises(JSONDecodeError, match="unknown variant"):
get_damage_analysis(config)

View File

@ -1,18 +0,0 @@
[package]
name = "genshin_artifact_core"
version = "0.1.4"
edition = "2021"
[lib]
name = "genshin_artifact_core"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.19.2", features = ["extension-module", "anyhow"] }
mona_wasm = { path = "../genshin_artifact/mona_wasm" }
mona = { path = "../genshin_artifact/mona_core" }
mona_generate = { path = "../genshin_artifact/mona_generate" }
num = "0.4"
serde="1.0"
serde_json = "1.0"
anyhow = "1.0.75"

View File

@ -1,2 +0,0 @@
pub mod generate;
pub mod wasm;

View File

@ -1,149 +0,0 @@
use crate::JSONDecodeError;
use anyhow::Context;
use mona::artifacts::effect_config::{ArtifactConfigInterface, ArtifactEffectConfig};
use mona::artifacts::{Artifact, ArtifactList};
use mona::attribute::{AttributeUtils, ComplicatedAttributeGraph, SimpleAttributeGraph2};
use mona::buffs::Buff;
use mona::character::Character;
use mona::damage::transformative_damage::TransformativeDamage;
use mona::damage::DamageContext;
use mona::enemies::Enemy;
use mona::utils;
use mona_wasm::applications::common::{
BuffInterface, CharacterInterface, EnemyInterface, SkillInterface, WeaponInterface,
};
use mona_wasm::CalculatorInterface;
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct CalculatorConfigInterface {
pub character: CharacterInterface,
pub weapon: WeaponInterface,
pub buffs: Vec<BuffInterface>,
pub artifacts: Vec<Artifact>,
pub artifact_config: Option<ArtifactConfigInterface>,
pub skill: SkillInterface,
pub enemy: Option<EnemyInterface>,
}
#[derive(Serialize, Deserialize)]
struct TransformativeDamageBridge {
swirl_cryo: f64,
swirl_hydro: f64,
swirl_pyro: f64,
swirl_electro: f64,
overload: f64,
electro_charged: f64,
shatter: f64,
superconduct: f64,
bloom: f64,
hyperbloom: f64,
burgeon: f64,
burning: f64,
crystallize: f64,
}
impl From<TransformativeDamage> for TransformativeDamageBridge {
fn from(damage: TransformativeDamage) -> Self {
Self {
swirl_cryo: damage.swirl_cryo,
swirl_hydro: damage.swirl_hydro,
swirl_pyro: damage.swirl_pyro,
swirl_electro: damage.swirl_electro,
overload: damage.overload,
electro_charged: damage.electro_charged,
shatter: damage.shatter,
superconduct: damage.superconduct,
bloom: damage.bloom,
hyperbloom: damage.hyperbloom,
burgeon: damage.burgeon,
burning: damage.burning,
crystallize: damage.crystallize,
}
}
}
#[pyfunction]
pub fn get_damage_analysis(value_str: String) -> PyResult<String> {
let input: CalculatorConfigInterface = serde_json::from_str(&value_str)
.map_err(|e| JSONDecodeError::new_err((e.to_string(), value_str.to_owned(), 0)))?;
let character: Character<ComplicatedAttributeGraph> = input.character.to_character();
let weapon = input.weapon.to_weapon(&character);
let buffs: Vec<Box<dyn Buff<ComplicatedAttributeGraph>>> =
input.buffs.iter().map(|x| x.to_buff()).collect();
let artifacts: Vec<&Artifact> = input.artifacts.iter().collect();
let artifact_config = match input.artifact_config {
Some(x) => x.to_config(),
None => ArtifactEffectConfig::default(),
};
let enemy = if let Some(x) = input.enemy {
x.to_enemy()
} else {
Enemy::default()
};
let result = CalculatorInterface::get_damage_analysis_internal(
&character,
&weapon,
&buffs,
artifacts,
&artifact_config,
input.skill.index,
&input.skill.config,
&enemy,
None,
);
let result_str = serde_json::to_string(&result).context("Failed to serialize json")?;
Ok(result_str)
}
#[pyfunction]
pub fn get_transformative_damage(value_str: String) -> PyResult<String> {
utils::set_panic_hook();
let input: CalculatorConfigInterface = serde_json::from_str(&value_str)
.map_err(|e| JSONDecodeError::new_err((e.to_string(), value_str.to_owned(), 0)))?;
let character: Character<SimpleAttributeGraph2> = input.character.to_character();
let weapon = input.weapon.to_weapon(&character);
let buffs: Vec<_> = input.buffs.iter().map(|x| x.to_buff()).collect();
let artifacts: Vec<&Artifact> = input.artifacts.iter().collect();
let artifact_config = match input.artifact_config {
Some(x) => x.to_config(),
None => ArtifactEffectConfig::default(),
};
let enemy = if let Some(x) = input.enemy {
x.to_enemy()
} else {
Enemy::default()
};
let attribute = AttributeUtils::create_attribute_from_big_config(
&ArtifactList {
artifacts: &artifacts,
},
&artifact_config,
&character,
&weapon,
&buffs,
);
let context: DamageContext<'_, SimpleAttributeGraph2> = DamageContext {
character_common_data: &character.common_data,
enemy: &enemy,
attribute: &attribute,
};
let result = context.transformative();
let bridge = TransformativeDamageBridge::from(result);
let result_str = serde_json::to_string(&bridge).context("Failed to serialize json")?;
Ok(result_str)
}

View File

@ -1,24 +0,0 @@
extern crate core;
mod applications;
use applications::generate::artifact::gen_artifact_meta_as_json;
use applications::generate::character::gen_character_meta_as_json;
use applications::generate::locale::gen_generate_locale_as_json;
use applications::generate::weapon::gen_weapon_meta_as_json;
use applications::wasm::{get_damage_analysis, get_transformative_damage};
use pyo3::import_exception;
use pyo3::prelude::*;
import_exception!(json, JSONDecodeError);
#[pymodule]
fn genshin_artifact_core(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add("JSONDecodeError", py.get_type::<JSONDecodeError>())?;
m.add_function(wrap_pyfunction!(get_damage_analysis, m)?)?;
m.add_function(wrap_pyfunction!(get_transformative_damage, m)?)?;
m.add_function(wrap_pyfunction!(gen_character_meta_as_json, m)?)?;
m.add_function(wrap_pyfunction!(gen_weapon_meta_as_json, m)?)?;
m.add_function(wrap_pyfunction!(gen_artifact_meta_as_json, m)?)?;
m.add_function(wrap_pyfunction!(gen_generate_locale_as_json, m)?)?;
Ok(())
}

View File

@ -0,0 +1,169 @@
use anyhow::anyhow;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use mona::artifacts::effect_config::{ArtifactConfigInterface, ArtifactEffectConfig};
use mona::artifacts::{Artifact, ArtifactList};
use mona::attribute::{AttributeUtils, ComplicatedAttributeGraph, SimpleAttributeGraph2};
use mona::buffs::Buff;
use mona::character::Character;
use mona::damage::DamageContext;
use mona::enemies::Enemy;
use mona_wasm::applications::common::{
BuffInterface, CharacterInterface, SkillInterface, WeaponInterface,
};
use mona_wasm::CalculatorInterface;
use pythonize::depythonize;
use crate::applications::input::calculator::PyCalculatorConfig;
use crate::applications::output::damage_analysis::PyDamageAnalysis;
use crate::applications::output::transformative_damage::PyTransformativeDamage;
#[pyfunction]
pub fn get_damage_analysis(calculator_config: PyCalculatorConfig) -> PyResult<PyDamageAnalysis> {
let character: CharacterInterface = calculator_config
.character
.try_into()
.map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?;
let character: Character<ComplicatedAttributeGraph> = character.to_character();
let weapon: WeaponInterface = calculator_config
.weapon
.try_into()
.map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?;
let weapon = weapon.to_weapon(&character);
let artifacts = calculator_config
.artifacts
.into_iter()
.map(|a| -> Result<Artifact, anyhow::Error> { a.try_into() })
.collect::<Result<Vec<Artifact>, anyhow::Error>>()?;
let artifacts = artifacts.iter().collect::<Vec<&Artifact>>();
let artifact_config_interface: Option<ArtifactConfigInterface> =
if let Some(artifact_config) = calculator_config.artifact_config {
Python::with_gil(|py| {
depythonize(artifact_config.as_ref(py))
.map_err(|err| anyhow!("Failed to deserialize artifact config: {}", err))
})?
} else {
None
};
let artifact_config =
if let Some(artifact_config) = artifact_config_interface {
artifact_config.to_config()
} else {
ArtifactEffectConfig::default()
};
let buffs = calculator_config
.buffs
.into_iter()
.map(|buff| -> Result<BuffInterface, anyhow::Error> { buff.try_into() })
.map(|buff| match buff {
Ok(b) => Ok(b.to_buff()),
Err(e) => Err(e),
})
.collect::<Result<Vec<Box<dyn Buff<ComplicatedAttributeGraph>>>, anyhow::Error>>()?;
let skill_config: SkillInterface = calculator_config
.skill
.try_into()
.map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?;
let enemy: Enemy = if let Some(enemy) = calculator_config.enemy {
enemy.try_into()?
} else {
Enemy::default()
};
let result = CalculatorInterface::get_damage_analysis_internal(
&character,
&weapon,
&buffs,
artifacts,
&artifact_config,
skill_config.index,
&skill_config.config,
&enemy,
None,
);
Ok(PyDamageAnalysis::from(result))
}
#[pyfunction]
pub fn get_transformative_damage(
calculator_config: PyCalculatorConfig,
) -> PyResult<PyTransformativeDamage> {
let character: CharacterInterface = calculator_config
.character
.try_into()
.map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?;
let character: Character<SimpleAttributeGraph2> = character.to_character();
let weapon: WeaponInterface = calculator_config
.weapon
.try_into()
.map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?;
let weapon = weapon.to_weapon(&character);
let artifacts = calculator_config
.artifacts
.into_iter()
.map(|a| -> Result<Artifact, anyhow::Error> { a.try_into() })
.collect::<Result<Vec<Artifact>, anyhow::Error>>()?;
let artifacts = artifacts.iter().collect::<Vec<&Artifact>>();
let artifact_config_interface: Option<ArtifactConfigInterface> =
if let Some(artifact_config) = calculator_config.artifact_config {
Python::with_gil(|py| {
depythonize(artifact_config.as_ref(py))
.map_err(|err| anyhow!("Failed to deserialize artifact config: {}", err))
})?
} else {
None
};
let artifact_config =
if let Some(artifact_config) = artifact_config_interface {
artifact_config.to_config()
} else {
ArtifactEffectConfig::default()
};
let buffs = calculator_config
.buffs
.into_iter()
.map(|buff| -> Result<BuffInterface, anyhow::Error> { buff.try_into() })
.map(|buff| match buff {
Ok(b) => Ok(b.to_buff()),
Err(e) => Err(e),
})
.collect::<Result<Vec<Box<dyn Buff<SimpleAttributeGraph2>>>, anyhow::Error>>()?;
let enemy: Enemy = if let Some(enemy) = calculator_config.enemy {
enemy.try_into()?
} else {
Enemy::default()
};
let attribute = AttributeUtils::create_attribute_from_big_config(
&ArtifactList {
artifacts: &artifacts,
},
&artifact_config,
&character,
&weapon,
&buffs,
);
let context: DamageContext<'_, SimpleAttributeGraph2> = DamageContext {
character_common_data: &character.common_data,
enemy: &enemy,
attribute: &attribute,
};
let result = context.transformative();
Ok(PyTransformativeDamage::from(result))
}

View File

@ -0,0 +1,25 @@
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::pyclass;
use std::fmt;
#[pyclass(extends = PyValueError)]
#[derive(Debug, Clone)]
pub struct ValidationError {
#[pyo3(get)]
message: String,
}
#[pymethods]
impl ValidationError {
#[new]
pub fn new_err(message: String) -> Self {
Self { message }
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ValidationError: {}", self.message)
}
}

View File

@ -0,0 +1,126 @@
use anyhow::anyhow;
use mona::artifacts::{Artifact as MonaArtifact, ArtifactSetName, ArtifactSlotName};
use mona::common::StatName;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyString};
use pythonize::depythonize;
#[pyclass(name = "Artifact")]
#[derive(Clone)]
pub struct PyArtifact {
#[pyo3(get, set)]
pub set_name: Py<PyString>,
#[pyo3(get, set)]
pub slot: Py<PyString>,
#[pyo3(get, set)]
pub level: i32,
#[pyo3(get, set)]
pub star: i32,
#[pyo3(get, set)]
pub sub_stats: Vec<(Py<PyString>, f64)>,
#[pyo3(get, set)]
pub main_stat: (Py<PyString>, f64),
#[pyo3(get, set)]
pub id: u64,
}
#[pymethods]
impl PyArtifact {
#[new]
pub fn py_new(
set_name: Py<PyString>,
slot: Py<PyString>,
level: i32,
star: i32,
sub_stats: Vec<(Py<PyString>, f64)>,
main_stat: (Py<PyString>, f64),
id: u64,
) -> PyResult<Self> {
Ok(Self {
set_name,
slot,
level,
star,
sub_stats,
main_stat,
id,
})
}
pub fn __repr__(&self, py: Python) -> PyResult<String> {
let set_name = self.set_name.as_ref(py).to_str()?;
let slot = self.slot.as_ref(py).to_str()?;
let main_stat = self.main_stat.0.as_ref(py).to_str()?;
let main_stat_value = self.main_stat.1;
Ok(format!(
"PyArtifact(set_name='{}', slot='{}', level={}, star={}, main_stat=({}, {}), id={})",
set_name, slot, self.level, self.star, main_stat, main_stat_value, self.id
))
}
#[getter]
pub fn __dict__(&self, py: Python) -> PyResult<PyObject> {
let dict = PyDict::new(py);
dict.set_item("set_name", self.set_name.as_ref(py))?;
dict.set_item("slot", self.slot.as_ref(py))?;
dict.set_item("level", self.level)?;
dict.set_item("star", self.star)?;
let sub_stats_pylist = PyList::new(py, self.sub_stats.iter().map(|(s, v)| {
let stat_str = s.as_ref(py).to_str().unwrap();
(stat_str, *v)
}));
dict.set_item("sub_stats", sub_stats_pylist)?;
let main_stat_tuple = (self.main_stat.0.as_ref(py), self.main_stat.1);
dict.set_item("main_stat", main_stat_tuple)?;
dict.set_item("id", self.id)?;
Ok(dict.into())
}
}
impl TryInto<MonaArtifact> for PyArtifact {
type Error = anyhow::Error;
fn try_into(self) -> Result<MonaArtifact, Self::Error> {
let name: ArtifactSetName = Python::with_gil(|py| {
let _string: &PyString = self.set_name.as_ref(py);
depythonize(_string)
.map_err(|err| anyhow!("Failed to deserialize artifact set name: {}", err))
})?;
let slot: ArtifactSlotName = Python::with_gil(|py| {
let _string: &PyString = self.slot.as_ref(py);
depythonize(_string)
.map_err(|err| anyhow!("Failed to deserialize artifact slot name: {}", err))
})?;
let main_stat_name: StatName = Python::with_gil(|py| {
depythonize(self.main_stat.0.as_ref(py))
.map_err(|err| anyhow!("Failed to deserialize main stat name: {}", err))
})?;
let sub_stats = Python::with_gil(|py| {
self.sub_stats
.iter()
.map(|s| {
let name: Result<StatName, anyhow::Error> = depythonize(s.0.as_ref(py))
.map_err(|err| anyhow!("Failed to deserialize sub stat name: {}", err));
match name {
Ok(n) => Ok((n, s.1)),
Err(e) => Err(e),
}
})
.collect::<Result<Vec<(StatName, f64)>, anyhow::Error>>()
})?;
Ok(MonaArtifact {
set_name: name,
slot,
level: self.level,
star: self.star,
sub_stats,
main_stat: (main_stat_name, self.main_stat.1),
id: self.id,
})
}
}

View File

@ -0,0 +1,70 @@
use anyhow::anyhow;
use mona::buffs::buff_name::BuffName;
use mona::buffs::BuffConfig;
use pyo3::prelude::*;
use pythonize::depythonize;
use pyo3::types::{PyDict, PyString};
use mona_wasm::applications::common::BuffInterface as MonaBuffInterface;
#[pyclass(name = "BuffInterface")]
#[derive(Clone)]
pub struct PyBuffInterface {
#[pyo3(get, set)]
pub name: Py<PyString>,
#[pyo3(get, set)]
pub config: Option<Py<PyDict>>,
}
#[pymethods]
impl PyBuffInterface {
#[new]
pub fn py_new(name: Py<PyString>, config: Option<Py<PyDict>>) -> PyResult<Self> {
Ok(Self { name, config })
}
pub fn __repr__(&self, py: Python) -> PyResult<String> {
let name = self.name.as_ref(py).to_str()?;
let config_repr = match &self.config {
Some(config) => config.as_ref(py).repr()?.to_str()?.to_string(),
None => "None".to_string(),
};
Ok(format!("BuffInterface(name={}, config={})", name, config_repr))
}
#[getter]
pub fn __dict__(&self, py: Python) -> PyResult<PyObject> {
let dict = PyDict::new(py);
let name_str = self.name.as_ref(py).to_str()?;
dict.set_item("name", name_str)?;
if let Some(config) = &self.config {
dict.set_item("config", config.as_ref(py))?;
} else {
dict.set_item("config", py.None())?;
}
Ok(dict.into())
}
}
impl TryInto<MonaBuffInterface> for PyBuffInterface {
type Error = anyhow::Error;
fn try_into(self) -> Result<MonaBuffInterface, Self::Error> {
let name: BuffName = Python::with_gil(|py| {
let _string: &PyString = self.name.as_ref(py);
depythonize(_string).map_err(|err| anyhow!("Failed to deserialize name: {}", err))
})?;
let config: BuffConfig = if let Some(value) = self.config {
Python::with_gil(|py| {
let _dict: &PyDict = value.as_ref(py);
depythonize(_dict).map_err(|err| anyhow!("Failed to deserialize config: {}", err))
})?
} else {
BuffConfig::NoConfig
};
Ok(MonaBuffInterface { name, config })
}
}

View File

@ -0,0 +1,53 @@
use crate::applications::input::artifact::PyArtifact;
use crate::applications::input::buff::PyBuffInterface;
use crate::applications::input::character::PyCharacterInterface;
use crate::applications::input::enemy::PyEnemyInterface;
use crate::applications::input::skill::PySkillInterface;
use crate::applications::input::weapon::PyWeaponInterface;
use pyo3::prelude::*;
use pyo3::types::PyDict;
#[pyclass(name = "CalculatorConfig")]
#[derive(Clone)]
pub struct PyCalculatorConfig {
#[pyo3(get, set)]
pub character: PyCharacterInterface,
#[pyo3(get, set)]
pub weapon: PyWeaponInterface,
#[pyo3(get, set)]
pub buffs: Vec<PyBuffInterface>,
#[pyo3(get, set)]
pub artifacts: Vec<PyArtifact>,
#[pyo3(get, set)]
pub artifact_config: Option<Py<PyDict>>,
#[pyo3(get, set)]
pub skill: PySkillInterface,
#[pyo3(get, set)]
pub enemy: Option<PyEnemyInterface>,
}
#[pymethods]
impl PyCalculatorConfig {
#[new]
#[pyo3(signature=(character, weapon, skill, buffs = None, artifacts = None, artifact_config = None, enemy = None))]
pub fn py_new(
character: PyCharacterInterface,
weapon: PyWeaponInterface,
skill: PySkillInterface,
buffs: Option<Vec<PyBuffInterface>>,
artifacts: Option<Vec<PyArtifact>>,
artifact_config: Option<Py<PyDict>>,
enemy: Option<PyEnemyInterface>,
) -> PyResult<Self> {
Ok(Self {
character,
weapon,
buffs: buffs.unwrap_or_default(),
artifacts: artifacts.unwrap_or_default(),
artifact_config,
skill,
enemy,
})
}
}

View File

@ -0,0 +1,198 @@
use anyhow::Context;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pythonize::depythonize;
use std::str::FromStr;
use mona::character::{CharacterConfig, CharacterName};
use mona_wasm::applications::common::CharacterInterface as MonaCharacterInterface;
#[pyclass(name = "CharacterInterface")]
#[derive(Clone)]
pub struct PyCharacterInterface {
#[pyo3(get, set)]
pub name: String,
#[pyo3(get, set)]
pub level: usize,
#[pyo3(get, set)]
pub ascend: bool,
#[pyo3(get, set)]
pub constellation: i32,
#[pyo3(get, set)]
pub skill1: usize,
#[pyo3(get, set)]
pub skill2: usize,
#[pyo3(get, set)]
pub skill3: usize,
#[pyo3(get, set)]
pub params: Option<Py<PyDict>>,
}
#[pymethods]
impl PyCharacterInterface {
#[new]
pub fn py_new(
name: String,
level: usize,
ascend: bool,
constellation: i32,
skill1: usize,
skill2: usize,
skill3: usize,
params: Option<Py<PyDict>>,
) -> PyResult<Self> {
Ok(Self {
name,
level,
ascend,
constellation,
skill1,
skill2,
skill3,
params,
})
}
pub fn __repr__(&self) -> PyResult<String> {
let params_repr = match &self.params {
Some(params) => format!("{:?}", params),
None => "None".to_string(),
};
Ok(format!(
"CharacterInterface(name='{}', level={}, ascend={}, constellation={}, skill1={}, skill2={}, skill3={}, params={})",
self.name, self.level, self.ascend, self.constellation, self.skill1, self.skill2, self.skill3, params_repr
))
}
pub fn __dict__(&self, py: Python) -> PyResult<PyObject> {
let dict = PyDict::new(py);
dict.set_item("name", &self.name)?;
dict.set_item("level", self.level)?;
dict.set_item("ascend", self.ascend)?;
dict.set_item("constellation", self.constellation)?;
dict.set_item("skill1", self.skill1)?;
dict.set_item("skill2", self.skill2)?;
dict.set_item("skill3", self.skill3)?;
if let Some(params) = &self.params {
dict.set_item("params", params)?;
} else {
dict.set_item("params", py.None())?;
}
Ok(dict.into())
}
}
impl TryInto<MonaCharacterInterface> for PyCharacterInterface {
type Error = anyhow::Error;
fn try_into(self) -> Result<MonaCharacterInterface, Self::Error> {
let name = CharacterName::from_str(&self.name).context("Failed to deserialize json")?;
let params: CharacterConfig = if let Some(value) = self.params {
Python::with_gil(|py| {
let _dict: &PyDict = value.as_ref(py);
depythonize(_dict).context("Failed to convert PyDict to CharacterConfig")
})?
} else {
CharacterConfig::NoConfig
};
Ok(MonaCharacterInterface {
name,
level: self.level,
ascend: self.ascend,
constellation: self.constellation,
skill1: self.skill1,
skill2: self.skill2,
skill3: self.skill3,
params,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use mona::attribute::{Attribute, AttributeName, ComplicatedAttributeGraph};
use mona::character::Character;
#[test]
fn test_character_interface() {
pyo3::prepare_freethreaded_python();
Python::with_gil(|py| {
let inner_dict = PyDict::new(py);
inner_dict.set_item("le_50", "true").unwrap();
let outer_dict = PyDict::new(py);
outer_dict.set_item("HuTao", inner_dict).unwrap();
let py_character_interface = PyCharacterInterface {
name: "HuTao".to_string(),
level: 90,
ascend: true,
constellation: 6,
skill1: 12,
skill2: 12,
skill3: 12,
params: Some(Py::from(outer_dict)),
};
assert_eq!(py_character_interface.name, "HuTao");
assert_eq!(py_character_interface.level, 90);
assert!(py_character_interface.ascend);
assert_eq!(py_character_interface.constellation, 6);
assert_eq!(py_character_interface.skill1, 12);
assert_eq!(py_character_interface.skill2, 12);
assert_eq!(py_character_interface.skill3, 12);
match &py_character_interface.params {
Some(value) => {
let py_dict = value.as_ref(py);
let hutao_dict = py_dict
.get_item("HuTao")
.unwrap()
.downcast::<PyDict>()
.unwrap();
assert_eq!(
hutao_dict
.get_item("le_50")
.unwrap()
.extract::<&str>()
.unwrap(),
"true"
);
}
None => panic!("Expected PyDict, got None"),
};
let mona_character_interface: MonaCharacterInterface =
py_character_interface.try_into().unwrap();
assert_eq!(mona_character_interface.name, CharacterName::HuTao);
assert_eq!(mona_character_interface.level, 90);
assert!(mona_character_interface.ascend);
assert_eq!(mona_character_interface.constellation, 6);
assert_eq!(mona_character_interface.skill1, 12);
assert_eq!(mona_character_interface.skill2, 12);
assert_eq!(mona_character_interface.skill3, 12);
let character: Character<ComplicatedAttributeGraph> =
mona_character_interface.to_character();
assert_eq!(character.common_data.name, CharacterName::HuTao);
match character.character_effect {
Some(effect) => {
let mut attribute = ComplicatedAttributeGraph::default();
effect.change_attribute(&mut attribute);
let value = attribute.get_value(AttributeName::BonusPyro);
assert_eq!(value, 0.33);
}
None => panic!("Expected character.character_effect, got None"),
}
println!("PyCharacterInterface 测试成功 遥遥领先!");
});
}
}

View File

@ -0,0 +1,103 @@
use pyo3::prelude::*;
use mona::enemies::Enemy as MomaEnemy;
use pyo3::types::PyDict;
#[pyclass(name = "EnemyInterface")]
#[derive(Clone)]
pub struct PyEnemyInterface {
#[pyo3(get, set)]
pub level: usize,
#[pyo3(get, set)]
pub electro_res: f64,
#[pyo3(get, set)]
pub pyro_res: f64,
#[pyo3(get, set)]
pub hydro_res: f64,
#[pyo3(get, set)]
pub cryo_res: f64,
#[pyo3(get, set)]
pub geo_res: f64,
#[pyo3(get, set)]
pub anemo_res: f64,
#[pyo3(get, set)]
pub dendro_res: f64,
#[pyo3(get, set)]
pub physical_res: f64,
}
#[pymethods]
impl PyEnemyInterface {
#[new]
fn py_new(
level: usize,
electro_res: f64,
pyro_res: f64,
hydro_res: f64,
cryo_res: f64,
geo_res: f64,
anemo_res: f64,
dendro_res: f64,
physical_res: f64,
) -> PyResult<Self> {
Ok(Self {
level,
electro_res,
pyro_res,
hydro_res,
cryo_res,
geo_res,
anemo_res,
dendro_res,
physical_res,
})
}
fn __repr__(&self) -> PyResult<String> {
Ok(format!(
"EnemyInterface(level={}, electro_res={}, pyro_res={}, hydro_res={}, cryo_res={}, geo_res={}, anemo_res={}, dendro_res={}, physical_res={})",
self.level,
self.electro_res,
self.pyro_res,
self.hydro_res,
self.cryo_res,
self.geo_res,
self.anemo_res,
self.dendro_res,
self.physical_res,
))
}
#[getter]
pub fn __dict__(&self, py: Python) -> PyResult<PyObject> {
let dict = PyDict::new(py);
dict.set_item("level", self.level)?;
dict.set_item("electro_res", self.electro_res)?;
dict.set_item("pyro_res", self.pyro_res)?;
dict.set_item("hydro_res", self.hydro_res)?;
dict.set_item("cryo_res", self.cryo_res)?;
dict.set_item("geo_res", self.geo_res)?;
dict.set_item("anemo_res", self.anemo_res)?;
dict.set_item("dendro_res", self.dendro_res)?;
dict.set_item("physical_res", self.physical_res)?;
Ok(dict.into())
}
}
impl TryInto<MomaEnemy> for PyEnemyInterface {
type Error = anyhow::Error;
fn try_into(self) -> Result<MomaEnemy, Self::Error> {
Ok(MomaEnemy {
level: self.level as i32,
electro_res: self.electro_res,
pyro_res: self.pyro_res,
hydro_res: self.hydro_res,
cryo_res: self.cryo_res,
geo_res: self.geo_res,
anemo_res: self.anemo_res,
dendro_res: self.dendro_res,
physical_res: self.physical_res,
})
}
}

View File

@ -0,0 +1,7 @@
pub mod artifact;
pub mod buff;
pub mod calculator;
pub mod character;
pub mod enemy;
pub mod skill;
pub mod weapon;

View File

@ -0,0 +1,59 @@
use anyhow::Context;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pythonize::depythonize;
use mona::character::skill_config::CharacterSkillConfig;
use mona_wasm::applications::common::SkillInterface as MonaSkillInterface;
#[pyclass(name = "SkillInterface")]
#[derive(Clone)]
pub struct PySkillInterface {
#[pyo3(get, set)]
pub index: usize,
#[pyo3(get, set)]
pub config: Option<Py<PyDict>>,
}
#[pymethods]
impl PySkillInterface {
#[new]
fn new(index: usize, config: Option<Py<PyDict>>) -> PyResult<Self> {
Ok(Self { index, config })
}
pub fn __repr__(&self) -> PyResult<String> {
Ok(format!("SkillInterface(index: {}, config: {:?})", self.index, self.config))
}
#[getter]
pub fn __dict__(&self, py: Python) -> PyResult<PyObject> {
let dict = PyDict::new(py);
dict.set_item("index", self.index)?;
if let Some(config) = &self.config {
dict.set_item("config", config.as_ref(py))?;
} else {
dict.set_item("config", py.None())?;
}
Ok(dict.into())
}
}
impl TryInto<MonaSkillInterface> for PySkillInterface {
type Error = anyhow::Error;
fn try_into(self) -> Result<MonaSkillInterface, Self::Error> {
let config: CharacterSkillConfig = if let Some(value) = self.config {
Python::with_gil(|py| {
let _dict: &PyDict = value.as_ref(py);
depythonize(_dict).context("Failed to convert PyDict to CharacterConfig")
})?
} else {
CharacterSkillConfig::NoConfig
};
Ok(MonaSkillInterface {
index: self.index,
config,
})
}
}

View File

@ -0,0 +1,201 @@
use anyhow::anyhow;
use mona::weapon::{WeaponConfig, WeaponName};
use mona_wasm::applications::common::WeaponInterface as MonaWeaponInterface;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyString};
use pythonize::depythonize;
#[pyclass(name = "WeaponInterface")]
#[derive(Clone)]
pub struct PyWeaponInterface {
#[pyo3(get, set)]
pub name: Py<PyString>,
#[pyo3(get, set)]
pub level: i32,
#[pyo3(get, set)]
pub ascend: bool,
#[pyo3(get, set)]
pub refine: i32,
#[pyo3(get, set)]
pub params: Option<Py<PyDict>>,
}
#[pymethods]
impl PyWeaponInterface {
#[new]
pub fn py_new(
name: Py<PyString>,
level: i32,
ascend: bool,
refine: i32,
params: Option<Py<PyDict>>,
) -> PyResult<Self> {
Ok(Self {
name,
level,
ascend,
refine,
params,
})
}
pub fn __repr__(&self, py: Python) -> PyResult<String> {
let name = self.name.as_ref(py).to_str()?;
let params_repr = match &self.params {
Some(params) => params.as_ref(py).repr()?.to_str()?.to_string(),
None => "None".to_string(),
};
Ok(format!(
"WeaponInterface(name='{}', level={}, ascend={}, refine={}, params={})",
name, self.level, self.ascend, self.refine, params_repr
))
}
#[getter]
pub fn __dict__(&self, py: Python) -> PyResult<PyObject> {
let dict = PyDict::new(py);
dict.set_item("name", self.name.as_ref(py))?;
dict.set_item("level", self.level)?;
dict.set_item("ascend", self.ascend)?;
dict.set_item("refine", self.refine)?;
if let Some(params) = &self.params {
dict.set_item("params", params.as_ref(py))?;
} else {
dict.set_item("params", py.None())?;
}
Ok(dict.into())
}
}
impl TryInto<MonaWeaponInterface> for PyWeaponInterface {
type Error = anyhow::Error;
fn try_into(self) -> Result<MonaWeaponInterface, Self::Error> {
let name: WeaponName = Python::with_gil(|py| {
let _string: &PyString = self.name.as_ref(py);
depythonize(_string).map_err(|err| anyhow!("Failed to deserialize name: {}", err))
})?;
let params: WeaponConfig = if let Some(value) = self.params {
Python::with_gil(|py| {
let _dict: &PyDict = value.as_ref(py);
depythonize(_dict).map_err(|err| anyhow!("Failed to deserialize params: {}", err))
})?
} else {
WeaponConfig::NoConfig
};
Ok(MonaWeaponInterface {
name,
level: self.level,
ascend: self.ascend,
refine: self.refine,
params,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use mona::attribute::ComplicatedAttributeGraph;
use mona::character::{Character, CharacterConfig, CharacterName};
use mona::weapon::Weapon;
#[test]
fn test_weapon_interface() {
pyo3::prepare_freethreaded_python();
Python::with_gil(|py| {
let inner_dict = PyDict::new(py);
inner_dict.set_item("be50_rate", 1.0).unwrap();
let params_dict = PyDict::new(py);
params_dict.set_item("StaffOfHoma", inner_dict).unwrap();
let name = PyString::new(py, "StaffOfHoma");
let py_weapon_interface = PyWeaponInterface {
name: Py::from(name),
level: 90,
ascend: true,
refine: 5,
params: Some(Py::from(params_dict)),
};
assert_eq!(
py_weapon_interface.name.as_ref(py).to_string(),
"StaffOfHoma"
);
assert_eq!(py_weapon_interface.level, 90);
assert!(py_weapon_interface.ascend);
assert_eq!(py_weapon_interface.refine, 5);
match &py_weapon_interface.params {
Some(value) => {
let py_dict = value.as_ref(py);
let params_dict = py_dict
.get_item("StaffOfHoma")
.unwrap()
.downcast::<PyDict>()
.unwrap();
assert_eq!(
params_dict
.get_item("be50_rate")
.unwrap()
.extract::<f64>()
.unwrap(),
1.0
);
}
None => panic!("Expected PyDict, got None"),
};
let mona_weapon_interface: MonaWeaponInterface =
py_weapon_interface.try_into().unwrap();
assert_eq!(mona_weapon_interface.name, WeaponName::StaffOfHoma);
assert_eq!(mona_weapon_interface.level, 90);
assert!(mona_weapon_interface.ascend);
assert_eq!(mona_weapon_interface.refine, 5);
let character: Character<ComplicatedAttributeGraph> = Character::new(
CharacterName::HuTao,
90,
true,
6,
12,
12,
12,
&CharacterConfig::HuTao { le_50: true },
);
let weapon: Weapon<ComplicatedAttributeGraph> =
mona_weapon_interface.to_weapon(&character);
assert_eq!(weapon.common_data.name, WeaponName::StaffOfHoma);
match weapon.effect {
Some(effect) => {
let mut attribute = ComplicatedAttributeGraph::default();
effect.apply(&weapon.common_data, &mut attribute);
assert!(
attribute
.edges
.iter()
.any(|item| item.key == "护摩之杖被动"),
"Expected to find key '护摩之杖被动'"
);
assert!(
attribute
.edges
.iter()
.any(|item| item.key == "护摩之杖被动等效"),
"Expected to find key '护摩之杖被动等效'"
);
}
None => panic!("Expected weapon.effect, got None"),
}
println!("PyWeaponInterface 测试成功 遥遥领先!");
});
}
}

5
src/applications/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod analysis;
pub mod errors;
pub mod generate;
pub mod input;
pub mod output;

View File

@ -0,0 +1,112 @@
use crate::applications::output::damage_result::PyDamageResult;
use mona::damage::DamageAnalysis as MonaDamageAnalysis;
use pyo3::prelude::*;
use std::collections::HashMap;
#[pyclass(name = "DamageAnalysis")]
#[derive(Clone)]
pub struct PyDamageAnalysis {
#[pyo3(get, set)]
pub atk: HashMap<String, f64>,
#[pyo3(get, set)]
pub atk_ratio: HashMap<String, f64>,
#[pyo3(get, set)]
pub hp: HashMap<String, f64>,
#[pyo3(get, set)]
pub hp_ratio: HashMap<String, f64>,
#[pyo3(get, set, name = "defense")]
pub def: HashMap<String, f64>,
#[pyo3(get, set)]
pub def_ratio: HashMap<String, f64>,
#[pyo3(get, set)]
pub em: HashMap<String, f64>,
#[pyo3(get, set)]
pub em_ratio: HashMap<String, f64>,
#[pyo3(get, set)]
pub extra_damage: HashMap<String, f64>,
#[pyo3(get, set)]
pub bonus: HashMap<String, f64>,
#[pyo3(get, set)]
pub critical: HashMap<String, f64>,
#[pyo3(get, set)]
pub critical_damage: HashMap<String, f64>,
#[pyo3(get, set)]
pub melt_enhance: HashMap<String, f64>,
#[pyo3(get, set)]
pub vaporize_enhance: HashMap<String, f64>,
#[pyo3(get, set)]
pub healing_bonus: HashMap<String, f64>,
#[pyo3(get, set)]
pub shield_strength: HashMap<String, f64>,
#[pyo3(get, set)]
pub spread_compose: HashMap<String, f64>,
#[pyo3(get, set)]
pub aggravate_compose: HashMap<String, f64>,
#[pyo3(get, set)]
pub def_minus: HashMap<String, f64>,
#[pyo3(get, set)]
pub def_penetration: HashMap<String, f64>,
#[pyo3(get, set)]
pub res_minus: HashMap<String, f64>,
#[pyo3(get, set)]
pub element: String,
#[pyo3(get, set)]
pub is_heal: bool,
#[pyo3(get, set)]
pub is_shield: bool,
#[pyo3(get, set)]
pub normal: PyDamageResult,
#[pyo3(get, set)]
pub melt: Option<PyDamageResult>,
#[pyo3(get, set)]
pub vaporize: Option<PyDamageResult>,
#[pyo3(get, set)]
pub spread: Option<PyDamageResult>,
#[pyo3(get, set)]
pub aggravate: Option<PyDamageResult>,
}
impl From<MonaDamageAnalysis> for PyDamageAnalysis {
fn from(damage_analysis: MonaDamageAnalysis) -> Self {
let element = damage_analysis.element.to_string();
let normal = PyDamageResult::from(damage_analysis.normal);
let melt = damage_analysis.melt.map(PyDamageResult::from);
let vaporize = damage_analysis.vaporize.map(PyDamageResult::from);
let spread = damage_analysis.spread.map(PyDamageResult::from);
let aggravate = damage_analysis.aggravate.map(PyDamageResult::from);
Self {
atk: damage_analysis.atk,
atk_ratio: damage_analysis.atk_ratio,
hp: damage_analysis.hp,
hp_ratio: damage_analysis.hp_ratio,
def: damage_analysis.def,
def_ratio: damage_analysis.def_ratio,
em: damage_analysis.em,
em_ratio: damage_analysis.em_ratio,
extra_damage: damage_analysis.extra_damage,
bonus: damage_analysis.bonus,
critical: damage_analysis.critical,
critical_damage: damage_analysis.critical_damage,
melt_enhance: damage_analysis.melt_enhance,
vaporize_enhance: damage_analysis.vaporize_enhance,
healing_bonus: damage_analysis.healing_bonus,
shield_strength: damage_analysis.shield_strength,
spread_compose: damage_analysis.spread_compose,
aggravate_compose: damage_analysis.aggravate_compose,
def_minus: damage_analysis.def_minus,
def_penetration: damage_analysis.def_penetration,
res_minus: damage_analysis.res_minus,
element,
is_heal: damage_analysis.is_heal,
is_shield: damage_analysis.is_shield,
normal,
melt,
vaporize,
spread,
aggravate,
}
}
}

View File

@ -0,0 +1,49 @@
use mona::damage::damage_result::DamageResult as MonaDamageResult;
use pyo3::prelude::*;
#[pyclass(name = "DamageResult")]
#[derive(Clone)]
pub struct PyDamageResult {
#[pyo3(get, set)]
pub critical: f64,
#[pyo3(get, set)]
pub non_critical: f64,
#[pyo3(get, set)]
pub expectation: f64,
#[pyo3(get, set)]
pub is_heal: bool,
#[pyo3(get, set)]
pub is_shield: bool,
}
#[pymethods]
impl PyDamageResult {
#[new]
fn py_new(
critical: f64,
non_critical: f64,
expectation: f64,
is_heal: bool,
is_shield: bool,
) -> PyResult<Self> {
Ok(Self {
critical,
non_critical,
expectation,
is_heal,
is_shield,
})
}
}
impl From<MonaDamageResult> for PyDamageResult {
fn from(damage_result: MonaDamageResult) -> Self {
Self {
critical: damage_result.critical,
non_critical: damage_result.non_critical,
expectation: damage_result.expectation,
is_heal: damage_result.is_heal,
is_shield: damage_result.is_shield,
}
}
}

View File

@ -0,0 +1,3 @@
pub mod damage_analysis;
pub mod damage_result;
pub mod transformative_damage;

View File

@ -0,0 +1,89 @@
use mona::damage::transformative_damage::TransformativeDamage as MonaTransformativeDamage;
use pyo3::prelude::*;
#[pyclass(name = "TransformativeDamage")]
#[derive(Clone)]
pub struct PyTransformativeDamage {
#[pyo3(get, set)]
swirl_cryo: f64,
#[pyo3(get, set)]
swirl_hydro: f64,
#[pyo3(get, set)]
swirl_pyro: f64,
#[pyo3(get, set)]
swirl_electro: f64,
#[pyo3(get, set)]
overload: f64,
#[pyo3(get, set)]
electro_charged: f64,
#[pyo3(get, set)]
shatter: f64,
#[pyo3(get, set)]
super_conduct: f64,
#[pyo3(get, set)]
bloom: f64,
#[pyo3(get, set)]
hyper_bloom: f64,
#[pyo3(get, set)]
burgeon: f64,
#[pyo3(get, set)]
burning: f64,
#[pyo3(get, set)]
crystallize: f64,
}
#[pymethods]
impl PyTransformativeDamage {
#[new]
fn py_new(
swirl_cryo: f64,
swirl_hydro: f64,
swirl_pyro: f64,
swirl_electro: f64,
overload: f64,
electro_charged: f64,
shatter: f64,
super_conduct: f64,
bloom: f64,
hyper_bloom: f64,
burgeon: f64,
burning: f64,
crystallize: f64,
) -> PyResult<Self> {
Ok(PyTransformativeDamage {
swirl_cryo,
swirl_hydro,
swirl_pyro,
swirl_electro,
overload,
electro_charged,
shatter,
super_conduct,
bloom,
hyper_bloom,
burgeon,
burning,
crystallize,
})
}
}
impl From<MonaTransformativeDamage> for PyTransformativeDamage {
fn from(damage: MonaTransformativeDamage) -> Self {
Self {
swirl_cryo: damage.swirl_cryo,
swirl_hydro: damage.swirl_hydro,
swirl_pyro: damage.swirl_pyro,
swirl_electro: damage.swirl_electro,
overload: damage.overload,
electro_charged: damage.electro_charged,
shatter: damage.shatter,
super_conduct: damage.superconduct,
bloom: damage.bloom,
hyper_bloom: damage.hyperbloom,
burgeon: damage.burgeon,
burning: damage.burning,
crystallize: damage.crystallize,
}
}
}

48
src/lib.rs Normal file
View File

@ -0,0 +1,48 @@
extern crate core;
mod applications;
use pyo3::import_exception;
use pyo3::prelude::*;
use crate::applications::errors::ValidationError;
use applications::analysis::{get_damage_analysis, get_transformative_damage};
use applications::generate::artifact::gen_artifact_meta_as_json;
use applications::generate::character::gen_character_meta_as_json;
use applications::generate::locale::gen_generate_locale_as_json;
use applications::generate::weapon::gen_weapon_meta_as_json;
use crate::applications::input::artifact::PyArtifact;
use crate::applications::input::buff::PyBuffInterface;
use crate::applications::input::calculator::PyCalculatorConfig;
use crate::applications::input::character::PyCharacterInterface;
use crate::applications::input::enemy::PyEnemyInterface;
use crate::applications::input::skill::PySkillInterface;
use crate::applications::input::weapon::PyWeaponInterface;
use crate::applications::output::damage_analysis::PyDamageAnalysis;
use crate::applications::output::damage_result::PyDamageResult;
use crate::applications::output::transformative_damage::PyTransformativeDamage;
import_exception!(json, JSONDecodeError);
#[pymodule]
fn _python_genshin_artifact(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add("JSONDecodeError", py.get_type::<JSONDecodeError>())?;
m.add_function(wrap_pyfunction!(get_damage_analysis, m)?)?;
m.add_function(wrap_pyfunction!(get_transformative_damage, m)?)?;
m.add_function(wrap_pyfunction!(gen_character_meta_as_json, m)?)?;
m.add_function(wrap_pyfunction!(gen_weapon_meta_as_json, m)?)?;
m.add_function(wrap_pyfunction!(gen_artifact_meta_as_json, m)?)?;
m.add_function(wrap_pyfunction!(gen_generate_locale_as_json, m)?)?;
m.add_class::<PyCalculatorConfig>()?;
m.add_class::<PyCharacterInterface>()?;
m.add_class::<PyBuffInterface>()?;
m.add_class::<PyWeaponInterface>()?;
m.add_class::<PyTransformativeDamage>()?;
m.add_class::<PySkillInterface>()?;
m.add_class::<PyEnemyInterface>()?;
m.add_class::<PyArtifact>()?;
m.add_class::<PyDamageResult>()?;
m.add_class::<PyDamageAnalysis>()?;
m.add_class::<ValidationError>()?;
Ok(())
}

View File

@ -0,0 +1,16 @@
from python_genshin_artifact import CharacterInterface
def test_character_interface():
params = {"HuTao": {"le_50": True}}
character = CharacterInterface(
name="HuTao", level=90, ascend=False, constellation=6, skill1=12, skill2=12, skill3=12, params=params
)
assert character.name == "HuTao"
assert character.level == 90
assert character.ascend is False
assert character.constellation == 6
assert character.skill1 == 12
assert character.skill2 == 12
assert character.skill3 == 12
assert character.params == params

View File

@ -0,0 +1,18 @@
from python_genshin_artifact import (
CalculatorConfig,
get_damage_analysis,
CharacterInterface,
SkillInterface,
WeaponInterface,
)
def test_damage_analysis():
character = CharacterInterface(
name="HuTao", level=90, ascend=False, constellation=6, skill1=12, skill2=12, skill3=12
)
skill = SkillInterface(index=1)
weapon = WeaponInterface(name="StaffOfHoma", level=90, ascend=False, refine=4)
calculator_config = CalculatorConfig(character=character, weapon=weapon, skill=skill)
damage_analysis = get_damage_analysis(calculator_config)
assert damage_analysis.is_heal is False