mirror of
https://github.com/PaiGramTeam/python-genshin-artifact.git
synced 2024-11-21 14:38:24 +00:00
✨ Refactor genshin artifact core
Co-authored-by: kotoriのねこ <minamiktr@outlook.com>
This commit is contained in:
parent
5f7033d331
commit
6847ba685a
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
@ -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
|
||||
|
38
Cargo.toml
Normal file
38
Cargo.toml
Normal 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
2
dev-requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
black~=23.10.1
|
||||
pytest~=4.1.0
|
@ -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"]
|
@ -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",
|
||||
)
|
234
python_genshin_artifact/_python_genshin_artifact.pyi
Normal file
234
python_genshin_artifact/_python_genshin_artifact.pyi
Normal 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": ...
|
@ -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,
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -1,7 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class BuffInfo(BaseModel):
|
||||
name: str
|
||||
config: str
|
||||
star: int
|
@ -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
|
@ -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"
|
@ -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
|
@ -1,10 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DamageResult(BaseModel):
|
||||
critical: float
|
||||
non_critical: float
|
||||
expectation: float
|
||||
|
||||
is_heal: bool
|
||||
is_shield: bool
|
@ -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
|
@ -1,7 +0,0 @@
|
||||
from typing import Union
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class SkillInfo(BaseModel):
|
||||
index: int
|
||||
config: Union[str, dict] = "NoConfig"
|
@ -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"
|
@ -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)
|
@ -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"
|
@ -1,2 +0,0 @@
|
||||
pub mod generate;
|
||||
pub mod wasm;
|
@ -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)
|
||||
}
|
@ -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(())
|
||||
}
|
169
src/applications/analysis.rs
Normal file
169
src/applications/analysis.rs
Normal 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))
|
||||
}
|
25
src/applications/errors.rs
Normal file
25
src/applications/errors.rs
Normal 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)
|
||||
}
|
||||
}
|
126
src/applications/input/artifact.rs
Normal file
126
src/applications/input/artifact.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
70
src/applications/input/buff.rs
Normal file
70
src/applications/input/buff.rs
Normal 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 })
|
||||
}
|
||||
}
|
53
src/applications/input/calculator.rs
Normal file
53
src/applications/input/calculator.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
198
src/applications/input/character.rs
Normal file
198
src/applications/input/character.rs
Normal 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 测试成功 遥遥领先!");
|
||||
});
|
||||
}
|
||||
}
|
103
src/applications/input/enemy.rs
Normal file
103
src/applications/input/enemy.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
7
src/applications/input/mod.rs
Normal file
7
src/applications/input/mod.rs
Normal 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;
|
59
src/applications/input/skill.rs
Normal file
59
src/applications/input/skill.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
201
src/applications/input/weapon.rs
Normal file
201
src/applications/input/weapon.rs
Normal 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
5
src/applications/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod analysis;
|
||||
pub mod errors;
|
||||
pub mod generate;
|
||||
pub mod input;
|
||||
pub mod output;
|
112
src/applications/output/damage_analysis.rs
Normal file
112
src/applications/output/damage_analysis.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
49
src/applications/output/damage_result.rs
Normal file
49
src/applications/output/damage_result.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
3
src/applications/output/mod.rs
Normal file
3
src/applications/output/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod damage_analysis;
|
||||
pub mod damage_result;
|
||||
pub mod transformative_damage;
|
89
src/applications/output/transformative_damage.rs
Normal file
89
src/applications/output/transformative_damage.rs
Normal 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
48
src/lib.rs
Normal 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(())
|
||||
}
|
16
tests/test_character_interface.py
Normal file
16
tests/test_character_interface.py
Normal 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
|
18
tests/test_damage_calculator.py
Normal file
18
tests/test_damage_calculator.py
Normal 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
|
Loading…
Reference in New Issue
Block a user