"""职业技能引用公共类型。 运行 scripts/generate_skill_refs.py 生成引用,追加 --check 只检查产物一致性。 公共技能在生成器的 COMMON_SKILLS、AWAKENING_SKILLS、COMBO_SKILLS 中维护, 统一生成到 common.py。职业类按实际技能树继承基础组、觉醒变体组、可选连招组, 只声明职业专属引用。基础组要求全部职业覆盖;觉醒组保留原始 code 的 1~5 后缀, 觉醒之心保留其名称差异;连招组排除创造者、黑暗武士,不暴露不存在的引用。 清单变动需校验所有职业的 code、名称及分组覆盖。 使用:from core.character.refs.extra.creator import CreatorSkillRef as Ref Ref.BACKSTEP 为当前角色的公共技能;Ref.CREATESPACE 为创造者专属技能。 Ref.members() 包含继承成员;Ref.from_code(code) 按原始 code 查找,缺失抛 KeyError。 SkillRef 与引用命名空间均只读;公共引用的 profession 为 None,职业引用带职业范围。 技能类通过 ref = Ref.XXX 绑定身份,Skill.code 从 ref 派生。角色初始化建立当前角色的 引用/code 索引;SkillEffect、Read、level_source 及 GetSkill 已统一解析引用。 字符串兼容旧显示名称,按 code 查询必须显式调用 GetSkillByCode,避免含义混淆。 公共引用绑定当前角色实例,职业引用额外校验范围;名称只用于提示,不参与身份比较。 计算器独有技能的扩展声明应单独维护,不手改自动生成的职业文件。 """ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass, field import re from types import MappingProxyType from typing import ClassVar @dataclass(frozen=True, slots=True) class SkillRef: """不可变技能身份,由职业范围和原始 code 共同确定。 Args: code: 技能树原始 code,保留连字符等原始字符。 name: 中文提示名称,仅展示,不参与身份绑定。 profession: 职业标识,例如 extra.creator;None 表示当前角色的公共技能。 """ code: str """技能树原始 code,不受 Python 成员名规范化影响。""" profession: str | None = None """所属职业;None 表示公共引用,须在解析时绑定当前角色。""" name: str = field(default='', compare=False) """中文提示名称,仅用于编辑器及诊断展示,不参与身份比较和哈希。""" def __post_init__(self): if not isinstance(self.name, str): raise TypeError('技能提示名称必须是字符串') if not isinstance(self.code, str) or not re.fullmatch(r'[A-Za-z0-9_\-]+', self.code): raise ValueError(f'无效技能 code: {self.code!r}') if self.profession is not None and ( not isinstance(self.profession, str) or not re.fullmatch(r'[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*', self.profession) ): raise ValueError(f'无效职业标识: {self.profession!r}') @property def qualified_key(self) -> str: """完整引用标识,例如 extra.creator:createspace 或 common:backstep。""" return f'{self.profession or "common"}:{self.code}' class _ReferenceMeta(type): """构建时检查继承、作用域和重复项,构建后冻结命名空间。""" def __new__(mcls, name, bases, namespace): inherited = {} for base in bases: for member, ref in getattr(base, '__members__', {}).items(): if member in inherited and inherited[member] != ref: raise ValueError(f'{name}: 继承成员冲突: {member}') inherited[member] = ref reserved = {'members', 'from_code', '__members__', '_by_code'} if any(isinstance(base, _ReferenceMeta) for base in bases): if reserved.intersection(namespace): raise ValueError(f'{name}: 不能覆盖公共查询接口') if inherited.keys() & namespace.keys(): raise ValueError(f'{name}: 不能覆盖继承的技能引用') members = dict(inherited) profession = namespace.get('__profession__', next((getattr(b, '__profession__', None) for b in bases), None)) for member, ref in namespace.items(): if isinstance(ref, SkillRef): if member.startswith('_') or member != member.upper(): raise ValueError(f'{name}: 技能引用成员必须使用大写名称: {member}') if ref.profession != profession: raise ValueError(f'{name}: 技能引用职业范围不一致: {member}') members[member] = ref if any(ref.profession not in (None, profession) for ref in inherited.values()): raise ValueError(f'{name}: 不能继承其他职业的专属引用') by_code = {} for member, ref in sorted(members.items()): if ref.code in by_code: raise ValueError(f'{name}: 重复技能 code: {ref.code} ({member})') by_code[ref.code] = ref namespace['__members__'] = MappingProxyType(dict(sorted(members.items()))) namespace['_by_code'] = MappingProxyType(by_code) return super().__new__(mcls, name, bases, namespace) def __setattr__(cls, name, value): raise AttributeError('技能引用命名空间只读,不能重新赋值') def __delattr__(cls, name): raise AttributeError('技能引用命名空间只读,不能删除成员') class SkillReferences(metaclass=_ReferenceMeta): """可继承的技能引用命名空间;通过类访问,无须创建实例。""" __profession__: ClassVar[str | None] = None """职业范围,由职业类声明;公共类为 None。""" __members__: ClassVar[Mapping[str, SkillRef]] """包含继承成员的只读名称映射,由公共构建过程生成。""" _by_code: ClassVar[Mapping[str, SkillRef]] def __new__(cls): raise TypeError('技能引用通过类访问,无须实例化') @classmethod def members(cls) -> tuple[SkillRef, ...]: """返回包含公共技能的全部引用,按成员名排序。""" return tuple(cls.__members__.values()) @classmethod def from_code(cls, code: str) -> SkillRef: """按原始 code 查找包含公共技能的引用;不存在时抛出 KeyError。""" return cls._by_code[code] def bind_skill_refs(character, profession: str) -> None: """为当前角色建立引用及 code 索引;拒绝重复绑定、跨职业引用和无效类型。 旧技能可暂不声明 ref;引用索引只保存当前角色实例,公共引用不会跨角色共享实例。 profession 使用技能数据来源职业,输出圣骑士等变体复用其来源职业标识。 """ by_ref = {} by_code = {} for skill in character.skills: ref = skill.ref if ref is None: continue if not isinstance(ref, SkillRef): raise TypeError(f'技能 ref 必须是 SkillRef: {skill.name}') if ref.profession not in (None, profession): raise ValueError(f'技能引用职业不匹配: {skill.name}: {ref.qualified_key} -> {profession}') if ref in by_ref or ref.code in by_code: raise ValueError(f'重复技能引用绑定: {skill.name}: {ref.qualified_key}') by_ref[ref] = skill by_code[ref.code] = skill character.skill_ref_profession = profession character.skills_ref_dict = by_ref character.skills_code_dict = by_code def resolve_skill(character, reference: SkillRef | str): """统一解析技能引用;字符串沿用显示名称查询,绝不隐式当作 code。 错误职业范围直接报错;有效范围内缺失技能返回 None,由调用入口决定缺失策略。 公共引用同样必须存在于当前角色引用索引中,不按名称或 code 猜测回退。 """ if isinstance(reference, str): return character.GetSkillByName(reference) if not isinstance(reference, SkillRef): raise TypeError('技能引用必须是 SkillRef 或旧技能名称字符串') profession = getattr(character, 'skill_ref_profession', None) if reference.profession not in (None, profession): raise ValueError(f'技能引用职业不匹配: {reference.qualified_key} -> {profession}') return character.skills_ref_dict.get(reference)