引入属性、伤害管线

This commit is contained in:
2026-09-18 21:43:22 +08:00
parent 92e1ea0d55
commit ba25ec1926
108 changed files with 2920 additions and 720 deletions
@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 多维属性修饰器管线(Attribute Pipeline)。
/// 管理角色所有属性维度,提供三段修饰求值、脏标记缓存与按 Source 批量移除机制。
/// 纯 C# 类,无 MonoBehaviour 依赖。
/// </summary>
public sealed class AttributePipeline : IAttributeProvider
{
private readonly Dictionary<StatType, CharacterStat> _stats = new();
/// <summary>
/// 当某个属性的值发生变动(添加/移除修饰器、修改基础值)时触发。
/// 参数:(StatType 属性类型, float 新的当前值)。
/// </summary>
public event Action<StatType, float> OnStatChanged;
public AttributePipeline()
{
// 预初始化已定义的所有 StatType 枚举
Array values = Enum.GetValues(typeof(StatType));
for (int i = 0; i < values.Length; i++)
{
StatType statType = (StatType)values.GetValue(i);
_stats[statType] = new CharacterStat(statType, 0f);
}
}
/// <summary>
/// 获取指定类型的属性容器。
/// </summary>
public CharacterStat Get(StatType statType)
{
if (!_stats.TryGetValue(statType, out CharacterStat stat))
{
stat = new CharacterStat(statType, 0f);
_stats.Add(statType, stat);
}
return stat;
}
/// <summary>
/// 获取指定属性的当前最终计算值(带脏标记缓存与边界钳制)。
/// </summary>
public float GetValue(StatType statType)
{
return Get(statType).Value;
}
/// <summary>
/// 设置指定属性的基础值。
/// </summary>
public void SetBaseValue(StatType statType, float baseValue)
{
CharacterStat stat = Get(statType);
float oldValue = stat.Value;
stat.BaseValue = baseValue;
float newValue = stat.Value;
if (Math.Abs(oldValue - newValue) > 1e-6f)
{
OnStatChanged?.Invoke(statType, newValue);
}
}
/// <summary>
/// 添加属性修饰器。
/// </summary>
public void AddModifier(StatType statType, StatModifier modifier)
{
if (modifier == null)
{
throw new ArgumentNullException(nameof(modifier));
}
CharacterStat stat = Get(statType);
stat.AddModifier(modifier);
OnStatChanged?.Invoke(statType, stat.Value);
}
/// <summary>
/// 移除属性修饰器。
/// </summary>
public bool RemoveModifier(StatType statType, StatModifier modifier)
{
if (modifier == null)
{
return false;
}
CharacterStat stat = Get(statType);
bool removed = stat.RemoveModifier(modifier);
if (removed)
{
OnStatChanged?.Invoke(statType, stat.Value);
}
return removed;
}
/// <summary>
/// 添加 Luban 导出的 modifier(自动转换为 StatModifier)。
/// </summary>
public void AddModifier(StatModifierConfig lubanMod, object source = null)
{
if (lubanMod == null)
{
throw new ArgumentNullException(nameof(lubanMod));
}
StatModifier statMod = StatModifier.FromLuban(lubanMod, source);
AddModifier(lubanMod.StatType, statMod);
}
/// <summary>
/// 移除由 Luban 导出的 modifier 所生成的对应修饰器。
/// </summary>
public bool RemoveModifier(StatModifierConfig lubanMod, object source = null)
{
if (lubanMod == null)
{
return false;
}
CharacterStat stat = Get(lubanMod.StatType);
float targetValue = lubanMod.Value;
ModifierType targetType = lubanMod.ModifierType;
IReadOnlyList<StatModifier> modifiers = stat.Modifiers;
for (int i = modifiers.Count - 1; i >= 0; i--)
{
StatModifier m = modifiers[i];
if (m.Type == targetType &&
Math.Abs(m.Value - targetValue) < 1e-6f &&
(source == null || ReferenceEquals(m.Source, source) || Equals(m.Source, source)))
{
return RemoveModifier(lubanMod.StatType, m);
}
}
return false;
}
/// <summary>
/// 一键移除所有属性中指定来源(道具、武器、羁绊、Buff 等)的所有修饰器。
/// </summary>
/// <param name="source">来源对象引用</param>
/// <returns>移除的修饰器总数</returns>
public int RemoveAllModifiersFromSource(object source)
{
if (source == null)
{
return 0;
}
int totalRemoved = 0;
foreach (KeyValuePair<StatType, CharacterStat> pair in _stats)
{
int removed = pair.Value.RemoveAllModifiersFromSource(source);
if (removed > 0)
{
totalRemoved += removed;
OnStatChanged?.Invoke(pair.Key, pair.Value.Value);
}
}
return totalRemoved;
}
/// <summary>
/// 清空所有属性的修饰器并标记为脏。
/// </summary>
public void Clear()
{
foreach (KeyValuePair<StatType, CharacterStat> pair in _stats)
{
pair.Value.ClearModifiers();
OnStatChanged?.Invoke(pair.Key, pair.Value.Value);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 312b0c5905f501449b3dc700f4ac0e5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using SepCore.Definition;
using UnityEngine;
namespace SepCore.Battle
{
/// <summary>
/// 单个属性实例容器。
/// 维护基础值、修饰器列表,通过脏标记(Dirty Flag)缓存计算结果,并基于 StatConfig 进行边界钳制。
/// </summary>
public sealed class CharacterStat
{
private readonly List<StatModifier> _modifiers = new();
private float _baseValue;
private bool _isDirty = true;
private float _cachedValue;
private StatConfig _config;
public StatType Type { get; }
public StatConfig Config
{
get
{
if (_config == null && GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
_config = GameEntry.Luban.Get<StatConfig>((int)Type);
}
return _config;
}
set
{
_config = value;
_isDirty = true;
}
}
public float BaseValue
{
get => _baseValue;
set
{
if (Math.Abs(_baseValue - value) > 1e-6f)
{
_baseValue = value;
_isDirty = true;
}
}
}
public float Value
{
get
{
if (_isDirty)
{
_cachedValue = CalculateFinalValue();
_isDirty = false;
}
return _cachedValue;
}
}
public IReadOnlyList<StatModifier> Modifiers => _modifiers;
public CharacterStat(StatType type, float baseValue = 0f, StatConfig config = null)
{
Type = type;
_baseValue = baseValue;
_config = config;
_isDirty = true;
}
public void AddModifier(StatModifier modifier)
{
if (modifier == null)
{
throw new ArgumentNullException(nameof(modifier));
}
_modifiers.Add(modifier);
_isDirty = true;
}
public bool RemoveModifier(StatModifier modifier)
{
if (modifier == null)
{
return false;
}
if (_modifiers.Remove(modifier))
{
_isDirty = true;
return true;
}
return false;
}
public int RemoveAllModifiersFromSource(object source)
{
if (source == null)
{
return 0;
}
int removed = _modifiers.RemoveAll(m => ReferenceEquals(m.Source, source) || Equals(m.Source, source));
if (removed > 0)
{
_isDirty = true;
}
return removed;
}
public void ClearModifiers()
{
if (_modifiers.Count > 0)
{
_modifiers.Clear();
_isDirty = true;
}
}
public void MarkDirty()
{
_isDirty = true;
}
private float CalculateFinalValue()
{
float sumFlat = 0f;
float sumPercentAdd = 0f;
float prodPercentMult = 1f;
for (int i = 0; i < _modifiers.Count; i++)
{
StatModifier mod = _modifiers[i];
switch (mod.Type)
{
case ModifierType.Flat:
sumFlat += mod.Value;
break;
case ModifierType.PercentAdd:
sumPercentAdd += mod.Value;
break;
case ModifierType.PercentMult:
prodPercentMult *= mod.Value;
break;
}
}
float finalValue = ((_baseValue + sumFlat) * (1f + sumPercentAdd)) * prodPercentMult;
return ClampStat(finalValue);
}
private float ClampStat(float value)
{
StatConfig config = Config;
if (config != null)
{
if (config.HasMin && value < config.MinValue)
{
value = config.MinValue;
}
if (config.HasMax && value > config.MaxValue)
{
value = config.MaxValue;
}
}
return value;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67040be8b42ae744a81f961aa4ec5333
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 伤害结算输入上下文。
/// 封装攻击方与受击方的属性管线、武器基础伤害、加成属性、暴击等参数。
/// </summary>
public struct DamageContext
{
/// <summary>
/// 基础伤害(武器攻击力或敌人基础伤害)。
/// </summary>
public int BaseDamage;
/// <summary>
/// 伤害加成属性(近战伤害/远程伤害/元素伤害/None)。
/// </summary>
public StatType DamageScalingStat;
/// <summary>
/// 伤害加成系数,默认 1.0f。
/// </summary>
public float ScalingRatio;
/// <summary>
/// 基础暴击率(例如来自武器或全局配置)。
/// </summary>
public float BaseCritChance;
/// <summary>
/// 暴击伤害倍率(例如 1.5f 或 2.0f)。
/// </summary>
public float CritMultiplier;
/// <summary>
/// 攻击方属性修饰管线(可为 null,如普通怪物未挂载管线)。
/// </summary>
public IAttributeProvider AttackerPipeline;
/// <summary>
/// 防御方属性修饰管线(可为 null,如普通怪物未挂载管线)。
/// </summary>
public IAttributeProvider DefenderPipeline;
/// <summary>
/// 防御方备用护甲(当 DefenderPipeline 为 null 时生效)。
/// </summary>
public float FallbackDefenderArmor;
/// <summary>
/// 防御方备用闪避率(当 DefenderPipeline 为 null 时生效)。
/// </summary>
public float FallbackDefenderDodge;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8192a0b127344919ba1812836248cc01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,134 @@
using SepCore.Definition;
using UnityEngine;
namespace SepCore.Battle
{
/// <summary>
/// 伤害结算管线(纯 C# 静态计算器)。
/// 实现武器类型加成、百分比放大、闪避判定、暴击判定、双曲护甲折减、保底伤害与生命偷取判定。
/// </summary>
public static class DamagePipeline
{
public const float ArmorDivisor = 15f; // Brotato 双曲护甲基数
public const float DefaultCritMultiplier = 1.5f;
public const float DefaultBaseCrit = 0.05f;
/// <summary>
/// 计算最终伤害结果。
/// </summary>
public static DamageResult Calculate(in DamageContext context)
{
// 1. 闪避判定 (Defender Dodge)
float dodgeChance = context.DefenderPipeline != null
? context.DefenderPipeline.GetValue(StatType.Dodge)
: context.FallbackDefenderDodge;
float maxDodge = 0.8f;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
}
dodgeChance = Mathf.Clamp(dodgeChance, 0f, maxDodge);
if (dodgeChance > 0f && Random.value < dodgeChance)
{
return new DamageResult
{
Damage = 0,
IsDodge = true,
IsCrit = false,
IsLifeSteal = false,
LifeStealAmount = 0
};
}
// 2. 武器类型伤害加成 (Melee / Ranged / Elemental)
float rawDamage = context.BaseDamage;
if (context.AttackerPipeline != null && context.DamageScalingStat != StatType.None)
{
float scalingBonus = context.AttackerPipeline.GetValue(context.DamageScalingStat);
float ratio = context.ScalingRatio > 0f ? context.ScalingRatio : 1f;
rawDamage += scalingBonus * ratio;
}
rawDamage = Mathf.Max(rawDamage, 1f);
// 3. 全局百分比增伤放大 (DamagePercent)
float damagePercent = context.AttackerPipeline != null
? context.AttackerPipeline.GetValue(StatType.DamagePercent)
: 0f;
float amplifiedDamage = rawDamage * (1f + damagePercent);
amplifiedDamage = Mathf.Max(amplifiedDamage, 1f);
// 4. 暴击判定 (CritChance & CritMultiplier)
float attackerCrit = context.AttackerPipeline != null
? context.AttackerPipeline.GetValue(StatType.CritChance)
: 0f;
float baseCrit = context.BaseCritChance;
if (baseCrit <= 0f && GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
baseCrit = GameEntry.Luban.Global.WeaponBaseCrit;
}
float totalCritChance = Mathf.Clamp(baseCrit + attackerCrit, 0f, 0.9f);
bool isCrit = totalCritChance > 0f && Random.value < totalCritChance;
float critMultiplier = context.CritMultiplier;
if (critMultiplier <= 0f)
{
critMultiplier = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.CritMultiplier
: DefaultCritMultiplier;
}
float damageAfterCrit = isCrit ? (amplifiedDamage * critMultiplier) : amplifiedDamage;
// 5. Brotato 标准双曲护甲折减 (Armor Mitigation)
float armor = context.DefenderPipeline != null
? context.DefenderPipeline.GetValue(StatType.Armor)
: context.FallbackDefenderArmor;
float mitigation;
if (armor >= 0f)
{
mitigation = 1f / (1f + armor / ArmorDivisor);
}
else
{
// 负护甲易伤放大: 2 - 1/(1 - armor/15)
mitigation = 2f - (1f / (1f - armor / ArmorDivisor));
}
float finalDamageFloat = damageAfterCrit * mitigation;
int minDamage = 1;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
minDamage = GameEntry.Luban.Global.MinDamage;
}
int finalDamage = Mathf.Max(Mathf.CeilToInt(finalDamageFloat), minDamage);
// 6. 生命偷取判定 (LifeSteal)
bool isLifeSteal = false;
int lifeStealAmount = 0;
if (finalDamage > 0 && context.AttackerPipeline != null)
{
float lifeStealRate = context.AttackerPipeline.GetValue(StatType.LifeSteal);
if (lifeStealRate > 0f && Random.value < lifeStealRate)
{
isLifeSteal = true;
lifeStealAmount = 1;
}
}
return new DamageResult
{
Damage = finalDamage,
IsDodge = false,
IsCrit = isCrit,
IsLifeSteal = isLifeSteal,
LifeStealAmount = lifeStealAmount
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a394c2d349564121dc3834058460ee23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
namespace SepCore.Battle
{
/// <summary>
/// 伤害计算输出结果。
/// </summary>
public struct DamageResult
{
/// <summary>
/// 最终扣除的生命值(若闪避则为 0)。
/// </summary>
public int Damage;
/// <summary>
/// 是否闪避。
/// </summary>
public bool IsDodge;
/// <summary>
/// 是否暴击。
/// </summary>
public bool IsCrit;
/// <summary>
/// 是否触发生命偷取。
/// </summary>
public bool IsLifeSteal;
/// <summary>
/// 偷取回复的生命值(通常为 1)。
/// </summary>
public int LifeStealAmount;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9283b1c238454920cb2923947359dd12
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: