539 lines
18 KiB
Python
539 lines
18 KiB
Python
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 .formula import 武器冷却惩罚
|
|
from database.connect import get_redis_client
|
|
from api.core.Redis import get_redis_info
|
|
from api.utils.openapi_data import OPENAPI_CUSTOM_CN_PATH, logical_skill_detail_path
|
|
|
|
if TYPE_CHECKING:
|
|
from core.basic.character import Character
|
|
|
|
characterLv = 115 + 5
|
|
|
|
|
|
def _identity(value: Any) -> Any:
|
|
return value
|
|
|
|
|
|
@cache
|
|
def _load_custom_cn() -> list[dict[str, Any]]:
|
|
with OPENAPI_CUSTOM_CN_PATH.open(encoding='utf-8') as f:
|
|
return json.load(f)
|
|
|
|
|
|
@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'))
|
|
|
|
|
|
@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):
|
|
prefix_parts = prefix.split('/')
|
|
job = prefix_parts[0]
|
|
jobGrow = prefix_parts[1]
|
|
skill = prefix_parts[-1]
|
|
|
|
def get_skill_info():
|
|
skill_data = _load_skill_detail(prefix)
|
|
custom_cn = get_redis_info(redis, 'dcalc:custom_cn', _load_custom_cn, without_gzip=True)
|
|
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 == "custom":
|
|
data = next((item.get('custom', 1.0) for item in custom_cn or []
|
|
if item['job'] == job and item['jobGrow'] == jobGrow and item['skillId'] == skill), 1.0)
|
|
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 []
|
|
if key == 'custom' and data is None:
|
|
return 1.0
|
|
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:
|
|
name: str = ''
|
|
"""技能名称"""
|
|
_lv: int = 0
|
|
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 = []
|
|
"""关联技能
|
|
type: 关联类型 *乘算 +加算 =直接赋值 $角色属性 默认'*skillDamage'
|
|
e.g.
|
|
*skillDamage 乘算技能伤害
|
|
*cdReduce 乘算技能冷却缩减
|
|
$*PATK 乘算角色物理攻击
|
|
~直接走type对应的函数名称 传参为old,new,data,skills,exceptSkills
|
|
skills: 关联技能 默认 '*'
|
|
exceptSkills: 例外技能 默认[]
|
|
"""
|
|
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 = []
|
|
custom: float = 1.0
|
|
"""国服特色加强"""
|
|
|
|
def __init__(self, char):
|
|
self.char = char
|
|
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):
|
|
"""技能等级"""
|
|
return self._lv
|
|
|
|
@lv.setter
|
|
def lv(self, value):
|
|
value = int(min(self.maxLv, value))
|
|
if self._lv != value:
|
|
self.effect(self._lv, value)
|
|
self._lv = value
|
|
|
|
def effect(self, old: int, new: int):
|
|
weapon_type = self.char.GetWeaponType()[0]
|
|
weapon_options = self.char.武器选项
|
|
for assoc in self.associate:
|
|
type = assoc.get('type', '*skillDamage')
|
|
skills = assoc.get('skills', '*')
|
|
data = assoc.get('data', [0] * self.maxLv)
|
|
ratio = assoc.get('ratio', 100)
|
|
weapon = assoc.get('weapon', weapon_options)
|
|
exceptSkills = assoc.get('exceptSkills', [])
|
|
if type[0] not in ['*', '+', '=', '$']:
|
|
if self.precondition() and (weapon_type in weapon or len(weapon) == len(weapon_options)):
|
|
getattr(self, type)(old, new, data, skills, exceptSkills, ratio, weapon)
|
|
continue
|
|
if self.precondition():
|
|
self.apply_association(type, old, new, data, skills, exceptSkills, ratio, weapon, weapon_type, weapon_options)
|
|
|
|
def precondition(self):
|
|
"""effect的前置条件"""
|
|
return True
|
|
|
|
def apply_association(self, type, old, new, data, skills, exceptSkills, ratio=100, weapon=(), weapon_type=None, weapon_options=()):
|
|
try:
|
|
if (weapon_type not in weapon) and not (weapon_type is None and len(weapon) == len(weapon_options)):
|
|
return
|
|
if type.startswith('$*'):
|
|
if 'cdReduce' in type:
|
|
value = (1 - self.get_data_value(data, new) / ratio) / (1 - self.get_data_value(data, old) / ratio)
|
|
else:
|
|
value = (1 + self.get_data_value(data, new) / ratio) / (1 + self.get_data_value(data, old) / ratio)
|
|
self.char.SetStatus(**{type[2:]: value})
|
|
elif type.startswith('$+'):
|
|
value = self.get_data_value(data, new) / ratio - self.get_data_value(data, old) / ratio
|
|
self.char.SetStatus(**{type[2:]: value})
|
|
else:
|
|
exceptSkills = set(exceptSkills)
|
|
if skills == '*':
|
|
for skill in self.char.GetSkills('all', True):
|
|
if skill.name in exceptSkills:
|
|
continue
|
|
self.update_skill_attribute(skill, type, old, new, data, ratio, weapon, weapon_type, weapon_options)
|
|
else:
|
|
for name in skills:
|
|
if name in exceptSkills:
|
|
continue
|
|
skill = self.char.GetSkillByName(name)
|
|
if skill is not None:
|
|
self.update_skill_attribute(skill, type, old, new, data, ratio, weapon, weapon_type, weapon_options)
|
|
except Exception as e:
|
|
# print(f"Error applying association {type} for skill {self.name}: {e}")
|
|
raise e
|
|
|
|
def update_skill_attribute(self, skill, type, old, new, data, ratio=100, weapon=(), weapon_type=None, weapon_options=()):
|
|
if (weapon_type not in weapon) and not (weapon_type is None and len(weapon) == len(weapon_options)):
|
|
return
|
|
old_value = self.get_data_value(data, old)
|
|
new_value = self.get_data_value(data, new)
|
|
if type.startswith('*'):
|
|
if 'cdReduce' in type or type == 'cd' or 'cdRatio' in type:
|
|
setattr(skill, type[1:], getattr(skill, type[1:]) * (1 - new_value / ratio) / (1 - old_value / ratio))
|
|
else:
|
|
setattr(skill, type[1:], getattr(skill, type[1:]) * (1 + new_value / ratio) / (1 + old_value / ratio))
|
|
elif type.startswith('+'):
|
|
if type[1:] == 'lv' and self.char.setInfo.get('skills', {}).get(str(skill.id),{}).get('lv',0) == 0:
|
|
return
|
|
else:
|
|
setattr(skill, type[1:], getattr(skill, type[1:]) + new_value / ratio - old_value / ratio)
|
|
elif type.startswith('='):
|
|
setattr(skill, type[1:], new_value)
|
|
|
|
@staticmethod
|
|
def get_data_value(data, index: int):
|
|
if 0 <= index < len(data):
|
|
return data[index]
|
|
return 0
|
|
|
|
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.custom, 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 getQuickCDRatio(self):
|
|
return 1.0
|
|
cdr = 0
|
|
if 15 <= self.learnLv <= 30:
|
|
cdr = 0.01
|
|
elif 35 <= self.learnLv <= 70:
|
|
cdr = 0.02
|
|
elif 75 <= self.learnLv <= 100:
|
|
cdr = 0.05
|
|
if self.learnLv in [50, 85, 100]:
|
|
cdr = 0.05
|
|
return 1 - cdr
|
|
|
|
def getSkillCD(self, mode=None):
|
|
return max(0, round(max(self.cd * 0.3, self.cd * self.cdReduce / self.cdRecover * self.getWeaponCDRatio() * self.getQuickCDRatio()) * self.cdRatio - self.cdCut, 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 getQuickCDRatio(self):
|
|
return 1.0
|
|
cdr = 0
|
|
if 15 <= self.learnLv <= 30:
|
|
cdr = 0.01
|
|
elif 35 <= self.learnLv <= 70:
|
|
cdr = 0.02
|
|
elif 75 <= self.learnLv <= 100:
|
|
cdr = 0.05
|
|
if self.learnLv in [50, 85, 100]:
|
|
cdr = 0.05
|
|
return 1 - cdr
|
|
|
|
def getSkillCD(self, mode=None):
|
|
return max(0, round(max(self.cd * 0.3, self.cd * self.cdReduce / self.cdRecover * self.getWeaponCDRatio() * self.getQuickCDRatio(), 1) - self.cdCut, 2))
|