feat:优化职业技能关联实现
This commit was merged in pull request #11.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Literal
|
||||
|
||||
from core.character.refs.base import SkillRef, resolve_skill
|
||||
from core.abstract.character import CharacterProperty
|
||||
from core.basic.skill import Skill
|
||||
from core.basic.skill_effects import SkillEffectEngine, skill_effect_batch
|
||||
@@ -35,6 +36,12 @@ class CharacterBase(CharacterProperty):
|
||||
BindAwake: int
|
||||
"""绑定觉醒等级"""
|
||||
skills: list['Skill']
|
||||
skills_ref_dict: dict[SkillRef, 'Skill']
|
||||
"""当前角色的技能引用索引;不会共享其他角色的技能实例。"""
|
||||
skills_code_dict: dict[str, 'Skill']
|
||||
"""当前角色的原始 code 索引。"""
|
||||
skill_ref_profession: str | None
|
||||
"""技能数据来源职业范围,职业变体可复用来源职业。"""
|
||||
skills_dict: dict[str, 'Skill']
|
||||
skills_id_dict: dict[str, 'Skill']
|
||||
skills_uuid_dict: dict[str, 'Skill']
|
||||
@@ -172,6 +179,9 @@ class CharacterBase(CharacterProperty):
|
||||
self.equ_options = {}
|
||||
self.max_point = 0
|
||||
self.skills = []
|
||||
self.skills_ref_dict = {}
|
||||
self.skills_code_dict = {}
|
||||
self.skill_ref_profession = None
|
||||
self.skills_dict = {}
|
||||
self.skills_id_dict = {}
|
||||
self.skills_uuid_dict = {}
|
||||
@@ -368,6 +378,20 @@ class CharacterBase(CharacterProperty):
|
||||
|
||||
# region 技能查询
|
||||
|
||||
def GetSkill(self, reference: SkillRef | str) -> 'Skill | None':
|
||||
"""按类型化引用查询当前角色技能,兼容旧显示名称字符串。"""
|
||||
return resolve_skill(self, reference)
|
||||
|
||||
def GetSkillByRef(self, reference: SkillRef) -> 'Skill | None':
|
||||
"""按技能身份查询;校验职业范围,缺失返回 None。"""
|
||||
if not isinstance(reference, SkillRef):
|
||||
raise TypeError('GetSkillByRef 必须传入 SkillRef')
|
||||
return resolve_skill(self, reference)
|
||||
|
||||
def GetSkillByCode(self, code: str) -> 'Skill | None':
|
||||
"""在当前角色中按原始 code 查询,不与显示名称隐式混用。"""
|
||||
return self.skills_code_dict.get(code)
|
||||
|
||||
def GetSkillByName(self, name) -> 'Skill':
|
||||
"""通过技能名获取技能"""
|
||||
return self.skills_dict.get(name, None)
|
||||
|
||||
@@ -2,6 +2,7 @@ import sys
|
||||
from functools import cache
|
||||
from uuid import uuid1
|
||||
|
||||
from core.character.refs.base import bind_skill_refs
|
||||
from core.basic.equipment import OathSuit, Suit, get_equipment
|
||||
from ..formula import 增幅计算, 强化技攻, 武器强化计算, 精通计算, 耳环计算, 左右计算, 锻造计算
|
||||
from ..roleinfo import CharacterEmblemInfo, CharacterEquipInfo, CharacterOathInfo, get_key_by_value
|
||||
@@ -85,6 +86,9 @@ class CalcEquipMixin(CharacterBase):
|
||||
self.skills_id_dict[str(skill.id)] = skill
|
||||
self.skills_uuid_dict[str(skill.uuid)] = skill
|
||||
self.skills.append(skill)
|
||||
# 职业变体通过数据 prefix 复用同一身份范围,不依赖显示名称或模块别名。
|
||||
prefix = getattr(sys.modules[self.moduleName], 'prefix', '')
|
||||
bind_skill_refs(self, '.'.join(prefix.split('/')[:2]))
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -97,7 +98,15 @@ class Skill:
|
||||
name: str = ''
|
||||
"""技能名称"""
|
||||
_lv: int = 0
|
||||
level_source: str | None = None
|
||||
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
|
||||
"""共享等级技能默认仅复用等级;需要独立提供关联效果时显式开启。"""
|
||||
@@ -147,7 +156,7 @@ class Skill:
|
||||
associate: list[dict[str, Any] | EffectDeclaration] = []
|
||||
"""关联声明;维护示例和扩展边界见 core/basic/skill_effects.py 文件顶部说明。
|
||||
|
||||
新规则可用 SkillEffect/CharacterEffect,与旧字典混用;目标仍使用技能名称数组。
|
||||
新规则使用 SkillEffect/CharacterEffect 和 Ref 目标数组,兼容旧字典及名称字符串。
|
||||
新声明 value 是实际倍率/增量/覆盖值,以下 type、data、ratio 约定仅适用于旧字典。
|
||||
type 默认 '*skillDamage':*乘算、+加算、=覆盖;$*PAtkP 等作用于角色。
|
||||
skills 默认 '*',表示所有伤害技能;显式列表使用技能名称,可指定被动技能。
|
||||
@@ -223,7 +232,7 @@ class Skill:
|
||||
if engine is not None and engine.owns(self):
|
||||
engine.read_level(self)
|
||||
elif self.level_source is not None:
|
||||
return self.char.GetSkillByName(self.level_source).lv
|
||||
return resolve_skill(self.char, self.level_source).lv
|
||||
return self._lv
|
||||
|
||||
@lv.setter
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""有类型的技能关联声明;完整维护说明见 skill_effects.py 文件顶部。
|
||||
|
||||
表达式不可变、无副作用,并在求值前声明依赖。关联目标仍使用技能名称数组。
|
||||
表达式不可变、无副作用,并在求值前声明依赖。关联目标使用 Ref 数组,兼容旧技能名称。
|
||||
公开类型、枚举成员和字段的中文文档用于编辑器补全与悬停提示。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from core.character.refs.base import SkillRef, resolve_skill
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
@@ -214,22 +215,22 @@ class Read(Expression):
|
||||
|
||||
Args:
|
||||
attribute: 要读取的等级、ratio、最大层数、VP 或 UP。
|
||||
skill: 被读取技能的名称,省略时读取自身。
|
||||
skill: 被读取技能的 Ref,兼容旧名称字符串;省略时读取自身。
|
||||
"""
|
||||
attribute: SkillAttribute
|
||||
"""要读取的等级、ratio、最大层数、VP 或 UP。"""
|
||||
skill: str | None = None
|
||||
"""源技能名称;省略时读取声明所属技能自身。"""
|
||||
skill: SkillRef | str | None = None
|
||||
"""源技能 Ref 或旧名称;省略时读取声明所属技能自身。"""
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.attribute, SkillAttribute) or self.attribute.value not in READ_ATTRIBUTES:
|
||||
raise ValueError('Read 仅支持等级、ratio、maxLayer、VP、UP;不能读取尚未汇总的输出属性')
|
||||
if self.skill is not None and (not isinstance(self.skill, str) or not self.skill):
|
||||
raise TypeError('Read 的技能引用必须是非空技能名称,省略时读取自身')
|
||||
if self.skill is not None and not isinstance(self.skill, SkillRef) and (not isinstance(self.skill, str) or not self.skill):
|
||||
raise TypeError('Read 的技能引用必须是 SkillRef 或非空技能名称,省略时读取自身')
|
||||
|
||||
def target(self, source: Skill) -> Skill:
|
||||
"""按名称解析被读取的技能,并检查目标属性是否存在。"""
|
||||
target = source if self.skill is None else source.char.GetSkillByName(self.skill)
|
||||
target = source if self.skill is None else resolve_skill(source.char, self.skill)
|
||||
if target is None:
|
||||
raise ValueError(f'效果表达式引用技能不存在: {source.name} -> {self.skill}')
|
||||
if not hasattr(target, self.attribute.value):
|
||||
@@ -445,6 +446,15 @@ def _names(values: Sequence[str], label: str) -> tuple[str, ...]:
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _skill_targets(values: Sequence[SkillRef | str], label: str) -> tuple[SkillRef | str, ...]:
|
||||
"""复制引用数组,兼容非空的旧技能名称;拒绝单个引用或字符串冒充数组。"""
|
||||
if isinstance(values, (str, SkillRef)) or not isinstance(values, Sequence):
|
||||
raise TypeError(f'{label} 必须是技能引用数组')
|
||||
if not all(isinstance(value, SkillRef) or isinstance(value, str) and value for value in values):
|
||||
raise TypeError(f'{label} 必须由 SkillRef 或非空技能名称组成')
|
||||
return tuple(values)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class _Effect:
|
||||
"""效果声明公共字段;职业代码使用 SkillEffect 或 CharacterEffect。"""
|
||||
@@ -485,24 +495,24 @@ class MissingTargetPolicy(str, Enum):
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SkillEffect(_Effect):
|
||||
"""作用于技能的效果声明;skills 继续接收技能名称数组,默认所有伤害技能。
|
||||
"""作用于技能的效果声明;skills 接收 Ref 数组并兼容旧名称,默认所有伤害技能。
|
||||
|
||||
Args:
|
||||
value: 实际贡献值或表达式;乘算使用倍率,加算使用增量。
|
||||
operation: 汇总运算,默认乘算。
|
||||
attribute: 目标属性,默认技能技攻;也支持编号分段和职业专用输出属性。
|
||||
skills: 目标技能名称数组,默认 '*' 表示所有伤害技能。
|
||||
except_skills: 排除的技能名称数组。
|
||||
skills: 目标技能 Ref 或旧名称数组,默认 '*' 表示所有伤害技能。
|
||||
except_skills: 排除的技能 Ref 或旧名称数组。
|
||||
when: 生效条件,默认无额外条件。
|
||||
weapon: 允许的武器类型数组,默认不限制。
|
||||
missing_target: 缺失目标时默认报错;迁移旧名称失配时可显式跳过并记录。
|
||||
"""
|
||||
attribute: SkillAttribute | ProfessionAttribute | ComponentAttribute = SkillAttribute.DAMAGE
|
||||
"""目标属性,默认技能技攻;分段属性使用 Power/Hit/ExtraData/ExtraPower,职业属性使用 ProfessionAttribute 子类。"""
|
||||
skills: Sequence[str] | Literal['*'] = '*'
|
||||
"""目标技能名称数组;'*' 仅选择伤害技能,指定名称时可关联被动技能。"""
|
||||
except_skills: Sequence[str] = ()
|
||||
"""从目标中排除的技能名称数组。"""
|
||||
skills: Sequence[SkillRef | str] | Literal['*'] = '*'
|
||||
"""目标技能 Ref 或旧名称数组;'*' 仅选择伤害技能,指定名称时可关联被动技能。"""
|
||||
except_skills: Sequence[SkillRef | str] = ()
|
||||
"""从目标中排除的技能 Ref 或旧名称数组。"""
|
||||
missing_target: MissingTargetPolicy = MissingTargetPolicy.ERROR
|
||||
"""缺失目标处理策略;SKIP 仅用于保留已确认的旧名称失配行为。"""
|
||||
|
||||
@@ -519,8 +529,8 @@ class SkillEffect(_Effect):
|
||||
):
|
||||
raise ValueError('效果目标必须是可写的 SkillAttribute;VP/UP 只能作为外部输入')
|
||||
if self.skills != '*':
|
||||
object.__setattr__(self, 'skills', _names(self.skills, 'skills'))
|
||||
object.__setattr__(self, 'except_skills', _names(self.except_skills, 'except_skills'))
|
||||
object.__setattr__(self, 'skills', _skill_targets(self.skills, 'skills'))
|
||||
object.__setattr__(self, 'except_skills', _skill_targets(self.except_skills, 'except_skills'))
|
||||
if not isinstance(self.missing_target, MissingTargetPolicy):
|
||||
raise TypeError('缺失目标策略必须使用 MissingTargetPolicy')
|
||||
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
技能关联由 `core/basic/skill_effects.py` 的 `SkillEffectEngine` 统一处理。
|
||||
`lv`、VP 等输入变化不再通过历史倍率乘除更新目标,而是重新解析最终状态并汇总本次关联贡献。
|
||||
|
||||
## 技能身份与引用维护
|
||||
|
||||
职业模块导入自己的 Ref;技能类声明 `ref = Ref.XXX`,`code` 从 ref 派生,name 只负责显示。
|
||||
角色初始化根据技能数据 prefix 校验职业范围,并创建独立的 ref/code 索引;输出圣骑士等职业变体复用数据来源范围。
|
||||
SkillEffect.skills、except_skills、Read.skill、level_source 统一接受 Ref,旧名称字符串继续兼容。
|
||||
公共技能引用只匹配当前角色实际绑定的技能实例。GetSkill(Ref.XXX) 查询引用,GetSkillByCode 查询原始 code;
|
||||
字符串不会隐式当作 code。重复绑定、跨职业引用直接报错;新增规则应使用 Ref,旧失配名称继续保留 SKIP 诊断。
|
||||
引用生成、公共分组、编辑器中文提示的维护说明见 core/character/refs/base.py。
|
||||
SkillRef.name 只影响提示,不参与相等性和哈希;技能名称变更不会改变引用身份。
|
||||
|
||||
## 计算阶段
|
||||
|
||||
1. `SetSkills` 批量读取基础等级、VP、UP。传入的等级是基础等级,不含关联技能提供的等级;重复传入同一配置不会重复加级。
|
||||
@@ -186,6 +196,7 @@ backend/.venv/bin/pytest backend/tests/smoke/test_calc_availability.py -k config
|
||||
|
||||
第二项验证技能配置顺序(包括搭配武器的场景),不验证装备执行顺序。修改公共引擎后再运行完整 `backend/tests` 回归。
|
||||
"""
|
||||
from core.character.refs.base import resolve_skill
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
@@ -331,14 +342,16 @@ class SkillEffectEngine:
|
||||
for name in declaration.skills:
|
||||
if name in declaration.except_skills:
|
||||
continue
|
||||
target = self.character.GetSkillByName(name)
|
||||
target = resolve_skill(self.character, name)
|
||||
if target is None:
|
||||
if declaration.missing_target == MissingTargetPolicy.ERROR:
|
||||
raise ValueError(f'类型化效果目标不存在: {source.name} -> {name}')
|
||||
self.missing_target(source, name)
|
||||
else:
|
||||
targets.append(target)
|
||||
targets = [target for target in targets if target.name not in declaration.except_skills]
|
||||
excluded = {id(target) for ref in declaration.except_skills
|
||||
if (target := resolve_skill(self.character, ref)) is not None}
|
||||
targets = [target for target in targets if id(target) not in excluded]
|
||||
for target in targets:
|
||||
if attribute in STATE_ATTRIBUTES - {'lv'} and (target, attribute) not in self._state_base:
|
||||
raise ValueError(f'类型化效果状态缺少默认值: {source.name} -> {target.name}.{attribute}')
|
||||
@@ -428,7 +441,7 @@ class SkillEffectEngine:
|
||||
graph = {skill: set() for skill in self.skills}
|
||||
for skill in self.skills:
|
||||
if skill.level_source is not None:
|
||||
source = self.character.GetSkillByName(skill.level_source)
|
||||
source = resolve_skill(self.character, skill.level_source)
|
||||
if source is None:
|
||||
raise ValueError(f'共享等级源技能不存在: {skill.name} -> {skill.level_source}')
|
||||
graph[skill].add(source)
|
||||
@@ -447,7 +460,7 @@ class SkillEffectEngine:
|
||||
for skill in order:
|
||||
# All upstream level/parameter contributions are ready now.
|
||||
if skill.level_source is not None:
|
||||
skill._lv = self.character.GetSkillByName(skill.level_source).lv
|
||||
skill._lv = resolve_skill(self.character, skill.level_source).lv
|
||||
contributions, reads = self._collect(skill)
|
||||
if not reads.issubset(graph[skill]):
|
||||
raise ValueError(f'技能关联状态依赖随计算改变: {skill.name}')
|
||||
|
||||
Reference in New Issue
Block a user