Phase 1: 复合属性修饰器管线与伤害结算系统

This commit is contained in:
2026-09-24 22:37:22 +08:00
parent ba25ec1926
commit 02f4c67464
35 changed files with 334 additions and 141 deletions
Binary file not shown.
@@ -3,19 +3,19 @@
"weaponCapacity": 6, "weaponCapacity": 6,
"propMaxCount": -1, "propMaxCount": -1,
"playerEntity": "Player", "playerEntity": "Player",
"absorbSpeed": 15000, "absorbSpeed": 12,
"collectDistance": 350, "collectDistance": 0.5,
"defaultAbsorbRange": 3000, "defaultAbsorbRange": 4,
"weaponRecycleRate": 300, "weaponRecycleRate": 0.3,
"shopGoodsCount": 4, "shopGoodsCount": 4,
"shopPriceRandomPercent": 50, "shopPriceRandomPercent": 0.05,
"shopMinPrice": 1, "shopMinPrice": 1,
"levelUpRefreshPriceBase": 2, "levelUpRefreshPriceBase": 2,
"levelUpRewardCount": 4, "levelUpRewardCount": 4,
"spawnEnemyMaxCount": 5000, "spawnEnemyMaxCount": 5000,
"enemySpawnDistanceMin": 14000, "enemySpawnDistanceMin": 14,
"enemySpawnDistanceMax": 18000, "enemySpawnDistanceMax": 18,
"weaponRotateSpeed": 5000, "weaponRotateSpeed": 5,
"weaponBaseCrit": 0.05, "weaponBaseCrit": 0.05,
"playerGroupName": "Player", "playerGroupName": "Player",
"enemyGroupName": "Enemy", "enemyGroupName": "Enemy",
Binary file not shown.
+21 -21
View File
@@ -6,7 +6,7 @@
"price": 120, "price": 120,
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 4, "statType": 5,
"value": 10, "value": 10,
"modifierType": 100 "modifierType": 100
}, },
@@ -35,7 +35,7 @@
"modifierType": 200 "modifierType": 200
}, },
{ {
"statType": 4, "statType": 5,
"value": 11, "value": 11,
"modifierType": 100 "modifierType": 100
} }
@@ -88,11 +88,11 @@
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 14, "statType": 14,
"value": 800, "value": 0.8,
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 4, "statType": 5,
"value": 10, "value": 10,
"modifierType": 100 "modifierType": 100
} }
@@ -107,11 +107,11 @@
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 13, "statType": 13,
"value": 10, "value": 0.1,
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 4, "statType": 5,
"value": 12, "value": 12,
"modifierType": 100 "modifierType": 100
}, },
@@ -144,13 +144,13 @@
"price": 433, "price": 433,
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 4, "statType": 6,
"value": 44, "value": 44,
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 13, "statType": 13,
"value": 27, "value": 0.27,
"modifierType": 100 "modifierType": 100
} }
], ],
@@ -168,7 +168,7 @@
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 4, "statType": 6,
"value": 7, "value": 7,
"modifierType": 100 "modifierType": 100
}, },
@@ -201,7 +201,7 @@
"price": 341, "price": 341,
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 4, "statType": 6,
"value": 57, "value": 57,
"modifierType": 100 "modifierType": 100
} }
@@ -221,7 +221,7 @@
}, },
{ {
"statType": 9, "statType": 9,
"value": 27, "value": 0.27,
"modifierType": 100 "modifierType": 100
} }
], ],
@@ -235,12 +235,12 @@
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 9, "statType": 9,
"value": 5, "value": 0.05,
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 13, "statType": 13,
"value": 7, "value": 0.07,
"modifierType": 100 "modifierType": 100
} }
], ],
@@ -254,7 +254,7 @@
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 14, "statType": 14,
"value": 600, "value": 0.6,
"modifierType": 100 "modifierType": 100
}, },
{ {
@@ -273,7 +273,7 @@
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 14, "statType": 14,
"value": 1600, "value": 1.6,
"modifierType": 100 "modifierType": 100
}, },
{ {
@@ -292,7 +292,7 @@
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 13, "statType": 13,
"value": 28, "value": 0.28,
"modifierType": 100 "modifierType": 100
}, },
{ {
@@ -301,7 +301,7 @@
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 4, "statType": 5,
"value": 24, "value": 24,
"modifierType": 100 "modifierType": 100
} }
@@ -329,7 +329,7 @@
"price": 204, "price": 204,
"statModifierConfigs": [ "statModifierConfigs": [
{ {
"statType": 4, "statType": 7,
"value": 17, "value": 17,
"modifierType": 100 "modifierType": 100
}, },
@@ -353,13 +353,13 @@
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 4, "statType": 7,
"value": 19, "value": 19,
"modifierType": 100 "modifierType": 100
}, },
{ {
"statType": 13, "statType": 13,
"value": 20, "value": 0.2,
"modifierType": 100 "modifierType": 100
} }
], ],
@@ -377,7 +377,7 @@
"modifierType": 200 "modifierType": 200
}, },
{ {
"statType": 4, "statType": 7,
"value": 29, "value": 29,
"modifierType": 100 "modifierType": 100
}, },
Binary file not shown.
+2 -2
View File
@@ -32,7 +32,7 @@
{ {
"statType": 4, "statType": 4,
"name": "伤害", "name": "伤害",
"defaultBaseValue": 0, "defaultBaseValue": 1,
"hasMin": true, "hasMin": true,
"minValue": -1, "minValue": -1,
"hasMax": false, "hasMax": false,
@@ -72,7 +72,7 @@
{ {
"statType": 8, "statType": 8,
"name": "攻击速度", "name": "攻击速度",
"defaultBaseValue": 0, "defaultBaseValue": 1,
"hasMin": true, "hasMin": true,
"minValue": -0.8, "minValue": -0.8,
"hasMax": false, "hasMax": false,
Binary file not shown.
+15 -15
View File
@@ -5,14 +5,14 @@
"rarity": 1, "rarity": 1,
"price": 120, "price": 120,
"attack": 100, "attack": 100,
"cooldown": 1500, "cooldown": 1.5,
"damageType": 1, "damageType": 1,
"attackRange": 5000, "attackRange": 5,
"attackSoundId": 10000, "attackSoundId": 10000,
"parameters": [ "parameters": [
[ [
1, 1,
2000 2
] ]
], ],
"statModifierConfigs": [], "statModifierConfigs": [],
@@ -25,9 +25,9 @@
"rarity": 1, "rarity": 1,
"price": 130, "price": 130,
"attack": 120, "attack": 120,
"cooldown": 1000, "cooldown": 1,
"damageType": 2, "damageType": 2,
"attackRange": 15000, "attackRange": 15,
"attackSoundId": 10000, "attackSoundId": 10000,
"parameters": [], "parameters": [],
"statModifierConfigs": [], "statModifierConfigs": [],
@@ -40,9 +40,9 @@
"rarity": 1, "rarity": 1,
"price": 100, "price": 100,
"attack": 150, "attack": 150,
"cooldown": 2000, "cooldown": 2,
"damageType": 1, "damageType": 1,
"attackRange": 5000, "attackRange": 5,
"attackSoundId": 10000, "attackSoundId": 10000,
"parameters": [ "parameters": [
[ [
@@ -60,14 +60,14 @@
"rarity": 1, "rarity": 1,
"price": 150, "price": 150,
"attack": 80, "attack": 80,
"cooldown": 3000, "cooldown": 3,
"damageType": 4, "damageType": 4,
"attackRange": 12000, "attackRange": 12,
"attackSoundId": 10000, "attackSoundId": 10000,
"parameters": [ "parameters": [
[ [
1, 1,
3000 3
] ]
], ],
"statModifierConfigs": [], "statModifierConfigs": [],
@@ -80,22 +80,22 @@
"rarity": 1, "rarity": 1,
"price": 100, "price": 100,
"attack": 100, "attack": 100,
"cooldown": 1500, "cooldown": 1.5,
"damageType": 1, "damageType": 1,
"attackRange": 5000, "attackRange": 5,
"attackSoundId": 10000, "attackSoundId": 10000,
"parameters": [ "parameters": [
[ [
3, 3,
700 0.7
], ],
[ [
4, 4,
4500 4.5
], ],
[ [
5, 5,
500 0.5
] ]
], ],
"statModifierConfigs": [], "statModifierConfigs": [],
@@ -32,32 +32,21 @@ namespace SepCore.Entity
public int Price => Config.Price; public int Price => Config.Price;
public float Cooldown => Config.Cooldown / 1000f; public float Cooldown => Config.Cooldown;
public float AttackRange => Config.AttackRange / 1000f; public float AttackRange => Config.AttackRange;
public int AttackSoundId => Config.AttackSoundId; public int AttackSoundId => Config.AttackSoundId;
public IReadOnlyDictionary<ParamType, int> Parameters => Config.Parameters; public IReadOnlyDictionary<ParamType, float> Parameters => Config.Parameters;
public IReadOnlyList<StatModifierConfig> Modifiers => Config.StatModifierConfigs; public IReadOnlyList<StatModifierConfig> Modifiers => Config.StatModifierConfigs;
public int GetParam(ParamType paramType)
{
if (Config.Parameters.TryGetValue(paramType, out int value))
{
return value;
}
Log.Error("Weapon parameter '{0}' not found for weapon '{1}'.", paramType, Config.WeaponType);
return 0;
}
public float GetParamFloat(ParamType paramType) public float GetParamFloat(ParamType paramType)
{ {
if (Config.Parameters.TryGetValue(paramType, out int val)) if (Config.Parameters.TryGetValue(paramType, out float val))
{ {
return paramType == ParamType.SectorAngle ? val : val / 1000f; return val;
} }
Log.Error("Weapon parameter '{0}' not found for weapon '{1}'.", paramType, Config.WeaponType); Log.Error("Weapon parameter '{0}' not found for weapon '{1}'.", paramType, Config.WeaponType);
@@ -65,4 +54,3 @@ namespace SepCore.Entity
} }
} }
} }
@@ -20,19 +20,19 @@ public sealed partial class GlobalConfig : Luban.BeanBase
PropMaxCount = _buf.ReadInt(); PropMaxCount = _buf.ReadInt();
PlayerEntity = _buf.ReadString(); PlayerEntity = _buf.ReadString();
PlayerEntity_Ref = null; PlayerEntity_Ref = null;
AbsorbSpeed = _buf.ReadInt(); AbsorbSpeed = _buf.ReadFloat();
CollectDistance = _buf.ReadInt(); CollectDistance = _buf.ReadFloat();
DefaultAbsorbRange = _buf.ReadInt(); DefaultAbsorbRange = _buf.ReadFloat();
WeaponRecycleRate = _buf.ReadInt(); WeaponRecycleRate = _buf.ReadFloat();
ShopGoodsCount = _buf.ReadInt(); ShopGoodsCount = _buf.ReadInt();
ShopPriceRandomPercent = _buf.ReadInt(); ShopPriceRandomPercent = _buf.ReadFloat();
ShopMinPrice = _buf.ReadInt(); ShopMinPrice = _buf.ReadInt();
LevelUpRefreshPriceBase = _buf.ReadInt(); LevelUpRefreshPriceBase = _buf.ReadInt();
LevelUpRewardCount = _buf.ReadInt(); LevelUpRewardCount = _buf.ReadInt();
SpawnEnemyMaxCount = _buf.ReadInt(); SpawnEnemyMaxCount = _buf.ReadInt();
EnemySpawnDistanceMin = _buf.ReadInt(); EnemySpawnDistanceMin = _buf.ReadFloat();
EnemySpawnDistanceMax = _buf.ReadInt(); EnemySpawnDistanceMax = _buf.ReadFloat();
WeaponRotateSpeed = _buf.ReadInt(); WeaponRotateSpeed = _buf.ReadFloat();
WeaponBaseCrit = _buf.ReadFloat(); WeaponBaseCrit = _buf.ReadFloat();
PlayerGroupName = _buf.ReadString(); PlayerGroupName = _buf.ReadString();
EnemyGroupName = _buf.ReadString(); EnemyGroupName = _buf.ReadString();
@@ -75,19 +75,19 @@ public sealed partial class GlobalConfig : Luban.BeanBase
/// <summary> /// <summary>
/// 自动收集速度 /// 自动收集速度
/// </summary> /// </summary>
public readonly int AbsorbSpeed; public readonly float AbsorbSpeed;
/// <summary> /// <summary>
/// 自动收集距离 /// 自动收集距离
/// </summary> /// </summary>
public readonly int CollectDistance; public readonly float CollectDistance;
/// <summary> /// <summary>
/// 默认吸收距离 /// 默认吸收距离
/// </summary> /// </summary>
public readonly int DefaultAbsorbRange; public readonly float DefaultAbsorbRange;
/// <summary> /// <summary>
/// 武器回收价比例 /// 武器回收价比例
/// </summary> /// </summary>
public readonly int WeaponRecycleRate; public readonly float WeaponRecycleRate;
/// <summary> /// <summary>
/// 商店商品数量 /// 商店商品数量
/// </summary> /// </summary>
@@ -95,7 +95,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
/// <summary> /// <summary>
/// 商品价格浮动 /// 商品价格浮动
/// </summary> /// </summary>
public readonly int ShopPriceRandomPercent; public readonly float ShopPriceRandomPercent;
/// <summary> /// <summary>
/// 商品最低价 /// 商品最低价
/// </summary> /// </summary>
@@ -115,15 +115,15 @@ public sealed partial class GlobalConfig : Luban.BeanBase
/// <summary> /// <summary>
/// 敌人生成最小距离 /// 敌人生成最小距离
/// </summary> /// </summary>
public readonly int EnemySpawnDistanceMin; public readonly float EnemySpawnDistanceMin;
/// <summary> /// <summary>
/// 敌人生成最大距离 /// 敌人生成最大距离
/// </summary> /// </summary>
public readonly int EnemySpawnDistanceMax; public readonly float EnemySpawnDistanceMax;
/// <summary> /// <summary>
/// 武器旋转速度 /// 武器旋转速度
/// </summary> /// </summary>
public readonly int WeaponRotateSpeed; public readonly float WeaponRotateSpeed;
/// <summary> /// <summary>
/// 武器基本暴击率 /// 武器基本暴击率
/// </summary> /// </summary>
@@ -45,19 +45,19 @@ public partial class TbGlobalConfig
/// <summary> /// <summary>
/// 自动收集速度 /// 自动收集速度
/// </summary> /// </summary>
public int AbsorbSpeed => _data.AbsorbSpeed; public float AbsorbSpeed => _data.AbsorbSpeed;
/// <summary> /// <summary>
/// 自动收集距离 /// 自动收集距离
/// </summary> /// </summary>
public int CollectDistance => _data.CollectDistance; public float CollectDistance => _data.CollectDistance;
/// <summary> /// <summary>
/// 默认吸收距离 /// 默认吸收距离
/// </summary> /// </summary>
public int DefaultAbsorbRange => _data.DefaultAbsorbRange; public float DefaultAbsorbRange => _data.DefaultAbsorbRange;
/// <summary> /// <summary>
/// 武器回收价比例 /// 武器回收价比例
/// </summary> /// </summary>
public int WeaponRecycleRate => _data.WeaponRecycleRate; public float WeaponRecycleRate => _data.WeaponRecycleRate;
/// <summary> /// <summary>
/// 商店商品数量 /// 商店商品数量
/// </summary> /// </summary>
@@ -65,7 +65,7 @@ public partial class TbGlobalConfig
/// <summary> /// <summary>
/// 商品价格浮动 /// 商品价格浮动
/// </summary> /// </summary>
public int ShopPriceRandomPercent => _data.ShopPriceRandomPercent; public float ShopPriceRandomPercent => _data.ShopPriceRandomPercent;
/// <summary> /// <summary>
/// 商品最低价 /// 商品最低价
/// </summary> /// </summary>
@@ -85,15 +85,15 @@ public partial class TbGlobalConfig
/// <summary> /// <summary>
/// 敌人生成最小距离 /// 敌人生成最小距离
/// </summary> /// </summary>
public int EnemySpawnDistanceMin => _data.EnemySpawnDistanceMin; public float EnemySpawnDistanceMin => _data.EnemySpawnDistanceMin;
/// <summary> /// <summary>
/// 敌人生成最大距离 /// 敌人生成最大距离
/// </summary> /// </summary>
public int EnemySpawnDistanceMax => _data.EnemySpawnDistanceMax; public float EnemySpawnDistanceMax => _data.EnemySpawnDistanceMax;
/// <summary> /// <summary>
/// 武器旋转速度 /// 武器旋转速度
/// </summary> /// </summary>
public int WeaponRotateSpeed => _data.WeaponRotateSpeed; public float WeaponRotateSpeed => _data.WeaponRotateSpeed;
/// <summary> /// <summary>
/// 武器基本暴击率 /// 武器基本暴击率
/// </summary> /// </summary>
@@ -21,11 +21,11 @@ public sealed partial class WeaponConfig : Luban.BeanBase
Rarity = (Rarity)_buf.ReadInt(); Rarity = (Rarity)_buf.ReadInt();
Price = _buf.ReadInt(); Price = _buf.ReadInt();
Attack = _buf.ReadInt(); Attack = _buf.ReadInt();
Cooldown = _buf.ReadInt(); Cooldown = _buf.ReadFloat();
DamageType = (DamageType)_buf.ReadInt(); DamageType = (DamageType)_buf.ReadInt();
AttackRange = _buf.ReadInt(); AttackRange = _buf.ReadFloat();
AttackSoundId = _buf.ReadInt(); AttackSoundId = _buf.ReadInt();
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);Parameters = new System.Collections.Generic.Dictionary<ParamType, int>(n0 * 3 / 2);for(var i0 = 0 ; i0 < n0 ; i0++) { ParamType _k0; _k0 = (ParamType)_buf.ReadInt(); int _v0; _v0 = _buf.ReadInt(); Parameters.Add(_k0, _v0);}} {int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);Parameters = new System.Collections.Generic.Dictionary<ParamType, float>(n0 * 3 / 2);for(var i0 = 0 ; i0 < n0 ; i0++) { ParamType _k0; _k0 = (ParamType)_buf.ReadInt(); float _v0; _v0 = _buf.ReadFloat(); Parameters.Add(_k0, _v0);}}
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);StatModifierConfigs = new System.Collections.Generic.List<StatModifierConfig>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { StatModifierConfig _e0; _e0 = StatModifierConfig.DeserializeStatModifierConfig(_buf); StatModifierConfigs.Add(_e0);}} {int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);StatModifierConfigs = new System.Collections.Generic.List<StatModifierConfig>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { StatModifierConfig _e0; _e0 = StatModifierConfig.DeserializeStatModifierConfig(_buf); StatModifierConfigs.Add(_e0);}}
Prefab = _buf.ReadString(); Prefab = _buf.ReadString();
Prefab_Ref = null; Prefab_Ref = null;
@@ -61,7 +61,7 @@ public sealed partial class WeaponConfig : Luban.BeanBase
/// <summary> /// <summary>
/// 冷却 /// 冷却
/// </summary> /// </summary>
public readonly int Cooldown; public readonly float Cooldown;
/// <summary> /// <summary>
/// 伤害类型 /// 伤害类型
/// </summary> /// </summary>
@@ -69,7 +69,7 @@ public sealed partial class WeaponConfig : Luban.BeanBase
/// <summary> /// <summary>
/// 攻击范围 /// 攻击范围
/// </summary> /// </summary>
public readonly int AttackRange; public readonly float AttackRange;
/// <summary> /// <summary>
/// 攻击音效编号 /// 攻击音效编号
/// </summary> /// </summary>
@@ -77,7 +77,7 @@ public sealed partial class WeaponConfig : Luban.BeanBase
/// <summary> /// <summary>
/// 参数类型 /// 参数类型
/// </summary> /// </summary>
public readonly System.Collections.Generic.Dictionary<ParamType, int> Parameters; public readonly System.Collections.Generic.Dictionary<ParamType, float> Parameters;
public readonly System.Collections.Generic.List<StatModifierConfig> StatModifierConfigs; public readonly System.Collections.Generic.List<StatModifierConfig> StatModifierConfigs;
/// <summary> /// <summary>
/// 预制体 /// 预制体
@@ -51,13 +51,39 @@ namespace SepCore.CustomUtility
string colorTag = statModifierConfig.Value >= 0 ? "green" : "red"; string colorTag = statModifierConfig.Value >= 0 ? "green" : "red";
string sign = statModifierConfig.Value >= 0 ? "+" : ""; string sign = statModifierConfig.Value >= 0 ? "+" : "";
if (statModifierConfig.ModifierType == ModifierType.PercentAdd) StatConfig statConfig = null;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
statConfig = GameEntry.Luban.Get<StatConfig>(index);
}
if (statConfig != null && !string.IsNullOrEmpty(statConfig.Name))
{
name = statConfig.Name;
}
// 是否以百分比展示:由 StatConfig.IsPercentDisplay 真正驱动(Luban 未就绪时 fallback 到已知百分比属性)
bool isPercentDisplay = statConfig != null
? statConfig.IsPercentDisplay
: (statModifierConfig.StatType is StatType.DamagePercent or StatType.AttackSpeed or StatType.CritChance or StatType.Dodge);
// 1. 修饰类型判断
bool isPercentAdd = statModifierConfig.ModifierType == ModifierType.PercentAdd;
bool isPercentMult = statModifierConfig.ModifierType == ModifierType.PercentMult;
bool isFlat = statModifierConfig.ModifierType == ModifierType.Flat;
// 2. UI 百分比展示裁决:
// - 任意属性的 PercentAdd 词条属于比例加成(如 攻速 +19%、伤害 +5%、移速 +15%)
// - 或属性自身在配置表中标记为百分比显示(IsPercentDisplay 为 true,如暴击率、闪避率等),其 Flat 加成应以百分比呈现(如 0.1 -> +10%)
bool shouldDisplayAsPercent = isPercentAdd || (isFlat && isPercentDisplay);
if (shouldDisplayAsPercent)
{ {
float displayPercent = statModifierConfig.Value * 100f; float displayPercent = statModifierConfig.Value * 100f;
return $"{name}: <color={colorTag}>{sign}{displayPercent:0.##}%</color>"; return $"{name}: <color={colorTag}>{sign}{displayPercent:0.##}%</color>";
} }
if (statModifierConfig.ModifierType == ModifierType.PercentMult) if (isPercentMult)
{ {
return $"{name}: <color={colorTag}>x{statModifierConfig.Value:0.##}</color>"; return $"{name}: <color={colorTag}>x{statModifierConfig.Value:0.##}</color>";
} }
@@ -89,8 +115,8 @@ namespace SepCore.CustomUtility
return null; return null;
} }
return CreateWeaponDescription(weaponConfig.Attack, weaponConfig.Cooldown / 1000f, return CreateWeaponDescription(weaponConfig.Attack, weaponConfig.Cooldown,
weaponConfig.AttackRange / 1000f, weaponConfig.StatModifierConfigs, weaponConfig.Parameters); weaponConfig.AttackRange, weaponConfig.StatModifierConfigs, weaponConfig.Parameters);
} }
public static string CreateWeaponDescription(WeaponBase weapon) public static string CreateWeaponDescription(WeaponBase weapon)
@@ -125,7 +151,7 @@ namespace SepCore.CustomUtility
} }
private static string CreateWeaponDescription(int attack, float cooldown, float attackRange, private static string CreateWeaponDescription(int attack, float cooldown, float attackRange,
IEnumerable<StatModifierConfig> modifiers, IReadOnlyDictionary<ParamType, int> parameters) IEnumerable<StatModifierConfig> modifiers, IReadOnlyDictionary<ParamType, float> parameters)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.AppendLine($"攻击: {attack}"); sb.AppendLine($"攻击: {attack}");
@@ -152,7 +178,7 @@ namespace SepCore.CustomUtility
string valStr = kvp.Key == ParamType.SectorAngle string valStr = kvp.Key == ParamType.SectorAngle
? kvp.Value.ToString() ? kvp.Value.ToString()
: (kvp.Value / 1000f).ToString("0.##"); : kvp.Value.ToString("0.##");
sb.AppendLine($"{paramName}: {valStr}"); sb.AppendLine($"{paramName}: {valStr}");
} }
@@ -13,6 +13,7 @@ namespace SepCore.Battle
{ {
private readonly List<StatModifier> _modifiers = new(); private readonly List<StatModifier> _modifiers = new();
private float _baseValue; private float _baseValue;
private bool _isBaseValueExplicitlySet;
private bool _isDirty = true; private bool _isDirty = true;
private float _cachedValue; private float _cachedValue;
private StatConfig _config; private StatConfig _config;
@@ -26,6 +27,11 @@ namespace SepCore.Battle
if (_config == null && GameEntry.Luban != null && GameEntry.Luban.IsReady) if (_config == null && GameEntry.Luban != null && GameEntry.Luban.IsReady)
{ {
_config = GameEntry.Luban.Get<StatConfig>((int)Type); _config = GameEntry.Luban.Get<StatConfig>((int)Type);
if (!_isBaseValueExplicitlySet && _config != null)
{
_baseValue = _config.DefaultBaseValue;
_isDirty = true;
}
} }
return _config; return _config;
@@ -33,15 +39,35 @@ namespace SepCore.Battle
set set
{ {
_config = value; _config = value;
if (!_isBaseValueExplicitlySet && _config != null)
{
_baseValue = _config.DefaultBaseValue;
}
_isDirty = true; _isDirty = true;
} }
} }
public float BaseValue public float BaseValue
{ {
get => _baseValue; get
{
if (!_isBaseValueExplicitlySet)
{
if (Config != null)
{
_baseValue = Config.DefaultBaseValue;
}
else if (Type is StatType.DamagePercent or StatType.AttackSpeed)
{
_baseValue = 1.0f;
}
}
return _baseValue;
}
set set
{ {
_isBaseValueExplicitlySet = true;
if (Math.Abs(_baseValue - value) > 1e-6f) if (Math.Abs(_baseValue - value) > 1e-6f)
{ {
_baseValue = value; _baseValue = value;
@@ -69,8 +95,21 @@ namespace SepCore.Battle
public CharacterStat(StatType type, float baseValue = 0f, StatConfig config = null) public CharacterStat(StatType type, float baseValue = 0f, StatConfig config = null)
{ {
Type = type; Type = type;
_baseValue = baseValue;
_config = config; _config = config;
if (Math.Abs(baseValue) > 1e-6f)
{
_baseValue = baseValue;
_isBaseValueExplicitlySet = true;
}
else if (_config != null)
{
_baseValue = _config.DefaultBaseValue;
_isBaseValueExplicitlySet = true;
}
else if (type is StatType.DamagePercent or StatType.AttackSpeed)
{
_baseValue = 1.0f;
}
_isDirty = true; _isDirty = true;
} }
@@ -154,7 +193,7 @@ namespace SepCore.Battle
} }
} }
float finalValue = ((_baseValue + sumFlat) * (1f + sumPercentAdd)) * prodPercentMult; float finalValue = ((BaseValue + sumFlat) * (1f + sumPercentAdd)) * prodPercentMult;
return ClampStat(finalValue); return ClampStat(finalValue);
} }
@@ -25,9 +25,17 @@ namespace SepCore.Battle
float maxDodge = 0.8f; float maxDodge = 0.8f;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady) if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
StatConfig dodgeConfig = GameEntry.Luban.Get<StatConfig>((int)StatType.Dodge);
if (dodgeConfig != null && dodgeConfig.HasMax)
{
maxDodge = dodgeConfig.MaxValue;
}
else
{ {
maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f; maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
} }
}
dodgeChance = Mathf.Clamp(dodgeChance, 0f, maxDodge); dodgeChance = Mathf.Clamp(dodgeChance, 0f, maxDodge);
if (dodgeChance > 0f && Random.value < dodgeChance) if (dodgeChance > 0f && Random.value < dodgeChance)
@@ -55,9 +63,11 @@ namespace SepCore.Battle
// 3. 全局百分比增伤放大 (DamagePercent) // 3. 全局百分比增伤放大 (DamagePercent)
float damagePercent = context.AttackerPipeline != null float damagePercent = context.AttackerPipeline != null
? context.AttackerPipeline.GetValue(StatType.DamagePercent) ? context.AttackerPipeline.GetValue(StatType.DamagePercent)
: 0f; : 1f;
float amplifiedDamage = rawDamage * (1f + damagePercent); // 允许百分比降至 0,防止负向修饰(如 -100% 增伤/重度虚弱)被错误重置为 1 倍全额伤害
amplifiedDamage = Mathf.Max(amplifiedDamage, 1f); damagePercent = Mathf.Max(0f, damagePercent);
float amplifiedDamage = rawDamage * damagePercent;
amplifiedDamage = Mathf.Max(amplifiedDamage, 0f);
// 4. 暴击判定 (CritChance & CritMultiplier) // 4. 暴击判定 (CritChance & CritMultiplier)
float attackerCrit = context.AttackerPipeline != null float attackerCrit = context.AttackerPipeline != null
@@ -65,12 +75,16 @@ namespace SepCore.Battle
: 0f; : 0f;
float baseCrit = context.BaseCritChance; float baseCrit = context.BaseCritChance;
if (baseCrit <= 0f && GameEntry.Luban != null && GameEntry.Luban.IsReady) float maxCrit = 0.9f;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{ {
baseCrit = GameEntry.Luban.Global.WeaponBaseCrit; StatConfig critConfig = GameEntry.Luban.Get<StatConfig>((int)StatType.CritChance);
if (critConfig != null && critConfig.HasMax)
{
maxCrit = critConfig.MaxValue;
} }
}
float totalCritChance = Mathf.Clamp(baseCrit + attackerCrit, 0f, 0.9f); float totalCritChance = Mathf.Clamp(baseCrit + attackerCrit, 0f, maxCrit);
bool isCrit = totalCritChance > 0f && Random.value < totalCritChance; bool isCrit = totalCritChance > 0f && Random.value < totalCritChance;
float critMultiplier = context.CritMultiplier; float critMultiplier = context.CritMultiplier;
@@ -8,13 +8,11 @@ namespace SepCore.Components
private Player _player; private Player _player;
private Transform _target; private Transform _target;
private float AbsorbRangeBase => GameEntry.Luban.Global.DefaultAbsorbRange / 1000f; public float AbsorbRange => GameEntry.Luban.Global.DefaultAbsorbRange;
private float AbsorbSpeed => GameEntry.Luban.Global.AbsorbSpeed / 1000f; public float AbsorbSpeed => GameEntry.Luban.Global.AbsorbSpeed;
private float CollectDistance => GameEntry.Luban.Global.CollectDistance / 1000f; public float CollectDistance => GameEntry.Luban.Global.CollectDistance;
private float AbsorbRange => AbsorbRangeBase; public void OnInit(Player player)
public void OnInit(Player player, StatComponent statComponent)
{ {
_player = player; _player = player;
_target = player.CachedTransform; _target = player.CachedTransform;
@@ -80,8 +80,8 @@ namespace SepCore.EnemyManager
_isStopped = false; _isStopped = false;
_spawnEnemyMaxCount = GameEntry.Luban.Global.SpawnEnemyMaxCount; _spawnEnemyMaxCount = GameEntry.Luban.Global.SpawnEnemyMaxCount;
float minDistance = GameEntry.Luban.Global.EnemySpawnDistanceMin / 1000f; float minDistance = GameEntry.Luban.Global.EnemySpawnDistanceMin;
float maxDistance = GameEntry.Luban.Global.EnemySpawnDistanceMax / 1000f; float maxDistance = GameEntry.Luban.Global.EnemySpawnDistanceMax;
_player = player.CachedTransform; _player = player.CachedTransform;
_spawnPositionStrategy = new RandomCircleSpawnStrategy(minDistance, maxDistance); _spawnPositionStrategy = new RandomCircleSpawnStrategy(minDistance, maxDistance);
@@ -23,19 +23,19 @@ namespace SepCore.Simulation
private float ResolveAbsorbRange() private float ResolveAbsorbRange()
{ {
if (_playerAbsorbRange >= 0f) return _playerAbsorbRange; if (_playerAbsorbRange >= 0f) return _playerAbsorbRange;
return GameEntry.Luban != null ? GameEntry.Luban.Global.DefaultAbsorbRange / 1000f : 2f; return GameEntry.Luban != null ? GameEntry.Luban.Global.DefaultAbsorbRange : 2f;
} }
private float ResolveAbsorbSpeed() private float ResolveAbsorbSpeed()
{ {
if (_playerAbsorbSpeed >= 0f) return _playerAbsorbSpeed; if (_playerAbsorbSpeed >= 0f) return _playerAbsorbSpeed;
return GameEntry.Luban != null ? GameEntry.Luban.Global.AbsorbSpeed / 1000f : 8f; return GameEntry.Luban != null ? GameEntry.Luban.Global.AbsorbSpeed : 8f;
} }
private float ResolveCollectDistance() private float ResolveCollectDistance()
{ {
if (_playerCollectDistance >= 0f) return _playerCollectDistance; if (_playerCollectDistance >= 0f) return _playerCollectDistance;
return GameEntry.Luban != null ? GameEntry.Luban.Global.CollectDistance / 1000f : 0.5f; return GameEntry.Luban != null ? GameEntry.Luban.Global.CollectDistance : 0.5f;
} }
[Header("敌人互斥参数")] [Header("敌人互斥参数")]
@@ -216,7 +216,7 @@ namespace SepCore.Entity
_inputComponent.OnInit(); _inputComponent.OnInit();
_inputComponent.SetListening(true); _inputComponent.SetListening(true);
_absorbComponent.OnInit(this, _statComponent); _absorbComponent.OnInit(this);
_weaponEnabled = true; _weaponEnabled = true;
_currentLevel = 0; _currentLevel = 0;
@@ -34,8 +34,8 @@ namespace SepCore.Entity
get get
{ {
if (WeaponData == null) return 0f; if (WeaponData == null) return 0f;
float attackSpeed = _ownerAttributeProvider?.GetValue(StatType.AttackSpeed) ?? 0f; float attackSpeed = _ownerAttributeProvider?.GetValue(StatType.AttackSpeed) ?? 1f;
float speedFactor = Mathf.Max(0.2f, 1f + attackSpeed); float speedFactor = Mathf.Max(0.2f, attackSpeed);
return Mathf.Max(0.02f, WeaponData.Cooldown / speedFactor); return Mathf.Max(0.02f, WeaponData.Cooldown / speedFactor);
} }
} }
@@ -46,8 +46,7 @@ namespace SepCore.Entity
{ {
if (WeaponData == null) return 0f; if (WeaponData == null) return 0f;
float rangeBonus = _ownerAttributeProvider?.GetValue(StatType.Range) ?? 0f; float rangeBonus = _ownerAttributeProvider?.GetValue(StatType.Range) ?? 0f;
float effectiveBonus = rangeBonus >= 50f ? rangeBonus / 1000f : rangeBonus; return Mathf.Max(0.5f, WeaponData.AttackRange + rangeBonus);
return Mathf.Max(0.5f, WeaponData.AttackRange + effectiveBonus);
} }
} }
@@ -124,7 +124,7 @@ namespace SepCore.UI
_selections.Clear(); _selections.Clear();
int currentLevel = _currentLevel; int currentLevel = _currentLevel;
float priceRandomPercent = GameEntry.Luban.Global.ShopPriceRandomPercent / 1000f; float priceRandomPercent = GameEntry.Luban.Global.ShopPriceRandomPercent;
for (int i = 0; i < finalCount; i++) for (int i = 0; i < finalCount; i++)
{ {
@@ -324,7 +324,7 @@ namespace SepCore.UI
public int CalculateRecyclePrice(int basePrice) public int CalculateRecyclePrice(int basePrice)
{ {
return Mathf.FloorToInt(basePrice * (GameEntry.Luban.Global.WeaponRecycleRate / 1000f)); return Mathf.FloorToInt(basePrice * GameEntry.Luban.Global.WeaponRecycleRate);
} }
} }
} }
@@ -169,5 +169,89 @@ namespace VampireLike.Tests.Gameplay.EditMode
stat.Config = new StatConfig(StatType.Dodge, hasMax: true, maxValue: 0.80f); stat.Config = new StatConfig(StatType.Dodge, hasMax: true, maxValue: 0.80f);
Assert.AreEqual(0.80f, stat.Value, 1e-4f); // 自动标记脏并重新钳制 Assert.AreEqual(0.80f, stat.Value, 1e-4f); // 自动标记脏并重新钳制
} }
[Test]
public void DamagePercentAndAttackSpeed_DefaultBaseValueIsOne_AndPercentAddScalesCorrectly()
{
// 验证 DamagePercent 配置初值为 1 时,+5% 增伤(PercentAdd = 0.05)正确求值为 1.05
var damageConfig = new StatConfig(StatType.DamagePercent, defaultBaseValue: 1.0f);
var damageStat = new CharacterStat(StatType.DamagePercent, config: damageConfig);
Assert.AreEqual(1.0f, damageStat.Value, 1e-4f);
// 模拟“肌肉”+5% 伤害
damageStat.AddModifier(new StatModifier(0.05f, ModifierType.PercentAdd));
Assert.AreEqual(1.05f, damageStat.Value, 1e-4f);
// 模拟额外叠加 +10% 增伤
damageStat.AddModifier(new StatModifier(0.10f, ModifierType.PercentAdd));
Assert.AreEqual(1.15f, damageStat.Value, 1e-4f);
// 验证 AttackSpeed 配置初值为 1 时,+19% 攻速(PercentAdd = 0.19)正确求值为 1.19
var speedConfig = new StatConfig(StatType.AttackSpeed, defaultBaseValue: 1.0f);
var speedStat = new CharacterStat(StatType.AttackSpeed, config: speedConfig);
Assert.AreEqual(1.0f, speedStat.Value, 1e-4f);
// 模拟“疾风手套”+19% 攻速
speedStat.AddModifier(new StatModifier(0.19f, ModifierType.PercentAdd));
Assert.AreEqual(1.19f, speedStat.Value, 1e-4f);
// 模拟叠加“鼻子”+5% 攻速
speedStat.AddModifier(new StatModifier(0.05f, ModifierType.PercentAdd));
Assert.AreEqual(1.24f, speedStat.Value, 1e-4f);
}
[Test]
public void ItemDescUtility_FormatsDodgeCritPercentAndMoveSpeedCorrectly()
{
// 1. 闪避率(Dodge)Flat 修饰器以百分比展示: 0.1 -> +10%, 0.27 -> +27%
var dodgeMod1 = new StatModifierConfig(StatType.Dodge, 0.1f, ModifierType.Flat);
string dodgeDesc1 = SepCore.CustomUtility.ItemDescUtility.Describe(dodgeMod1);
Assert.IsTrue(dodgeDesc1.Contains("+10%"), $"Expected +10% in: {dodgeDesc1}");
var dodgeMod2 = new StatModifierConfig(StatType.Dodge, 0.27f, ModifierType.Flat);
string dodgeDesc2 = SepCore.CustomUtility.ItemDescUtility.Describe(dodgeMod2);
Assert.IsTrue(dodgeDesc2.Contains("+27%"), $"Expected +27% in: {dodgeDesc2}");
// 2. 暴击率(CritChance)Flat 修饰器以百分比展示: 0.27 -> +27%, 0.05 -> +5%
var critMod1 = new StatModifierConfig(StatType.CritChance, 0.27f, ModifierType.Flat);
string critDesc1 = SepCore.CustomUtility.ItemDescUtility.Describe(critMod1);
Assert.IsTrue(critDesc1.Contains("+27%"), $"Expected +27% in: {critDesc1}");
var critMod2 = new StatModifierConfig(StatType.CritChance, 0.05f, ModifierType.Flat);
string critDesc2 = SepCore.CustomUtility.ItemDescUtility.Describe(critMod2);
Assert.IsTrue(critDesc2.Contains("+5%"), $"Expected +5% in: {critDesc2}");
// 3. 移速(MoveSpeed)Flat 修饰器展示原小数: 0.8 -> +0.8
var speedMod = new StatModifierConfig(StatType.MoveSpeed, 0.8f, ModifierType.Flat);
string speedDesc = SepCore.CustomUtility.ItemDescUtility.Describe(speedMod);
Assert.IsTrue(speedDesc.Contains("+0.8"), $"Expected +0.8 in: {speedDesc}");
Assert.IsFalse(speedDesc.Contains("%"), $"Expected no % in: {speedDesc}");
// 4. 攻速(AttackSpeed)PercentAdd 修饰器以百分比展示: 0.19 -> +19%
var atkSpeedMod = new StatModifierConfig(StatType.AttackSpeed, 0.19f, ModifierType.PercentAdd);
string atkSpeedDesc = SepCore.CustomUtility.ItemDescUtility.Describe(atkSpeedMod);
Assert.IsTrue(atkSpeedDesc.Contains("+19%"), $"Expected +19% in: {atkSpeedDesc}");
// 5. 伤害(DamagePercent)PercentAdd 修饰器以百分比展示: 0.05 -> +5%
var dmgMod = new StatModifierConfig(StatType.DamagePercent, 0.05f, ModifierType.PercentAdd);
string dmgDesc = SepCore.CustomUtility.ItemDescUtility.Describe(dmgMod);
Assert.IsTrue(dmgDesc.Contains("+5%"), $"Expected +5% in: {dmgDesc}");
// 6. 近战伤害(MeleeDamage,非百分比属性)Flat 修饰器展示原数值: 12 -> +12,不带%
var meleeFlatMod = new StatModifierConfig(StatType.MeleeDamage, 12f, ModifierType.Flat);
string meleeFlatDesc = SepCore.CustomUtility.ItemDescUtility.Describe(meleeFlatMod);
Assert.IsTrue(meleeFlatDesc.Contains("+12"), $"Expected +12 in: {meleeFlatDesc}");
Assert.IsFalse(meleeFlatDesc.Contains("%"), $"Expected no % in: {meleeFlatDesc}");
// 7. 移速(MoveSpeed)若为 PercentAdd 修饰器,以百分比展示: 0.15 -> +15%
var speedPercentMod = new StatModifierConfig(StatType.MoveSpeed, 0.15f, ModifierType.PercentAdd);
string speedPercentDesc = SepCore.CustomUtility.ItemDescUtility.Describe(speedPercentMod);
Assert.IsTrue(speedPercentDesc.Contains("+15%"), $"Expected +15% in: {speedPercentDesc}");
// 8. PercentMult 乘数修饰器以 x 表示: 1.25 -> x1.25
var multMod = new StatModifierConfig(StatType.MaxHp, 1.25f, ModifierType.PercentMult);
string multDesc = SepCore.CustomUtility.ItemDescUtility.Describe(multMod);
Assert.IsTrue(multDesc.Contains("x1.25"), $"Expected x1.25 in: {multDesc}");
}
} }
} }
@@ -12,7 +12,8 @@ namespace VampireLike.Tests.Gameplay.EditMode
{ {
var attackerPipeline = new AttributePipeline(); var attackerPipeline = new AttributePipeline();
attackerPipeline.SetBaseValue(StatType.MeleeDamage, 20f); attackerPipeline.SetBaseValue(StatType.MeleeDamage, 20f);
attackerPipeline.SetBaseValue(StatType.DamagePercent, 0.50f); // +50% attackerPipeline.SetBaseValue(StatType.DamagePercent, 1.0f); // 基础 100% 伤害
attackerPipeline.AddModifier(StatType.DamagePercent, new StatModifier(0.50f, ModifierType.PercentAdd)); // +50% 增伤加成
var context = new DamageContext var context = new DamageContext
{ {
@@ -28,7 +29,7 @@ namespace VampireLike.Tests.Gameplay.EditMode
}; };
// Raw = 100 + 20 = 120 // Raw = 100 + 20 = 120
// Amplified = 120 * (1 + 0.5) = 180 // Amplified = 120 * (1.0 * (1 + 0.5)) = 180
// Armor = 0 -> Mitigation = 1.0 -> 180 // Armor = 0 -> Mitigation = 1.0 -> 180
DamageResult result = DamagePipeline.Calculate(in context); DamageResult result = DamagePipeline.Calculate(in context);
Assert.AreEqual(180, result.Damage); Assert.AreEqual(180, result.Damage);
@@ -246,19 +247,22 @@ namespace VampireLike.Tests.Gameplay.EditMode
[Test] [Test]
public void RangeBonus_CalculatesAttackRangeCorrectly() public void RangeBonus_CalculatesAttackRangeCorrectly()
{ {
float baseRange = 5.0f; // 5m float baseRange = 5.0f; // 5m(统一米制)
// 1. 增加 2m (direct units) // 1. 增加 2.0m 攻击范围 -> 5.0 + 2.0 = 7.0m
float bonus1 = 2.0f; float bonus1 = 2.0f;
float eff1 = bonus1 >= 50f ? bonus1 / 1000f : bonus1; float range1 = UnityEngine.Mathf.Max(0.5f, baseRange + bonus1);
float range1 = UnityEngine.Mathf.Max(0.5f, baseRange + eff1);
Assert.AreEqual(7.0f, range1, 1e-4f); Assert.AreEqual(7.0f, range1, 1e-4f);
// 2. 增加 1500 (millimeters) // 2. 减少 1.5m 攻击范围 -> 5.0 - 1.5 = 3.5m
float bonus2 = 1500f; float bonus2 = -1.5f;
float eff2 = bonus2 >= 50f ? bonus2 / 1000f : bonus2; float range2 = UnityEngine.Mathf.Max(0.5f, baseRange + bonus2);
float range2 = UnityEngine.Mathf.Max(0.5f, baseRange + eff2); Assert.AreEqual(3.5f, range2, 1e-4f);
Assert.AreEqual(6.5f, range2, 1e-4f);
// 3. 极端负向减射程(-10m)触发 0.5m 最小射程保护
float bonus3 = -10.0f;
float range3 = UnityEngine.Mathf.Max(0.5f, baseRange + bonus3);
Assert.AreEqual(0.5f, range3, 1e-4f);
} }
[Test] [Test]
@@ -312,5 +316,45 @@ namespace VampireLike.Tests.Gameplay.EditMode
UnityEngine.Object.DestroyImmediate(go); UnityEngine.Object.DestroyImmediate(go);
} }
[Test]
public void ZeroBaseCrit_NeverProducesCriticalHit()
{
var context = new DamageContext
{
BaseDamage = 100,
BaseCritChance = 0f,
CritMultiplier = 2.0f,
FallbackDefenderArmor = 0f
};
for (int i = 0; i < 100; i++)
{
DamageResult result = DamagePipeline.Calculate(in context);
Assert.IsFalse(result.IsCrit, "Zero BaseCritChance should never produce a critical hit.");
Assert.AreEqual(100, result.Damage);
}
}
[Test]
public void NegativeDamagePercent_DoesNotRevertToOneMultiplier_ScalesDownToMinDamage()
{
var attackerPipeline = new AttributePipeline();
attackerPipeline.SetBaseValue(StatType.DamagePercent, 1.0f);
// 施加 -100% 伤害削弱(伤害倍率降为 0)
attackerPipeline.AddModifier(StatType.DamagePercent, new StatModifier(-1.0f, ModifierType.PercentAdd));
var context = new DamageContext
{
BaseDamage = 100,
AttackerPipeline = attackerPipeline,
FallbackDefenderArmor = 0f
};
DamageResult result = DamagePipeline.Calculate(in context);
// 修复前: damagePercent <= 0 会被重置为 1f,导致造成 100 满额伤害
// 修复后: 伤害倍率为 0,最终受到全局 MinDamage(默认为 1)保底
Assert.AreEqual(1, result.Damage, "Negative or zero damage percent must scale down to min damage (1), never bounce to full damage.");
}
} }
} }
@@ -6,7 +6,8 @@
"UnityEngine.TestRunner", "UnityEngine.TestRunner",
"UnityEditor.TestRunner", "UnityEditor.TestRunner",
"SepCore.Runtime", "SepCore.Runtime",
"SepCore.Base" "SepCore.Base",
"SepCore.Presentation"
], ],
"includePlatforms": [ "includePlatforms": [
"Editor" "Editor"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.