update
This commit is contained in:
+18
-169
@@ -8,9 +8,9 @@ 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 OPENAPI_CUSTOM_CN_PATH, logical_skill_detail_path
|
||||
from .skill_effects import CHARACTER_ATTRIBUTES, INPUT_ATTRIBUTES
|
||||
from .skill_effect_types import CharacterEffect, EffectDeclaration, SkillEffect
|
||||
from api.utils.openapi_data import logical_skill_detail_path
|
||||
from .skill_effects import INPUT_ATTRIBUTES
|
||||
from .skill_effect_types import EffectDeclaration
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.basic.character import Character
|
||||
@@ -22,12 +22,6 @@ def _identity(value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
@cache
|
||||
def _load_custom_cn() -> list[dict[str, Any]]:
|
||||
with OPENAPI_CUSTOM_CN_PATH.open(encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
@cache
|
||||
def _load_skill_detail(prefix: str) -> dict[str, Any]:
|
||||
with logical_skill_detail_path(prefix).open(encoding='utf-8') as f:
|
||||
@@ -51,23 +45,14 @@ def _get_skill_dataplus_suffixes(skill_class: type) -> tuple[str, ...]:
|
||||
|
||||
@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_skill_info():
|
||||
skill_data = _load_skill_detail(prefix)
|
||||
custom_cn = get_redis_info(redis, 'dcalc:custom_cn', _load_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)
|
||||
elif key == "cdList":
|
||||
data = [0] + [row.get("coolTime", 0) for row in skill_data["levelInfo"]["rows"]]
|
||||
except Exception:
|
||||
@@ -77,8 +62,6 @@ def get_data(prefix: str, key: int | str, func: Callable[[Any], Any] = _identity
|
||||
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:
|
||||
@@ -153,18 +136,12 @@ class Skill:
|
||||
"""技能冷却倍率,直接体现在面板上,不被上限影响"""
|
||||
cdRecover: float = 1
|
||||
"""技能冷却恢复"""
|
||||
associate: list[dict[str, Any] | EffectDeclaration] = []
|
||||
"""关联声明;维护示例和扩展边界见 core/basic/skill_effects.py 文件顶部说明。
|
||||
associate: list[EffectDeclaration] = []
|
||||
"""类型化关联声明;仅接受 SkillEffect/CharacterEffect。
|
||||
|
||||
新规则使用 SkillEffect/CharacterEffect 和 Ref 目标数组,兼容旧字典及名称字符串。
|
||||
新声明 value 是实际倍率/增量/覆盖值,以下 type、data、ratio 约定仅适用于旧字典。
|
||||
type 默认 '*skillDamage':*乘算、+加算、=覆盖;$*PAtkP 等作用于角色。
|
||||
skills 默认 '*',表示所有伤害技能;显式列表使用技能名称,可指定被动技能。
|
||||
exceptSkills 默认 [];weapon 默认职业武器选项;precondition() 控制前置条件。
|
||||
data 按来源等级取值;ratio 是数据除数,默认 100,与技能的 ratio 属性不同。
|
||||
+lv 的整数加级通常设置 ratio=1;=覆盖直接使用 data 值,不除以 ratio。
|
||||
动态 effect 以 (0, 最终等级) 多次收集,只构造声明并调用 super().effect。
|
||||
自定义 type 方法仍有兼容入口,但直接修改目标会绕过贡献收集,新增规则勿照搬。
|
||||
value 是实际倍率、增量或覆盖值,目标优先使用 Ref 数组。
|
||||
动态 effect 只构造声明并调用 super().effect,由引擎统一收集和应用。
|
||||
维护示例见 core/basic/skill_effects.py 文件顶部说明。
|
||||
"""
|
||||
mode: list[str] = ['']
|
||||
currentMode: str = ''
|
||||
@@ -179,8 +156,6 @@ class Skill:
|
||||
"""是否有技能强化"""
|
||||
upType: Literal['damage', 'buff', 'heal'] = 'damage'
|
||||
vps: list = []
|
||||
custom: float = 1.0
|
||||
"""国服特色加强"""
|
||||
|
||||
def __init__(self, char):
|
||||
self.char = char
|
||||
@@ -249,118 +224,17 @@ class Skill:
|
||||
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:
|
||||
if isinstance(assoc, (SkillEffect, CharacterEffect)):
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is None:
|
||||
raise RuntimeError('类型化效果需要 SkillEffectEngine')
|
||||
engine.collect_effect(self, assoc)
|
||||
continue
|
||||
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)
|
||||
"""收集类型化声明;动态目标规则可重写此入口。"""
|
||||
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):
|
||||
"""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)
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is not None and engine.collecting is self:
|
||||
if type[2:] in CHARACTER_ATTRIBUTES:
|
||||
engine.record(self, self.char, type[2:], '*', value)
|
||||
else:
|
||||
self.char.SetStatus(**{type[2:]: value})
|
||||
elif type.startswith('$+'):
|
||||
value = self.get_data_value(data, new) / ratio - self.get_data_value(data, old) / ratio
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is not None and engine.collecting is self:
|
||||
if type[2:] in CHARACTER_ATTRIBUTES:
|
||||
engine.record(self, self.char, type[2:], '+', value)
|
||||
else:
|
||||
self.char.SetStatus(**{type[2:]: value})
|
||||
else:
|
||||
exceptSkills = set(exceptSkills)
|
||||
if skills == '*':
|
||||
for skill in self.char.GetSkills('all', True):
|
||||
if skill.name in exceptSkills:
|
||||
continue
|
||||
self.update_skill_attribute(skill, type, old, new, data, ratio, weapon, weapon_type, weapon_options)
|
||||
else:
|
||||
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)
|
||||
else:
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is not None and engine.collecting is self:
|
||||
engine.missing_target(self, name)
|
||||
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)
|
||||
engine = getattr(self.char, 'skill_effects', None)
|
||||
if engine is not None and engine.collecting is self:
|
||||
if type[1:] == 'lv' and skill.level_source is not None:
|
||||
return
|
||||
if type.startswith('*'):
|
||||
if 'cdReduce' in type or 'cdRatio' in type:
|
||||
value = (1 - new_value / ratio) / (1 - old_value / ratio)
|
||||
else:
|
||||
value = (1 + new_value / ratio) / (1 + old_value / ratio)
|
||||
elif type.startswith('+'):
|
||||
if type[1:] == 'lv' and not engine.is_learned(skill):
|
||||
return
|
||||
value = (new_value - old_value) / ratio
|
||||
else:
|
||||
value = new_value
|
||||
engine.record(self, skill, type[1:], type[0], value)
|
||||
return
|
||||
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
|
||||
|
||||
@@ -401,7 +275,7 @@ class ActiveSkill(Skill):
|
||||
basic.setVP()
|
||||
date = basic.getSkillData(self.lv)
|
||||
cd = basic.getSkillCD(mode)
|
||||
return date * basic.skillRation * basic.custom, basic.skillDamage, cd
|
||||
return date * basic.skillRation, basic.skillDamage, cd
|
||||
|
||||
def setMode(self, mode: str):
|
||||
pass
|
||||
@@ -453,23 +327,11 @@ class ActiveSkill(Skill):
|
||||
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):
|
||||
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.getQuickCDRatio()) * self.cdRatio - self.cdCut / self.cdRecover , 2))
|
||||
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
|
||||
@@ -600,18 +462,5 @@ class ActiveBufferSkill(BuffSkill):
|
||||
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))
|
||||
return max(0, round(max(self.cd * 0.3, self.cd * self.cdReduce / self.cdRecover * self.getWeaponCDRatio(), 1) - self.cdCut, 2))
|
||||
|
||||
@@ -26,7 +26,7 @@ SkillRef.name 只影响提示,不参与相等性和哈希;技能名称变更
|
||||
| 新增或修改的影响 | 维护位置 | 要点 |
|
||||
| --- | --- | --- |
|
||||
| 技伤、冷却、攻击段数等普通关联 | 对应 `core/character/GF/职业/角色.py` 的技能类 `associate` | 通常不改引擎;目标属性应有默认值 |
|
||||
| 随 VP、等级或自身参数变化的关联 | `associate` 中的 `Read`、`LevelTable`、`Select`、`when` | 旧动态 `effect` 仍兼容,新规则优先用表达式 |
|
||||
| 随 VP、等级或自身参数变化的关联 | `associate` 中的 `Read`、`LevelTable`、`Select`、`when` | 动态目标使用 `effect`,数值和条件使用表达式 |
|
||||
| 技能给其他技能加级 | `SkillEffect(attribute=SkillAttribute.LEVEL, operation=Operation.ADD, ...)` | value 直接填整数增量;只给已学习的目标加级 |
|
||||
| 生命之树这类共享等级 | 目标技能的 `level_source` | 指向源技能名称,不再写等级 getter/no-op setter |
|
||||
| 新增会被关联修改、并影响后续关联的参数 | 技能类默认值 + `skill_effect_types.py` 的 `SkillAttribute`、`STATE_ATTRIBUTES` | 新参数同时检查读写范围;状态属性自动属于输入属性 |
|
||||
@@ -41,7 +41,7 @@ SkillRef.name 只影响提示,不参与相等性和哈希;技能名称变更
|
||||
|
||||
## 声明示例和数值约定
|
||||
|
||||
新代码优先使用 `skill_effect_types.py` 的不可变声明,`associate` 继续是数组,目标仍然使用技能名称数组:
|
||||
关联统一使用 `skill_effect_types.py` 的不可变声明,`associate` 继续是数组,目标仍然使用技能名称数组:
|
||||
|
||||
```python
|
||||
associate = [
|
||||
@@ -94,41 +94,9 @@ associate = [SkillEffect(attribute=HunterAttribute.MARK_RATIO,
|
||||
再使用 Read 表达式,不能直接读取尚未汇总的输出。条件可组合 `All`、`Not`、`GreaterThan`;
|
||||
角色输出类型用 `OutputTypeStartsWith` 判断,所有条件均在建图时提供完整依赖。
|
||||
|
||||
### 旧字典兼容
|
||||
|
||||
以下片段放在对应技能类内,`data0` 为该来源技能的等级数据表:
|
||||
|
||||
```python
|
||||
# 默认百分比:data0[lv] = 110 时,倍率为 1 + 110 / 100。
|
||||
associate = [{'type': '*skillDamage', 'data': data0, 'skills': ['目标技能名称']}]
|
||||
|
||||
# 加级:data0[lv] = 1 时,给已学习目标增加 1 级。
|
||||
associate = [{'type': '+lv', 'data': data0, 'ratio': 1, 'skills': ['目标技能名称']}]
|
||||
```
|
||||
|
||||
上面是两种独立示例;需要同时生效时放进同一列表。`skills='*'` 仅选择伤害技能,关联被动技能时使用明确的名称列表。名称必须与目标 `name` 完全一致,当前不是按 UUID 匹配。
|
||||
|
||||
数据以来源技能等级索引,通常令 `data[0] = 0`。收集仍兼容旧公式:普通乘算为 `(1 + data[lv]/ratio) / (1 + data[0]/ratio)`,`cdReduce`、`cdRatio` 改用减号;加算为 `(data[lv] - data[0])/ratio`;覆盖 `=` 直接取 `data[lv]`。这里字典中的 `ratio` 是除数,区别于手雷精通类上的 `ratio` 参数。
|
||||
|
||||
女弹药的动态关联是现成参考:
|
||||
|
||||
```python
|
||||
ratio = 1.0
|
||||
|
||||
def effect(self, old, new):
|
||||
self.associate = [{
|
||||
'type': '*skillDamage',
|
||||
'data': [value * self.ratio for value in self.data0],
|
||||
'skills': ['G-14手雷', 'G-35L感电手雷', 'G-18C冰冻手雷'],
|
||||
}]
|
||||
return super().effect(old, new)
|
||||
```
|
||||
|
||||
三觉被动通过 `*ratio` 关联手雷精通。引擎先确定最终 `ratio=1.2`,再收集手雷倍率,所以精通 11 级对应 `1 + 110% × 1.2 = 2.32`。
|
||||
|
||||
## 职业规则
|
||||
|
||||
`associate` 可以混用新声明和旧字典。现有动态 `effect(old, new)` 作为兼容入口,在收集模式下以 `(0, 最终等级)` 执行;公共入口记录贡献,不修改目标。每次收集都从原始 `associate` 声明开始,取消 VP 时不会保留上一次分支。
|
||||
`associate` 仅接受 SkillEffect/CharacterEffect,旧字典会直接报错。动态 `effect(old, new)` 作为目标构造入口,在收集模式下以 `(0, 最终等级)` 执行;公共入口记录贡献,不修改目标。每次收集都从原始 `associate` 声明开始,取消 VP 时不会保留上一次分支。
|
||||
|
||||
动态规则只能生成关联声明,不能在收集过程中直接修改角色或目标技能。需要修改关联产生方式时,应改用声明,而不是另写参数 setter 撤销旧倍率。元素师原先的 `ratio` 补偿 setter 已移除,避免与公共引擎重复应用。
|
||||
|
||||
@@ -212,7 +180,7 @@ from .skill_effect_types import (
|
||||
# 状态属性定义在 skill_effect_types;技能类仍须提供相应默认值。
|
||||
# 外部赋值触发重算的输入;登记在此不等于自动跟踪跨技能的该属性读取。
|
||||
INPUT_ATTRIBUTES = STATE_ATTRIBUTES | {'vp', 'up', 'comboCol', 'stage', 'currentMode'}
|
||||
# These are the scalar keys actually supported by CharacterBase.SetStatus.
|
||||
# CharacterBase.SetStatus 实际支持的标量属性。
|
||||
# 扩展前核对 SetStatus 的实际运算语义;特殊字段不能仅加入白名单。
|
||||
CHARACTER_ATTRIBUTES = frozenset(attribute.value for attribute in CharacterAttribute)
|
||||
_MISSING = object()
|
||||
@@ -220,7 +188,7 @@ _MISSING = object()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkillContribution:
|
||||
"""统一后的来源贡献;新声明和旧字典最终都编译为此结构。"""
|
||||
"""类型化效果求值后生成的不可变贡献,记录来源、目标及实际运算值。"""
|
||||
source: object
|
||||
"""提供本条效果的来源技能实例。"""
|
||||
index: int
|
||||
@@ -236,9 +204,10 @@ class SkillContribution:
|
||||
|
||||
|
||||
def skill_effect_batch(method):
|
||||
"""Keep external skill/stat changes outside the association baseline."""
|
||||
"""将角色修改方法包入关联批次,使外部加成写入基础值,并在批次结束后重算。"""
|
||||
@wraps(method)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
"""有引擎时在批次内调用原方法;角色尚未初始化引擎时直接调用。"""
|
||||
engine = getattr(self, 'skill_effects', None)
|
||||
if engine is None:
|
||||
return method(self, *args, **kwargs)
|
||||
@@ -248,7 +217,11 @@ def skill_effect_batch(method):
|
||||
|
||||
|
||||
class SkillEffectEngine:
|
||||
"""技能关联引擎:管理输入基线,按依赖顺序解析状态,再汇总技能和角色输出。
|
||||
|
||||
每个角色独立持有一个实例;仅接受类型化声明,临时技能副本不受其管理。"""
|
||||
def __init__(self, character):
|
||||
"""绑定角色的技能实例,校验声明,并保存等级、状态参数和关联的初始基线。"""
|
||||
self.character = character
|
||||
self.skills = tuple(sorted(character.skills, key=lambda skill: skill.id))
|
||||
self._members = {id(skill) for skill in self.skills}
|
||||
@@ -259,6 +232,9 @@ class SkillEffectEngine:
|
||||
if hasattr(skill, attr)
|
||||
}
|
||||
self._associations = {skill: skill.associate for skill in self.skills}
|
||||
for skill, declarations in self._associations.items():
|
||||
for declaration in declarations:
|
||||
self._validate_declaration(skill, declaration)
|
||||
self._output_base = {}
|
||||
self._depth = 0
|
||||
self.resolving = False
|
||||
@@ -272,60 +248,77 @@ class SkillEffectEngine:
|
||||
self.unresolved_targets = set()
|
||||
|
||||
def owns(self, skill):
|
||||
# skillInfo uses shallow copies; these are local render contexts.
|
||||
# skillInfo 使用浅拷贝,仅用于本次技能输出计算。
|
||||
"""判断技能是否为本引擎管理的原实例,排除 skillInfo 创建的临时副本。"""
|
||||
return id(skill) in self._members
|
||||
|
||||
def changed(self):
|
||||
"""标记状态与输出需要重算;不在批次内时立即解析。"""
|
||||
self._dirty = self._state_dirty = True
|
||||
if not self._depth:
|
||||
self.resolve()
|
||||
|
||||
def set_level(self, skill, value):
|
||||
"""按当前最终等级与 value 的差值调整基础等级,避免装备加级重复计入关联加级。"""
|
||||
if skill.level_source is not None:
|
||||
return
|
||||
# += equipment bonuses must change the input, not copy already-derived
|
||||
# mastery levels back into it. Input configuration uses set_input_level.
|
||||
# 装备 += 加级只调整输入,不能把已计算的精通加级写回基础等级。
|
||||
# 配置中的绝对基础等级通过 set_input_level 写入。
|
||||
self._levels[skill] = min(skill.maxLv, max(0, self._levels[skill] + int(value) - skill.lv))
|
||||
self.changed()
|
||||
|
||||
def set_input_level(self, skill, value):
|
||||
"""将 value 作为配置中的基础等级写入并标记待重算,不立即解析;共享等级技能忽略此输入。"""
|
||||
if skill.level_source is not None:
|
||||
return
|
||||
self._levels[skill] = min(skill.maxLv, max(0, int(value)))
|
||||
self._dirty = self._state_dirty = True
|
||||
|
||||
def set_attribute(self, skill, name, value):
|
||||
"""写入外部输入属性;状态属性同时更新基线,并触发或排队重算。"""
|
||||
if name in STATE_ATTRIBUTES:
|
||||
self._state_base[skill, name] = value
|
||||
object.__setattr__(skill, name, value)
|
||||
self.changed()
|
||||
|
||||
def is_learned(self, skill):
|
||||
"""按基础等级判断是否已学习;共享等级技能按其最终等级判断。"""
|
||||
if skill.level_source is not None:
|
||||
return skill.lv > 0
|
||||
return self._levels[skill] > 0
|
||||
|
||||
def read_level(self, skill):
|
||||
"""收集时记录跨技能等级依赖;普通读取时按需解析尚未更新的状态。"""
|
||||
if self.collecting is not None and self.collecting is not skill:
|
||||
self._reads.add(skill)
|
||||
elif self._state_dirty and not self.resolving:
|
||||
self.resolve_states()
|
||||
|
||||
def record(self, source, target, attribute, operation, value):
|
||||
"""将已求值的效果追加到当前来源的待汇总贡献中,不直接修改目标。"""
|
||||
self._pending.append(SkillContribution(
|
||||
source, len(self._pending), target, attribute, operation, value,
|
||||
))
|
||||
|
||||
def missing_target(self, source, name):
|
||||
"""记录被跳过的目标引用及来源技能,供职业规则审查。"""
|
||||
self.unresolved_targets.add((source.name, name))
|
||||
|
||||
def collect_effect(self, source, declaration: EffectDeclaration):
|
||||
"""Compile typed declarations into the same contributions as legacy rules.
|
||||
@staticmethod
|
||||
def _validate_declaration(source, declaration):
|
||||
"""校验来源技能的单条声明类型;旧字典及其他不支持的类型直接报错。"""
|
||||
if not isinstance(declaration, (SkillEffect, CharacterEffect)):
|
||||
raise TypeError(
|
||||
f'技能 {source.name} 的 associate 仅接受 SkillEffect/CharacterEffect,'
|
||||
f'不支持 {type(declaration).__name__}'
|
||||
)
|
||||
|
||||
Discovery reads structure only: no arithmetic or conditional branch is
|
||||
evaluated before upstream state is ready. Inactive branches still expose
|
||||
dependencies and possible state targets.
|
||||
"""
|
||||
def collect_effect(self, source, declaration: EffectDeclaration):
|
||||
"""将来源技能的一条类型化声明解析为目标贡献。
|
||||
|
||||
建图阶段仅收集依赖和可能的状态目标,不计算表达式或按临时条件裁剪依赖。
|
||||
求值阶段检查前置条件、武器和 when 条件,再记录实际贡献;必须在来源收集上下文中调用。"""
|
||||
self._validate_declaration(source, declaration)
|
||||
if self.collecting is not source:
|
||||
raise RuntimeError('类型化效果只能由技能关联引擎收集')
|
||||
for dependency in declaration.dependencies():
|
||||
@@ -357,8 +350,8 @@ class SkillEffectEngine:
|
||||
raise ValueError(f'类型化效果状态缺少默认值: {source.name} -> {target.name}.{attribute}')
|
||||
|
||||
if self._discovering:
|
||||
# Retain legacy precondition level reads, but never prune graph edges
|
||||
# based on its provisional result. New conditions should use `when`.
|
||||
# 保留 precondition 中的等级读取依赖,但不根据临时结果裁剪依赖边。
|
||||
# 新条件优先通过 when 声明。
|
||||
source.precondition()
|
||||
value = 0
|
||||
else:
|
||||
@@ -380,12 +373,15 @@ class SkillEffectEngine:
|
||||
|
||||
def _collect(self, skill, *, discover=False):
|
||||
# 一个重算周期会多次调用 effect;职业实现只能构造声明并调用公共收集入口。
|
||||
"""收集单个技能的贡献与读取依赖,并在结束时清理收集上下文。
|
||||
|
||||
discover=True 表示仅发现依赖;每次从初始声明开始,避免残留上次动态分支。"""
|
||||
self._pending = []
|
||||
self._reads = set()
|
||||
if not skill.association_source:
|
||||
return (), set()
|
||||
# VP branches often only assign associate when enabled. Start from the
|
||||
# declaration every time so disabling VP also removes the old branch.
|
||||
# 动态 VP 分支可能仅在启用时覆盖声明;每次恢复初始声明,
|
||||
# 确保取消 VP 后不会保留上次的分支。
|
||||
skill.associate = self._associations[skill]
|
||||
self.collecting = skill
|
||||
self._discovering = discover
|
||||
@@ -393,11 +389,9 @@ class SkillEffectEngine:
|
||||
if skill.lv > 0:
|
||||
skill.effect(0, skill.lv)
|
||||
elif discover:
|
||||
# A typed level assignment can activate an initially unlearned
|
||||
# source; its static dependencies must already be in the graph.
|
||||
# 等级覆盖可能激活尚未学习的来源,因此建图时也要收集其静态依赖。
|
||||
for declaration in skill.associate:
|
||||
if isinstance(declaration, (SkillEffect, CharacterEffect)):
|
||||
self.collect_effect(skill, declaration)
|
||||
self.collect_effect(skill, declaration)
|
||||
return tuple(self._pending), self._reads.copy()
|
||||
finally:
|
||||
self.collecting = None
|
||||
@@ -405,6 +399,7 @@ class SkillEffectEngine:
|
||||
|
||||
@staticmethod
|
||||
def _combine(base, contributions):
|
||||
"""按覆盖、加算、乘算的顺序汇总贡献;不同来源给出不同覆盖值时抛出冲突。"""
|
||||
contributions = sorted(contributions, key=lambda item: (item.source.id, item.index))
|
||||
overrides = {item.value for item in contributions if item.operation == '='}
|
||||
if len(overrides) > 1:
|
||||
@@ -417,6 +412,7 @@ class SkillEffectEngine:
|
||||
)
|
||||
|
||||
def _restore_outputs(self):
|
||||
"""撤销上次技能输出贡献并恢复基础值;原先不存在的属性一并删除。"""
|
||||
for (target, attr), value in self._output_base.items():
|
||||
if value is _MISSING:
|
||||
object.__delattr__(target, attr)
|
||||
@@ -427,6 +423,9 @@ class SkillEffectEngine:
|
||||
def resolve_states(self):
|
||||
# 依赖来源:level_source、跨技能 lv 读取、指向 STATE_ATTRIBUTES 的关联。
|
||||
# 类型化 Read 可声明跨技能状态参数依赖;普通 Python 属性读取仍不会自动建边。
|
||||
"""恢复输入基线、建立依赖图,并按拓扑顺序解析最终等级和状态参数。
|
||||
|
||||
共享等级源缺失、循环依赖或求值时新增依赖与状态目标均报错;本阶段不应用输出贡献。"""
|
||||
if self.resolving or not self._state_dirty:
|
||||
return
|
||||
self.resolving = True
|
||||
@@ -458,7 +457,7 @@ class SkillEffectEngine:
|
||||
|
||||
states = defaultdict(list)
|
||||
for skill in order:
|
||||
# All upstream level/parameter contributions are ready now.
|
||||
# 拓扑顺序保证上游等级和参数贡献已完成计算。
|
||||
if skill.level_source is not None:
|
||||
skill._lv = resolve_skill(self.character, skill.level_source).lv
|
||||
contributions, reads = self._collect(skill)
|
||||
@@ -483,6 +482,7 @@ class SkillEffectEngine:
|
||||
self.resolving = False
|
||||
|
||||
def resolve(self):
|
||||
"""先解析最终状态,再按目标属性汇总并应用输出贡献;没有待处理变化时直接返回。"""
|
||||
if self.resolving or not self._dirty:
|
||||
return
|
||||
self.resolve_states()
|
||||
@@ -513,6 +513,9 @@ class SkillEffectEngine:
|
||||
def batch(self):
|
||||
# 最外层先撤销技能输出贡献,让装备等外部写入落在基础值上,再统一重建。
|
||||
# 这里只隔离技能贡献,不解决装备规则彼此之间的执行顺序依赖。
|
||||
"""批量接收外部修改,在最外层批次正常结束后统一重算。
|
||||
|
||||
进入最外层时先撤销技能输出贡献,使修改落在基础值上;支持嵌套,但不处理装备间的顺序依赖。"""
|
||||
outermost = not self._depth
|
||||
if outermost:
|
||||
self.resolving = True
|
||||
|
||||
Reference in New Issue
Block a user