Files
dcalc/backend/core/basic/skill.py
T
2025-08-20 12:45:33 +08:00

477 lines
16 KiB
Python

from copy import deepcopy
from functools import cache
import json
import re
from typing import Literal, TYPE_CHECKING
from .formula import 武器冷却惩罚
from database.connect import get_redis
from api.core.Redis import get_redis_info
if TYPE_CHECKING:
from core.basic.character import Character
characterLv = 115 + 5
@cache
def get_data(prefix: str, key: int | str, func = lambda x: x):
redis = next(get_redis())
def get_skill_info():
# This function should retrieve the skill info based on job and jobGrow
# For now, we return a placeholder dictionary
with open(f'./openapi/data/{prefix}.json', encoding='utf-8') as f:
skill_data = json.load(f)
try:
if isinstance(key, int):
data = [0] + list(map(lambda x: x["optionValue"].get("value" + str(key+1), 0), 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 = skill_data.get("custom",1.0)
except Exception as e:
print(f"get skill {prefix}/{key} data error",e)
data = None
return data
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: 关联类型 *乘算 +加算 =直接赋值 $角色属性 默认'*skillRation'
e.g.
*skillRation 乘算技能面板倍率
*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}/skill')
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 - self.learnLv) / 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):
for assoc in self.associate:
type = assoc.get('type', '*skillRation')
skills = assoc.get('skills', '*')
data = assoc.get('data', [0] * self.maxLv)
ratio = assoc.get('ratio', 100)
weapon = assoc.get('weapon', self.char.武器选项)
exceptSkills = assoc.get('exceptSkills', [])
if type[0] not in ['*', '+', '=', '$']:
if self.precondition() and (self.char.GetWeaponType()[0] in weapon or len(weapon) == len(self.char.武器选项)):
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)
def precondition(self):
"""effect的前置条件"""
return True
def apply_association(self, type, old, new, data, skills, exceptSkills, ratio=100, weapon=[]):
try:
if (self.char.GetWeaponType()[0] not in weapon) and not (self.char.GetWeaponType()[0] is None and len(weapon) == len(self.char.武器选项)):
return
if type.startswith('$*'):
if 'cdReduce' in type:
value = (1 - data[new] / ratio) / (1 - data[old] / ratio)
else:
value = (1 + data[new] / ratio) / (1 + data[old] / ratio)
eval(f'self.char.SetStatus({type[2:]}={value})')
elif type.startswith('$+'):
value = data[new] / ratio - data[old] / ratio
eval(f'self.char.SetStatus({type[2:]}={value})')
else:
if skills == '*':
skills = self.char.GetSkillNames('all', True)
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)
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=[]):
if (self.char.GetWeaponType()[0] not in weapon) and not (self.char.GetWeaponType()[0] is None and len(weapon) == len(self.char.武器选项)):
return
if type.startswith('*'):
if 'cdReduce' in type or type == 'cd' or 'cdRatio' in type:
setattr(skill, type[1:], getattr(skill, type[1:]) * (1 - data[new] / ratio) / (1 - data[old] / ratio))
else:
setattr(skill, type[1:], getattr(skill, type[1:]) * (1 + data[new] / ratio) / (1 + data[old] / 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:]) + data[new] / ratio - data[old] / ratio)
elif type.startswith('='):
setattr(skill, type[1:], data[new])
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)
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('plus')]
for i in keys:
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 = deepcopy(self)
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
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')]
for i in keys:
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)
keys = [key.replace('dataplus', '') for key in dir(self) if key.startswith('dataplus')]
for i in keys:
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)
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('plus')]
for i in keys:
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
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))