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 BACKEND_DIR, get_redis_client from api.core.Redis import get_redis_info if TYPE_CHECKING: from core.basic.character import Character characterLv = 115 + 5 OPENAPI_DIR = BACKEND_DIR / 'openapi' def _identity(value: Any) -> Any: return value @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_custom_cn(): with (OPENAPI_DIR / 'custom_cn.json').open(encoding='utf-8') as f: custom_cn = json.load(f) return custom_cn def get_skill_info(): with (OPENAPI_DIR / 'data' / f'{prefix}.json').open(encoding='utf-8') as f: skill_data = json.load(f) custom_cn = get_redis_info(redis, 'dcalc:custom_cn', get_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) 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}/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): 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: if skills == '*': skills = self.char.GetSkillNames('all', True) exceptSkills = set(exceptSkills) 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))