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))