codex/backend-optimization #2
@@ -0,0 +1,36 @@
|
|||||||
|
# dcalc backend
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Entry
|
||||||
|
|
||||||
|
- `main.py`: FastAPI application and uvicorn entry.
|
||||||
|
- `api/routes/calc.py`: `/api/calc` HTTP route.
|
||||||
|
- `services/calc_service.py`: calculation orchestration.
|
||||||
|
- `core/basic/character/`: character calculation implementation.
|
||||||
|
- `core/basic/equipment.py`: equipment data models and compatibility access.
|
||||||
|
- `core/basic/equipment_registry.py`: equipment version registry and cache.
|
||||||
|
- `database/connect.py`: SQLite and Redis connections.
|
||||||
|
|
||||||
|
## Compatibility Rules
|
||||||
|
|
||||||
|
- Do not change `/calc` request and response contract without frontend migration approval.
|
||||||
|
- Do not change frontendNew result data shape during backend refactors.
|
||||||
|
- Keep Skill and Forge business formulas behavior-compatible unless a dedicated formula task says otherwise.
|
||||||
@@ -2,7 +2,7 @@ from api.core.Gzip import register_gzip_request
|
|||||||
from fastapi import APIRouter, Body
|
from fastapi import APIRouter, Body
|
||||||
from api.core.Response import response
|
from api.core.Response import response
|
||||||
from api.dp import AltersDep
|
from api.dp import AltersDep
|
||||||
from core.basic.character import createCharacter
|
from services.calc_service import calculate
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -11,5 +11,4 @@ register_gzip_request(router)
|
|||||||
|
|
||||||
@router.post('/calc')
|
@router.post('/calc')
|
||||||
async def calc(state: AltersDep, config=Body(None)):
|
async def calc(state: AltersDep, config=Body(None)):
|
||||||
character = createCharacter(state.alter, state.equVersion)
|
return response(data=calculate(state, config))
|
||||||
return response(data=character.calc(config))
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class Config(BaseSettings):
|
|||||||
HELPER_ID: str = ''
|
HELPER_ID: str = ''
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=('config/.env', 'config/.env.dev', 'config/.env.prod')
|
env_file=('config/.env', 'config/.env.dev', 'config/.env.prod'),
|
||||||
|
extra='ignore',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from core.basic.equipment import OathSuit, Suit, get_equipment
|
|||||||
from ..formula import 增幅计算, 强化技攻, 武器强化计算, 精通计算, 耳环计算, 左右计算, 锻造计算
|
from ..formula import 增幅计算, 强化技攻, 武器强化计算, 精通计算, 耳环计算, 左右计算, 锻造计算
|
||||||
from ..roleinfo import CharacterEmblemInfo, CharacterEquipInfo, CharacterOathInfo, get_key_by_value
|
from ..roleinfo import CharacterEmblemInfo, CharacterEquipInfo, CharacterOathInfo, get_key_by_value
|
||||||
from .base import CharacterBase
|
from .base import CharacterBase
|
||||||
|
from .config_view import CalcConfigView
|
||||||
|
from .effect_apply import apply_uppercase_attrs
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
@@ -162,8 +164,7 @@ class CalcEquipMixin(CharacterBase):
|
|||||||
# 如果存在誓约和套装是同一个套装
|
# 如果存在誓约和套装是同一个套装
|
||||||
# 则直接不执行原套装效果,执行誓约套装增强的效果
|
# 则直接不执行原套装效果,执行誓约套装增强的效果
|
||||||
oathEffect = oath_suit_by_suit_id.get(suit.suitId, None)
|
oathEffect = oath_suit_by_suit_id.get(suit.suitId, None)
|
||||||
filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()}
|
apply_uppercase_attrs(self, suit)
|
||||||
self.SetStatus(**filtered_dict)
|
|
||||||
if func is not None and oathEffect is None:
|
if func is not None and oathEffect is None:
|
||||||
# print(f'套装 {suit.name} 生效')
|
# print(f'套装 {suit.name} 生效')
|
||||||
func(self)
|
func(self)
|
||||||
@@ -184,13 +185,11 @@ class CalcEquipMixin(CharacterBase):
|
|||||||
skills = [x for x in oathSuit.skills if x.suitId == oathSuit.suitId]
|
skills = [x for x in oathSuit.skills if x.suitId == oathSuit.suitId]
|
||||||
skill = next((x for x in oathSuit.skills if str(x.skillId) == str(self.charOathSkillId)), None)
|
skill = next((x for x in oathSuit.skills if str(x.skillId) == str(self.charOathSkillId)), None)
|
||||||
# 誓约套装效果生效
|
# 誓约套装效果生效
|
||||||
filtered_dict = {k: v for k, v in oathSuit.__dict__.items() if k[0].isupper()}
|
apply_uppercase_attrs(self, oathSuit)
|
||||||
self.SetStatus(**filtered_dict)
|
|
||||||
if skill is not None:
|
if skill is not None:
|
||||||
oathSkillEffect.append(skill)
|
oathSkillEffect.append(skill)
|
||||||
# 誓约技能效果生效
|
# 誓约技能效果生效
|
||||||
filtered_dict = {k: v for k, v in skill.__dict__.items() if k[0].isupper()}
|
apply_uppercase_attrs(self, skill)
|
||||||
self.SetStatus(**filtered_dict)
|
|
||||||
self.oathSkillInfo.append(skill)
|
self.oathSkillInfo.append(skill)
|
||||||
self.oathSuitInfo.append(oathSuit)
|
self.oathSuitInfo.append(oathSuit)
|
||||||
if oathSuit.suitId == max_point_oath_suit:
|
if oathSuit.suitId == max_point_oath_suit:
|
||||||
@@ -235,8 +234,7 @@ class CalcEquipMixin(CharacterBase):
|
|||||||
fun = effects.execture(f'equ_{equ.id}')
|
fun = effects.execture(f'equ_{equ.id}')
|
||||||
if fun is None:
|
if fun is None:
|
||||||
continue
|
continue
|
||||||
filtered_dict = {k: v for k, v in equ.__dict__.items() if k[0].isupper()}
|
apply_uppercase_attrs(self, equ)
|
||||||
self.SetStatus(**filtered_dict)
|
|
||||||
fun(self)
|
fun(self)
|
||||||
fun = effects.execture(f'enchant_{enchat}')
|
fun = effects.execture(f'enchant_{enchat}')
|
||||||
if fun is not None:
|
if fun is not None:
|
||||||
@@ -286,8 +284,7 @@ class CalcEquipMixin(CharacterBase):
|
|||||||
fun = effects.execture(f'oath_{oath.id}')
|
fun = effects.execture(f'oath_{oath.id}')
|
||||||
if fun is None:
|
if fun is None:
|
||||||
continue
|
continue
|
||||||
filtered_dict = {k: v for k, v in oath.__dict__.items() if k[0].isupper()}
|
apply_uppercase_attrs(self, oath)
|
||||||
self.SetStatus(**filtered_dict)
|
|
||||||
fun(self)
|
fun(self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -403,12 +400,13 @@ class CalcEquipMixin(CharacterBase):
|
|||||||
# region 主计算入口
|
# region 主计算入口
|
||||||
|
|
||||||
def calc(self, setInfo: dict[str, dict]):
|
def calc(self, setInfo: dict[str, dict]):
|
||||||
self.calc_init(setInfo)
|
config = CalcConfigView(setInfo).as_legacy_dict()
|
||||||
|
self.calc_init(config)
|
||||||
self.SetBaseStatus()
|
self.SetBaseStatus()
|
||||||
self.calc_jade(setInfo.get('jades', []))
|
self.calc_jade(config.get('jades', []))
|
||||||
self.calc_avatar(setInfo.get('avatar', {}))
|
self.calc_avatar(config.get('avatar', {}))
|
||||||
self.calc_basic()
|
self.calc_basic()
|
||||||
self.calc_sundry(setInfo.get('sundry', {}))
|
self.calc_sundry(config.get('sundry', {}))
|
||||||
self.calc_emblems()
|
self.calc_emblems()
|
||||||
suit = self.calc_suits()
|
suit = self.calc_suits()
|
||||||
self.calc_equs()
|
self.calc_equs()
|
||||||
@@ -430,7 +428,7 @@ class CalcEquipMixin(CharacterBase):
|
|||||||
self.输出类型 = '魔法百分比'
|
self.输出类型 = '魔法百分比'
|
||||||
else:
|
else:
|
||||||
self.输出类型 = '物理百分比'
|
self.输出类型 = '物理百分比'
|
||||||
skillInfos = self.calc_carry_skills(setInfo.get('DSB', False), setInfo.get('BUFF', False))
|
skillInfos = self.calc_carry_skills(config.get('DSB', False), config.get('BUFF', False))
|
||||||
info = self.get_carry_info()
|
info = self.get_carry_info()
|
||||||
return {'uuid': uuid1().hex, 'skills': skillInfos, 'info': info, 'suits': suit, 'buffer': self.buffer}
|
return {'uuid': uuid1().hex, 'skills': skillInfos, 'info': info, 'suits': suit, 'buffer': self.buffer}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class CalcConfigView:
|
||||||
|
def __init__(self, raw: Mapping[str, Any] | None):
|
||||||
|
self.raw = dict(raw or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def equips(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('equips') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oaths(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('oaths') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emblems(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('emblems') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skills(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('skills') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('avatar') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sundry(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('sundry') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jades(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('jades') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('options') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsb(self) -> bool:
|
||||||
|
return bool(self.raw.get('DSB', False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def buff(self) -> bool:
|
||||||
|
return bool(self.raw.get('BUFF', False))
|
||||||
|
|
||||||
|
def as_legacy_dict(self) -> dict[str, Any]:
|
||||||
|
data = dict(self.raw)
|
||||||
|
data.setdefault('equips', {})
|
||||||
|
data.setdefault('oaths', {})
|
||||||
|
data.setdefault('emblems', {})
|
||||||
|
data.setdefault('skills', {})
|
||||||
|
data.setdefault('avatar', {})
|
||||||
|
data.setdefault('sundry', {})
|
||||||
|
data.setdefault('jades', {})
|
||||||
|
data.setdefault('options', {})
|
||||||
|
data.setdefault('DSB', False)
|
||||||
|
data.setdefault('BUFF', False)
|
||||||
|
return data
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def uppercase_attrs(source: Any) -> dict[str, Any]:
|
||||||
|
return {key: value for key, value in source.__dict__.items() if key and key[0].isupper()}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_uppercase_attrs(character: Any, source: Any) -> None:
|
||||||
|
character.SetStatus(**uppercase_attrs(source))
|
||||||
|
|
||||||
|
|
||||||
|
def execute_effect(effects: Any, key: str, *args: Any) -> bool:
|
||||||
|
fun = effects.execture(key)
|
||||||
|
if fun is None:
|
||||||
|
return False
|
||||||
|
fun(*args)
|
||||||
|
return True
|
||||||
@@ -4,6 +4,7 @@ from functools import cache
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from database.models import EmblemData, OathData, OathSuitData, OathSuitSkillData, PrimerData, EquData, SuitData, EnchantData, JadeData
|
from database.models import EmblemData, OathData, OathSuitData, OathSuitSkillData, PrimerData, EquData, SuitData, EnchantData, JadeData
|
||||||
from database.connect import get_db_engine as get_engine
|
from database.connect import get_db_engine as get_engine
|
||||||
|
from core.basic.equipment_registry import EquipmentRegistry
|
||||||
|
|
||||||
|
|
||||||
def parse_to_number_list(info: str, default: Sequence[float] = (0,)) -> list[float]:
|
def parse_to_number_list(info: str, default: Sequence[float] = (0,)) -> list[float]:
|
||||||
@@ -422,14 +423,8 @@ class OathSuit:
|
|||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
global equ0
|
_equipment_registry = EquipmentRegistry(Equipments)
|
||||||
equ0 = Equipments('0')
|
|
||||||
|
|
||||||
|
|
||||||
def get_equipment(version: str = '0') -> Equipments:
|
def get_equipment(version: str = '0') -> Equipments:
|
||||||
# print('加载装备信息')
|
return _equipment_registry.get(version)
|
||||||
try:
|
|
||||||
temp = eval(f'equ{version}'.format(version))
|
|
||||||
except Exception:
|
|
||||||
temp = equ0
|
|
||||||
return temp
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from functools import cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from database.connect import BACKEND_DIR
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentRegistry:
|
||||||
|
def __init__(self, equipment_cls):
|
||||||
|
self.equipment_cls = equipment_cls
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get(self, version: str = '0'):
|
||||||
|
normalized = version or '0'
|
||||||
|
if not self._version_exists(normalized):
|
||||||
|
if normalized == '0':
|
||||||
|
return self.equipment_cls(normalized)
|
||||||
|
return self.get('0')
|
||||||
|
return self.equipment_cls(normalized)
|
||||||
|
|
||||||
|
def _version_exists(self, version: str) -> bool:
|
||||||
|
db_path = Path(BACKEND_DIR) / 'dataFiles' / f'dcalc_{version}.db'
|
||||||
|
return db_path.exists()
|
||||||
+41
-41
@@ -1,10 +1,10 @@
|
|||||||
#8d4d2001cdb357e41633c234eb7501b5
|
#8d4d2001cdb357e41633c234eb7501b5
|
||||||
from core.basic.skill import PassiveSkill, ActiveSkill, get_data
|
from core.basic.skill import PassiveSkill, ActiveSkill, get_data
|
||||||
from core.basic.character import Character
|
from core.basic.character import Character
|
||||||
prefix = "imperialknight/infighter/cn/skillDetail"
|
prefix = "imperialknight/breaker/cn/skillDetail"
|
||||||
|
|
||||||
# 레이즈 어퍼컷
|
# 레이즈 어퍼컷
|
||||||
# imperialknight/infighter/3c5604bdbb0240b8f130f59ab40509c3
|
# imperialknight/breaker/3c5604bdbb0240b8f130f59ab40509c3
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/3c5604bdbb0240b8f130f59ab40509c3
|
# 8d4d2001cdb357e41633c234eb7501b5/3c5604bdbb0240b8f130f59ab40509c3
|
||||||
class Skill0(ActiveSkill):
|
class Skill0(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -29,7 +29,7 @@ class Skill0(ActiveSkill):
|
|||||||
data0 = get_data(f'{prefix}/{uuid}', 0)
|
data0 = get_data(f'{prefix}/{uuid}', 0)
|
||||||
|
|
||||||
# 덱 브레이커
|
# 덱 브레이커
|
||||||
# imperialknight/infighter/717f1e2104fe4b796f800352fa143ecc
|
# imperialknight/breaker/717f1e2104fe4b796f800352fa143ecc
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/717f1e2104fe4b796f800352fa143ecc
|
# 8d4d2001cdb357e41633c234eb7501b5/717f1e2104fe4b796f800352fa143ecc
|
||||||
class Skill1(PassiveSkill):
|
class Skill1(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -56,7 +56,7 @@ class Skill1(PassiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 聚焦射击
|
# 聚焦射击
|
||||||
# imperialknight/infighter/eb71e1d82d92c7e1d40500a0dcd77aa6
|
# imperialknight/breaker/eb71e1d82d92c7e1d40500a0dcd77aa6
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/eb71e1d82d92c7e1d40500a0dcd77aa6
|
# 8d4d2001cdb357e41633c234eb7501b5/eb71e1d82d92c7e1d40500a0dcd77aa6
|
||||||
class Skill2(ActiveSkill):
|
class Skill2(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -80,7 +80,7 @@ class Skill2(ActiveSkill):
|
|||||||
data0 = get_data(f'{prefix}/{uuid}', 0)
|
data0 = get_data(f'{prefix}/{uuid}', 0)
|
||||||
|
|
||||||
# 后跳
|
# 后跳
|
||||||
# imperialknight/infighter/7822d6d52e10964a6755f142c666b494
|
# imperialknight/breaker/7822d6d52e10964a6755f142c666b494
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/7822d6d52e10964a6755f142c666b494
|
# 8d4d2001cdb357e41633c234eb7501b5/7822d6d52e10964a6755f142c666b494
|
||||||
class Skill3(ActiveSkill):
|
class Skill3(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -101,7 +101,7 @@ class Skill3(ActiveSkill):
|
|||||||
|
|
||||||
|
|
||||||
# 基础精通
|
# 基础精通
|
||||||
# imperialknight/infighter/5a56514f35cf0270ae8d6c65f8fefd78
|
# imperialknight/breaker/5a56514f35cf0270ae8d6c65f8fefd78
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/5a56514f35cf0270ae8d6c65f8fefd78
|
# 8d4d2001cdb357e41633c234eb7501b5/5a56514f35cf0270ae8d6c65f8fefd78
|
||||||
class Skill4(PassiveSkill):
|
class Skill4(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -132,7 +132,7 @@ class Skill4(PassiveSkill):
|
|||||||
data2 = get_data(f'{prefix}/{uuid}', 2)
|
data2 = get_data(f'{prefix}/{uuid}', 2)
|
||||||
|
|
||||||
# 受身蹲伏
|
# 受身蹲伏
|
||||||
# imperialknight/infighter/ce26c6b69d02a440a81b552bec94f03b
|
# imperialknight/breaker/ce26c6b69d02a440a81b552bec94f03b
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/ce26c6b69d02a440a81b552bec94f03b
|
# 8d4d2001cdb357e41633c234eb7501b5/ce26c6b69d02a440a81b552bec94f03b
|
||||||
class Skill5(ActiveSkill):
|
class Skill5(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -163,7 +163,7 @@ class Skill5(ActiveSkill):
|
|||||||
data2 = get_data(f'{prefix}/{uuid}', 2)
|
data2 = get_data(f'{prefix}/{uuid}', 2)
|
||||||
|
|
||||||
# 技能连招
|
# 技能连招
|
||||||
# imperialknight/infighter/10b31c7e8cd2d60b9bcf3b1d3ada4b0a
|
# imperialknight/breaker/10b31c7e8cd2d60b9bcf3b1d3ada4b0a
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/10b31c7e8cd2d60b9bcf3b1d3ada4b0a
|
# 8d4d2001cdb357e41633c234eb7501b5/10b31c7e8cd2d60b9bcf3b1d3ada4b0a
|
||||||
class Skill6(ActiveSkill):
|
class Skill6(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -206,7 +206,7 @@ class Skill6(ActiveSkill):
|
|||||||
|
|
||||||
|
|
||||||
# 防具精通
|
# 防具精通
|
||||||
# imperialknight/infighter/892ef624d8bf3d7fc045f84825fd6104
|
# imperialknight/breaker/892ef624d8bf3d7fc045f84825fd6104
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/892ef624d8bf3d7fc045f84825fd6104
|
# 8d4d2001cdb357e41633c234eb7501b5/892ef624d8bf3d7fc045f84825fd6104
|
||||||
class Skill7(PassiveSkill):
|
class Skill7(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -228,7 +228,7 @@ class Skill7(PassiveSkill):
|
|||||||
|
|
||||||
|
|
||||||
# 近敌灭杀
|
# 近敌灭杀
|
||||||
# imperialknight/infighter/9dda3f4a849dba1a288dd65e116860f2
|
# imperialknight/breaker/9dda3f4a849dba1a288dd65e116860f2
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/9dda3f4a849dba1a288dd65e116860f2
|
# 8d4d2001cdb357e41633c234eb7501b5/9dda3f4a849dba1a288dd65e116860f2
|
||||||
class Skill8(ActiveSkill):
|
class Skill8(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -257,7 +257,7 @@ class Skill8(ActiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 加农炮
|
# 加农炮
|
||||||
# imperialknight/infighter/4655101518604f874721b3cc249aae10
|
# imperialknight/breaker/4655101518604f874721b3cc249aae10
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/4655101518604f874721b3cc249aae10
|
# 8d4d2001cdb357e41633c234eb7501b5/4655101518604f874721b3cc249aae10
|
||||||
class Skill9(ActiveSkill):
|
class Skill9(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -286,7 +286,7 @@ class Skill9(ActiveSkill):
|
|||||||
# 施放技能过程中所受伤害 -90%
|
# 施放技能过程中所受伤害 -90%
|
||||||
|
|
||||||
# 퀵 바이섹션
|
# 퀵 바이섹션
|
||||||
# imperialknight/infighter/0969cd4054d93da07708108c0cc1c4b5
|
# imperialknight/breaker/0969cd4054d93da07708108c0cc1c4b5
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/0969cd4054d93da07708108c0cc1c4b5
|
# 8d4d2001cdb357e41633c234eb7501b5/0969cd4054d93da07708108c0cc1c4b5
|
||||||
class Skill10(ActiveSkill):
|
class Skill10(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -310,7 +310,7 @@ class Skill10(ActiveSkill):
|
|||||||
data0 = get_data(f'{prefix}/{uuid}', 0)
|
data0 = get_data(f'{prefix}/{uuid}', 0)
|
||||||
|
|
||||||
# 러프 보딩
|
# 러프 보딩
|
||||||
# imperialknight/infighter/c9664191611af31142e052dfaef84530
|
# imperialknight/breaker/c9664191611af31142e052dfaef84530
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/c9664191611af31142e052dfaef84530
|
# 8d4d2001cdb357e41633c234eb7501b5/c9664191611af31142e052dfaef84530
|
||||||
class Skill11(ActiveSkill):
|
class Skill11(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -346,7 +346,7 @@ class Skill11(ActiveSkill):
|
|||||||
data2 = get_data(f'{prefix}/{uuid}', 2)
|
data2 = get_data(f'{prefix}/{uuid}', 2)
|
||||||
|
|
||||||
# 强化 - 后跳
|
# 强化 - 后跳
|
||||||
# imperialknight/infighter/2b340542e776818b78f3212af184bd6b
|
# imperialknight/breaker/2b340542e776818b78f3212af184bd6b
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/2b340542e776818b78f3212af184bd6b
|
# 8d4d2001cdb357e41633c234eb7501b5/2b340542e776818b78f3212af184bd6b
|
||||||
class Skill12(PassiveSkill):
|
class Skill12(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -379,7 +379,7 @@ class Skill12(PassiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 跃翔
|
# 跃翔
|
||||||
# imperialknight/infighter/1fea5a626f15230237946a11a9d11582
|
# imperialknight/breaker/1fea5a626f15230237946a11a9d11582
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/1fea5a626f15230237946a11a9d11582
|
# 8d4d2001cdb357e41633c234eb7501b5/1fea5a626f15230237946a11a9d11582
|
||||||
class Skill13(ActiveSkill):
|
class Skill13(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -406,7 +406,7 @@ class Skill13(ActiveSkill):
|
|||||||
data0 = get_data(f'{prefix}/{uuid}', 0)
|
data0 = get_data(f'{prefix}/{uuid}', 0)
|
||||||
|
|
||||||
# 노 쿼터
|
# 노 쿼터
|
||||||
# imperialknight/infighter/f2fb27162beb0b87a7cb9af7900e95f2
|
# imperialknight/breaker/f2fb27162beb0b87a7cb9af7900e95f2
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/f2fb27162beb0b87a7cb9af7900e95f2
|
# 8d4d2001cdb357e41633c234eb7501b5/f2fb27162beb0b87a7cb9af7900e95f2
|
||||||
class Skill14(PassiveSkill):
|
class Skill14(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -431,7 +431,7 @@ class Skill14(PassiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 将飞锚附加到震动箭上, 驱动迷雾能量发射。
|
# 将飞锚附加到震动箭上, 驱动迷雾能量发射。
|
||||||
# imperialknight/infighter/cfacda0647b9a0f595df2c2aad30c18d
|
# imperialknight/breaker/cfacda0647b9a0f595df2c2aad30c18d
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/cfacda0647b9a0f595df2c2aad30c18d
|
# 8d4d2001cdb357e41633c234eb7501b5/cfacda0647b9a0f595df2c2aad30c18d
|
||||||
class Skill15(ActiveSkill):
|
class Skill15(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -460,7 +460,7 @@ class Skill15(ActiveSkill):
|
|||||||
# 施放技能过程中所受伤害 -90%
|
# 施放技能过程中所受伤害 -90%
|
||||||
|
|
||||||
# 훅 샷
|
# 훅 샷
|
||||||
# imperialknight/infighter/2f5d03c7848effbc0a23f4df45d9ca46
|
# imperialknight/breaker/2f5d03c7848effbc0a23f4df45d9ca46
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/2f5d03c7848effbc0a23f4df45d9ca46
|
# 8d4d2001cdb357e41633c234eb7501b5/2f5d03c7848effbc0a23f4df45d9ca46
|
||||||
class Skill16(ActiveSkill):
|
class Skill16(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -497,7 +497,7 @@ class Skill16(ActiveSkill):
|
|||||||
data2 = get_data(f'{prefix}/{uuid}', 2)
|
data2 = get_data(f'{prefix}/{uuid}', 2)
|
||||||
|
|
||||||
# 어베스트
|
# 어베스트
|
||||||
# imperialknight/infighter/45442bbbe33540b4deeec29437dae70c
|
# imperialknight/breaker/45442bbbe33540b4deeec29437dae70c
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/45442bbbe33540b4deeec29437dae70c
|
# 8d4d2001cdb357e41633c234eb7501b5/45442bbbe33540b4deeec29437dae70c
|
||||||
class Skill17(ActiveSkill):
|
class Skill17(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -525,7 +525,7 @@ class Skill17(ActiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 선체 파괴
|
# 선체 파괴
|
||||||
# imperialknight/infighter/01c3a2fb793d293a25ed8dc7a0d70c1a
|
# imperialknight/breaker/01c3a2fb793d293a25ed8dc7a0d70c1a
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/01c3a2fb793d293a25ed8dc7a0d70c1a
|
# 8d4d2001cdb357e41633c234eb7501b5/01c3a2fb793d293a25ed8dc7a0d70c1a
|
||||||
class Skill18(ActiveSkill):
|
class Skill18(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -555,7 +555,7 @@ class Skill18(ActiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 暴击
|
# 暴击
|
||||||
# imperialknight/infighter/fc1262c19f3d0477ee8eda47b8db8696
|
# imperialknight/breaker/fc1262c19f3d0477ee8eda47b8db8696
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/fc1262c19f3d0477ee8eda47b8db8696
|
# 8d4d2001cdb357e41633c234eb7501b5/fc1262c19f3d0477ee8eda47b8db8696
|
||||||
class Skill19(PassiveSkill):
|
class Skill19(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -577,7 +577,7 @@ class Skill19(PassiveSkill):
|
|||||||
data0 = get_data(f'{prefix}/{uuid}', 0)
|
data0 = get_data(f'{prefix}/{uuid}', 0)
|
||||||
|
|
||||||
# 스위핑 스트라이크
|
# 스위핑 스트라이크
|
||||||
# imperialknight/infighter/8f73f243041c2d27739fe7696f02bf9b
|
# imperialknight/breaker/8f73f243041c2d27739fe7696f02bf9b
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/8f73f243041c2d27739fe7696f02bf9b
|
# 8d4d2001cdb357e41633c234eb7501b5/8f73f243041c2d27739fe7696f02bf9b
|
||||||
class Skill20(ActiveSkill):
|
class Skill20(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -612,7 +612,7 @@ class Skill20(ActiveSkill):
|
|||||||
data3 = get_data(f'{prefix}/{uuid}', 3)
|
data3 = get_data(f'{prefix}/{uuid}', 3)
|
||||||
|
|
||||||
# 백병전 전문가
|
# 백병전 전문가
|
||||||
# imperialknight/infighter/8c2379737c5acc935c1731f67f607655
|
# imperialknight/breaker/8c2379737c5acc935c1731f67f607655
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/8c2379737c5acc935c1731f67f607655
|
# 8d4d2001cdb357e41633c234eb7501b5/8c2379737c5acc935c1731f67f607655
|
||||||
class Skill21(PassiveSkill):
|
class Skill21(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -639,7 +639,7 @@ class Skill21(PassiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 타이드 피어서
|
# 타이드 피어서
|
||||||
# imperialknight/infighter/9cb6f9ed646fa87f9b7680a42ce83d1a
|
# imperialknight/breaker/9cb6f9ed646fa87f9b7680a42ce83d1a
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/9cb6f9ed646fa87f9b7680a42ce83d1a
|
# 8d4d2001cdb357e41633c234eb7501b5/9cb6f9ed646fa87f9b7680a42ce83d1a
|
||||||
class Skill22(ActiveSkill):
|
class Skill22(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -670,7 +670,7 @@ class Skill22(ActiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 딥 워터 콜
|
# 딥 워터 콜
|
||||||
# imperialknight/infighter/3829c15bf5f520c13998a3479ba0ce7b
|
# imperialknight/breaker/3829c15bf5f520c13998a3479ba0ce7b
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/3829c15bf5f520c13998a3479ba0ce7b
|
# 8d4d2001cdb357e41633c234eb7501b5/3829c15bf5f520c13998a3479ba0ce7b
|
||||||
class Skill23(ActiveSkill):
|
class Skill23(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -717,7 +717,7 @@ class Skill23(ActiveSkill):
|
|||||||
data6 = get_data(f'{prefix}/{uuid}', 6)
|
data6 = get_data(f'{prefix}/{uuid}', 6)
|
||||||
|
|
||||||
# 狂风可以在空中使用机枪和导弹攻击敌人。 持续时间结束、 召唤出新的狂风机器人, 或机械师死亡后, 狂风会爆炸并给予周围敌人一定伤害。
|
# 狂风可以在空中使用机枪和导弹攻击敌人。 持续时间结束、 召唤出新的狂风机器人, 或机械师死亡后, 狂风会爆炸并给予周围敌人一定伤害。
|
||||||
# imperialknight/infighter/4224f9b0b8c7c903e9a1e0f9d9f6d04d
|
# imperialknight/breaker/4224f9b0b8c7c903e9a1e0f9d9f6d04d
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/4224f9b0b8c7c903e9a1e0f9d9f6d04d
|
# 8d4d2001cdb357e41633c234eb7501b5/4224f9b0b8c7c903e9a1e0f9d9f6d04d
|
||||||
class Skill24(ActiveSkill):
|
class Skill24(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -771,7 +771,7 @@ class Skill24(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 타이드 바운드
|
# 타이드 바운드
|
||||||
# imperialknight/infighter/78bd107acd474518b606be1e4fd38239
|
# imperialknight/breaker/78bd107acd474518b606be1e4fd38239
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/78bd107acd474518b606be1e4fd38239
|
# 8d4d2001cdb357e41633c234eb7501b5/78bd107acd474518b606be1e4fd38239
|
||||||
class Skill25(ActiveSkill):
|
class Skill25(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -850,7 +850,7 @@ class Skill25(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 역조연격
|
# 역조연격
|
||||||
# imperialknight/infighter/d085127b0edd719782bd618d5688f4a1
|
# imperialknight/breaker/d085127b0edd719782bd618d5688f4a1
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/d085127b0edd719782bd618d5688f4a1
|
# 8d4d2001cdb357e41633c234eb7501b5/d085127b0edd719782bd618d5688f4a1
|
||||||
class Skill26(ActiveSkill):
|
class Skill26(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -906,7 +906,7 @@ class Skill26(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 黑暗明王阵
|
# 黑暗明王阵
|
||||||
# imperialknight/infighter/42c82812f86ff6704ae9952a2e6093a4
|
# imperialknight/breaker/42c82812f86ff6704ae9952a2e6093a4
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/42c82812f86ff6704ae9952a2e6093a4
|
# 8d4d2001cdb357e41633c234eb7501b5/42c82812f86ff6704ae9952a2e6093a4
|
||||||
class Skill27(ActiveSkill):
|
class Skill27(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -968,7 +968,7 @@ class Skill27(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 골칫거리
|
# 골칫거리
|
||||||
# imperialknight/infighter/3d8f3d438405d79f8d3ed68072674d1e
|
# imperialknight/breaker/3d8f3d438405d79f8d3ed68072674d1e
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/3d8f3d438405d79f8d3ed68072674d1e
|
# 8d4d2001cdb357e41633c234eb7501b5/3d8f3d438405d79f8d3ed68072674d1e
|
||||||
class Skill28(PassiveSkill):
|
class Skill28(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -999,7 +999,7 @@ class Skill28(PassiveSkill):
|
|||||||
data1 = get_data(f'{prefix}/{uuid}', 1)
|
data1 = get_data(f'{prefix}/{uuid}', 1)
|
||||||
|
|
||||||
# 개전의 팡파르
|
# 개전의 팡파르
|
||||||
# imperialknight/infighter/ade01c1d6afc8a05055225045e89fe49
|
# imperialknight/breaker/ade01c1d6afc8a05055225045e89fe49
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/ade01c1d6afc8a05055225045e89fe49
|
# 8d4d2001cdb357e41633c234eb7501b5/ade01c1d6afc8a05055225045e89fe49
|
||||||
class Skill29(ActiveSkill):
|
class Skill29(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1036,7 +1036,7 @@ class Skill29(ActiveSkill):
|
|||||||
data4 = get_data(f'{prefix}/{uuid}', 4)
|
data4 = get_data(f'{prefix}/{uuid}', 4)
|
||||||
|
|
||||||
# 破冰飞刃
|
# 破冰飞刃
|
||||||
# imperialknight/infighter/ff171dc487807bb9aa28900ca9a46b41
|
# imperialknight/breaker/ff171dc487807bb9aa28900ca9a46b41
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/ff171dc487807bb9aa28900ca9a46b41
|
# 8d4d2001cdb357e41633c234eb7501b5/ff171dc487807bb9aa28900ca9a46b41
|
||||||
class Skill30(ActiveSkill):
|
class Skill30(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1093,7 +1093,7 @@ class Skill30(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 마엘스트롬
|
# 마엘스트롬
|
||||||
# imperialknight/infighter/547ab2b2bd860d3e37355a9cfbc1077c
|
# imperialknight/breaker/547ab2b2bd860d3e37355a9cfbc1077c
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/547ab2b2bd860d3e37355a9cfbc1077c
|
# 8d4d2001cdb357e41633c234eb7501b5/547ab2b2bd860d3e37355a9cfbc1077c
|
||||||
class Skill31(ActiveSkill):
|
class Skill31(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1156,7 +1156,7 @@ class Skill31(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 철의 노래
|
# 철의 노래
|
||||||
# imperialknight/infighter/506e7ed77d517419a6e1c437a2cedb17
|
# imperialknight/breaker/506e7ed77d517419a6e1c437a2cedb17
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/506e7ed77d517419a6e1c437a2cedb17
|
# 8d4d2001cdb357e41633c234eb7501b5/506e7ed77d517419a6e1c437a2cedb17
|
||||||
class Skill32(PassiveSkill):
|
class Skill32(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1180,7 +1180,7 @@ class Skill32(PassiveSkill):
|
|||||||
data0 = get_data(f'{prefix}/{uuid}', 0)
|
data0 = get_data(f'{prefix}/{uuid}', 0)
|
||||||
|
|
||||||
# 冷血突刺
|
# 冷血突刺
|
||||||
# imperialknight/infighter/a5fa08f5d509e6ff2ebc68856a470b5a
|
# imperialknight/breaker/a5fa08f5d509e6ff2ebc68856a470b5a
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/a5fa08f5d509e6ff2ebc68856a470b5a
|
# 8d4d2001cdb357e41633c234eb7501b5/a5fa08f5d509e6ff2ebc68856a470b5a
|
||||||
class Skill33(ActiveSkill):
|
class Skill33(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1246,7 +1246,7 @@ class Skill33(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 교룡아
|
# 교룡아
|
||||||
# imperialknight/infighter/5dc7008b12a459325b548b0715c6b73c
|
# imperialknight/breaker/5dc7008b12a459325b548b0715c6b73c
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/5dc7008b12a459325b548b0715c6b73c
|
# 8d4d2001cdb357e41633c234eb7501b5/5dc7008b12a459325b548b0715c6b73c
|
||||||
class Skill34(ActiveSkill):
|
class Skill34(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1315,7 +1315,7 @@ class Skill34(ActiveSkill):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# 씨 베인
|
# 씨 베인
|
||||||
# imperialknight/infighter/8ee0099656df08a0b39225f8a21d514b
|
# imperialknight/breaker/8ee0099656df08a0b39225f8a21d514b
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/8ee0099656df08a0b39225f8a21d514b
|
# 8d4d2001cdb357e41633c234eb7501b5/8ee0099656df08a0b39225f8a21d514b
|
||||||
class Skill35(ActiveSkill):
|
class Skill35(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1356,7 +1356,7 @@ class Skill35(ActiveSkill):
|
|||||||
data4 = get_data(f'{prefix}/{uuid}', 4)
|
data4 = get_data(f'{prefix}/{uuid}', 4)
|
||||||
|
|
||||||
# 역조
|
# 역조
|
||||||
# imperialknight/infighter/9bff7f2559e003766fee2853dca00631
|
# imperialknight/breaker/9bff7f2559e003766fee2853dca00631
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/9bff7f2559e003766fee2853dca00631
|
# 8d4d2001cdb357e41633c234eb7501b5/9bff7f2559e003766fee2853dca00631
|
||||||
class Skill36(PassiveSkill):
|
class Skill36(PassiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1389,7 +1389,7 @@ class Skill36(PassiveSkill):
|
|||||||
# 타이드 피어서 받는 데미지 감소율 : 30%
|
# 타이드 피어서 받는 데미지 감소율 : 30%
|
||||||
|
|
||||||
# 언바운드 템페스트
|
# 언바운드 템페스트
|
||||||
# imperialknight/infighter/da6e37c1e3f0e8867f70007d89c239ff
|
# imperialknight/breaker/da6e37c1e3f0e8867f70007d89c239ff
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/da6e37c1e3f0e8867f70007d89c239ff
|
# 8d4d2001cdb357e41633c234eb7501b5/da6e37c1e3f0e8867f70007d89c239ff
|
||||||
class Skill37(ActiveSkill):
|
class Skill37(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1427,7 +1427,7 @@ class Skill37(ActiveSkill):
|
|||||||
data3 = get_data(f'{prefix}/{uuid}', 3)
|
data3 = get_data(f'{prefix}/{uuid}', 3)
|
||||||
|
|
||||||
# 해신낙연
|
# 해신낙연
|
||||||
# imperialknight/infighter/a6c8f69107f8c4f5d1a0c7a57d000290
|
# imperialknight/breaker/a6c8f69107f8c4f5d1a0c7a57d000290
|
||||||
# 8d4d2001cdb357e41633c234eb7501b5/a6c8f69107f8c4f5d1a0c7a57d000290
|
# 8d4d2001cdb357e41633c234eb7501b5/a6c8f69107f8c4f5d1a0c7a57d000290
|
||||||
class Skill38(ActiveSkill):
|
class Skill38(ActiveSkill):
|
||||||
"""
|
"""
|
||||||
@@ -1466,7 +1466,7 @@ class Skill38(ActiveSkill):
|
|||||||
class classChange(Character):
|
class classChange(Character):
|
||||||
def __init__(self, equVersion):
|
def __init__(self, equVersion):
|
||||||
|
|
||||||
self.name = 'infighter'
|
self.name = 'breaker'
|
||||||
self.nameCN = '鸣锋·破浪者'
|
self.nameCN = '鸣锋·破浪者'
|
||||||
self.role = 'imperialknight'
|
self.role = 'imperialknight'
|
||||||
self.角色 = '帝国骑士'
|
self.角色 = '帝国骑士'
|
||||||
+1
-1
Submodule backend/openapi updated: abf3eca40c...9d741b23d2
@@ -15,5 +15,6 @@ dependencies = [
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"pytest>=8.4.0",
|
||||||
"ruff>=0.15.15",
|
"ruff>=0.15.15",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Application services for backend API orchestration."""
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from api.core.Auth import AlterState
|
||||||
|
from core.basic.character import createCharacter
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('dcalc.calc')
|
||||||
|
|
||||||
|
|
||||||
|
def calculate(state: AlterState, config: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
started_at = time.perf_counter()
|
||||||
|
safe_config = config or {}
|
||||||
|
character = createCharacter(state.alter, state.equVersion)
|
||||||
|
created_at = time.perf_counter()
|
||||||
|
result = character.calc(safe_config)
|
||||||
|
finished_at = time.perf_counter()
|
||||||
|
logger.info(
|
||||||
|
'calc completed alter=%s equVersion=%s create_ms=%.2f calc_ms=%.2f total_ms=%.2f',
|
||||||
|
state.alter,
|
||||||
|
state.equVersion,
|
||||||
|
(created_at - started_at) * 1000,
|
||||||
|
(finished_at - created_at) * 1000,
|
||||||
|
(finished_at - started_at) * 1000,
|
||||||
|
)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
from main import application
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return TestClient(application)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def alter_token():
|
||||||
|
payload = {
|
||||||
|
'alter': 'MDAwMDAw',
|
||||||
|
'equVersion': '0',
|
||||||
|
'time': int(time.time() * 1000),
|
||||||
|
}
|
||||||
|
return base64.b64encode(json.dumps(payload).encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def minimal_calc_config():
|
||||||
|
parts = [
|
||||||
|
'武器',
|
||||||
|
'副武器',
|
||||||
|
'称号',
|
||||||
|
'上衣',
|
||||||
|
'头肩',
|
||||||
|
'下装',
|
||||||
|
'腰带',
|
||||||
|
'鞋',
|
||||||
|
'手镯',
|
||||||
|
'项链',
|
||||||
|
'戒指',
|
||||||
|
'辅助装备',
|
||||||
|
'魔法石',
|
||||||
|
'耳环',
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'equips': {
|
||||||
|
part: {
|
||||||
|
'id': '',
|
||||||
|
'reinforce': 0,
|
||||||
|
'reinforceType': 0,
|
||||||
|
'enchant': '0',
|
||||||
|
'refine': 0,
|
||||||
|
'adaptation': 0,
|
||||||
|
}
|
||||||
|
for part in parts
|
||||||
|
},
|
||||||
|
'oaths': {},
|
||||||
|
'emblems': {},
|
||||||
|
'skills': {},
|
||||||
|
'avatar': {},
|
||||||
|
'sundry': {},
|
||||||
|
'jades': {},
|
||||||
|
'options': {},
|
||||||
|
'DSB': False,
|
||||||
|
'BUFF': False,
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
def test_calc_response_contract(client, alter_token, minimal_calc_config):
|
||||||
|
response = client.post(
|
||||||
|
'/api/calc',
|
||||||
|
headers={'alter-token': alter_token},
|
||||||
|
json=minimal_calc_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload['code'] == 200
|
||||||
|
assert payload['message'] == 'Success'
|
||||||
|
assert isinstance(payload['data'], dict)
|
||||||
|
|
||||||
|
data = payload['data']
|
||||||
|
assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(data)
|
||||||
|
assert isinstance(data['uuid'], str)
|
||||||
|
assert isinstance(data['skills'], list)
|
||||||
|
assert isinstance(data['info'], list)
|
||||||
|
assert isinstance(data['buffer'], bool)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from core.basic.character import createCharacter
|
||||||
|
|
||||||
|
|
||||||
|
def test_character_calc_returns_result_shape(minimal_calc_config):
|
||||||
|
character = createCharacter('GF.swordman_male.weapon_master', '0')
|
||||||
|
|
||||||
|
result = character.calc(minimal_calc_config)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(result)
|
||||||
|
assert isinstance(result['skills'], list)
|
||||||
|
assert isinstance(result['info'], list)
|
||||||
|
assert result['buffer'] is False
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from core.basic.character.config_view import CalcConfigView
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_view_provides_defaults():
|
||||||
|
view = CalcConfigView(None)
|
||||||
|
|
||||||
|
assert view.equips == {}
|
||||||
|
assert view.oaths == {}
|
||||||
|
assert view.emblems == {}
|
||||||
|
assert view.skills == {}
|
||||||
|
assert view.avatar == {}
|
||||||
|
assert view.sundry == {}
|
||||||
|
assert view.jades == {}
|
||||||
|
assert view.options == {}
|
||||||
|
assert view.dsb is False
|
||||||
|
assert view.buff is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_view_preserves_legacy_keys():
|
||||||
|
view = CalcConfigView({'skills': {'100': {'lv': 1}}, 'BUFF': True})
|
||||||
|
|
||||||
|
data = view.as_legacy_dict()
|
||||||
|
|
||||||
|
assert data['skills'] == {'100': {'lv': 1}}
|
||||||
|
assert data['BUFF'] is True
|
||||||
|
assert data['DSB'] is False
|
||||||
|
assert data['equips'] == {}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from api.core.Auth import AlterState
|
||||||
|
from services.calc_service import calculate
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_returns_character_calc_result(minimal_calc_config):
|
||||||
|
state = AlterState('GF.swordman_male.weapon_master', 'token', {}, '0', 0)
|
||||||
|
|
||||||
|
result = calculate(state, minimal_calc_config)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_logs_timing(caplog, minimal_calc_config):
|
||||||
|
state = AlterState('GF.swordman_male.weapon_master', 'token', {}, '0', 0)
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO, logger='dcalc.calc'):
|
||||||
|
calculate(state, minimal_calc_config)
|
||||||
|
|
||||||
|
assert any('calc completed alter=GF.swordman_male.weapon_master' in record.message for record in caplog.records)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from config.main import Config
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_ignores_extra_environment_fields():
|
||||||
|
assert Config.model_config.get('extra') == 'ignore'
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from core.basic.character.effect_apply import execute_effect, uppercase_attrs
|
||||||
|
|
||||||
|
|
||||||
|
class Source:
|
||||||
|
STR = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.INT = 20
|
||||||
|
self.name = 'ignored'
|
||||||
|
|
||||||
|
|
||||||
|
class Effects:
|
||||||
|
def execture(self, key):
|
||||||
|
if key != 'known':
|
||||||
|
return None
|
||||||
|
|
||||||
|
def effect(target):
|
||||||
|
target.append(key)
|
||||||
|
|
||||||
|
return effect
|
||||||
|
|
||||||
|
|
||||||
|
def test_uppercase_attrs_returns_only_uppercase_instance_keys():
|
||||||
|
assert uppercase_attrs(Source()) == {'INT': 20}
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_effect_returns_false_for_missing_effect():
|
||||||
|
assert execute_effect(Effects(), 'missing', []) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_effect_calls_existing_effect():
|
||||||
|
target = []
|
||||||
|
|
||||||
|
assert execute_effect(Effects(), 'known', target) is True
|
||||||
|
assert target == ['known']
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from core.basic.equipment import get_equipment
|
||||||
|
from core.basic.equipment_registry import EquipmentRegistry
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_equipment_returns_cached_default_version():
|
||||||
|
first = get_equipment('0')
|
||||||
|
second = get_equipment('0')
|
||||||
|
|
||||||
|
assert first is second
|
||||||
|
assert first.version == '0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_equipment_falls_back_to_default_version():
|
||||||
|
equipment = get_equipment('missing-version')
|
||||||
|
|
||||||
|
assert equipment.version == '0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_equipment_registry_caches_by_version():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class Equipment:
|
||||||
|
def __init__(self, version):
|
||||||
|
calls.append(version)
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
registry = EquipmentRegistry(Equipment)
|
||||||
|
|
||||||
|
assert registry.get('0') is registry.get('0')
|
||||||
|
assert calls == ['0']
|
||||||
Generated
+41
-1
@@ -546,6 +546,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -560,7 +561,10 @@ requires-dist = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "ruff", specifier = ">=0.15.15" }]
|
dev = [
|
||||||
|
{ name = "pytest", specifier = ">=8.4.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.15.15" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "detect-installer"
|
name = "detect-installer"
|
||||||
@@ -1151,6 +1155,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itsdangerous"
|
name = "itsdangerous"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -1600,6 +1613,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "propcache"
|
name = "propcache"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -1944,6 +1966,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# Backend 渐进式优化设计
|
||||||
|
|
||||||
|
> 本文面向 dcalc backend 的渐进式架构优化。目标是在不改变 `/calc` API 契约、不破坏 frontendNew 计算链路的前提下,逐步提升后端可维护性、扩展性、计算效率与开发规范。
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前仓库前端迁移主线是 `frontend -> frontendNew`,backend 必须优先服务 Dcalc Character 模块的稳定计算链路:equipment、skill、forge、result。backend 现有结构已经完成过一次 `core/basic/character_old.py` 到 `core/basic/character/` 多文件 mixin 的拆分,说明当前更适合继续做小步收口,而不是推倒重写。
|
||||||
|
|
||||||
|
关键入口:
|
||||||
|
|
||||||
|
- `/calc` 路由:`backend/api/routes/calc.py`
|
||||||
|
- 应用入口:`backend/main.py`
|
||||||
|
- 角色计算核心:`backend/core/basic/character/`
|
||||||
|
- 装备数据装载:`backend/core/basic/equipment.py`
|
||||||
|
- 数据库连接:`backend/database/connect.py`
|
||||||
|
- Redis 预热:`backend/api/core/RedisPreheat.py`
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
- 不改 `/calc` 的请求语义、响应包装、`data` 主体字段结构。
|
||||||
|
- 不改 frontendNew 依赖的 IResult 数据消费方式。
|
||||||
|
- 不重写 Skill、Forge、装备效果、职业技能的核心业务逻辑。
|
||||||
|
- 不在 backend 优化中引入新的 UI、前端状态管理或前端接口契约调整。
|
||||||
|
- 不一次性格式化全仓 Python 文件,避免制造大规模无关 diff。
|
||||||
|
|
||||||
|
## 现状问题
|
||||||
|
|
||||||
|
### API 与计算耦合
|
||||||
|
|
||||||
|
`backend/api/routes/calc.py` 直接 `createCharacter(...).calc(config)`。这让鉴权、请求适配、计算编排、错误处理和未来耗时统计都挤在 API 调用路径上,不利于扩展。
|
||||||
|
|
||||||
|
### config 读取分散
|
||||||
|
|
||||||
|
计算过程中大量直接访问 `setInfo.get(...)`、`info['equips']`、`info['oaths']`。默认值、兼容逻辑和输入容错散落在 mixin 中,后续 Skill/Forge 配置演进时容易漏字段。
|
||||||
|
|
||||||
|
### 装备数据全局缓存边界不清
|
||||||
|
|
||||||
|
`backend/core/basic/equipment.py` 启动时构造 `equ0 = Equipments('0')`,`get_equipment()` 通过动态变量和 `eval` 获取版本。该方式对当前性能有帮助,但多版本扩展、测试隔离、热更新和错误定位较弱。
|
||||||
|
|
||||||
|
### 计算对象状态可变
|
||||||
|
|
||||||
|
`Character` 实例在 `calc_init -> calc_* -> calc_*_skills` 过程中持续变异属性、技能等级、装备效果和输出类型。该模式符合当前业务写法,但缺少明确的一次性计算上下文,测试和复用成本偏高。
|
||||||
|
|
||||||
|
### 性能缺少量化
|
||||||
|
|
||||||
|
已有内存化装备数据、技能类缓存、Redis 预热等优化点,但 `/calc` 的耗时瓶颈没有分段统计。后续不应凭感觉优化。
|
||||||
|
|
||||||
|
### 测试安全网不足
|
||||||
|
|
||||||
|
当前仅看到 `backend/test.py`,没有正式 `tests/` 体系。后续重构计算链路时,缺少 golden case 来确认字段与数值没有回退。
|
||||||
|
|
||||||
|
## 优化原则
|
||||||
|
|
||||||
|
1. 接口冻结:先用测试固定 `/calc` 外部行为,再改内部结构。
|
||||||
|
2. 小步可回滚:每个阶段能单独合并、单独验证。
|
||||||
|
3. 先边界后算法:优先整理 service、config view、repository/registry,再考虑公式与缓存。
|
||||||
|
4. 只搬运不改义:拆文件、封 helper 时不改变公式含义。
|
||||||
|
5. 先量化后性能:先加耗时指标,再按热点优化。
|
||||||
|
|
||||||
|
## 目标架构
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/
|
||||||
|
api/
|
||||||
|
routes/
|
||||||
|
calc.py # 只负责 HTTP 层:依赖注入、Body、response
|
||||||
|
services/
|
||||||
|
calc_service.py # /calc 应用服务:创建角色、执行计算、计时、错误上下文
|
||||||
|
schemas/
|
||||||
|
calc.py # 宽松 config schema 与响应类型别名
|
||||||
|
response.py # 通用响应类型,保持旧格式兼容
|
||||||
|
core/
|
||||||
|
basic/
|
||||||
|
character/
|
||||||
|
config_view.py # CalcConfigView:统一默认值、兼容旧 config
|
||||||
|
effect_apply.py # 统一属性提取与 effect 执行 helper
|
||||||
|
equipment.py # 保留现有模型,逐步委托给 registry
|
||||||
|
equipment_registry.py # EquipmentRegistry:装备版本懒加载与缓存
|
||||||
|
tests/
|
||||||
|
contract/
|
||||||
|
test_calc_contract.py
|
||||||
|
golden/
|
||||||
|
test_calc_golden.py
|
||||||
|
unit/
|
||||||
|
test_equipment_registry.py
|
||||||
|
test_calc_config_view.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP POST /api/calc
|
||||||
|
-> AltersDep 解析 alter/equVersion
|
||||||
|
-> calc_service.calculate(state, raw_config)
|
||||||
|
-> CalcConfigView.from_raw(raw_config)
|
||||||
|
-> createCharacter(state.alter, state.equVersion)
|
||||||
|
-> character.calc(config_view 或兼容 dict)
|
||||||
|
-> calc_service 返回原始计算 data
|
||||||
|
-> response(data=data)
|
||||||
|
```
|
||||||
|
|
||||||
|
短期内 `Character.calc()` 仍接收原 `dict`,`CalcConfigView` 先作为内部读取辅助逐步接入。等测试稳定后,再考虑让 `Character.calc()` 接收 `CalcConfigView`。
|
||||||
|
|
||||||
|
## 分阶段设计
|
||||||
|
|
||||||
|
### Phase 0:测试安全网
|
||||||
|
|
||||||
|
建立 pytest 测试结构,固定 `/calc` 契约和典型职业结果结构。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- 增加 `pytest` 开发依赖。
|
||||||
|
- 增加可复用的最小 config fixture。
|
||||||
|
- 增加 `/api/calc` HTTP contract 测试。
|
||||||
|
- 增加 `createCharacter(...).calc(...)` golden smoke 测试。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- `uv run pytest` 可执行。
|
||||||
|
- `/calc` 响应仍包含 `code/message/data`。
|
||||||
|
- `data` 至少包含 `uuid/skills/info/suits/buffer`。
|
||||||
|
|
||||||
|
### Phase 1:API service 边界
|
||||||
|
|
||||||
|
新增 `backend/services/calc_service.py`,API route 不再直接编排计算。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- route 保持原路径、请求体和响应格式。
|
||||||
|
- service 负责 `createCharacter`、空 config 兜底、计算执行。
|
||||||
|
- 为后续日志、耗时、缓存预留 service 层。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- `/api/calc` 行为不变。
|
||||||
|
- route 文件只保留 HTTP 层职责。
|
||||||
|
- Phase 0 测试全部通过。
|
||||||
|
|
||||||
|
### Phase 2:config view 与计算输入收口
|
||||||
|
|
||||||
|
新增 `CalcConfigView`,集中处理 config 的默认值与字段读取。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- `equips/oaths/emblems/skills/avatar/sundry/jades/options` 都有稳定默认值。
|
||||||
|
- 先在 `calc_init()`、`SetDetail()`、`SetSkills()` 等入口使用,不一次性替换所有 mixin。
|
||||||
|
- 保留向后兼容,仍支持原始 dict。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- 缺少可选字段时不抛出 KeyError。
|
||||||
|
- Skill/Forge 配置链路不变:config 变化 -> result.execute -> saveConfig。
|
||||||
|
- golden case 结果字段不变。
|
||||||
|
|
||||||
|
### Phase 3:装备数据 registry
|
||||||
|
|
||||||
|
新增 `EquipmentRegistry`,替换 `get_equipment()` 内部的 `eval` 与模块全局变量访问。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- `get_equipment(version)` 作为兼容门面继续存在。
|
||||||
|
- registry 内部使用 `functools.cache` 或显式 dict 缓存。
|
||||||
|
- 版本不存在时保持回退 `0` 的现有行为,但记录原因。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- 现有调用方无需一次性改动。
|
||||||
|
- 多版本装备数据加载路径清晰。
|
||||||
|
- 单元测试覆盖缓存、回退和不存在版本。
|
||||||
|
|
||||||
|
### Phase 4:计算核心 helper 化
|
||||||
|
|
||||||
|
抽取重复且无业务争议的 helper,降低 mixin 复杂度。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- `apply_uppercase_attrs(character, source)` 统一处理首字母大写属性应用。
|
||||||
|
- `execute_effect(effects, key, *args)` 统一处理效果函数不存在时的行为。
|
||||||
|
- 优先处理 `calc_suits/calc_equs/calc_oaths/calc_emblems` 中重复逻辑。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- helper 有单元测试。
|
||||||
|
- 业务公式位置不变或仅做等价搬迁。
|
||||||
|
- `calc_equip.py` 可读性提升,结果不变。
|
||||||
|
|
||||||
|
### Phase 5:性能观测与热点优化
|
||||||
|
|
||||||
|
在 service 层引入轻量计时,再针对热点做优化。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- 记录 `createCharacter`、`calc` 总耗时。
|
||||||
|
- 可选记录 `calc_*` 分段耗时,默认不影响响应结构。
|
||||||
|
- 优化重复 `获取基础属性()` 调用。
|
||||||
|
- 评估技能实例模板缓存,但必须避免可变技能状态污染请求。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- 能拿到典型配置的平均耗时和 p95。
|
||||||
|
- 优化前后 golden case 一致。
|
||||||
|
- 性能优化有数据支撑。
|
||||||
|
|
||||||
|
### Phase 6:规范与文档
|
||||||
|
|
||||||
|
清理运行缓存、废弃文件和文档入口。
|
||||||
|
|
||||||
|
重点:
|
||||||
|
|
||||||
|
- 确认 `.venv/.ruff_cache/__pycache__/.DS_Store` 不进入 git。
|
||||||
|
- 评估 `character_old.py` 移到归档或删除。
|
||||||
|
- 补充 backend README:运行、测试、质量检查、目录职责。
|
||||||
|
- 不在本阶段大规模格式化老文件。
|
||||||
|
|
||||||
|
成功标准:
|
||||||
|
|
||||||
|
- 新人可通过文档理解 backend 入口和测试方式。
|
||||||
|
- 废弃代码不会继续误导维护。
|
||||||
|
- 规范变更不产生大规模业务 diff。
|
||||||
|
|
||||||
|
## 风险与缓解
|
||||||
|
|
||||||
|
| 风险 | 缓解 |
|
||||||
|
| --- | --- |
|
||||||
|
| 重构导致 `/calc` 字段变化 | Phase 0 先做 contract/golden 测试 |
|
||||||
|
| 技能对象可变状态污染缓存 | 技能模板缓存必须后置,且只缓存类或不可变模板 |
|
||||||
|
| 装备版本回退行为改变 | `get_equipment()` 保留兼容门面,并测试回退 |
|
||||||
|
| config 字段默认值与前端不一致 | `CalcConfigView` 先只集中现有默认值,不新增语义 |
|
||||||
|
| 性能统计污染响应 | 默认只写日志,不改变 API data |
|
||||||
|
|
||||||
|
## 验收总标准
|
||||||
|
|
||||||
|
- `uv run pytest` 通过。
|
||||||
|
- `/api/calc` 响应契约不变。
|
||||||
|
- frontendNew 的 result.execute 与 saveConfig 链路无需调整。
|
||||||
|
- 典型职业 golden case 在重构前后字段一致,关键数值在允许误差内。
|
||||||
|
- backend 文档说明清晰,后续任务可以分阶段执行。
|
||||||
@@ -0,0 +1,908 @@
|
|||||||
|
# Backend 渐进式优化执行清单
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在保持 `/calc` API 契约和 frontendNew 计算链路不变的前提下,逐步提升 backend 的可维护性、扩展性、计算效率与开发规范。
|
||||||
|
|
||||||
|
**Architecture:** 先建立测试安全网,再把 HTTP route 与计算编排拆到 service 层,随后用 config view 和 equipment registry 收口隐式边界。最后基于耗时观测做性能优化,并补齐 backend 文档与规范。
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.10+、FastAPI、Pydantic、SQLAlchemy、Redis、SQLite、pytest、ruff、uv。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构规划
|
||||||
|
|
||||||
|
- Create: `backend/tests/conftest.py`
|
||||||
|
提供测试用 FastAPI client、alter token、最小 calc config fixture。
|
||||||
|
|
||||||
|
- Create: `backend/tests/contract/test_calc_contract.py`
|
||||||
|
固定 `/api/calc` HTTP 响应契约。
|
||||||
|
|
||||||
|
- Create: `backend/tests/golden/test_calc_golden.py`
|
||||||
|
固定典型职业计算结果结构,后续可扩展数值快照。
|
||||||
|
|
||||||
|
- Create: `backend/services/__init__.py`
|
||||||
|
标记 service 包。
|
||||||
|
|
||||||
|
- Create: `backend/services/calc_service.py`
|
||||||
|
承接 `/calc` 应用服务编排。
|
||||||
|
|
||||||
|
- Create: `backend/core/basic/character/config_view.py`
|
||||||
|
集中处理 calc config 默认值与读取。
|
||||||
|
|
||||||
|
- Create: `backend/core/basic/equipment_registry.py`
|
||||||
|
装备版本 registry 与缓存。
|
||||||
|
|
||||||
|
- Modify: `backend/api/routes/calc.py`
|
||||||
|
改为调用 `calc_service.calculate()`,保持 route 契约不变。
|
||||||
|
|
||||||
|
- Modify: `backend/core/basic/character/calc_equip.py`
|
||||||
|
逐步使用 `CalcConfigView` 与 helper,不改计算语义。
|
||||||
|
|
||||||
|
- Modify: `backend/core/basic/equipment.py`
|
||||||
|
`get_equipment()` 委托 `EquipmentRegistry`,保留兼容函数。
|
||||||
|
|
||||||
|
- Modify: `backend/pyproject.toml`
|
||||||
|
增加 `pytest`、必要时增加测试配置。
|
||||||
|
|
||||||
|
- Create or Modify: `backend/README.md`
|
||||||
|
说明运行、测试、质量检查、目录职责。
|
||||||
|
|
||||||
|
## Task 1:建立 pytest 基础与最小 contract 测试
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `backend/pyproject.toml`
|
||||||
|
- Create: `backend/tests/conftest.py`
|
||||||
|
- Create: `backend/tests/contract/test_calc_contract.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 pytest 开发依赖**
|
||||||
|
|
||||||
|
在 `backend/pyproject.toml` 的 `[dependency-groups].dev` 中加入:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.4.0",
|
||||||
|
"ruff>=0.15.15",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 同步依赖**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `pytest` 安装完成,`uv.lock` 更新。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 创建测试 fixture**
|
||||||
|
|
||||||
|
Create `backend/tests/conftest.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from main import application
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return TestClient(application)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def alter_token():
|
||||||
|
payload = {
|
||||||
|
'alter': '1.1.1',
|
||||||
|
'equVersion': '0',
|
||||||
|
'time': int(time.time() * 1000),
|
||||||
|
}
|
||||||
|
return base64.b64encode(json.dumps(payload).encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def minimal_calc_config():
|
||||||
|
parts = [
|
||||||
|
'武器',
|
||||||
|
'副武器',
|
||||||
|
'称号',
|
||||||
|
'上衣',
|
||||||
|
'头肩',
|
||||||
|
'下装',
|
||||||
|
'腰带',
|
||||||
|
'鞋',
|
||||||
|
'手镯',
|
||||||
|
'项链',
|
||||||
|
'戒指',
|
||||||
|
'辅助装备',
|
||||||
|
'魔法石',
|
||||||
|
'耳环',
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
'equips': {
|
||||||
|
part: {
|
||||||
|
'id': '',
|
||||||
|
'reinforce': 0,
|
||||||
|
'reinforceType': 0,
|
||||||
|
'enchant': '0',
|
||||||
|
'refine': 0,
|
||||||
|
'adaptation': 0,
|
||||||
|
}
|
||||||
|
for part in parts
|
||||||
|
},
|
||||||
|
'oaths': {},
|
||||||
|
'emblems': {},
|
||||||
|
'skills': {},
|
||||||
|
'avatar': {},
|
||||||
|
'sundry': {},
|
||||||
|
'jades': {},
|
||||||
|
'options': {},
|
||||||
|
'DSB': False,
|
||||||
|
'BUFF': False,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 创建 contract 测试**
|
||||||
|
|
||||||
|
Create `backend/tests/contract/test_calc_contract.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_calc_response_contract(client, alter_token, minimal_calc_config):
|
||||||
|
response = client.post(
|
||||||
|
'/api/calc',
|
||||||
|
headers={'alter-token': alter_token},
|
||||||
|
json=minimal_calc_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload['code'] == 200
|
||||||
|
assert payload['message'] == 'Success'
|
||||||
|
assert isinstance(payload['data'], dict)
|
||||||
|
|
||||||
|
data = payload['data']
|
||||||
|
assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(data)
|
||||||
|
assert isinstance(data['uuid'], str)
|
||||||
|
assert isinstance(data['skills'], list)
|
||||||
|
assert isinstance(data['info'], list)
|
||||||
|
assert isinstance(data['buffer'], bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行测试,记录真实失败**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/contract/test_calc_contract.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 如果 Redis 依赖导致失败,记录失败原因;不要绕过 `/calc` 鉴权契约。后续可在 fixture 中 monkeypatch `api.dp.get_redis_info` 或提供本地 Redis。
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/pyproject.toml backend/uv.lock backend/tests/conftest.py backend/tests/contract/test_calc_contract.py
|
||||||
|
git commit -m "test: add backend calc contract coverage"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 2:增加 core golden smoke 测试
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `backend/tests/golden/test_calc_golden.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建核心计算 smoke 测试**
|
||||||
|
|
||||||
|
Create `backend/tests/golden/test_calc_golden.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.basic.character import createCharacter
|
||||||
|
|
||||||
|
|
||||||
|
def test_character_calc_returns_result_shape(minimal_calc_config):
|
||||||
|
character = createCharacter('1.1.1', '0')
|
||||||
|
|
||||||
|
result = character.calc(minimal_calc_config)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(result)
|
||||||
|
assert isinstance(result['skills'], list)
|
||||||
|
assert isinstance(result['info'], list)
|
||||||
|
assert result['buffer'] is False
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行 golden smoke**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/golden/test_calc_golden.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。若职业 `1.1.1` 不存在,改用 `core.character.adventure` 中第一个 `open=True` 的职业,并同步更新 Task 1 fixture。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 运行全部测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,或仅 contract 测试因外部 Redis 环境失败且原因明确。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/tests/golden/test_calc_golden.py
|
||||||
|
git commit -m "test: add backend calc golden smoke"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 3:引入 calc service,瘦身 API route
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `backend/services/__init__.py`
|
||||||
|
- Create: `backend/services/calc_service.py`
|
||||||
|
- Modify: `backend/api/routes/calc.py`
|
||||||
|
- Test: `backend/tests/contract/test_calc_contract.py`
|
||||||
|
- Test: `backend/tests/golden/test_calc_golden.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 service 包**
|
||||||
|
|
||||||
|
Create `backend/services/__init__.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Application services for backend API orchestration."""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建 calc service**
|
||||||
|
|
||||||
|
Create `backend/services/calc_service.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from api.core.Auth import AlterState
|
||||||
|
from core.basic.character import createCharacter
|
||||||
|
|
||||||
|
|
||||||
|
def calculate(state: AlterState, config: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
safe_config = config or {}
|
||||||
|
character = createCharacter(state.alter, state.equVersion)
|
||||||
|
return character.calc(safe_config)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 修改 route 调用 service**
|
||||||
|
|
||||||
|
Modify `backend/api/routes/calc.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from api.core.Gzip import register_gzip_request
|
||||||
|
from fastapi import APIRouter, Body
|
||||||
|
from api.core.Response import response
|
||||||
|
from api.dp import AltersDep
|
||||||
|
from services.calc_service import calculate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
register_gzip_request(router)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/calc')
|
||||||
|
async def calc(state: AltersDep, config=Body(None)):
|
||||||
|
return response(data=calculate(state, config))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/contract/test_calc_contract.py backend/tests/golden/test_calc_golden.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/services/__init__.py backend/services/calc_service.py backend/api/routes/calc.py
|
||||||
|
git commit -m "refactor: route calc through service layer"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 4:增加 CalcConfigView,集中 config 默认值
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `backend/core/basic/character/config_view.py`
|
||||||
|
- Modify: `backend/core/basic/character/calc_equip.py`
|
||||||
|
- Test: `backend/tests/unit/test_calc_config_view.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 config view**
|
||||||
|
|
||||||
|
Create `backend/core/basic/character/config_view.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class CalcConfigView:
|
||||||
|
def __init__(self, raw: Mapping[str, Any] | None):
|
||||||
|
self.raw = dict(raw or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def equips(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('equips') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oaths(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('oaths') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def emblems(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('emblems') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def skills(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return dict(self.raw.get('skills') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('avatar') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sundry(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('sundry') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jades(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('jades') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> dict[str, Any]:
|
||||||
|
return dict(self.raw.get('options') or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsb(self) -> bool:
|
||||||
|
return bool(self.raw.get('DSB', False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def buff(self) -> bool:
|
||||||
|
return bool(self.raw.get('BUFF', False))
|
||||||
|
|
||||||
|
def as_legacy_dict(self) -> dict[str, Any]:
|
||||||
|
data = dict(self.raw)
|
||||||
|
data.setdefault('equips', {})
|
||||||
|
data.setdefault('oaths', {})
|
||||||
|
data.setdefault('emblems', {})
|
||||||
|
data.setdefault('skills', {})
|
||||||
|
data.setdefault('avatar', {})
|
||||||
|
data.setdefault('sundry', {})
|
||||||
|
data.setdefault('jades', {})
|
||||||
|
data.setdefault('options', {})
|
||||||
|
data.setdefault('DSB', False)
|
||||||
|
data.setdefault('BUFF', False)
|
||||||
|
return data
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加单元测试**
|
||||||
|
|
||||||
|
Create `backend/tests/unit/test_calc_config_view.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.basic.character.config_view import CalcConfigView
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_view_provides_defaults():
|
||||||
|
view = CalcConfigView(None)
|
||||||
|
|
||||||
|
assert view.equips == {}
|
||||||
|
assert view.oaths == {}
|
||||||
|
assert view.emblems == {}
|
||||||
|
assert view.skills == {}
|
||||||
|
assert view.avatar == {}
|
||||||
|
assert view.sundry == {}
|
||||||
|
assert view.jades == {}
|
||||||
|
assert view.options == {}
|
||||||
|
assert view.dsb is False
|
||||||
|
assert view.buff is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_view_preserves_legacy_keys():
|
||||||
|
view = CalcConfigView({'skills': {'100': {'lv': 1}}, 'BUFF': True})
|
||||||
|
|
||||||
|
data = view.as_legacy_dict()
|
||||||
|
|
||||||
|
assert data['skills'] == {'100': {'lv': 1}}
|
||||||
|
assert data['BUFF'] is True
|
||||||
|
assert data['DSB'] is False
|
||||||
|
assert data['equips'] == {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 calc_init 入口使用兼容 dict**
|
||||||
|
|
||||||
|
Modify `backend/core/basic/character/calc_equip.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .config_view import CalcConfigView
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `calc()` 起始处改为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calc(self, setInfo: dict[str, dict]):
|
||||||
|
config = CalcConfigView(setInfo).as_legacy_dict()
|
||||||
|
self.calc_init(config)
|
||||||
|
self.SetBaseStatus()
|
||||||
|
self.calc_jade(config.get('jades', []))
|
||||||
|
self.calc_avatar(config.get('avatar', {}))
|
||||||
|
self.calc_basic()
|
||||||
|
self.calc_sundry(config.get('sundry', {}))
|
||||||
|
self.calc_emblems()
|
||||||
|
suit = self.calc_suits()
|
||||||
|
self.calc_equs()
|
||||||
|
self.calc_oaths()
|
||||||
|
skillInfos = []
|
||||||
|
if self.buffer:
|
||||||
|
skillInfos = self.calc_buffer_skills()
|
||||||
|
info = self.get_buffer_info()
|
||||||
|
else:
|
||||||
|
if len(self.输出类型选项) > 1:
|
||||||
|
temp = self.输出类型选项[0]
|
||||||
|
if '固伤' in temp:
|
||||||
|
if self.INT > self.STR:
|
||||||
|
self.输出类型 = '魔法固伤'
|
||||||
|
else:
|
||||||
|
self.输出类型 = '物理固伤'
|
||||||
|
else:
|
||||||
|
if self.INT > self.STR and self.AtkM > self.AtkP:
|
||||||
|
self.输出类型 = '魔法百分比'
|
||||||
|
else:
|
||||||
|
self.输出类型 = '物理百分比'
|
||||||
|
skillInfos = self.calc_carry_skills(config.get('DSB', False), config.get('BUFF', False))
|
||||||
|
info = self.get_carry_info()
|
||||||
|
return {'uuid': uuid1().hex, 'skills': skillInfos, 'info': info, 'suits': suit, 'buffer': self.buffer}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/unit/test_calc_config_view.py backend/tests/golden/test_calc_golden.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/core/basic/character/config_view.py backend/core/basic/character/calc_equip.py backend/tests/unit/test_calc_config_view.py
|
||||||
|
git commit -m "refactor: centralize calc config defaults"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 5:引入 EquipmentRegistry,替换 eval 获取装备数据
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `backend/core/basic/equipment_registry.py`
|
||||||
|
- Modify: `backend/core/basic/equipment.py`
|
||||||
|
- Test: `backend/tests/unit/test_equipment_registry.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 registry**
|
||||||
|
|
||||||
|
Create `backend/core/basic/equipment_registry.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from functools import cache
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentRegistry:
|
||||||
|
def __init__(self, equipment_cls):
|
||||||
|
self.equipment_cls = equipment_cls
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get(self, version: str = '0'):
|
||||||
|
normalized = version or '0'
|
||||||
|
try:
|
||||||
|
return self.equipment_cls(normalized)
|
||||||
|
except Exception:
|
||||||
|
if normalized == '0':
|
||||||
|
raise
|
||||||
|
return self.get('0')
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 修改 equipment.py 兼容门面**
|
||||||
|
|
||||||
|
Modify `backend/core/basic/equipment.py` 底部:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.basic.equipment_registry import EquipmentRegistry
|
||||||
|
|
||||||
|
|
||||||
|
_equipment_registry = EquipmentRegistry(Equipments)
|
||||||
|
|
||||||
|
|
||||||
|
def get_equipment(version: str = '0') -> Equipments:
|
||||||
|
return _equipment_registry.get(version)
|
||||||
|
```
|
||||||
|
|
||||||
|
删除或停止使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
global equ0
|
||||||
|
equ0 = Equipments('0')
|
||||||
|
```
|
||||||
|
|
||||||
|
以及 `eval(f'equ{version}'.format(version))` 相关逻辑。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 添加 registry 测试**
|
||||||
|
|
||||||
|
Create `backend/tests/unit/test_equipment_registry.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.basic.equipment import get_equipment
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_equipment_returns_cached_default_version():
|
||||||
|
first = get_equipment('0')
|
||||||
|
second = get_equipment('0')
|
||||||
|
|
||||||
|
assert first is second
|
||||||
|
assert first.version == '0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_equipment_falls_back_to_default_version():
|
||||||
|
equipment = get_equipment('missing-version')
|
||||||
|
|
||||||
|
assert equipment.version == '0'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/unit/test_equipment_registry.py backend/tests/golden/test_calc_golden.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/core/basic/equipment_registry.py backend/core/basic/equipment.py backend/tests/unit/test_equipment_registry.py
|
||||||
|
git commit -m "refactor: add equipment registry"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 6:增加 calc service 耗时观测
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `backend/services/calc_service.py`
|
||||||
|
- Test: `backend/tests/contract/test_calc_contract.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加轻量日志计时**
|
||||||
|
|
||||||
|
Modify `backend/services/calc_service.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from api.core.Auth import AlterState
|
||||||
|
from core.basic.character import createCharacter
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('dcalc.calc')
|
||||||
|
|
||||||
|
|
||||||
|
def calculate(state: AlterState, config: dict[str, Any] | None) -> dict[str, Any]:
|
||||||
|
started_at = time.perf_counter()
|
||||||
|
safe_config = config or {}
|
||||||
|
character = createCharacter(state.alter, state.equVersion)
|
||||||
|
created_at = time.perf_counter()
|
||||||
|
result = character.calc(safe_config)
|
||||||
|
finished_at = time.perf_counter()
|
||||||
|
logger.info(
|
||||||
|
'calc completed alter=%s equVersion=%s create_ms=%.2f calc_ms=%.2f total_ms=%.2f',
|
||||||
|
state.alter,
|
||||||
|
state.equVersion,
|
||||||
|
(created_at - started_at) * 1000,
|
||||||
|
(finished_at - created_at) * 1000,
|
||||||
|
(finished_at - started_at) * 1000,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行 contract 测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/contract/test_calc_contract.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。响应 data 不新增耗时字段。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 手动观察日志**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 服务正常启动。发起 `/api/calc` 后可在日志处理器中接入 `dcalc.calc` logger;本任务不要求改变线上日志格式。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/services/calc_service.py
|
||||||
|
git commit -m "chore: add calc timing logs"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 7:抽取 effect 应用 helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `backend/core/basic/character/effect_apply.py`
|
||||||
|
- Modify: `backend/core/basic/character/calc_equip.py`
|
||||||
|
- Test: `backend/tests/unit/test_effect_apply.py`
|
||||||
|
- Test: `backend/tests/golden/test_calc_golden.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 helper**
|
||||||
|
|
||||||
|
Create `backend/core/basic/character/effect_apply.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def uppercase_attrs(source: Any) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in source.__dict__.items()
|
||||||
|
if key and key[0].isupper()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_uppercase_attrs(character: Any, source: Any) -> None:
|
||||||
|
character.SetStatus(**uppercase_attrs(source))
|
||||||
|
|
||||||
|
|
||||||
|
def execute_effect(effects: Any, key: str, *args: Any) -> bool:
|
||||||
|
fun = effects.execture(key)
|
||||||
|
if fun is None:
|
||||||
|
return False
|
||||||
|
fun(*args)
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加 helper 测试**
|
||||||
|
|
||||||
|
Create `backend/tests/unit/test_effect_apply.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from core.basic.character.effect_apply import uppercase_attrs
|
||||||
|
|
||||||
|
|
||||||
|
class Source:
|
||||||
|
STR = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.INT = 20
|
||||||
|
self.name = 'ignored'
|
||||||
|
|
||||||
|
|
||||||
|
def test_uppercase_attrs_returns_only_uppercase_keys():
|
||||||
|
assert uppercase_attrs(Source()) == {'INT': 20}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: 在 calc_equip.py 中逐步替换重复代码**
|
||||||
|
|
||||||
|
Modify selected occurrences in `backend/core/basic/character/calc_equip.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .effect_apply import apply_uppercase_attrs, execute_effect, uppercase_attrs
|
||||||
|
```
|
||||||
|
|
||||||
|
将简单重复片段:
|
||||||
|
|
||||||
|
```python
|
||||||
|
filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()}
|
||||||
|
self.SetStatus(**filtered_dict)
|
||||||
|
```
|
||||||
|
|
||||||
|
替换为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
apply_uppercase_attrs(self, suit)
|
||||||
|
```
|
||||||
|
|
||||||
|
将:
|
||||||
|
|
||||||
|
```python
|
||||||
|
fun = effects.execture(f'equ_{equ.id}')
|
||||||
|
if fun is None:
|
||||||
|
continue
|
||||||
|
filtered_dict = {k: v for k, v in equ.__dict__.items() if k[0].isupper()}
|
||||||
|
self.SetStatus(**filtered_dict)
|
||||||
|
fun(self)
|
||||||
|
```
|
||||||
|
|
||||||
|
替换为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
fun = effects.execture(f'equ_{equ.id}')
|
||||||
|
if fun is None:
|
||||||
|
continue
|
||||||
|
apply_uppercase_attrs(self, equ)
|
||||||
|
fun(self)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest backend/tests/unit/test_effect_apply.py backend/tests/golden/test_calc_golden.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/core/basic/character/effect_apply.py backend/core/basic/character/calc_equip.py backend/tests/unit/test_effect_apply.py
|
||||||
|
git commit -m "refactor: extract character effect helpers"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 8:补充 backend README
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create or Modify: `backend/README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 编写 backend README**
|
||||||
|
|
||||||
|
Create or Modify `backend/README.md`:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
# dcalc backend
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Entry
|
||||||
|
|
||||||
|
- `main.py`: FastAPI application and uvicorn entry.
|
||||||
|
- `api/routes/calc.py`: `/api/calc` HTTP route.
|
||||||
|
- `services/calc_service.py`: calculation orchestration.
|
||||||
|
- `core/basic/character/`: character calculation implementation.
|
||||||
|
- `core/basic/equipment.py`: equipment data models and compatibility access.
|
||||||
|
- `core/basic/equipment_registry.py`: equipment version registry and cache.
|
||||||
|
- `database/connect.py`: SQLite and Redis connections.
|
||||||
|
|
||||||
|
## Compatibility Rules
|
||||||
|
|
||||||
|
- Do not change `/calc` request and response contract without frontend migration approval.
|
||||||
|
- Do not change frontendNew result data shape during backend refactors.
|
||||||
|
- Keep Skill and Forge business formulas behavior-compatible unless a dedicated formula task says otherwise.
|
||||||
|
````
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行文档相关检查**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,或只出现既有遗留问题。若有既有问题,不在本任务做大范围修复。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 提交本任务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/README.md
|
||||||
|
git commit -m "docs: document backend runtime structure"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 9:阶段性全量验证
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- No source changes expected.
|
||||||
|
|
||||||
|
- [ ] **Step 1: 运行测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行 lint**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS,或输出仅包含已登记的遗留问题。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 启动服务**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && uv run python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 输出 `Application startup complete.`,服务监听 `config.PORT`。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 手动验证 frontendNew 链路**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontendNew && pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 在 Character 模块修改 Skill/Forge 配置后,仍满足 `config 变化 -> result.execute -> saveConfig`。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交验证记录**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 工作区干净,所有阶段任务已独立提交。
|
||||||
+1
-1
Submodule images updated: 0991b6efa4...051a7cde33
Reference in New Issue
Block a user