update
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
from copy import copy
|
||||
from collections.abc import Callable
|
||||
from functools import cache
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
from core.character.refs.base import SkillRef, resolve_skill
|
||||
from ..formula import 武器冷却惩罚
|
||||
from database.connect import get_redis_client
|
||||
from api.core.Redis import get_redis_info
|
||||
from api.utils.openapi_data import logical_skill_detail_path
|
||||
from .effects import INPUT_ATTRIBUTES
|
||||
from .effect_types import EffectDeclaration
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.basic.character import Character
|
||||
|
||||
characterLv = 115 + 5
|
||||
|
||||
|
||||
def _identity(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
@cache
|
||||
def _load_skill_detail(prefix: str) -> dict[str, Any]:
|
||||
with logical_skill_detail_path(prefix).open(encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@cache
|
||||
def _get_skill_data_suffixes(skill_class: type) -> tuple[str, ...]:
|
||||
return tuple(key.replace('data', '') for key in dir(skill_class) if key.startswith('data') and not key.startswith('plus'))
|
||||
|
||||
|
||||
@cache
|
||||
def _get_skill_damage_suffixes(skill_class: type) -> tuple[str, ...]:
|
||||
return tuple(key.replace('data', '') for key in dir(skill_class) if key.startswith('data') and not key.startswith('dataplus') and not key.startswith('dataType'))
|
||||
|
||||
|
||||
@cache
|
||||
def _get_skill_dataplus_suffixes(skill_class: type) -> tuple[str, ...]:
|
||||
return tuple(key.replace('dataplus', '') for key in dir(skill_class) if key.startswith('dataplus'))
|
||||
|
||||
|
||||
@cache
|
||||
def get_data(prefix: str, key: int | str, func: Callable[[Any], Any] = _identity):
|
||||
def get_skill_info():
|
||||
skill_data = _load_skill_detail(prefix)
|
||||
try:
|
||||
if isinstance(key, int):
|
||||
data = [0] + [row["optionValue"].get("value" + str(key+1), 0) for row in skill_data["levelInfo"]["rows"]]
|
||||
data = [0 if x is None or x == "-" else x for x in data]
|
||||
elif key == "vps":
|
||||
data = [ {"name":x["name"],"desc":x["desc"]} for x in skill_data.get("evolution",[])]
|
||||
elif key == "cdList":
|
||||
data = [0] + [row.get("coolTime", 0) for row in skill_data["levelInfo"]["rows"]]
|
||||
except Exception:
|
||||
# print(f"get skill {prefix}/{key} data error")
|
||||
data = None
|
||||
if isinstance(key, int) and not data:
|
||||
return [0]
|
||||
if key == 'vps' and data is None:
|
||||
return []
|
||||
return data
|
||||
|
||||
with get_redis_client() as redis:
|
||||
data = get_redis_info(redis, f'dcalc:skill:{prefix}:{key}', get_skill_info, without_gzip=True)
|
||||
return func(data)
|
||||
|
||||
|
||||
class Skill:
|
||||
def __setattr__(self, name, value):
|
||||
char = self.__dict__.get('char')
|
||||
engine = getattr(char, 'skill_effects', None)
|
||||
if name != 'lv' and name in INPUT_ATTRIBUTES and engine is not None and engine.owns(self) and not engine.resolving:
|
||||
engine.set_attribute(self, name, value)
|
||||
else:
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
name: str = ''
|
||||
"""技能名称"""
|
||||
_lv: int = 0
|
||||
ref: SkillRef | None = None
|
||||
"""技能身份引用;职业类绑定 Ref 成员,code 由此派生。"""
|
||||
|
||||
@property
|
||||
def code(self) -> str | None:
|
||||
"""技能树原始 code;未迁移的技能返回 None,不单独维护副本。"""
|
||||
return self.ref.code if self.ref is not None else None
|
||||
|
||||
level_source: SkillRef | str | None = None
|
||||
"""共享等级的源技能;目标不独立加级,也不使用自身 maxLv 截断。"""
|
||||
association_source: bool | None = None
|
||||
"""共享等级技能默认仅复用等级;需要独立提供关联效果时显式开启。"""
|
||||
id: int = 0
|
||||
"""技能ID"""
|
||||
icon: str = '$char/$uuid'
|
||||
"""技能图标,默认当前职业下该技能名称对应的图标,支持以下模板字符串
|
||||
$common 通用图标路径
|
||||
$char 当前职业图标路径
|
||||
$name 技能名称
|
||||
$id 技能ID
|
||||
"""
|
||||
type: Literal['active', 'passive'] = 'active'
|
||||
"""技能类型 active主动 passive被动"""
|
||||
learnLv: int = 0
|
||||
"""学习等级"""
|
||||
masterLv: int = 0
|
||||
"""精通等级"""
|
||||
maxLv: int = 0
|
||||
"""最大等级"""
|
||||
line: int = None
|
||||
"""技能行"""
|
||||
position: int = 0
|
||||
"""技能树位置"""
|
||||
rangeLv: int = 1
|
||||
"""学习间隔"""
|
||||
cube: int = 0
|
||||
"""无色消耗"""
|
||||
skillRation: int = 1
|
||||
"""技能面板倍率 [改变技能面板]"""
|
||||
skillDamage: int = 1
|
||||
"""技能技攻 [不改变技能面板]"""
|
||||
char: 'Character' = None
|
||||
"""技能所属角色"""
|
||||
damage: bool = False
|
||||
"""是否是伤害技能"""
|
||||
cd: float = 0
|
||||
"""技能冷却时间"""
|
||||
cdCut: float = 0
|
||||
"""技能冷却时间直接减少"""
|
||||
cdReduce: float = 1
|
||||
"""技能冷却缩减"""
|
||||
cdRatio: float = 1
|
||||
"""技能冷却倍率,直接体现在面板上,不被上限影响"""
|
||||
cdRecover: float = 1
|
||||
"""技能冷却恢复"""
|
||||
associate: list[EffectDeclaration] = []
|
||||
"""类型化关联声明;仅接受 SkillEffect/CharacterEffect。
|
||||
|
||||
value 是实际倍率、增量或覆盖值,目标优先使用 Ref 数组。
|
||||
动态 effect 只构造声明并调用 super().effect,由引擎统一收集和应用。
|
||||
维护示例见 core/basic/skill/effects.py 文件顶部说明。
|
||||
"""
|
||||
mode: list[str] = ['']
|
||||
currentMode: str = ''
|
||||
"""技能模式"""
|
||||
buffer: bool = False
|
||||
"""是否是增益技能"""
|
||||
bind: bool = None
|
||||
uuid: str = None
|
||||
hasVP: bool = None
|
||||
"""是否有VP形态"""
|
||||
hasUP: bool = None
|
||||
"""是否有技能强化"""
|
||||
upType: Literal['damage', 'buff', 'heal'] = 'damage'
|
||||
vps: list = []
|
||||
|
||||
def __init__(self, char):
|
||||
self.char = char
|
||||
# Shared-level display skills override the setter with a no-op. They
|
||||
# did not apply their own associations in the original calculation.
|
||||
if self.association_source is None:
|
||||
self.association_source = self.level_source is None and type(self).lv.fset is Skill.lv.fset
|
||||
self.id = self._extract_id()
|
||||
self.icon = self._generate_icon()
|
||||
if self.line is None:
|
||||
self.line = self.learnLv
|
||||
if self.bind is None:
|
||||
self.bind = False
|
||||
if self.learnLv in [50, 85, 100]:
|
||||
self.bind = True
|
||||
if self.hasVP is None:
|
||||
if self.learnLv not in [50, 85, 100] and self.learnLv >= 35 and self.learnLv <= 80 and self.damage:
|
||||
self.hasVP = True
|
||||
else:
|
||||
self.hasVP = False
|
||||
if self.hasUP is None:
|
||||
if self.learnLv not in [50, 85, 100] and self.learnLv >= 15 and self.learnLv <= 80 and self.damage:
|
||||
self.hasUP = True
|
||||
else:
|
||||
self.hasUP = False
|
||||
# self.lv = self._calculate_lv()
|
||||
pass
|
||||
|
||||
def _extract_id(self):
|
||||
className = self.__class__.__name__
|
||||
return int(re.search(r'\d+', className).group()) if re.search(r'\d+', className) else 0
|
||||
|
||||
def _generate_icon(self):
|
||||
icon = self.icon or '$char/$uuid'
|
||||
icon = icon.replace('$char', f'/characters/{self.char.role}/{self.char.name}/skillCN')
|
||||
icon = icon.replace('$common', '/characters/common')
|
||||
icon = icon.replace('$name', f'{self.name}')
|
||||
icon = icon.replace('$uuid', f'{self.uuid}')
|
||||
icon = icon.replace('$id', f'{self.id}')
|
||||
return f'{icon}.png'
|
||||
|
||||
def calculate_lv(self):
|
||||
return min(int((characterLv - (15 if self.learnLv is None else self.learnLv)) / max(1,(1 if self.rangeLv is None else self.rangeLv)) + 1), self.masterLv) if self._lv == 0 else self._lv
|
||||
|
||||
@property
|
||||
def lv(self):
|
||||
"""技能等级"""
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is not None and engine.owns(self):
|
||||
engine.read_level(self)
|
||||
elif self.level_source is not None:
|
||||
return resolve_skill(self.char, self.level_source).lv
|
||||
return self._lv
|
||||
|
||||
@lv.setter
|
||||
def lv(self, value):
|
||||
if self.level_source is not None:
|
||||
return
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is not None and engine.owns(self) and not engine.resolving:
|
||||
engine.set_level(self, max(0, int(value)))
|
||||
return
|
||||
value = int(max(0, min(self.maxLv, value)))
|
||||
if self._lv != value:
|
||||
self.effect(self._lv, value)
|
||||
self._lv = value
|
||||
|
||||
def effect(self, old: int, new: int):
|
||||
"""收集类型化声明;动态目标规则可重写此入口。"""
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
for declaration in self.associate:
|
||||
if engine is None:
|
||||
raise RuntimeError('类型化效果需要 SkillEffectEngine')
|
||||
engine.collect_effect(self, declaration)
|
||||
|
||||
def precondition(self):
|
||||
"""效果的前置条件。"""
|
||||
return True
|
||||
|
||||
def skillInfo(self, mode: str | None = None):
|
||||
pass
|
||||
|
||||
|
||||
class ActiveSkill(Skill):
|
||||
type: str = 'active'
|
||||
"""技能类型 active主动 passive被动"""
|
||||
damage: bool = True
|
||||
"""是否是伤害技能"""
|
||||
buffer: bool = False
|
||||
"""是否是buff技能"""
|
||||
vp: int = 0
|
||||
"""技能VP形态"""
|
||||
up: int = 0
|
||||
"""技能强化"""
|
||||
|
||||
def __init__(self, char):
|
||||
super().__init__(char)
|
||||
for i in _get_skill_data_suffixes(self.__class__):
|
||||
power = getattr(self, f'power{i}', None)
|
||||
if power is None:
|
||||
setattr(self, f'power{i}', 1)
|
||||
hit = getattr(self, f'hit{i}', None)
|
||||
if hit is None:
|
||||
setattr(self, f'hit{i}', 0)
|
||||
|
||||
def skillInfo(self, mode: str | None = None):
|
||||
basic = copy(self)
|
||||
for key, value in self.__dict__.items():
|
||||
if isinstance(value, list):
|
||||
setattr(basic, key, value.copy())
|
||||
elif isinstance(value, dict):
|
||||
setattr(basic, key, value.copy())
|
||||
basic.setUP()
|
||||
if mode is not None:
|
||||
basic.currentMode = mode
|
||||
basic.setMode(mode)
|
||||
basic.setVP()
|
||||
date = basic.getSkillData(self.lv)
|
||||
cd = basic.getSkillCD(mode)
|
||||
return date * basic.skillRation, basic.skillDamage, cd
|
||||
|
||||
def setMode(self, mode: str):
|
||||
pass
|
||||
|
||||
def setVP(self):
|
||||
if self.vp == 1:
|
||||
self.vp_1()
|
||||
elif self.vp == 2:
|
||||
self.vp_2()
|
||||
|
||||
def setUP(self):
|
||||
if not self.hasUP:
|
||||
return
|
||||
if self.up == 1:
|
||||
if self.learnLv < 35:
|
||||
self.skillRation *= 1.6
|
||||
else:
|
||||
self.skillRation *= 1.55
|
||||
elif self.up == 2:
|
||||
self.cdReduce *= 1 - 0.15
|
||||
if self.learnLv < 35:
|
||||
self.skillRation *= 1.43
|
||||
else:
|
||||
self.skillRation *= 1.38
|
||||
|
||||
def getSkillData(self, lv: int = 0):
|
||||
res = 0
|
||||
for i in _get_skill_damage_suffixes(self.__class__):
|
||||
data = getattr(self, f'data{i}', [])
|
||||
plus = getattr(self, f'plus{i}', 0)
|
||||
hit = getattr(self, f'hit{i}', 0)
|
||||
if len(data) == 0 or hit == 0:
|
||||
continue
|
||||
if lv < len(data):
|
||||
hit = getattr(self, f'hit{i}', 0)
|
||||
power = getattr(self, f'power{i}', 1)
|
||||
res += hit * power * (data[lv] + plus)
|
||||
for i in _get_skill_dataplus_suffixes(self.__class__):
|
||||
data = getattr(self, f'dataplus{i}', 0)
|
||||
hit = getattr(self, f'hitplus{i}', 1)
|
||||
power = getattr(self, f'powerplus{i}', 1)
|
||||
res += hit * power * data
|
||||
return res
|
||||
|
||||
def getWeaponCDRatio(self):
|
||||
weapon = self.char.charEquipInfo['武器'].equInfo
|
||||
if weapon is None or '传世武器' in weapon.categorize:
|
||||
return 1
|
||||
else:
|
||||
return 武器冷却惩罚(weapon.itemDetailType, self.char.输出类型)
|
||||
|
||||
|
||||
def getSkillCD(self, mode=None):
|
||||
if not isinstance(self.cd, (int, float)) or not self.cd:
|
||||
return '-'
|
||||
return max(0, round(max(self.cd * 0.3, self.cd * self.cdReduce / self.cdRecover * self.getWeaponCDRatio()) * self.cdRatio - self.cdCut / self.cdRecover , 2))
|
||||
|
||||
def vp_1(self):
|
||||
pass
|
||||
|
||||
def vp_2(self):
|
||||
pass
|
||||
|
||||
|
||||
class PassiveSkill(Skill):
|
||||
type: str = 'passive'
|
||||
"""技能类型 active主动 passive被动"""
|
||||
damage: bool = False
|
||||
"""是否是伤害技能"""
|
||||
buffer: bool = False
|
||||
"""是否是buff技能"""
|
||||
|
||||
|
||||
class BuffSkill(Skill):
|
||||
buffer: bool = True
|
||||
"""是否是增益技能"""
|
||||
damage: bool = False
|
||||
"""是否是伤害技能"""
|
||||
buffType = 'normal'
|
||||
"""增益类型 normal普通 buff 主BUFF awake 觉醒技能"""
|
||||
|
||||
INT: list[float] = []
|
||||
"""智力"""
|
||||
STR: list[float] = []
|
||||
"""力量"""
|
||||
Spirit: list[float] = []
|
||||
"""精神"""
|
||||
Vitality: list[float] = []
|
||||
"""体力"""
|
||||
"""主要属性"""
|
||||
|
||||
ATK: list[float] = []
|
||||
"""三攻加成"""
|
||||
ATKRatio: float = 1
|
||||
"""三攻*算倍率"""
|
||||
ATKPLUS: float = 0
|
||||
"""三攻加算加成"""
|
||||
|
||||
STRINT: list[float] = []
|
||||
"""力量智力加成"""
|
||||
STRINTRatio: float = 1
|
||||
"""力量智力*算倍率"""
|
||||
STRINTPLUS: float = 0
|
||||
"""力量智力加算加成"""
|
||||
|
||||
CarryRatio: list[float] = []
|
||||
"""对C的额外增伤倍率"""
|
||||
vp: int = 0
|
||||
"""技能VP形态"""
|
||||
up: int = 0
|
||||
"""技能强化"""
|
||||
|
||||
def __init__(self, char):
|
||||
super().__init__(char)
|
||||
for i in _get_skill_data_suffixes(self.__class__):
|
||||
power = getattr(self, f'power{i}', None)
|
||||
if power is None:
|
||||
setattr(self, f'power{i}', 1)
|
||||
hit = getattr(self, f'hit{i}', None)
|
||||
if hit is None:
|
||||
setattr(self, f'hit{i}', 0)
|
||||
|
||||
def getSkillCD(self, mode=None):
|
||||
return '-'
|
||||
|
||||
def skillInfo(self, mode: str | None = None):
|
||||
"""
|
||||
Returns:
|
||||
tuple: (奶自己的面板加成,[三攻,三攻倍率,三攻额外加成],[力智,力智倍率,力智额外加成],对C增伤倍率,cd)
|
||||
"""
|
||||
lv = self.lv
|
||||
value = 0
|
||||
value1 = 0 if lv >= len(self.ATK) else self.ATK[lv]
|
||||
value2 = 0 if lv >= len(self.STRINT) else self.STRINT[lv]
|
||||
value3 = 0 if lv >= len(self.CarryRatio) else self.CarryRatio[lv]
|
||||
self.setUP()
|
||||
self.setVP()
|
||||
if self.char.适用属性 == '智力':
|
||||
value = 0 if lv >= len(self.INT) else self.INT[lv]
|
||||
elif self.char.适用属性 == '力量':
|
||||
value = 0 if lv >= len(self.STR) else self.STR[lv]
|
||||
elif self.char.适用属性 == '体力':
|
||||
value = 0 if lv >= len(self.Vitality) else self.Vitality[lv]
|
||||
elif self.char.适用属性 == '精神':
|
||||
value = 0 if lv >= len(self.Spirit) else self.Spirit[lv]
|
||||
return value, [value1, self.ATKRatio, self.ATKPLUS], [value2, self.STRINTRatio, self.STRINTPLUS], value3, self.getSkillCD(mode)
|
||||
|
||||
def setVP(self):
|
||||
if self.vp == 1:
|
||||
self.vp_1()
|
||||
elif self.vp == 2:
|
||||
self.vp_2()
|
||||
|
||||
def setUP(self):
|
||||
if not self.hasUP:
|
||||
return
|
||||
self.char.SetStatus(四维=40)
|
||||
if self.up == 1:
|
||||
...
|
||||
elif self.up == 2:
|
||||
if self.upType == 'damage':
|
||||
self.cdReduce *= 1 - 0.15
|
||||
elif self.upType == 'heal':
|
||||
self.cdReduce *= 1 - 0.1
|
||||
|
||||
def vp_1(self):
|
||||
pass
|
||||
|
||||
def vp_2(self):
|
||||
pass
|
||||
|
||||
|
||||
class PassiveBufferSkill(BuffSkill):
|
||||
type: str = 'passive'
|
||||
|
||||
|
||||
class ActiveBufferSkill(BuffSkill):
|
||||
type: str = 'active'
|
||||
|
||||
def getWeaponCDRatio(self):
|
||||
weapon = self.char.charEquipInfo['武器'].equInfo
|
||||
if weapon is None or '传世武器' in weapon.categorize:
|
||||
return 1
|
||||
else:
|
||||
return 武器冷却惩罚(weapon.itemDetailType, self.char.输出类型)
|
||||
|
||||
def getSkillCD(self, mode=None):
|
||||
return max(0, round(max(self.cd * 0.3, self.cd * self.cdReduce / self.cdRecover * self.getWeaponCDRatio(), 1) - self.cdCut, 2))
|
||||
Reference in New Issue
Block a user