From 6847ba685ac5e2c8d20de913a14bee5006383b7d Mon Sep 17 00:00:00 2001 From: luoshuijs Date: Wed, 8 Nov 2023 00:42:44 +0800 Subject: [PATCH] :sparkles: Refactor genshin artifact core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: kotoriのねこ --- .github/workflows/build.yml | 39 +-- Cargo.toml | 38 +++ dev-requirements.txt | 2 + pyproject.toml | 57 +++-- python_genshin_artifact/__init__.py | 37 +++ .../_python_genshin_artifact.pyi | 234 ++++++++++++++++++ python_genshin_artifact/assets.py | 2 +- python_genshin_artifact/calculator.py | 26 -- python_genshin_artifact/enka/enka_parser.py | 24 +- python_genshin_artifact/enka/fight.py | 6 +- python_genshin_artifact/models/artifact.py | 13 - python_genshin_artifact/models/buff.py | 7 - python_genshin_artifact/models/calculator.py | 20 -- .../models/characterInfo.py | 14 -- .../models/damage/__init__.py | 0 .../models/damage/analysis.py | 56 ----- .../models/damage/result.py | 10 - python_genshin_artifact/models/element.py | 0 python_genshin_artifact/models/enemy.py | 13 - python_genshin_artifact/models/skill.py | 7 - python_genshin_artifact/models/weapon.py | 11 - .../{models/__init__.py => py.typed} | 0 .../tests/test_damage_calculator.py | 18 -- python_genshin_artifact_core/Cargo.toml | 18 -- .../src/applications/mod.rs | 2 - .../src/applications/wasm.rs | 149 ----------- python_genshin_artifact_core/src/lib.rs | 24 -- ...rust-toolchain.toml => rust-toolchain.toml | 0 src/applications/analysis.rs | 169 +++++++++++++ src/applications/errors.rs | 25 ++ .../applications/generate/artifact.rs | 0 .../applications/generate/character.rs | 0 .../applications/generate/locale.rs | 0 .../src => src}/applications/generate/mod.rs | 0 .../applications/generate/weapon.rs | 0 src/applications/input/artifact.rs | 126 ++++++++++ src/applications/input/buff.rs | 70 ++++++ src/applications/input/calculator.rs | 53 ++++ src/applications/input/character.rs | 198 +++++++++++++++ src/applications/input/enemy.rs | 103 ++++++++ src/applications/input/mod.rs | 7 + src/applications/input/skill.rs | 59 +++++ src/applications/input/weapon.rs | 201 +++++++++++++++ src/applications/mod.rs | 5 + src/applications/output/damage_analysis.rs | 112 +++++++++ src/applications/output/damage_result.rs | 49 ++++ src/applications/output/mod.rs | 3 + .../output/transformative_damage.rs | 89 +++++++ src/lib.rs | 48 ++++ .../input/invalid_enka_name.json | 0 tests/test_character_interface.py | 16 ++ tests/test_damage_calculator.py | 18 ++ 52 files changed, 1718 insertions(+), 460 deletions(-) create mode 100644 Cargo.toml create mode 100644 dev-requirements.txt create mode 100644 python_genshin_artifact/_python_genshin_artifact.pyi delete mode 100644 python_genshin_artifact/calculator.py delete mode 100644 python_genshin_artifact/models/artifact.py delete mode 100644 python_genshin_artifact/models/buff.py delete mode 100644 python_genshin_artifact/models/calculator.py delete mode 100644 python_genshin_artifact/models/characterInfo.py delete mode 100644 python_genshin_artifact/models/damage/__init__.py delete mode 100644 python_genshin_artifact/models/damage/analysis.py delete mode 100644 python_genshin_artifact/models/damage/result.py delete mode 100644 python_genshin_artifact/models/element.py delete mode 100644 python_genshin_artifact/models/enemy.py delete mode 100644 python_genshin_artifact/models/skill.py delete mode 100644 python_genshin_artifact/models/weapon.py rename python_genshin_artifact/{models/__init__.py => py.typed} (100%) delete mode 100644 python_genshin_artifact/tests/test_damage_calculator.py delete mode 100644 python_genshin_artifact_core/Cargo.toml delete mode 100644 python_genshin_artifact_core/src/applications/mod.rs delete mode 100644 python_genshin_artifact_core/src/applications/wasm.rs delete mode 100644 python_genshin_artifact_core/src/lib.rs rename python_genshin_artifact_core/rust-toolchain.toml => rust-toolchain.toml (100%) create mode 100644 src/applications/analysis.rs create mode 100644 src/applications/errors.rs rename {python_genshin_artifact_core/src => src}/applications/generate/artifact.rs (100%) rename {python_genshin_artifact_core/src => src}/applications/generate/character.rs (100%) rename {python_genshin_artifact_core/src => src}/applications/generate/locale.rs (100%) rename {python_genshin_artifact_core/src => src}/applications/generate/mod.rs (100%) rename {python_genshin_artifact_core/src => src}/applications/generate/weapon.rs (100%) create mode 100644 src/applications/input/artifact.rs create mode 100644 src/applications/input/buff.rs create mode 100644 src/applications/input/calculator.rs create mode 100644 src/applications/input/character.rs create mode 100644 src/applications/input/enemy.rs create mode 100644 src/applications/input/mod.rs create mode 100644 src/applications/input/skill.rs create mode 100644 src/applications/input/weapon.rs create mode 100644 src/applications/mod.rs create mode 100644 src/applications/output/damage_analysis.rs create mode 100644 src/applications/output/damage_result.rs create mode 100644 src/applications/output/mod.rs create mode 100644 src/applications/output/transformative_damage.rs create mode 100644 src/lib.rs rename {python_genshin_artifact/tests => tests}/input/invalid_enka_name.json (100%) create mode 100644 tests/test_character_interface.py create mode 100644 tests/test_damage_calculator.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6df0f7a..d179288 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,21 +67,19 @@ jobs: shell: bash run: | source activate_env.sh - cd python_genshin_artifact_core maturin develop - name: Export install file shell: bash run: | source activate_env.sh - cd python_genshin_artifact_core maturin build --out ./dist - name: Upload Artifact uses: actions/upload-artifact@v3 with: name: genshin-artifact-core - path: python_genshin_artifact_core/dist + path: ./dist upload-core: runs-on: ubuntu-latest @@ -110,38 +108,3 @@ jobs: env: TWINE_USERNAME: __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 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..317c9ec --- /dev/null +++ b/Cargo.toml @@ -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"] diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..3e695a0 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,2 @@ +black~=23.10.1 +pytest~=4.1.0 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e872528..04c0c5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,32 +1,55 @@ -[tool.poetry] -name = "Python-Genshin-Artifact" +[project] +name = "python_genshin_artifact" +requires-python = ">=3.8" version = "0.1.4" -description = "A Python library that binds to Genshin Artifact damage calculation and analysis engine." -authors = ["luoshuijs"] -license = "MIT license" -readme = "README.md" -packages = [ - { include = "python_genshin_artifact" }, +authors = [ + {name = "luoshuijs", email = "luoshuijs@outlook.com"}, + {name = "kotori", email = "minamiktr@outlook.com"} +] +classifiers = [ + "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] -python = "^3.8" -pydantic = "^1.10.7" +[tool.maturin] +module-name = "python_genshin_artifact._python_genshin_artifact" +bindings = "pyo3" +#python-source = "python" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" [tool.pytest.ini_options] -asyncio_mode = "auto" log_cli = true log_cli_level = "INFO" log_cli_format = "%(message)s" log_cli_date_format = "%Y-%m-%d %H:%M:%S" - [tool.black] include = '\.pyi?$' line-length = 120 -target-version = ['py38'] +target-version = ["py38"] \ No newline at end of file diff --git a/python_genshin_artifact/__init__.py b/python_genshin_artifact/__init__.py index e69de29..02b2fcf 100644 --- a/python_genshin_artifact/__init__.py +++ b/python_genshin_artifact/__init__.py @@ -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", +) diff --git a/python_genshin_artifact/_python_genshin_artifact.pyi b/python_genshin_artifact/_python_genshin_artifact.pyi new file mode 100644 index 0000000..3094b97 --- /dev/null +++ b/python_genshin_artifact/_python_genshin_artifact.pyi @@ -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": ... diff --git a/python_genshin_artifact/assets.py b/python_genshin_artifact/assets.py index 4dd1c48..4fc1179 100644 --- a/python_genshin_artifact/assets.py +++ b/python_genshin_artifact/assets.py @@ -1,7 +1,7 @@ import json from typing import Dict, Tuple, List -from genshin_artifact_core import ( +from python_genshin_artifact import ( gen_character_meta_as_json, gen_weapon_meta_as_json, gen_artifact_meta_as_json, diff --git a/python_genshin_artifact/calculator.py b/python_genshin_artifact/calculator.py deleted file mode 100644 index 7292946..0000000 --- a/python_genshin_artifact/calculator.py +++ /dev/null @@ -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) diff --git a/python_genshin_artifact/enka/enka_parser.py b/python_genshin_artifact/enka/enka_parser.py index 5f7f38c..cc9be85 100644 --- a/python_genshin_artifact/enka/enka_parser.py +++ b/python_genshin_artifact/enka/enka_parser.py @@ -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.assets import Assets 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.error import EnkaParseException -from python_genshin_artifact.models.artifact import ArtifactInfo -from python_genshin_artifact.models.characterInfo import CharacterInfo -from python_genshin_artifact.models.weapon import WeaponInfo +from python_genshin_artifact import Artifact, CharacterInterface, WeaponInterface assets = Assets() @@ -23,7 +21,7 @@ def is_ascend(level: int, promote_level: int) -> bool: 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) if character_info is None: 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"): skill_info["skill3"] += 3 character_name = characters_map.get(avatar_id) - character = CharacterInfo( + character = CharacterInterface( name=character_name, level=level, constellation=len(talent_id_list), @@ -66,9 +64,9 @@ def enka_parser(data: dict, avatar_id: int) -> Tuple[CharacterInfo, WeaponInfo, return character, weapon, artifacts -def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInfo, List[ArtifactInfo]]: - weapon: Optional[WeaponInfo] = None - artifacts: List[ArtifactInfo] = [] +def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInterface, List[Artifact]]: + weapon: Optional[WeaponInterface] = None + artifacts: List[Artifact] = [] for _equip in equip_list: _weapon = _equip.get("weapon") _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"] _main_prop_id = _reliquary_main_stat["mainPropId"] 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) for _reliquary_sub_stats in _flat["reliquarySubstats"]: _append_prop_id = _reliquary_sub_stats["appendPropId"] 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.append(_sub_stats) slot = equip_type_map[_flat["equipType"]] star = _flat["rankLevel"] artifacts.append( - ArtifactInfo( + Artifact( set_name=set_name, id=artifact_id, level=_level, @@ -118,5 +116,5 @@ def de_equip_list(equip_list: list[dict]) -> Tuple[WeaponInfo, List[ArtifactInfo ascend = False if _promote_level is not None: 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 diff --git a/python_genshin_artifact/enka/fight.py b/python_genshin_artifact/enka/fight.py index f0c5138..814cae9 100644 --- a/python_genshin_artifact/enka/fight.py +++ b/python_genshin_artifact/enka/fight.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, Set fight_map: Dict[str, str] = { "FIGHT_PROP_ATTACK": "ATKFixed", @@ -22,7 +22,7 @@ fight_map: Dict[str, str] = { "FIGHT_PROP_GRASS_ADD_HURT": "DendroBonus", } -fixed: List[str] = { +fixed: Set[str] = { "FIGHT_PROP_ATTACK", "FIGHT_PROP_DEFENSE", "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) diff --git a/python_genshin_artifact/models/artifact.py b/python_genshin_artifact/models/artifact.py deleted file mode 100644 index bda071a..0000000 --- a/python_genshin_artifact/models/artifact.py +++ /dev/null @@ -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 diff --git a/python_genshin_artifact/models/buff.py b/python_genshin_artifact/models/buff.py deleted file mode 100644 index fb8846c..0000000 --- a/python_genshin_artifact/models/buff.py +++ /dev/null @@ -1,7 +0,0 @@ -from pydantic import BaseModel - - -class BuffInfo(BaseModel): - name: str - config: str - star: int diff --git a/python_genshin_artifact/models/calculator.py b/python_genshin_artifact/models/calculator.py deleted file mode 100644 index bc9470d..0000000 --- a/python_genshin_artifact/models/calculator.py +++ /dev/null @@ -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 diff --git a/python_genshin_artifact/models/characterInfo.py b/python_genshin_artifact/models/characterInfo.py deleted file mode 100644 index 630558b..0000000 --- a/python_genshin_artifact/models/characterInfo.py +++ /dev/null @@ -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" diff --git a/python_genshin_artifact/models/damage/__init__.py b/python_genshin_artifact/models/damage/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/python_genshin_artifact/models/damage/analysis.py b/python_genshin_artifact/models/damage/analysis.py deleted file mode 100644 index f7b78f2..0000000 --- a/python_genshin_artifact/models/damage/analysis.py +++ /dev/null @@ -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 diff --git a/python_genshin_artifact/models/damage/result.py b/python_genshin_artifact/models/damage/result.py deleted file mode 100644 index 80a321d..0000000 --- a/python_genshin_artifact/models/damage/result.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import BaseModel - - -class DamageResult(BaseModel): - critical: float - non_critical: float - expectation: float - - is_heal: bool - is_shield: bool diff --git a/python_genshin_artifact/models/element.py b/python_genshin_artifact/models/element.py deleted file mode 100644 index e69de29..0000000 diff --git a/python_genshin_artifact/models/enemy.py b/python_genshin_artifact/models/enemy.py deleted file mode 100644 index d023544..0000000 --- a/python_genshin_artifact/models/enemy.py +++ /dev/null @@ -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 diff --git a/python_genshin_artifact/models/skill.py b/python_genshin_artifact/models/skill.py deleted file mode 100644 index b3f048f..0000000 --- a/python_genshin_artifact/models/skill.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Union -from pydantic import BaseModel - - -class SkillInfo(BaseModel): - index: int - config: Union[str, dict] = "NoConfig" diff --git a/python_genshin_artifact/models/weapon.py b/python_genshin_artifact/models/weapon.py deleted file mode 100644 index 214638e..0000000 --- a/python_genshin_artifact/models/weapon.py +++ /dev/null @@ -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" diff --git a/python_genshin_artifact/models/__init__.py b/python_genshin_artifact/py.typed similarity index 100% rename from python_genshin_artifact/models/__init__.py rename to python_genshin_artifact/py.typed diff --git a/python_genshin_artifact/tests/test_damage_calculator.py b/python_genshin_artifact/tests/test_damage_calculator.py deleted file mode 100644 index ce625ea..0000000 --- a/python_genshin_artifact/tests/test_damage_calculator.py +++ /dev/null @@ -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) diff --git a/python_genshin_artifact_core/Cargo.toml b/python_genshin_artifact_core/Cargo.toml deleted file mode 100644 index 507365a..0000000 --- a/python_genshin_artifact_core/Cargo.toml +++ /dev/null @@ -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" diff --git a/python_genshin_artifact_core/src/applications/mod.rs b/python_genshin_artifact_core/src/applications/mod.rs deleted file mode 100644 index 43e823b..0000000 --- a/python_genshin_artifact_core/src/applications/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod generate; -pub mod wasm; diff --git a/python_genshin_artifact_core/src/applications/wasm.rs b/python_genshin_artifact_core/src/applications/wasm.rs deleted file mode 100644 index aecc56e..0000000 --- a/python_genshin_artifact_core/src/applications/wasm.rs +++ /dev/null @@ -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, - pub artifacts: Vec, - pub artifact_config: Option, - pub skill: SkillInterface, - pub enemy: Option, -} - -#[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 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 { - 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 = 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 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 { - 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 = 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) -} diff --git a/python_genshin_artifact_core/src/lib.rs b/python_genshin_artifact_core/src/lib.rs deleted file mode 100644 index a6d1e50..0000000 --- a/python_genshin_artifact_core/src/lib.rs +++ /dev/null @@ -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::())?; - 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(()) -} diff --git a/python_genshin_artifact_core/rust-toolchain.toml b/rust-toolchain.toml similarity index 100% rename from python_genshin_artifact_core/rust-toolchain.toml rename to rust-toolchain.toml diff --git a/src/applications/analysis.rs b/src/applications/analysis.rs new file mode 100644 index 0000000..5994b1b --- /dev/null +++ b/src/applications/analysis.rs @@ -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 { + let character: CharacterInterface = calculator_config + .character + .try_into() + .map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?; + let character: Character = 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 { a.try_into() }) + .collect::, anyhow::Error>>()?; + let artifacts = artifacts.iter().collect::>(); + + let artifact_config_interface: Option = + 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 { buff.try_into() }) + .map(|buff| match buff { + Ok(b) => Ok(b.to_buff()), + Err(e) => Err(e), + }) + .collect::>>, 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 { + let character: CharacterInterface = calculator_config + .character + .try_into() + .map_err(|e: anyhow::Error| PyValueError::new_err(e.to_string()))?; + let character: Character = 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 { a.try_into() }) + .collect::, anyhow::Error>>()?; + let artifacts = artifacts.iter().collect::>(); + + let artifact_config_interface: Option = + 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 { buff.try_into() }) + .map(|buff| match buff { + Ok(b) => Ok(b.to_buff()), + Err(e) => Err(e), + }) + .collect::>>, 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)) +} diff --git a/src/applications/errors.rs b/src/applications/errors.rs new file mode 100644 index 0000000..587bdf1 --- /dev/null +++ b/src/applications/errors.rs @@ -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) + } +} diff --git a/python_genshin_artifact_core/src/applications/generate/artifact.rs b/src/applications/generate/artifact.rs similarity index 100% rename from python_genshin_artifact_core/src/applications/generate/artifact.rs rename to src/applications/generate/artifact.rs diff --git a/python_genshin_artifact_core/src/applications/generate/character.rs b/src/applications/generate/character.rs similarity index 100% rename from python_genshin_artifact_core/src/applications/generate/character.rs rename to src/applications/generate/character.rs diff --git a/python_genshin_artifact_core/src/applications/generate/locale.rs b/src/applications/generate/locale.rs similarity index 100% rename from python_genshin_artifact_core/src/applications/generate/locale.rs rename to src/applications/generate/locale.rs diff --git a/python_genshin_artifact_core/src/applications/generate/mod.rs b/src/applications/generate/mod.rs similarity index 100% rename from python_genshin_artifact_core/src/applications/generate/mod.rs rename to src/applications/generate/mod.rs diff --git a/python_genshin_artifact_core/src/applications/generate/weapon.rs b/src/applications/generate/weapon.rs similarity index 100% rename from python_genshin_artifact_core/src/applications/generate/weapon.rs rename to src/applications/generate/weapon.rs diff --git a/src/applications/input/artifact.rs b/src/applications/input/artifact.rs new file mode 100644 index 0000000..7b1df2e --- /dev/null +++ b/src/applications/input/artifact.rs @@ -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, + #[pyo3(get, set)] + pub slot: Py, + #[pyo3(get, set)] + pub level: i32, + #[pyo3(get, set)] + pub star: i32, + #[pyo3(get, set)] + pub sub_stats: Vec<(Py, f64)>, + #[pyo3(get, set)] + pub main_stat: (Py, f64), + #[pyo3(get, set)] + pub id: u64, +} + +#[pymethods] +impl PyArtifact { + #[new] + pub fn py_new( + set_name: Py, + slot: Py, + level: i32, + star: i32, + sub_stats: Vec<(Py, f64)>, + main_stat: (Py, f64), + id: u64, + ) -> PyResult { + Ok(Self { + set_name, + slot, + level, + star, + sub_stats, + main_stat, + id, + }) + } + + pub fn __repr__(&self, py: Python) -> PyResult { + 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 { + 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 for PyArtifact { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + 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 = 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::, 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, + }) + } +} diff --git a/src/applications/input/buff.rs b/src/applications/input/buff.rs new file mode 100644 index 0000000..61144a6 --- /dev/null +++ b/src/applications/input/buff.rs @@ -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, + #[pyo3(get, set)] + pub config: Option>, +} + +#[pymethods] +impl PyBuffInterface { + #[new] + pub fn py_new(name: Py, config: Option>) -> PyResult { + Ok(Self { name, config }) + } + + pub fn __repr__(&self, py: Python) -> PyResult { + 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 { + 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 for PyBuffInterface { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + 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 }) + } +} diff --git a/src/applications/input/calculator.rs b/src/applications/input/calculator.rs new file mode 100644 index 0000000..bfaa951 --- /dev/null +++ b/src/applications/input/calculator.rs @@ -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, + #[pyo3(get, set)] + pub artifacts: Vec, + #[pyo3(get, set)] + pub artifact_config: Option>, + #[pyo3(get, set)] + pub skill: PySkillInterface, + #[pyo3(get, set)] + pub enemy: Option, +} + +#[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>, + artifacts: Option>, + artifact_config: Option>, + enemy: Option, + ) -> PyResult { + Ok(Self { + character, + weapon, + buffs: buffs.unwrap_or_default(), + artifacts: artifacts.unwrap_or_default(), + artifact_config, + skill, + enemy, + }) + } +} diff --git a/src/applications/input/character.rs b/src/applications/input/character.rs new file mode 100644 index 0000000..fe222e0 --- /dev/null +++ b/src/applications/input/character.rs @@ -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>, +} + +#[pymethods] +impl PyCharacterInterface { + #[new] + pub fn py_new( + name: String, + level: usize, + ascend: bool, + constellation: i32, + skill1: usize, + skill2: usize, + skill3: usize, + params: Option>, + ) -> PyResult { + Ok(Self { + name, + level, + ascend, + constellation, + skill1, + skill2, + skill3, + params, + }) + } + + pub fn __repr__(&self) -> PyResult { + 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 { + 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 for PyCharacterInterface { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + 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::() + .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 = + 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 测试成功 遥遥领先!"); + }); + } +} diff --git a/src/applications/input/enemy.rs b/src/applications/input/enemy.rs new file mode 100644 index 0000000..3267b71 --- /dev/null +++ b/src/applications/input/enemy.rs @@ -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 { + Ok(Self { + level, + electro_res, + pyro_res, + hydro_res, + cryo_res, + geo_res, + anemo_res, + dendro_res, + physical_res, + }) + } + + fn __repr__(&self) -> PyResult { + 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 { + 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 for PyEnemyInterface { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + 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, + }) + } +} diff --git a/src/applications/input/mod.rs b/src/applications/input/mod.rs new file mode 100644 index 0000000..d5945cf --- /dev/null +++ b/src/applications/input/mod.rs @@ -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; diff --git a/src/applications/input/skill.rs b/src/applications/input/skill.rs new file mode 100644 index 0000000..acad3f6 --- /dev/null +++ b/src/applications/input/skill.rs @@ -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>, +} + +#[pymethods] +impl PySkillInterface { + #[new] + fn new(index: usize, config: Option>) -> PyResult { + Ok(Self { index, config }) + } + pub fn __repr__(&self) -> PyResult { + Ok(format!("SkillInterface(index: {}, config: {:?})", self.index, self.config)) + } + + #[getter] + pub fn __dict__(&self, py: Python) -> PyResult { + 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 for PySkillInterface { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + 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, + }) + } +} diff --git a/src/applications/input/weapon.rs b/src/applications/input/weapon.rs new file mode 100644 index 0000000..2b35a79 --- /dev/null +++ b/src/applications/input/weapon.rs @@ -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, + #[pyo3(get, set)] + pub level: i32, + #[pyo3(get, set)] + pub ascend: bool, + #[pyo3(get, set)] + pub refine: i32, + #[pyo3(get, set)] + pub params: Option>, +} + +#[pymethods] +impl PyWeaponInterface { + #[new] + pub fn py_new( + name: Py, + level: i32, + ascend: bool, + refine: i32, + params: Option>, + ) -> PyResult { + Ok(Self { + name, + level, + ascend, + refine, + params, + }) + } + + pub fn __repr__(&self, py: Python) -> PyResult { + 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 { + 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 for PyWeaponInterface { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + 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::() + .unwrap(); + assert_eq!( + params_dict + .get_item("be50_rate") + .unwrap() + .extract::() + .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 = Character::new( + CharacterName::HuTao, + 90, + true, + 6, + 12, + 12, + 12, + &CharacterConfig::HuTao { le_50: true }, + ); + + let weapon: Weapon = + 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 测试成功 遥遥领先!"); + }); + } +} diff --git a/src/applications/mod.rs b/src/applications/mod.rs new file mode 100644 index 0000000..58576f8 --- /dev/null +++ b/src/applications/mod.rs @@ -0,0 +1,5 @@ +pub mod analysis; +pub mod errors; +pub mod generate; +pub mod input; +pub mod output; diff --git a/src/applications/output/damage_analysis.rs b/src/applications/output/damage_analysis.rs new file mode 100644 index 0000000..3e002e0 --- /dev/null +++ b/src/applications/output/damage_analysis.rs @@ -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, + #[pyo3(get, set)] + pub atk_ratio: HashMap, + #[pyo3(get, set)] + pub hp: HashMap, + #[pyo3(get, set)] + pub hp_ratio: HashMap, + #[pyo3(get, set, name = "defense")] + pub def: HashMap, + #[pyo3(get, set)] + pub def_ratio: HashMap, + #[pyo3(get, set)] + pub em: HashMap, + #[pyo3(get, set)] + pub em_ratio: HashMap, + #[pyo3(get, set)] + pub extra_damage: HashMap, + #[pyo3(get, set)] + pub bonus: HashMap, + #[pyo3(get, set)] + pub critical: HashMap, + #[pyo3(get, set)] + pub critical_damage: HashMap, + #[pyo3(get, set)] + pub melt_enhance: HashMap, + #[pyo3(get, set)] + pub vaporize_enhance: HashMap, + #[pyo3(get, set)] + pub healing_bonus: HashMap, + #[pyo3(get, set)] + pub shield_strength: HashMap, + #[pyo3(get, set)] + pub spread_compose: HashMap, + #[pyo3(get, set)] + pub aggravate_compose: HashMap, + + #[pyo3(get, set)] + pub def_minus: HashMap, + #[pyo3(get, set)] + pub def_penetration: HashMap, + #[pyo3(get, set)] + pub res_minus: HashMap, + + #[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, + #[pyo3(get, set)] + pub vaporize: Option, + #[pyo3(get, set)] + pub spread: Option, + #[pyo3(get, set)] + pub aggravate: Option, +} + +impl From 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, + } + } +} diff --git a/src/applications/output/damage_result.rs b/src/applications/output/damage_result.rs new file mode 100644 index 0000000..c4fe14d --- /dev/null +++ b/src/applications/output/damage_result.rs @@ -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 { + Ok(Self { + critical, + non_critical, + expectation, + is_heal, + is_shield, + }) + } +} + +impl From 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, + } + } +} diff --git a/src/applications/output/mod.rs b/src/applications/output/mod.rs new file mode 100644 index 0000000..d25c694 --- /dev/null +++ b/src/applications/output/mod.rs @@ -0,0 +1,3 @@ +pub mod damage_analysis; +pub mod damage_result; +pub mod transformative_damage; diff --git a/src/applications/output/transformative_damage.rs b/src/applications/output/transformative_damage.rs new file mode 100644 index 0000000..b388394 --- /dev/null +++ b/src/applications/output/transformative_damage.rs @@ -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 { + Ok(PyTransformativeDamage { + swirl_cryo, + swirl_hydro, + swirl_pyro, + swirl_electro, + overload, + electro_charged, + shatter, + super_conduct, + bloom, + hyper_bloom, + burgeon, + burning, + crystallize, + }) + } +} + +impl From 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, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bf7354d --- /dev/null +++ b/src/lib.rs @@ -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::())?; + 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::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/python_genshin_artifact/tests/input/invalid_enka_name.json b/tests/input/invalid_enka_name.json similarity index 100% rename from python_genshin_artifact/tests/input/invalid_enka_name.json rename to tests/input/invalid_enka_name.json diff --git a/tests/test_character_interface.py b/tests/test_character_interface.py new file mode 100644 index 0000000..1d5f62d --- /dev/null +++ b/tests/test_character_interface.py @@ -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 diff --git a/tests/test_damage_calculator.py b/tests/test_damage_calculator.py new file mode 100644 index 0000000..4702128 --- /dev/null +++ b/tests/test_damage_calculator.py @@ -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