引入属性、伤害管线

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
Binary file not shown.
@@ -9,13 +9,14 @@
"weaponRecycleRate": 300,
"shopGoodsCount": 4,
"shopPriceRandomPercent": 50,
"shopMinPrice": 0,
"shopMinPrice": 1,
"levelUpRefreshPriceBase": 2,
"levelUpRewardCount": 4,
"spawnEnemyMaxCount": 5000,
"enemySpawnDistanceMin": 14000,
"enemySpawnDistanceMax": 18000,
"weaponRotateSpeed": 5000,
"weaponBaseCrit": 0.05,
"playerGroupName": "Player",
"enemyGroupName": "Enemy",
"projectileGroupName": "Projectile",
@@ -30,6 +31,7 @@
"projectileEntity": "Projectile",
"damageTextYOffset": 1100,
"entityAsyncTimeout": 30000,
"critMultiplier": 1.5,
"enemyBodyRadius": 450,
"enemySeparationIterations": 2
}
@@ -1 +1 @@
┐ИХ┌▄Х┌┴2Х┌▄Х┌┴┐ЙГ┴≥И╫©2Г┴≥И╫©┐КХ└▒2Х└▒┐ЛИ╪╩Е╜░ 2И╪╩Е╜░┐МХ┘©2Х┘©┐НГ°╪Г²⌡2Г°╪Г²⌡┐ОХ┌▄Х┌┴─√Х┌▄Х┌┴┐ПГ┴≥И╫©─√Г┴≥И╫©┐ЯХ└▒─√Х└▒┐РИ╪╩Е╜░ ─√И╪╩Е╜░┐СХ┘©─√Х┘©┐ТГ°╪Г²⌡─√Г°╪Г²⌡┐УХ┌▄Х┌┴│,Х┌▄Х┌┴┐ЖГ┴≥И╫©│,Г┴≥И╫©┐ВХ└▒│,Х└▒┐ЬИ╪╩Е╜░ │,И╪╩Е╜░┐ЫХ┘©│,Х┘©┐ЗГ°╪Г²⌡│,Г°╪Г²⌡
┐ИХ┌▄Х┌┴млL=─хХ┌▄Х┌┴┐ЙГ┴≥И╫©млL=─хГ┴≥И╫©┐КХ└▒ млL=─хХ└▒┐ЛИ╪╩Е╜░млL=─хИ╪╩Е╜░┐МХ┘©млL=─хХ┘©┐НГ°╪Г²⌡млL=─хГ°╪Г²⌡┐ОХ┌▄Х┌┴ ≥>─хХ┌▄Х┌┴┐ПГ┴≥И╫© ≥>─хГ┴≥И╫©┐ЯХ└▒  ≥>─хХ└▒┐РИ╪╩Е╜░ ≥>─хИ╪╩Е╜░┐СХ┘© ≥>─хХ┘©┐ТГ°╪Г²⌡ ≥>─хГ°╪Г²⌡┐УХ┌▄Х┌┴ ≥≥>─хХ┌▄Х┌┴┐ЖГ┴≥И╫© ≥≥>─хГ┴≥И╫©┐ВХ└▒  ≥≥>─хХ└▒┐ЬИ╪╩Е╜░ ≥≥>─хИ╪╩Е╜░┐ЫХ┘© ≥≥>─хХ┘©┐ЗГ°╪Г²⌡ ≥≥>─хГ°╪Г²⌡
@@ -3,11 +3,11 @@
"id": 1001,
"title": "肌肉",
"rarity": 1,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"value": 50,
"isPercent": true
"statType": 4,
"value": 0.05,
"modifierType": 200
}
],
"sprite": "肌肉"
@@ -16,11 +16,11 @@
"id": 1002,
"title": "牙齿",
"rarity": 1,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"value": 50,
"isPercent": true
"statType": 4,
"value": 0.05,
"modifierType": 200
}
],
"sprite": "牙齿"
@@ -29,11 +29,11 @@
"id": 1003,
"title": "脑",
"rarity": 1,
"modifiers": [
"statModifierConfigs": [
{
"key": 6,
"value": 50,
"isPercent": true
"statType": 9,
"value": 0.05,
"modifierType": 200
}
],
"sprite": "脑"
@@ -42,11 +42,11 @@
"id": 1004,
"title": "鼻子",
"rarity": 1,
"modifiers": [
"statModifierConfigs": [
{
"key": 9,
"value": 50,
"isPercent": true
"statType": 8,
"value": 0.05,
"modifierType": 200
}
],
"sprite": "鼻子"
@@ -55,11 +55,11 @@
"id": 1005,
"title": "腿",
"rarity": 1,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"value": 50,
"isPercent": true
"statType": 14,
"value": 0.05,
"modifierType": 200
}
],
"sprite": "腿"
@@ -68,11 +68,11 @@
"id": 1006,
"title": "眼睛",
"rarity": 1,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"value": 50,
"isPercent": true
"statType": 1,
"value": 0.05,
"modifierType": 200
}
],
"sprite": "眼睛"
@@ -81,11 +81,11 @@
"id": 1007,
"title": "肌肉",
"rarity": 2,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"value": 150,
"isPercent": true
"statType": 4,
"value": 0.15,
"modifierType": 200
}
],
"sprite": "肌肉"
@@ -94,11 +94,11 @@
"id": 1008,
"title": "牙齿",
"rarity": 2,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"value": 150,
"isPercent": true
"statType": 4,
"value": 0.15,
"modifierType": 200
}
],
"sprite": "牙齿"
@@ -107,11 +107,11 @@
"id": 1009,
"title": "脑",
"rarity": 2,
"modifiers": [
"statModifierConfigs": [
{
"key": 6,
"value": 150,
"isPercent": true
"statType": 9,
"value": 0.15,
"modifierType": 200
}
],
"sprite": "脑"
@@ -120,11 +120,11 @@
"id": 1010,
"title": "鼻子",
"rarity": 2,
"modifiers": [
"statModifierConfigs": [
{
"key": 9,
"value": 150,
"isPercent": true
"statType": 8,
"value": 0.15,
"modifierType": 200
}
],
"sprite": "鼻子"
@@ -133,11 +133,11 @@
"id": 1011,
"title": "腿",
"rarity": 2,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"value": 150,
"isPercent": true
"statType": 14,
"value": 0.15,
"modifierType": 200
}
],
"sprite": "腿"
@@ -146,11 +146,11 @@
"id": 1012,
"title": "眼睛",
"rarity": 2,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"value": 150,
"isPercent": true
"statType": 1,
"value": 0.15,
"modifierType": 200
}
],
"sprite": "眼睛"
@@ -159,11 +159,11 @@
"id": 1013,
"title": "肌肉",
"rarity": 3,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"value": 300,
"isPercent": true
"statType": 4,
"value": 0.3,
"modifierType": 200
}
],
"sprite": "肌肉"
@@ -172,11 +172,11 @@
"id": 1014,
"title": "牙齿",
"rarity": 3,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"value": 300,
"isPercent": true
"statType": 4,
"value": 0.3,
"modifierType": 200
}
],
"sprite": "牙齿"
@@ -185,11 +185,11 @@
"id": 1015,
"title": "脑",
"rarity": 3,
"modifiers": [
"statModifierConfigs": [
{
"key": 6,
"value": 300,
"isPercent": true
"statType": 9,
"value": 0.3,
"modifierType": 200
}
],
"sprite": "脑"
@@ -198,11 +198,11 @@
"id": 1016,
"title": "鼻子",
"rarity": 3,
"modifiers": [
"statModifierConfigs": [
{
"key": 9,
"value": 300,
"isPercent": true
"statType": 8,
"value": 0.3,
"modifierType": 200
}
],
"sprite": "鼻子"
@@ -211,11 +211,11 @@
"id": 1017,
"title": "腿",
"rarity": 3,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"value": 300,
"isPercent": true
"statType": 14,
"value": 0.3,
"modifierType": 200
}
],
"sprite": "腿"
@@ -224,11 +224,11 @@
"id": 1018,
"title": "眼睛",
"rarity": 3,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"value": 300,
"isPercent": true
"statType": 1,
"value": 0.3,
"modifierType": 200
}
],
"sprite": "眼睛"
Binary file not shown.
+107 -107
View File
@@ -4,16 +4,16 @@
"name": "铁护腕",
"rarity": 1,
"price": 120,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"statType": 4,
"value": 10,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 12,
"isPercent": false
"modifierType": 100
}
],
"sprite": "铁护腕"
@@ -23,21 +23,21 @@
"name": "狂战士药剂",
"rarity": 2,
"price": 250,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"statType": 1,
"value": 53,
"isPercent": false
"modifierType": 100
},
{
"key": 5,
"value": 90,
"isPercent": true
"statType": 8,
"value": 0.09,
"modifierType": 200
},
{
"key": 3,
"statType": 4,
"value": 11,
"isPercent": false
"modifierType": 100
}
],
"sprite": "狂战士药剂"
@@ -47,21 +47,21 @@
"name": "龙鳞护符",
"rarity": 3,
"price": 330,
"modifiers": [
"statModifierConfigs": [
{
"key": 6,
"statType": 9,
"value": 15,
"isPercent": false
"modifierType": 100
},
{
"key": 7,
"statType": 5,
"value": 21,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 23,
"isPercent": false
"modifierType": 100
}
],
"sprite": "龙鳞护符"
@@ -71,11 +71,11 @@
"name": "泰坦壁垒",
"rarity": 5,
"price": 423,
"modifiers": [
"statModifierConfigs": [
{
"key": 4,
"statType": 12,
"value": 79,
"isPercent": false
"modifierType": 100
}
],
"sprite": "泰坦壁垒"
@@ -85,16 +85,16 @@
"name": "轻便短靴",
"rarity": 1,
"price": 120,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"statType": 14,
"value": 800,
"isPercent": false
"modifierType": 100
},
{
"key": 3,
"statType": 4,
"value": 10,
"isPercent": false
"modifierType": 100
}
],
"sprite": "轻便短靴"
@@ -104,21 +104,21 @@
"name": "幽灵披风",
"rarity": 2,
"price": 195,
"modifiers": [
"statModifierConfigs": [
{
"key": 8,
"statType": 13,
"value": 10,
"isPercent": false
"modifierType": 100
},
{
"key": 3,
"statType": 4,
"value": 12,
"isPercent": false
"modifierType": 100
},
{
"key": 1,
"statType": 1,
"value": 44,
"isPercent": false
"modifierType": 100
}
],
"sprite": "幽灵披风"
@@ -128,11 +128,11 @@
"name": "精钢胸甲",
"rarity": 3,
"price": 335,
"modifiers": [
"statModifierConfigs": [
{
"key": 4,
"statType": 12,
"value": 62,
"isPercent": false
"modifierType": 100
}
],
"sprite": "精钢胸甲"
@@ -142,16 +142,16 @@
"name": "暗影之刃",
"rarity": 5,
"price": 433,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"statType": 4,
"value": 44,
"isPercent": false
"modifierType": 100
},
{
"key": 8,
"statType": 13,
"value": 27,
"isPercent": false
"modifierType": 100
}
],
"sprite": "暗影之刃"
@@ -161,21 +161,21 @@
"name": "旅行者徽章",
"rarity": 1,
"price": 121,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"statType": 1,
"value": 32,
"isPercent": false
"modifierType": 100
},
{
"key": 3,
"statType": 4,
"value": 7,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 8,
"isPercent": false
"modifierType": 100
}
],
"sprite": "旅行者徽章"
@@ -185,11 +185,11 @@
"name": "疾风手套",
"rarity": 2,
"price": 198,
"modifiers": [
"statModifierConfigs": [
{
"key": 5,
"value": 190,
"isPercent": true
"statType": 8,
"value": 0.19,
"modifierType": 200
}
],
"sprite": "疾风手套"
@@ -199,11 +199,11 @@
"name": "巨人之力卷轴",
"rarity": 3,
"price": 341,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"statType": 4,
"value": 57,
"isPercent": false
"modifierType": 100
}
],
"sprite": "巨人之力卷轴"
@@ -213,16 +213,16 @@
"name": "不死鸟之心",
"rarity": 5,
"price": 443,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"statType": 1,
"value": 139,
"isPercent": false
"modifierType": 100
},
{
"key": 6,
"statType": 9,
"value": 27,
"isPercent": false
"modifierType": 100
}
],
"sprite": "不死鸟之心"
@@ -232,16 +232,16 @@
"name": "幸运草",
"rarity": 1,
"price": 122,
"modifiers": [
"statModifierConfigs": [
{
"key": 6,
"statType": 9,
"value": 5,
"isPercent": false
"modifierType": 100
},
{
"key": 8,
"statType": 13,
"value": 7,
"isPercent": false
"modifierType": 100
}
],
"sprite": "幸运草"
@@ -251,16 +251,16 @@
"name": "石肤戒指",
"rarity": 2,
"price": 201,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"statType": 14,
"value": 600,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 38,
"isPercent": false
"modifierType": 100
}
],
"sprite": "石肤戒指"
@@ -270,16 +270,16 @@
"name": "风行者胫甲",
"rarity": 3,
"price": 346,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"statType": 14,
"value": 1600,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 32,
"isPercent": false
"modifierType": 100
}
],
"sprite": "风行者胫甲"
@@ -289,21 +289,21 @@
"name": "幻影斗篷",
"rarity": 5,
"price": 454,
"modifiers": [
"statModifierConfigs": [
{
"key": 8,
"statType": 13,
"value": 28,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 24,
"isPercent": false
"modifierType": 100
},
{
"key": 3,
"statType": 4,
"value": 24,
"isPercent": false
"modifierType": 100
}
],
"sprite": "幻影斗篷"
@@ -313,11 +313,11 @@
"name": "木质盾牌",
"rarity": 1,
"price": 124,
"modifiers": [
"statModifierConfigs": [
{
"key": 4,
"statType": 12,
"value": 23,
"isPercent": false
"modifierType": 100
}
],
"sprite": "木质盾牌"
@@ -327,16 +327,16 @@
"name": "生命之戒",
"rarity": 2,
"price": 204,
"modifiers": [
"statModifierConfigs": [
{
"key": 3,
"statType": 4,
"value": 17,
"isPercent": false
"modifierType": 100
},
{
"key": 1,
"statType": 1,
"value": 69,
"isPercent": false
"modifierType": 100
}
],
"sprite": "生命之戒"
@@ -346,21 +346,21 @@
"name": "精灵耳环",
"rarity": 3,
"price": 352,
"modifiers": [
"statModifierConfigs": [
{
"key": 1,
"statType": 1,
"value": 74,
"isPercent": false
"modifierType": 100
},
{
"key": 3,
"statType": 4,
"value": 19,
"isPercent": false
"modifierType": 100
},
{
"key": 8,
"statType": 13,
"value": 20,
"isPercent": false
"modifierType": 100
}
],
"sprite": "精灵耳环"
@@ -370,21 +370,21 @@
"name": "战神勋章",
"rarity": 5,
"price": 464,
"modifiers": [
"statModifierConfigs": [
{
"key": 5,
"value": 150,
"isPercent": true
"statType": 8,
"value": 0.15,
"modifierType": 200
},
{
"key": 3,
"statType": 4,
"value": 29,
"isPercent": false
"modifierType": 100
},
{
"key": 4,
"statType": 12,
"value": 27,
"isPercent": false
"modifierType": 100
}
],
"sprite": "战神勋章"
Binary file not shown.
+8 -8
View File
@@ -4,16 +4,16 @@
"maxHealth": 100,
"speed": 6,
"coin": 10,
"modifiers": [
"statModifierConfigs": [
{
"key": 2,
"value": 500,
"isPercent": true
"statType": 14,
"value": 0.5,
"modifierType": 200
},
{
"key": 1,
"value": 200,
"isPercent": true
"statType": 1,
"value": 0.2,
"modifierType": 200
}
],
"initialItems": [],
@@ -25,7 +25,7 @@
"maxHealth": 200,
"speed": 4,
"coin": 5,
"modifiers": [],
"statModifierConfigs": [],
"initialItems": [],
"prefab": "Player",
"sprite": "老人"
Binary file not shown.
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0ee410edd089fef4d91515cb4a75589d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,162 @@
[
{
"statType": 1,
"name": "最大生命",
"defaultBaseValue": 100,
"hasMin": true,
"minValue": 1,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 2,
"name": "生命恢复",
"defaultBaseValue": 5,
"hasMin": true,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 3,
"name": "生命窃取",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 4,
"name": "伤害",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": -1,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": true
},
{
"statType": 5,
"name": "近战伤害",
"defaultBaseValue": 0,
"hasMin": false,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 6,
"name": "远程伤害",
"defaultBaseValue": 0,
"hasMin": false,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 7,
"name": "元素伤害",
"defaultBaseValue": 0,
"hasMin": false,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 8,
"name": "攻击速度",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": -0.8,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": true
},
{
"statType": 9,
"name": "暴击率",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": true,
"maxValue": 0.9,
"isPercentDisplay": true
},
{
"statType": 10,
"name": "工程学",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 11,
"name": "攻击范围",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 12,
"name": "护甲",
"defaultBaseValue": 0,
"hasMin": false,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 13,
"name": "闪避率",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": true,
"maxValue": 0.8,
"isPercentDisplay": true
},
{
"statType": 14,
"name": "移动速度",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0.5,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 15,
"name": "收获",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
},
{
"statType": 16,
"name": "幸运",
"defaultBaseValue": 0,
"hasMin": true,
"minValue": 0,
"hasMax": false,
"maxValue": 0,
"isPercentDisplay": false
}
]
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b66a5cb55d1374a45b0f74f7baba5283
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
+10 -5
View File
@@ -6,6 +6,7 @@
"price": 120,
"attack": 100,
"cooldown": 1500,
"damageType": 1,
"attackRange": 5000,
"attackSoundId": 10000,
"parameters": [
@@ -14,7 +15,7 @@
2000
]
],
"modifiers": [],
"statModifierConfigs": [],
"prefab": "WeaponKnife",
"sprite": "小刀"
},
@@ -25,10 +26,11 @@
"price": 130,
"attack": 120,
"cooldown": 1000,
"damageType": 2,
"attackRange": 15000,
"attackSoundId": 10000,
"parameters": [],
"modifiers": [],
"statModifierConfigs": [],
"prefab": "WeaponHandgun",
"sprite": "手枪"
},
@@ -39,6 +41,7 @@
"price": 100,
"attack": 150,
"cooldown": 2000,
"damageType": 1,
"attackRange": 5000,
"attackSoundId": 10000,
"parameters": [
@@ -47,7 +50,7 @@
120
]
],
"modifiers": [],
"statModifierConfigs": [],
"prefab": "WeaponSlash",
"sprite": "斧头"
},
@@ -58,6 +61,7 @@
"price": 150,
"attack": 80,
"cooldown": 3000,
"damageType": 4,
"attackRange": 12000,
"attackSoundId": 10000,
"parameters": [
@@ -66,7 +70,7 @@
3000
]
],
"modifiers": [],
"statModifierConfigs": [],
"prefab": "WeaponLightning",
"sprite": "闪电"
},
@@ -77,6 +81,7 @@
"price": 100,
"attack": 100,
"cooldown": 1500,
"damageType": 1,
"attackRange": 5000,
"attackSoundId": 10000,
"parameters": [
@@ -93,7 +98,7 @@
500
]
],
"modifiers": [],
"statModifierConfigs": [],
"prefab": "WeaponLance",
"sprite": "长枪"
}
@@ -0,0 +1,20 @@
namespace SepCore.Definition
{
public static class DamageTypeExtensions
{
public static StatType ToScalingStat(this DamageType damageType)
{
switch (damageType)
{
case DamageType.Melee:
return StatType.MeleeDamage;
case DamageType.Ranged:
return StatType.RangedDamage;
case DamageType.Element:
return StatType.ElementalDamage;
default:
return StatType.None;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e6128fa910e4a778c187123490b4119
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
namespace SepCore.Definition
{
/// <summary>
/// 属性查询接口。
/// 供战斗碰撞(ImpactData)与伤害管线解耦读取属性值。
/// </summary>
public interface IAttributeProvider
{
float GetValue(StatType statType);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b483f12479014631bb5914285260aa31
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,37 +1,45 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using System.Runtime.InteropServices;
using SepCore.Definition;
namespace SepCore.Definition
{
[StructLayout(LayoutKind.Auto)]
public struct ImpactData
{
public ImpactData(CampType camp /*, int health*/, int attackBase, StatProperty attackStat = null,
StatProperty defenseStat = null, StatProperty dodgeStat = null)
{
Camp = camp;
//Health = health;
AttackBase = attackBase;
AttackStat = attackStat;
DefenseStat = defenseStat;
DodgeStat = dodgeStat;
}
public CampType Camp { get; }
public int AttackBase { get; }
public StatProperty AttackStat { get; }
public DamageType DamageType { get; }
public StatProperty DefenseStat { get; }
public float ScalingRatio { get; }
public StatProperty DodgeStat { get; }
public float BaseCritChance { get; }
public float CritMultiplier { get; }
public IAttributeProvider AttributeProvider { get; }
public ImpactData(
CampType camp,
int attackBase,
IAttributeProvider attributeProvider = null,
DamageType damageType = DamageType.None,
float scalingRatio = 1.0f,
float baseCritChance = 0f,
float critMultiplier = 0f)
{
Camp = camp;
AttackBase = attackBase;
AttributeProvider = attributeProvider;
DamageType = damageType;
ScalingRatio = scalingRatio;
BaseCritChance = baseCritChance;
CritMultiplier = critMultiplier;
}
public ImpactData(CampType camp, int attackBase)
: this(camp, attackBase, null, DamageType.None, 1.0f, 0f, 0f)
{
}
}
}
@@ -4,24 +4,24 @@ namespace SepCore.Definition
{
public class PropItem
{
private readonly List<modifier> _modifiers = new();
private readonly List<StatModifierConfig> _modifiers = new();
public string Title { get; private set; }
public string IconAssetName { get; private set; }
public Rarity Rarity { get; private set; }
public IReadOnlyList<modifier> Modifiers => _modifiers;
public IReadOnlyList<StatModifierConfig> Modifiers => _modifiers;
public PropItem(PropConfig prop)
{
if (prop.Modifiers != null)
if (prop.StatModifierConfigs != null)
{
_modifiers.AddRange(prop.Modifiers);
_modifiers.AddRange(prop.StatModifierConfigs);
}
Title = prop.Name;
Rarity = prop.Rarity;
IconAssetName = prop.Sprite;
}
public PropItem(IEnumerable<modifier> modifiers)
public PropItem(IEnumerable<StatModifierConfig> modifiers)
{
if (modifiers != null)
{
@@ -29,7 +29,7 @@ namespace SepCore.Definition
}
}
public PropItem(IEnumerable<modifier> modifiers, Rarity rarity, string title, string iconAssetName)
public PropItem(IEnumerable<StatModifierConfig> modifiers, Rarity rarity, string title, string iconAssetName)
{
if (modifiers != null)
{
@@ -0,0 +1,29 @@
namespace SepCore.Definition
{
public sealed partial class StatConfig
{
public StatConfig()
{
}
public StatConfig(
StatType statType,
string name = "",
float defaultBaseValue = 0f,
bool hasMin = false,
float minValue = 0f,
bool hasMax = false,
float maxValue = 0f,
bool isPercentDisplay = false)
{
StatType = statType;
Name = name;
DefaultBaseValue = defaultBaseValue;
HasMin = hasMin;
MinValue = minValue;
HasMax = hasMax;
MaxValue = maxValue;
IsPercentDisplay = isPercentDisplay;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b910e53a2454b5f903517865c36ad18
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,44 @@
using System;
using UnityEngine;
namespace SepCore.Definition
{
/// <summary>
/// 属性修饰器。
/// 包含修饰数值、修饰类型(固定值/百分比加算/独立乘区)以及来源对象(道具/武器/羁绊/特质/Buff)。
/// </summary>
public sealed class StatModifier
{
public float Value { get; }
public ModifierType Type { get; }
public object Source { get; }
public StatModifier(float value, ModifierType type, object source = null)
{
Value = value;
Type = type;
Source = source;
}
/// <summary>
/// 从 Luban 导出的 modifier 数据结构转换。
/// </summary>
/// <param name="mod">Luban modifier 数据实例</param>
/// <param name="source">来源对象引用</param>
public static StatModifier FromLuban(StatModifierConfig mod, object source = null)
{
if (mod == null)
{
throw new ArgumentNullException(nameof(mod));
}
ModifierType type = mod.ModifierType;
return new StatModifier(mod.Value, type, source);
}
public override string ToString()
{
return $"[StatModifier Type={Type}, Value={Value}, Source={Source}]";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 887b4b22fce86cb43b95cfdf3c6b967c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
using System;
using UnityEngine;
namespace SepCore.Definition
{
public sealed partial class StatModifierConfig
{
public StatModifierConfig()
{
}
public StatModifierConfig(StatType statType, float value, ModifierType modifierType)
{
StatType = statType;
Value = value;
ModifierType = modifierType;
}
}
}
@@ -1,20 +0,0 @@
namespace SepCore.Definition
{
public class StatProperty
{
public float Value;
public float Percent;
public StatProperty()
{
Value = 0f;
Percent = 1f;
}
public StatProperty(float value, float percent)
{
Value = value;
Percent = percent;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 18e7bc9587114ae4a91c70f829543de8
timeCreated: 1770353735
@@ -1,70 +0,0 @@
using System;
using UnityEngine;
namespace SepCore.Definition
{
public sealed partial class modifier
{
public StatType StatType => Key;
/// <summary>
/// 实际参与数值计算的浮点数值。
/// 1. 百分比加成(IsPercent == true):表内千分比(如 200 代表 20%),转换为 0.2f。
/// 2. 非百分比加成(IsPercent == false):移动速度原为 float 放大了 1000 倍(如 800 代表 0.8f),除以 1000f;其余(如生命+53、攻击+10)直接使用原值。
/// </summary>
public float RealValue
{
get
{
if (IsPercent)
{
return Value / 1000f;
}
if (Key == StatType.MovementSpeed)
{
return Value / 1000f;
}
return Value;
}
}
public modifier()
{
}
public modifier(StatType key, int value, bool isPercent)
{
Key = key;
Value = value;
IsPercent = isPercent;
}
public modifier(StatType key, float value, bool isPercent)
{
Key = key;
Value = (isPercent || key == StatType.MovementSpeed)
? Mathf.RoundToInt(value * 1000f)
: Mathf.RoundToInt(value);
IsPercent = isPercent;
}
public string ToDisplayString()
{
if (IsPercent)
{
float percent = Value / 10f;
return $"{Key} +{percent:0.##}%";
}
if (Key == StatType.MovementSpeed)
{
float speed = Value / 1000f;
return $"{Key} +{speed:0.##}";
}
return $"{Key} +{Value}";
}
}
}
@@ -22,6 +22,8 @@ namespace SepCore.Entity
public int Attack => Config.Attack;
public DamageType DamageType => Config.DamageType;
public string Title => Config.WeaponType.ToString();
public string Sprite => Config.Sprite;
@@ -38,7 +40,7 @@ namespace SepCore.Entity
public IReadOnlyDictionary<ParamType, int> Parameters => Config.Parameters;
public IReadOnlyList<modifier> Modifiers => Config.Modifiers;
public IReadOnlyList<StatModifierConfig> Modifiers => Config.StatModifierConfigs;
public int GetParam(ParamType paramType)
{
@@ -0,0 +1,39 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SepCore.Definition
{
/// <summary>
/// 伤害类型
/// </summary>
[System.Flags]
public enum DamageType : int
{
/// <summary>
/// 无
/// </summary>
None = 0,
/// <summary>
/// 近战伤害
/// </summary>
Melee = 1,
/// <summary>
/// 远程伤害
/// </summary>
Ranged = 2,
/// <summary>
/// 元素伤害
/// </summary>
Element = 4,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 178341689d25049458e8460d398db2dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -33,6 +33,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
EnemySpawnDistanceMin = _buf.ReadInt();
EnemySpawnDistanceMax = _buf.ReadInt();
WeaponRotateSpeed = _buf.ReadInt();
WeaponBaseCrit = _buf.ReadFloat();
PlayerGroupName = _buf.ReadString();
EnemyGroupName = _buf.ReadString();
ProjectileGroupName = _buf.ReadString();
@@ -48,6 +49,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
ProjectileEntity_Ref = null;
DamageTextYOffset = _buf.ReadInt();
EntityAsyncTimeout = _buf.ReadInt();
CritMultiplier = _buf.ReadFloat();
EnemyBodyRadius = _buf.ReadInt();
EnemySeparationIterations = _buf.ReadInt();
}
@@ -123,6 +125,10 @@ public sealed partial class GlobalConfig : Luban.BeanBase
/// </summary>
public readonly int WeaponRotateSpeed;
/// <summary>
/// 武器基本暴击率
/// </summary>
public readonly float WeaponBaseCrit;
/// <summary>
/// 玩家对象组名
/// </summary>
public readonly string PlayerGroupName;
@@ -180,6 +186,10 @@ public sealed partial class GlobalConfig : Luban.BeanBase
/// </summary>
public readonly int EntityAsyncTimeout;
/// <summary>
/// 暴击伤害倍率
/// </summary>
public readonly float CritMultiplier;
/// <summary>
/// 敌人碰撞半径
/// </summary>
public readonly int EnemyBodyRadius;
@@ -218,6 +228,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
ProjectileEntity_Ref = tables.TbEntityConfig.GetOrDefault(ProjectileEntity);
@@ -225,6 +236,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
}
public override string ToString()
@@ -246,6 +258,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
+ "enemySpawnDistanceMin:" + EnemySpawnDistanceMin + ","
+ "enemySpawnDistanceMax:" + EnemySpawnDistanceMax + ","
+ "weaponRotateSpeed:" + WeaponRotateSpeed + ","
+ "weaponBaseCrit:" + WeaponBaseCrit + ","
+ "playerGroupName:" + PlayerGroupName + ","
+ "enemyGroupName:" + EnemyGroupName + ","
+ "projectileGroupName:" + ProjectileGroupName + ","
@@ -260,6 +273,7 @@ public sealed partial class GlobalConfig : Luban.BeanBase
+ "projectileEntity:" + ProjectileEntity + ","
+ "damageTextYOffset:" + DamageTextYOffset + ","
+ "entityAsyncTimeout:" + EntityAsyncTimeout + ","
+ "critMultiplier:" + CritMultiplier + ","
+ "enemyBodyRadius:" + EnemyBodyRadius + ","
+ "enemySeparationIterations:" + EnemySeparationIterations + ","
+ "}";
@@ -17,7 +17,7 @@ public sealed partial class LevelConfig : Luban.BeanBase
public LevelConfig(ByteBuf _buf)
{
Id = _buf.ReadInt();
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);EnemySpawnConfigs = new System.Collections.Generic.List<enemySpawnConfig>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { enemySpawnConfig _e0; _e0 = enemySpawnConfig.DeserializeenemySpawnConfig(_buf); EnemySpawnConfigs.Add(_e0);}}
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);EnemySpawnConfigs = new System.Collections.Generic.List<EnemySpawnConfig>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { EnemySpawnConfig _e0; _e0 = EnemySpawnConfig.DeserializeEnemySpawnConfig(_buf); EnemySpawnConfigs.Add(_e0);}}
Duration = _buf.ReadInt();
}
@@ -30,7 +30,7 @@ public sealed partial class LevelConfig : Luban.BeanBase
/// 关卡id
/// </summary>
public readonly int Id;
public readonly System.Collections.Generic.List<enemySpawnConfig> EnemySpawnConfigs;
public readonly System.Collections.Generic.List<EnemySpawnConfig> EnemySpawnConfigs;
/// <summary>
/// 关卡时间
/// </summary>
@@ -19,7 +19,7 @@ public sealed partial class LevelUpRewardConfig : Luban.BeanBase
Id = _buf.ReadInt();
Title = _buf.ReadString();
Rarity = (Rarity)_buf.ReadInt();
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);Modifiers = new System.Collections.Generic.List<modifier>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { modifier _e0; _e0 = modifier.Deserializemodifier(_buf); Modifiers.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);}}
Sprite = _buf.ReadString();
Sprite_Ref = null;
}
@@ -41,7 +41,7 @@ public sealed partial class LevelUpRewardConfig : Luban.BeanBase
/// 道具品质
/// </summary>
public readonly Rarity Rarity;
public readonly System.Collections.Generic.List<modifier> Modifiers;
public readonly System.Collections.Generic.List<StatModifierConfig> StatModifierConfigs;
/// <summary>
/// 图标
/// </summary>
@@ -56,7 +56,7 @@ public sealed partial class LevelUpRewardConfig : Luban.BeanBase
foreach (var _e in Modifiers) { _e?.ResolveRef(tables); }
foreach (var _e in StatModifierConfigs) { _e?.ResolveRef(tables); }
Sprite_Ref = tables.TbSpriteConfig.GetOrDefault(Sprite);
}
@@ -66,7 +66,7 @@ public sealed partial class LevelUpRewardConfig : Luban.BeanBase
+ "id:" + Id + ","
+ "title:" + Title + ","
+ "rarity:" + Rarity + ","
+ "modifiers:" + Luban.StringUtil.CollectionToString(Modifiers) + ","
+ "statModifierConfigs:" + Luban.StringUtil.CollectionToString(StatModifierConfigs) + ","
+ "sprite:" + Sprite + ","
+ "}";
}
@@ -0,0 +1,38 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace SepCore.Definition
{
/// <summary>
/// 修饰器类型
/// </summary>
public enum ModifierType : int
{
/// <summary>
/// 无
/// </summary>
None = 0,
/// <summary>
/// 固定值
/// </summary>
Flat = 100,
/// <summary>
/// 百分比累加
/// </summary>
PercentAdd = 200,
/// <summary>
/// 最终百分比独立乘区
/// </summary>
PercentMult = 300,
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a95015722b22de41a817f1d17e7d560
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -20,7 +20,7 @@ public sealed partial class PropConfig : Luban.BeanBase
Name = _buf.ReadString();
Rarity = (Rarity)_buf.ReadInt();
Price = _buf.ReadInt();
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);Modifiers = new System.Collections.Generic.List<modifier>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { modifier _e0; _e0 = modifier.Deserializemodifier(_buf); Modifiers.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);}}
Sprite = _buf.ReadString();
Sprite_Ref = null;
}
@@ -46,7 +46,7 @@ public sealed partial class PropConfig : Luban.BeanBase
/// 道具价格
/// </summary>
public readonly int Price;
public readonly System.Collections.Generic.List<modifier> Modifiers;
public readonly System.Collections.Generic.List<StatModifierConfig> StatModifierConfigs;
/// <summary>
/// 图标
/// </summary>
@@ -62,7 +62,7 @@ public sealed partial class PropConfig : Luban.BeanBase
foreach (var _e in Modifiers) { _e?.ResolveRef(tables); }
foreach (var _e in StatModifierConfigs) { _e?.ResolveRef(tables); }
Sprite_Ref = tables.TbSpriteConfig.GetOrDefault(Sprite);
}
@@ -73,7 +73,7 @@ public sealed partial class PropConfig : Luban.BeanBase
+ "name:" + Name + ","
+ "rarity:" + Rarity + ","
+ "price:" + Price + ","
+ "modifiers:" + Luban.StringUtil.CollectionToString(Modifiers) + ","
+ "statModifierConfigs:" + Luban.StringUtil.CollectionToString(StatModifierConfigs) + ","
+ "sprite:" + Sprite + ","
+ "}";
}
@@ -20,7 +20,7 @@ public sealed partial class RoleConfig : Luban.BeanBase
MaxHealth = _buf.ReadInt();
Speed = _buf.ReadFloat();
Coin = _buf.ReadInt();
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);Modifiers = new System.Collections.Generic.List<modifier>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { modifier _e0; _e0 = modifier.Deserializemodifier(_buf); Modifiers.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);}}
{int n0 = System.Math.Min(_buf.ReadSize(), _buf.Size);InitialItems = new System.Collections.Generic.List<int>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { int _e0; _e0 = _buf.ReadInt(); InitialItems.Add(_e0);}}
Prefab = _buf.ReadString();
Prefab_Ref = null;
@@ -38,9 +38,9 @@ public sealed partial class RoleConfig : Luban.BeanBase
public readonly float Speed;
public readonly int Coin;
/// <summary>
/// key
/// statType
/// </summary>
public readonly System.Collections.Generic.List<modifier> Modifiers;
public readonly System.Collections.Generic.List<StatModifierConfig> StatModifierConfigs;
public readonly System.Collections.Generic.List<int> InitialItems;
public readonly string Prefab;
public EntityConfig Prefab_Ref;
@@ -56,7 +56,7 @@ public sealed partial class RoleConfig : Luban.BeanBase
foreach (var _e in Modifiers) { _e?.ResolveRef(tables); }
foreach (var _e in StatModifierConfigs) { _e?.ResolveRef(tables); }
Prefab_Ref = tables.TbEntityConfig.GetOrDefault(Prefab);
Sprite_Ref = tables.TbSpriteConfig.GetOrDefault(Sprite);
@@ -69,7 +69,7 @@ public sealed partial class RoleConfig : Luban.BeanBase
+ "maxHealth:" + MaxHealth + ","
+ "speed:" + Speed + ","
+ "coin:" + Coin + ","
+ "modifiers:" + Luban.StringUtil.CollectionToString(Modifiers) + ","
+ "statModifierConfigs:" + Luban.StringUtil.CollectionToString(StatModifierConfigs) + ","
+ "initialItems:" + Luban.StringUtil.CollectionToString(InitialItems) + ","
+ "prefab:" + Prefab + ","
+ "sprite:" + Sprite + ","
@@ -0,0 +1,97 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using Luban;
namespace SepCore.Definition
{
public sealed partial class StatConfig : Luban.BeanBase
{
public StatConfig(ByteBuf _buf)
{
StatType = (StatType)_buf.ReadInt();
Name = _buf.ReadString();
DefaultBaseValue = _buf.ReadFloat();
HasMin = _buf.ReadBool();
MinValue = _buf.ReadFloat();
HasMax = _buf.ReadBool();
MaxValue = _buf.ReadFloat();
IsPercentDisplay = _buf.ReadBool();
}
public static StatConfig DeserializeStatConfig(ByteBuf _buf)
{
return new StatConfig(_buf);
}
/// <summary>
/// 属性类型
/// </summary>
public readonly StatType StatType;
/// <summary>
/// 名称
/// </summary>
public readonly string Name;
/// <summary>
/// 默认值
/// </summary>
public readonly float DefaultBaseValue;
/// <summary>
/// 是否有最小值
/// </summary>
public readonly bool HasMin;
/// <summary>
/// 最小值
/// </summary>
public readonly float MinValue;
/// <summary>
/// 是否有最大值
/// </summary>
public readonly bool HasMax;
/// <summary>
/// 最大值
/// </summary>
public readonly float MaxValue;
/// <summary>
/// UI 文本是否百分比
/// </summary>
public readonly bool IsPercentDisplay;
public const int __ID__ = 807092950;
public override int GetTypeId() => __ID__;
public void ResolveRef(Tables tables)
{
}
public override string ToString()
{
return "{ "
+ "statType:" + StatType + ","
+ "name:" + Name + ","
+ "defaultBaseValue:" + DefaultBaseValue + ","
+ "hasMin:" + HasMin + ","
+ "minValue:" + MinValue + ","
+ "hasMax:" + HasMax + ","
+ "maxValue:" + MaxValue + ","
+ "isPercentDisplay:" + IsPercentDisplay + ","
+ "}";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2dfe101008e1ded4ea35681a5a656d2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -15,34 +15,34 @@ namespace SepCore.Definition
/// <summary>
/// 修饰器
/// </summary>
public sealed partial class modifier : Luban.BeanBase
public sealed partial class StatModifierConfig : Luban.BeanBase
{
public modifier(ByteBuf _buf)
public StatModifierConfig(ByteBuf _buf)
{
Key = (StatType)_buf.ReadInt();
Value = _buf.ReadInt();
IsPercent = _buf.ReadBool();
StatType = (StatType)_buf.ReadInt();
Value = _buf.ReadFloat();
ModifierType = (ModifierType)_buf.ReadInt();
}
public static modifier Deserializemodifier(ByteBuf _buf)
public static StatModifierConfig DeserializeStatModifierConfig(ByteBuf _buf)
{
return new modifier(_buf);
return new StatModifierConfig(_buf);
}
/// <summary>
/// 属性类型
/// </summary>
public readonly StatType Key;
public readonly StatType StatType;
/// <summary>
/// 值
/// </summary>
public readonly int Value;
public readonly float Value;
/// <summary>
/// 是否为百分比
/// 修饰器类型
/// </summary>
public readonly bool IsPercent;
public readonly ModifierType ModifierType;
public const int __ID__ = -615513385;
public const int __ID__ = 235118733;
public override int GetTypeId() => __ID__;
public void ResolveRef(Tables tables)
@@ -55,9 +55,9 @@ public sealed partial class modifier : Luban.BeanBase
public override string ToString()
{
return "{ "
+ "key:" + Key + ","
+ "statType:" + StatType + ","
+ "value:" + Value + ","
+ "isPercent:" + IsPercent + ","
+ "modifierType:" + ModifierType + ","
+ "}";
}
}
+42 -14
View File
@@ -23,39 +23,67 @@ namespace SepCore.Definition
/// <summary>
/// 最大生命
/// </summary>
MaxHealth = 1,
MaxHp = 1,
/// <summary>
/// 移动速度
/// 生命恢复
/// </summary>
MovementSpeed = 2,
HpRegen = 2,
/// <summary>
/// 攻击力
/// 生命窃取
/// </summary>
Attack = 3,
LifeSteal = 3,
/// <summary>
/// 防御力
/// 伤害
/// </summary>
Defense = 4,
DamagePercent = 4,
/// <summary>
/// 近战伤害
/// </summary>
MeleeDamage = 5,
/// <summary>
/// 远程伤害
/// </summary>
RangedDamage = 6,
/// <summary>
/// 元素伤害
/// </summary>
ElementalDamage = 7,
/// <summary>
/// 攻击速度
/// </summary>
AttackSpeed = 5,
AttackSpeed = 8,
/// <summary>
/// 暴击率
/// </summary>
Critical = 6,
CritChance = 9,
/// <summary>
/// 暴击伤害
/// 工程学
/// </summary>
CriticalDamage = 7,
Engineering = 10,
/// <summary>
/// 攻击范围
/// </summary>
Range = 11,
/// <summary>
/// 护甲
/// </summary>
Armor = 12,
/// <summary>
/// 闪避率
/// </summary>
Dodge = 8,
Dodge = 13,
/// <summary>
/// 自动吸收范围
/// 移动速度
/// </summary>
AbsorbRange = 9,
MoveSpeed = 14,
/// <summary>
/// 收获
/// </summary>
Harvesting = 15,
/// <summary>
/// 幸运
/// </summary>
Luck = 16,
}
}
@@ -61,6 +61,10 @@ public partial class Tables
/// 界面配置
/// </summary>
public TbWeaponConfig TbWeaponConfig {get; }
/// <summary>
/// 角色属性配置
/// </summary>
public TbStatConfig TbStatConfig {get; }
public Tables(System.Func<string, ByteBuf> loader)
{
@@ -82,6 +86,7 @@ public partial class Tables
TbUIFormConfig = new TbUIFormConfig(loader("tbuiformconfig"));
TbUISoundConfig = new TbUISoundConfig(loader("tbuisoundconfig"));
TbWeaponConfig = new TbWeaponConfig(loader("tbweaponconfig"));
TbStatConfig = new TbStatConfig(loader("tbstatconfig"));
ResolveRef();
}
@@ -105,6 +110,7 @@ public partial class Tables
TbUIFormConfig.ResolveRef(this);
TbUISoundConfig.ResolveRef(this);
TbWeaponConfig.ResolveRef(this);
TbStatConfig.ResolveRef(this);
}
}
@@ -95,6 +95,10 @@ public partial class TbGlobalConfig
/// </summary>
public int WeaponRotateSpeed => _data.WeaponRotateSpeed;
/// <summary>
/// 武器基本暴击率
/// </summary>
public float WeaponBaseCrit => _data.WeaponBaseCrit;
/// <summary>
/// 玩家对象组名
/// </summary>
public string PlayerGroupName => _data.PlayerGroupName;
@@ -151,6 +155,10 @@ public partial class TbGlobalConfig
/// </summary>
public int EntityAsyncTimeout => _data.EntityAsyncTimeout;
/// <summary>
/// 暴击伤害倍率
/// </summary>
public float CritMultiplier => _data.CritMultiplier;
/// <summary>
/// 敌人碰撞半径
/// </summary>
public int EnemyBodyRadius => _data.EnemyBodyRadius;
@@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using Luban;
namespace SepCore.Definition
{
/// <summary>
/// 角色属性配置
/// </summary>
public partial class TbStatConfig
{
private readonly System.Collections.Generic.Dictionary<StatType, StatConfig> _dataMap;
private readonly System.Collections.Generic.List<StatConfig> _dataList;
public TbStatConfig(ByteBuf _buf)
{
_dataMap = new System.Collections.Generic.Dictionary<StatType, StatConfig>();
_dataList = new System.Collections.Generic.List<StatConfig>();
for(int n = _buf.ReadSize() ; n > 0 ; --n)
{
StatConfig _v;
_v = StatConfig.DeserializeStatConfig(_buf);
_dataList.Add(_v);
_dataMap.Add(_v.StatType, _v);
}
}
public System.Collections.Generic.Dictionary<StatType, StatConfig> DataMap => _dataMap;
public System.Collections.Generic.List<StatConfig> DataList => _dataList;
public StatConfig GetOrDefault(StatType key) => _dataMap.TryGetValue(key, out var v) ? v : null;
public StatConfig Get(StatType key) => _dataMap[key];
public StatConfig this[StatType key] => _dataMap[key];
public void ResolveRef(Tables tables)
{
foreach(var _v in _dataList)
{
_v.ResolveRef(tables);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54c58e47ce828d246ae57a00a9704563
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -22,10 +22,11 @@ public sealed partial class WeaponConfig : Luban.BeanBase
Price = _buf.ReadInt();
Attack = _buf.ReadInt();
Cooldown = _buf.ReadInt();
DamageType = (DamageType)_buf.ReadInt();
AttackRange = _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);Modifiers = new System.Collections.Generic.List<modifier>(n0);for(var i0 = 0 ; i0 < n0 ; i0++) { modifier _e0; _e0 = modifier.Deserializemodifier(_buf); Modifiers.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_Ref = null;
Sprite = _buf.ReadString();
@@ -62,6 +63,10 @@ public sealed partial class WeaponConfig : Luban.BeanBase
/// </summary>
public readonly int Cooldown;
/// <summary>
/// 伤害类型
/// </summary>
public readonly DamageType DamageType;
/// <summary>
/// 攻击范围
/// </summary>
public readonly int AttackRange;
@@ -73,7 +78,7 @@ public sealed partial class WeaponConfig : Luban.BeanBase
/// 参数类型
/// </summary>
public readonly System.Collections.Generic.Dictionary<ParamType, int> Parameters;
public readonly System.Collections.Generic.List<modifier> Modifiers;
public readonly System.Collections.Generic.List<StatModifierConfig> StatModifierConfigs;
/// <summary>
/// 预制体
/// </summary>
@@ -99,7 +104,8 @@ public sealed partial class WeaponConfig : Luban.BeanBase
foreach (var _e in Modifiers) { _e?.ResolveRef(tables); }
foreach (var _e in StatModifierConfigs) { _e?.ResolveRef(tables); }
Prefab_Ref = tables.TbEntityConfig.GetOrDefault(Prefab);
Sprite_Ref = tables.TbSpriteConfig.GetOrDefault(Sprite);
}
@@ -113,10 +119,11 @@ public sealed partial class WeaponConfig : Luban.BeanBase
+ "price:" + Price + ","
+ "attack:" + Attack + ","
+ "cooldown:" + Cooldown + ","
+ "damageType:" + DamageType + ","
+ "attackRange:" + AttackRange + ","
+ "attackSoundId:" + AttackSoundId + ","
+ "parameters:" + Luban.StringUtil.CollectionToString(Parameters) + ","
+ "modifiers:" + Luban.StringUtil.CollectionToString(Modifiers) + ","
+ "statModifierConfigs:" + Luban.StringUtil.CollectionToString(StatModifierConfigs) + ","
+ "prefab:" + Prefab + ","
+ "sprite:" + Sprite + ","
+ "}";
@@ -15,18 +15,18 @@ namespace SepCore.Definition
/// <summary>
/// 出怪配置
/// </summary>
public sealed partial class enemySpawnConfig : Luban.BeanBase
public sealed partial class EnemySpawnConfig : Luban.BeanBase
{
public enemySpawnConfig(ByteBuf _buf)
public EnemySpawnConfig(ByteBuf _buf)
{
EnemyType = (EnemyType)_buf.ReadInt();
EnemyCount = _buf.ReadInt();
Interval = _buf.ReadInt();
}
public static enemySpawnConfig DeserializeenemySpawnConfig(ByteBuf _buf)
public static EnemySpawnConfig DeserializeEnemySpawnConfig(ByteBuf _buf)
{
return new enemySpawnConfig(_buf);
return new EnemySpawnConfig(_buf);
}
/// <summary>
@@ -42,7 +42,7 @@ public sealed partial class enemySpawnConfig : Luban.BeanBase
/// </summary>
public readonly int Interval;
public const int __ID__ = -1182208523;
public const int __ID__ = -2024984043;
public override int GetTypeId() => __ID__;
public void ResolveRef(Tables tables)
@@ -192,7 +192,7 @@ namespace SepCore.Editor
EditorGUILayout.LabelField("Title", selectedProp.Name);
EditorGUILayout.LabelField("Rarity", selectedProp.Rarity.ToString());
EditorGUILayout.LabelField("Description");
EditorGUILayout.HelpBox(ItemDescUtility.CreatePropDescription(selectedProp.Modifiers), MessageType.None);
EditorGUILayout.HelpBox(ItemDescUtility.CreatePropDescription(selectedProp.StatModifierConfigs), MessageType.None);
EditorGUILayout.EndScrollView();
}
@@ -47,7 +47,7 @@ namespace SepCore.UI
Title = reward.Title,
Icon = null,
Rarity = reward.Rarity,
Description = ItemDescUtility.CreatePropDescription(reward.Modifiers),
Description = ItemDescUtility.CreatePropDescription(reward.StatModifierConfigs),
IconAssetName = reward.Sprite
});
}
@@ -48,7 +48,7 @@ namespace SepCore.UI
{
var prop = (PropConfig)rawData.DataRow;
Name = prop.Name;
Description = ItemDescUtility.CreatePropDescription(prop.Modifiers);
Description = ItemDescUtility.CreatePropDescription(prop.StatModifierConfigs);
}
}
}
@@ -20,46 +20,52 @@ namespace SepCore.CustomUtility
private static readonly string[] _statTypeNames =
{
"无效",
"最大生命",
"移动速度",
"伤害",
"防御",
"冷却",
"暴击率",
"暴击伤害",
"闪避",
"金币/经验吸收范围"
"无效", // None = 0
"最大生命", // MaxHp = 1
"生命恢复", // HpRegen = 2
"生命窃取", // LifeSteal = 3
"伤害加成", // DamagePercent = 4
"近战伤害", // MeleeDamage = 5
"远程伤害", // RangedDamage = 6
"元素伤害", // ElementalDamage = 7
"攻击速度", // AttackSpeed = 8
"暴击率", // CritChance = 9
"工程学", // Engineering = 10
"攻击范围", // Range = 11
"护甲", // Armor = 12
"闪避率", // Dodge = 13
"移动速度", // MoveSpeed = 14
"收获", // Harvesting = 15
"幸运" // Luck = 16
};
public static string Describe(modifier modifier)
public static string Describe(StatModifierConfig statModifierConfig)
{
if (modifier == null)
if (statModifierConfig == null)
{
return string.Empty;
}
string name = _statTypeNames[(int)modifier.Key];
string colorTag = modifier.Value > 0 ? "green" : "red";
string suffix = modifier.IsPercent ? "%" : string.Empty;
float displayValue;
if (modifier.IsPercent)
int index = (int)statModifierConfig.StatType;
string name = (index >= 0 && index < _statTypeNames.Length) ? _statTypeNames[index] : statModifierConfig.StatType.ToString();
string colorTag = statModifierConfig.Value >= 0 ? "green" : "red";
string sign = statModifierConfig.Value >= 0 ? "+" : "";
if (statModifierConfig.ModifierType == ModifierType.PercentAdd)
{
displayValue = modifier.Value / 10f;
}
else if (modifier.Key == StatType.MovementSpeed)
{
displayValue = modifier.Value / 1000f;
}
else
{
displayValue = modifier.Value;
float displayPercent = statModifierConfig.Value * 100f;
return $"{name}: <color={colorTag}>{sign}{displayPercent:0.##}%</color>";
}
return $"{name}: <color={colorTag}>+{displayValue:0.##}{suffix}</color>";
if (statModifierConfig.ModifierType == ModifierType.PercentMult)
{
return $"{name}: <color={colorTag}>x{statModifierConfig.Value:0.##}</color>";
}
public static string CreatePropDescription(IEnumerable<modifier> modifiers)
return $"{name}: <color={colorTag}>{sign}{statModifierConfig.Value:0.##}</color>";
}
public static string CreatePropDescription(IEnumerable<StatModifierConfig> modifiers)
{
if (modifiers == null)
{
@@ -67,7 +73,7 @@ namespace SepCore.CustomUtility
}
StringBuilder sb = new StringBuilder();
foreach (modifier modifier in modifiers)
foreach (StatModifierConfig modifier in modifiers)
{
sb.Append(Describe(modifier));
sb.Append('\n');
@@ -84,7 +90,7 @@ namespace SepCore.CustomUtility
}
return CreateWeaponDescription(weaponConfig.Attack, weaponConfig.Cooldown / 1000f,
weaponConfig.AttackRange / 1000f, weaponConfig.Modifiers, weaponConfig.Parameters);
weaponConfig.AttackRange / 1000f, weaponConfig.StatModifierConfigs, weaponConfig.Parameters);
}
public static string CreateWeaponDescription(WeaponBase weapon)
@@ -119,7 +125,7 @@ namespace SepCore.CustomUtility
}
private static string CreateWeaponDescription(int attack, float cooldown, float attackRange,
IEnumerable<modifier> modifiers, IReadOnlyDictionary<ParamType, int> parameters)
IEnumerable<StatModifierConfig> modifiers, IReadOnlyDictionary<ParamType, int> parameters)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"攻击: {attack}");
@@ -214,7 +214,7 @@ namespace SepCore.DebugPanel
if (selectedProp == null) return;
GUILayout.Label($"Selected: {selectedProp.Name} ({selectedProp.Rarity})");
GUILayout.Label(ItemDescUtility.CreatePropDescription(selectedProp.Modifiers), GUILayout.Height(40f));
GUILayout.Label(ItemDescUtility.CreatePropDescription(selectedProp.StatModifierConfigs), GUILayout.Height(40f));
GUILayout.BeginHorizontal();
GUILayout.Label("Count", GUILayout.Width(78f));
@@ -255,16 +255,16 @@ namespace SepCore.DebugPanel
if (_showCollisionStats)
{
SimulationWorld simulationWorld = GameEntry.SimulationWorld;
SimulationStats simulationStats = GameEntry.SimulationWorld.Stats;
GUILayout.Space(4f);
GUILayout.Label(
$"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})");
$"Collision Queries: total {simulationStats.LastCollisionQueryCount} (Projectile {simulationStats.LastProjectileCollisionQueryCount} / Area {simulationStats.LastAreaCollisionQueryCount})");
GUILayout.Label(
$"Collision Candidates: total {simulationWorld.LastCollisionCandidateCount} (Projectile {simulationWorld.LastProjectileCollisionCandidateCount} / Area {simulationWorld.LastAreaCollisionCandidateCount})");
$"Collision Candidates: total {simulationStats.LastCollisionCandidateCount} (Projectile {simulationStats.LastProjectileCollisionCandidateCount} / Area {simulationStats.LastAreaCollisionCandidateCount})");
GUILayout.Label(
$"Area Resolve: hits {simulationWorld.LastResolvedAreaHitCount}");
$"Area Resolve: hits {simulationStats.LastResolvedAreaHitCount}");
GUILayout.Label(
$"Broad Phase: cell {simulationWorld.LastCollisionCellSize:F2}, hasEnemyTargets {(simulationWorld.LastCollisionHasEnemyTargets ? "Yes" : "No")}");
$"Broad Phase: cell {simulationStats.LastCollisionCellSize:F2}, hasEnemyTargets {(simulationStats.LastCollisionHasEnemyTargets ? "Yes" : "No")}");
}
if (_showSpawnControls)
@@ -63,7 +63,7 @@ namespace SepCore.Procedure
_enemyManager.OnUpdate(elapseSeconds, realElapseSeconds);
Vector3 playerPosition = Player.CachedTransform.position;
GameEntry.SimulationWorld.Tick(new SimulationTickContext(elapseSeconds, realElapseSeconds, playerPosition));
GameEntry.SimulationWorld.Tick(new SimulationTickContext(elapseSeconds, playerPosition));
int timeLeft = Mathf.Max(0, (int)GameEntry.Timer.GetRemainingTime(_levelTimerHandle));
GameEntry.Event.Fire(this, LevelProcessEventArgs.Create(timeLeft));
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Simulation.EditModeTests")]
[assembly: InternalsVisibleTo("Simulation.PlayModeTests")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d68a67948722984392ea42a1cec3594
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -24,7 +24,7 @@ public partial class GameEntry
public static EnemyManagerComponent EnemyManager { get; private set; }
public static SimulationWorld SimulationWorld { get; private set; }
public static ISimulationWorld SimulationWorld { get; private set; }
public static UIRouterComponent UIRouter { get; private set; }
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 715771c24b633ee49b200700baec66de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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:
@@ -1,5 +1,3 @@
using System;
using SepCore.Definition;
using SepCore.Entity;
using UnityEngine;
@@ -7,85 +5,32 @@ namespace SepCore.Components
{
public class AbsorbComponent : MonoBehaviour
{
[SerializeField] private int _maxDetectCount = 64;
private Player _player;
private Transform _target;
private StatComponent _statComponent;
private Collider[] _buffer;
private StatProperty _absorbRangeStat;
private Action<modifier, bool> _absorbRangeStatCallback;
private float AbsorbRangeBase => GameEntry.Luban.Global.DefaultAbsorbRange / 1000f;
private float AbsorbSpeed => GameEntry.Luban.Global.AbsorbSpeed / 1000f;
private float CollectDistance => GameEntry.Luban.Global.CollectDistance / 1000f;
private float AbsorbRange => Mathf.Max(0f, (AbsorbRangeBase + _absorbRangeStat.Value) * _absorbRangeStat.Percent);
private float AbsorbRange => AbsorbRangeBase;
public void OnInit(Player player, StatComponent statComponent)
{
_player = player;
_target = player.CachedTransform;
_statComponent = statComponent;
_buffer = new Collider[Mathf.Max(1, _maxDetectCount)];
_absorbRangeStat = _statComponent.GetStat(StatType.AbsorbRange);
_absorbRangeStatCallback = (modifier, isApply) =>
_statComponent.UpdateStat(_absorbRangeStat, modifier, isApply);
_statComponent.Subscribe(StatType.AbsorbRange, _absorbRangeStatCallback);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_player.Available) return;
int hitCount = Physics.OverlapSphereNonAlloc(
_target.position,
AbsorbRange,
_buffer,
Constant.Layer.DropLayerMask
);
for (int i = 0; i < hitCount; i++)
{
Collider hit = _buffer[i];
if (hit == null) continue;
CoinEntity coin = hit.GetComponent<CoinEntity>();
if (coin != null && coin.Available)
{
AbsorbDrop(coin.CachedTransform, elapseSeconds);
coin.TryCollect(_player, CollectDistance);
continue;
}
ExpEntity exp = hit.GetComponent<ExpEntity>();
if (exp != null && exp.Available)
{
AbsorbDrop(exp.CachedTransform, elapseSeconds);
exp.TryCollect(_player, CollectDistance);
}
}
GameEntry.SimulationWorld.SyncPlayerAbsorbSettings(AbsorbRange, AbsorbSpeed, CollectDistance);
}
public void OnReset()
{
if (_statComponent != null)
{
_statComponent.Unsubscribe(StatType.AbsorbRange, _absorbRangeStatCallback);
_absorbRangeStatCallback = null;
}
_statComponent = null;
_target = null;
_player = null;
_buffer = null;
}
private void AbsorbDrop(Transform drop, float deltaTime)
{
drop.position = Vector3.MoveTowards(drop.position, _target.position, AbsorbSpeed * deltaTime);
}
}
}
@@ -13,23 +13,17 @@ namespace SepCore.Components
private StatComponent _statComponent;
private StatProperty _maxHealthStat;
private Action<modifier, bool> _maxHealthStatCallback;
public StatProperty MaxHealthStat => _maxHealthStat;
private StatProperty _defenseStat;
private Action<modifier, bool> _defenseStatCallback;
public StatProperty DefenseStat => _defenseStat;
private StatProperty _dodgeStat;
private Action<modifier, bool> _dodgeStatCallback;
public StatProperty DodgeStat => _dodgeStat;
private Action<StatType, float> _onStatChangedAction;
private float _lastLifeStealTime = -1f;
public const float LifeStealIcd = 0.1f;
private bool _isPlayer => _statComponent != null;
public float HealthRatio => MaxHealth > 0 ? (float)CurrentHealth / MaxHealth : 0f;
public int MaxHealth => Mathf.CeilToInt((_maxHealthBase + _maxHealthStat.Value) * _maxHealthStat.Percent);
public int MaxHealth => _statComponent != null
? Mathf.CeilToInt(_statComponent.GetStatValue(StatType.MaxHp))
: _maxHealthBase;
public int CurrentHealth
{
@@ -46,82 +40,115 @@ namespace SepCore.Components
_statComponent = statComponent;
if (_statComponent != null)
{
_maxHealthStat = _statComponent.GetStat(StatType.MaxHealth);
_maxHealthStatCallback = (modifier, isApply) =>
{
_statComponent.UpdateStat(_maxHealthStat, modifier, isApply);
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(0, CurrentHealth, MaxHealth));
};
_statComponent.Subscribe(StatType.MaxHealth, _maxHealthStatCallback);
_defenseStat = _statComponent.GetStat(StatType.Defense);
_defenseStatCallback = (modifier, isApply) =>
_statComponent.UpdateStat(_defenseStat, modifier, isApply);
_statComponent.Subscribe(StatType.Defense, _defenseStatCallback);
_dodgeStat = _statComponent.GetStat(StatType.Dodge);
_dodgeStatCallback = (modifier, isApply) =>
_statComponent.UpdateStat(_dodgeStat, modifier, isApply);
_statComponent.Subscribe(StatType.Dodge, _dodgeStatCallback);
}
else
{
_maxHealthStat = new StatProperty();
_defenseStat = new StatProperty();
_dodgeStat = new StatProperty();
_statComponent.Pipeline.SetBaseValue(StatType.MaxHp, maxHealth);
_onStatChangedAction = OnStatChanged;
_statComponent.Pipeline.OnStatChanged += _onStatChangedAction;
}
}
public void OnReset()
{
if (_statComponent != null)
if (_statComponent != null && _onStatChangedAction != null)
{
_statComponent.Unsubscribe(StatType.MaxHealth, _maxHealthStatCallback);
_maxHealthStatCallback = null;
_statComponent.Unsubscribe(StatType.Defense, _defenseStatCallback);
_defenseStatCallback = null;
_statComponent.Unsubscribe(StatType.Dodge, _dodgeStatCallback);
_dodgeStatCallback = null;
_statComponent.Pipeline.OnStatChanged -= _onStatChangedAction;
_onStatChangedAction = null;
}
_statComponent = null;
_lastLifeStealTime = -1f;
}
private void OnStatChanged(StatType statType, float newValue)
{
if (statType == StatType.MaxHp)
{
if (_currentHealth > MaxHealth)
{
_currentHealth = MaxHealth;
}
if (GameEntry.Event != null)
{
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(0, CurrentHealth, MaxHealth));
}
}
}
#endregion
public void TakeDamage(int damage)
public void TakeDamage(int damage, bool isCrit = false, bool isDodge = false, Vector3? hitPosition = null)
{
if (_isDead) return;
float maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
if (UnityEngine.Random.value < Mathf.Clamp(_dodgeStat.Value, 0f, maxDodge))
if (isDodge)
{
damage = 0;
float yOffset = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.DamageTextYOffset / 1000f
: 1f;
Vector3 pos = hitPosition ?? (transform.position + Vector3.up * yOffset);
if (GameEntry.DamageText != null)
{
GameEntry.DamageText.ShowDamage(pos, 0, false, true);
}
else
{
int defenseValue = (int)_defenseStat.Value;
float defensePercent = _defenseStat.Percent;
damage -= defenseValue;
damage = Mathf.CeilToInt(damage / defensePercent);
int minDamage = GameEntry.Luban.Global.MinDamage;
if (damage < minDamage) damage = minDamage;
return;
}
_currentHealth -= damage;
CauseDamage(damage);
if (_isPlayer)
if (damage <= 0) return;
_currentHealth = Mathf.Max(0, _currentHealth - damage);
CauseDamage(damage, isCrit, hitPosition);
if (_isPlayer && GameEntry.Event != null)
{
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(damage, CurrentHealth, MaxHealth));
}
}
private void CauseDamage(int damage)
public void TakeDamage(int damage)
{
TakeDamage(damage, false, false, null);
}
public void Heal(int healAmount)
{
if (_isDead || healAmount <= 0) return;
int oldHealth = _currentHealth;
_currentHealth = Mathf.Min(_currentHealth + healAmount, MaxHealth);
int healed = _currentHealth - oldHealth;
if (healed > 0 && _isPlayer && GameEntry.Event != null)
{
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(-healed, CurrentHealth, MaxHealth));
}
}
public bool TryTriggerLifeSteal(int healAmount, float? customTime = null)
{
if (_isDead || healAmount <= 0) return false;
float currentTime = customTime ?? Time.time;
if (_lastLifeStealTime >= 0f && currentTime - _lastLifeStealTime < LifeStealIcd)
{
return false;
}
_lastLifeStealTime = currentTime;
Heal(healAmount);
return true;
}
private void CauseDamage(int damage, bool isCrit = false, Vector3? hitPosition = null)
{
if (damage <= 0) return;
float yOffset = GameEntry.Luban.Global.DamageTextYOffset / 1000f;
GameEntry.DamageText.ShowDamage(transform.position + Vector3.up * yOffset, damage);
float yOffset = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.DamageTextYOffset / 1000f
: 1f;
Vector3 pos = hitPosition ?? (transform.position + Vector3.up * yOffset);
if (GameEntry.DamageText != null)
{
GameEntry.DamageText.ShowDamage(pos, damage, isCrit, false);
}
}
}
}
@@ -16,7 +16,7 @@ namespace SepCore.Components
[SerializeField] private float _enemyBodyRadius;
[SerializeField] private int _separationIterations;
public float Speed => (_speedBase + _movementStat.Value) * _movementStat.Percent;
public float Speed => _statComponent != null ? _statComponent.GetStatValue(StatType.MoveSpeed) : _speedBase;
public bool AvoidEnemyOverlap => _avoidEnemyOverlap;
public float EnemyBodyRadius => _enemyBodyRadius;
public int SeparationIterations => _separationIterations;
@@ -28,9 +28,7 @@ namespace SepCore.Components
private StatComponent _statComponent;
private Player _player;
private EnemyBase _enemy;
private StatProperty _movementStat;
private Action<modifier, bool> _movementStatCallback;
private Action<StatType, float> _onStatChangedAction;
public void OnInit(float speed, Transform target, StatComponent statComponent = null,
bool avoidEnemyOverlap = false, float? enemyBodyRadius = null, int? separationIterations = null)
@@ -39,8 +37,15 @@ namespace SepCore.Components
_cachedTransform = target;
_direction = Vector3.forward;
_avoidEnemyOverlap = avoidEnemyOverlap;
_enemyBodyRadius = Mathf.Max(0.01f, enemyBodyRadius ?? (GameEntry.Luban.Global.EnemyBodyRadius / 1000f));
_separationIterations = Mathf.Max(1, separationIterations ?? GameEntry.Luban.Global.EnemySeparationIterations);
float defaultRadius = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemyBodyRadius / 1000f
: 0.5f;
int defaultIterations = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemySeparationIterations
: 4;
_enemyBodyRadius = Mathf.Max(0.01f, enemyBodyRadius ?? defaultRadius);
_separationIterations = Mathf.Max(1, separationIterations ?? defaultIterations);
TryGetComponent(out _player);
TryGetComponent(out _enemy);
@@ -48,17 +53,9 @@ namespace SepCore.Components
_statComponent = statComponent;
if (_statComponent != null)
{
_movementStat = _statComponent.GetStat(StatType.MovementSpeed);
_movementStatCallback = (modifier, isApply) =>
{
_statComponent.UpdateStat(_movementStat, modifier, isApply);
SyncToSimulationWorld();
};
_statComponent.Subscribe(StatType.MovementSpeed, _movementStatCallback);
}
else
{
_movementStat = new StatProperty();
_statComponent.Pipeline.SetBaseValue(StatType.MoveSpeed, speed);
_onStatChangedAction = OnStatChanged;
_statComponent.Pipeline.OnStatChanged += _onStatChangedAction;
}
SyncToSimulationWorld();
@@ -73,25 +70,32 @@ namespace SepCore.Components
{
Transform transformToUnregister = _cachedTransform;
float defaultRadius = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemyBodyRadius / 1000f
: 0.5f;
int defaultIterations = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemySeparationIterations
: 4;
_speedBase = 0;
_cachedTransform = null;
_direction = Vector3.zero;
_isMoving = false;
_avoidEnemyOverlap = false;
_enemyBodyRadius = GameEntry.Luban.Global.EnemyBodyRadius / 1000f;
_separationIterations = GameEntry.Luban.Global.EnemySeparationIterations;
_enemyBodyRadius = defaultRadius;
_separationIterations = defaultIterations;
_player = null;
_enemy = null;
if (_statComponent != null)
if (_statComponent != null && _onStatChangedAction != null)
{
_statComponent.Unsubscribe(StatType.MovementSpeed, _movementStatCallback);
_movementStatCallback = null;
_statComponent.Pipeline.OnStatChanged -= _onStatChangedAction;
_onStatChangedAction = null;
}
_statComponent = null;
if (transformToUnregister != null)
if (transformToUnregister != null && GameEntry.SimulationWorld != null)
{
GameEntry.SimulationWorld.UnregisterPlayerMovement(transformToUnregister);
}
@@ -113,7 +117,7 @@ namespace SepCore.Components
{
using (CustomProfilerMarker.Movement_Update.Auto())
{
if (_cachedTransform == null)
if (_cachedTransform == null || GameEntry.SimulationWorld == null)
{
return;
}
@@ -138,5 +142,13 @@ namespace SepCore.Components
}
}
}
private void OnStatChanged(StatType statType, float newValue)
{
if (statType == StatType.MoveSpeed)
{
SyncToSimulationWorld();
}
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using SepCore.Battle;
using SepCore.Definition;
using UnityEngine;
@@ -7,8 +8,12 @@ namespace SepCore.Components
{
public class StatComponent : MonoBehaviour
{
private readonly Dictionary<StatType, List<modifier>> _statModifiers = new();
private readonly Dictionary<StatType, Action<modifier, bool>> _statModifierCallbacks = new();
private readonly AttributePipeline _pipeline = new();
/// <summary>
/// 纯逻辑属性修饰器管线。
/// </summary>
public AttributePipeline Pipeline => _pipeline;
public void OnInit()
{
@@ -16,82 +21,42 @@ namespace SepCore.Components
public void OnReset()
{
_statModifiers.Clear();
_statModifierCallbacks.Clear();
_pipeline.Clear();
}
public void AddModifier(modifier modifier)
public void AddModifier(StatType statType, StatModifier modifier)
{
if (!_statModifiers.TryGetValue(modifier.StatType, out var modifiers))
{
modifiers = new List<modifier>();
_statModifiers.Add(modifier.StatType, modifiers);
}
modifiers.Add(modifier);
if (_statModifierCallbacks.TryGetValue(modifier.StatType, out var callbacks))
callbacks?.Invoke(modifier, true);
_pipeline.AddModifier(statType, modifier);
}
public void RemoveModifier(modifier modifier)
public bool RemoveModifier(StatType statType, StatModifier modifier)
{
if (_statModifiers.TryGetValue(modifier.StatType, out var modifiers))
{
modifiers.Remove(modifier);
return _pipeline.RemoveModifier(statType, modifier);
}
if (_statModifierCallbacks.TryGetValue(modifier.StatType, out var callbacks))
callbacks?.Invoke(modifier, false);
public void AddModifier(StatModifierConfig statModifierConfig, object source = null)
{
_pipeline.AddModifier(statModifierConfig, source);
}
public void Subscribe(StatType statType, Action<modifier, bool> callback)
public void RemoveModifier(StatModifierConfig statModifierConfig, object source = null)
{
if (!_statModifierCallbacks.TryAdd(statType, callback))
{
_statModifierCallbacks[statType] += callback;
}
_pipeline.RemoveModifier(statModifierConfig, source);
}
public void Unsubscribe(StatType statType, Action<modifier, bool> callback)
public int RemoveAllModifiersFromSource(object source)
{
if (!_statModifierCallbacks.TryGetValue(statType, out var callbacks))
{
return;
return _pipeline.RemoveAllModifiersFromSource(source);
}
callbacks -= callback;
if (callbacks == null)
public float GetStatValue(StatType statType)
{
_statModifierCallbacks.Remove(statType);
}
return _pipeline.GetValue(statType);
}
public StatProperty GetStat(StatType statType)
public CharacterStat GetCharacterStat(StatType statType)
{
var result = new StatProperty();
if (!_statModifiers.TryGetValue(statType, out var modifiers)) return result;
foreach (var modifier in modifiers)
{
if (modifier.IsPercent) result.Percent += modifier.RealValue;
else result.Value += modifier.RealValue;
}
return result;
}
public void UpdateStat(StatProperty stat, modifier modifier, bool isApply)
{
float coef = isApply ? 1f : -1f;
if (modifier.IsPercent)
{
stat.Percent += modifier.RealValue * coef;
}
else
{
stat.Value += modifier.RealValue * coef;
}
return _pipeline.Get(statType);
}
}
}
@@ -28,12 +28,12 @@ namespace SepCore.DamageText
_instancePoolCapacity);
}
public void ShowDamage(Vector3 worldPosition, int damage)
public void ShowDamage(Vector3 worldPosition, int damage, bool isCrit = false, bool isDodge = false)
{
if (damage <= 0) return;
if (damage <= 0 && !isDodge) return;
DamageTextItem item = CreateDamageTextItem();
item.Show(worldPosition, damage, _canvas, OnItemComplete);
item.Show(worldPosition, damage, isCrit, isDodge, _canvas, OnItemComplete);
_activeDamageTextItems.Add(item);
}
@@ -12,6 +12,8 @@ namespace SepCore.DamageText
private const float FadeDelay = 0.2f;
private const float FadeDuration = 0.35f;
private static readonly Color NormalColor = new Color(1f, 0.32f, 0.23f, 1f);
private static readonly Color CritColor = new Color(1f, 0.85f, 0.15f, 1f);
private static readonly Color DodgeColor = new Color(0.75f, 0.75f, 0.75f, 1f);
[SerializeField] private RectTransform _cachedTransform = null;
@@ -22,7 +24,7 @@ namespace SepCore.DamageText
private Sequence _sequence;
private Action<DamageTextItem> _onComplete;
public void Show(Vector3 worldPosition, int damage, Canvas canvas, Action<DamageTextItem> onComplete)
public void Show(Vector3 worldPosition, int damage, bool isCrit, bool isDodge, Canvas canvas, Action<DamageTextItem> onComplete)
{
_onComplete = onComplete;
gameObject.SetActive(true);
@@ -30,8 +32,27 @@ namespace SepCore.DamageText
_canvasGroup.alpha = 1f;
_cachedTransform.localScale = Vector3.one;
Vector3 punchScale;
if (isDodge)
{
_text.color = DodgeColor;
_text.text = "MISS";
punchScale = new Vector3(0.1f, 0.1f, 0f);
}
else if (isCrit)
{
_text.color = CritColor;
_text.text = damage.ToString() + "!";
_cachedTransform.localScale = new Vector3(1.3f, 1.3f, 1f);
punchScale = new Vector3(0.35f, 0.35f, 0f);
}
else
{
_text.color = NormalColor;
_text.text = damage.ToString();
punchScale = new Vector3(0.15f, 0.15f, 0f);
}
Vector3 screenPos = GameEntry.Scene.MainCamera.WorldToScreenPoint(worldPosition);
RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)canvas.transform, screenPos,
@@ -42,11 +63,16 @@ namespace SepCore.DamageText
_sequence = DOTween.Sequence();
_sequence.Append(_cachedTransform.DOAnchorPosY(localPos.y + RiseDistance, RiseDuration)
.SetEase(Ease.OutQuad));
_sequence.Join(_cachedTransform.DOPunchScale(new Vector3(0.15f, 0.15f, 0f), 0.18f, 6, 0.75f));
_sequence.Join(_cachedTransform.DOPunchScale(punchScale, 0.18f, 6, 0.75f));
_sequence.Join(_canvasGroup.DOFade(0f, FadeDuration).SetDelay(FadeDelay));
_sequence.OnComplete(() => _onComplete(this));
}
public void Show(Vector3 worldPosition, int damage, Canvas canvas, Action<DamageTextItem> onComplete)
{
Show(worldPosition, damage, false, false, canvas, onComplete);
}
public void ResetItem()
{
KillSequence();
@@ -35,7 +35,8 @@ namespace SepCore.CustomComponent
"tbspriteconfig",
"tbuiformconfig",
"tbuisoundconfig",
"tbweaponconfig"
"tbweaponconfig",
"tbstatconfig"
};
private sealed class TableAccessor
@@ -133,6 +134,11 @@ namespace SepCore.CustomComponent
Accessor(tables => id => tables.TbWeaponConfig.GetOrDefault((WeaponType)id),
tables => tables.TbWeaponConfig.DataList)
},
{
typeof(StatConfig),
Accessor(tables => id => tables.TbStatConfig.GetOrDefault((StatType)id),
tables => tables.TbStatConfig.DataList)
},
};
private static TableAccessor Accessor<TValue>(Func<Tables, Func<int, object>> rowGetterFactory,
@@ -1,3 +1,4 @@
using SepCore.Battle;
using SepCore.Components;
using SepCore.Debugger;
using SepCore.CustomUtility;
@@ -143,16 +144,22 @@ namespace SepCore.Simulation
return false;
}
damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat,
targetImpact.DefenseStat,
targetImpact.DodgeStat);
shouldDispatchPresentation = true;
if (damage <= 0)
DamageContext context = new DamageContext
{
return true;
}
BaseDamage = sourceImpact.AttackBase,
DamageScalingStat = sourceImpact.DamageType.ToScalingStat(),
ScalingRatio = sourceImpact.ScalingRatio,
BaseCritChance = sourceImpact.BaseCritChance,
CritMultiplier = sourceImpact.CritMultiplier,
AttackerPipeline = sourceImpact.AttributeProvider,
DefenderPipeline = targetImpact.AttributeProvider
};
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, damage);
DamageResult result = DamagePipeline.Calculate(in context);
damage = result.Damage;
shouldDispatchPresentation = true;
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, in result, hitPosition);
return true;
}
@@ -18,6 +18,7 @@ namespace SepCore.Simulation
[SerializeField]
private bool _projectileHitMarkerEnabled = true;
#pragma warning disable CS0414
[Tooltip("命中标记尺寸。")][SerializeField] private float _projectileHitMarkerSize = 0.2f;
[Tooltip("命中标记在目标上的高度偏移。")]
@@ -27,6 +28,7 @@ namespace SepCore.Simulation
[Tooltip("命中标记持续时间。")]
[SerializeField]
private float _projectileHitMarkerDuration = 0.15f;
#pragma warning restore CS0414
[Tooltip("命中标记颜色。")][SerializeField] private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
@@ -1,3 +1,4 @@
using SepCore.Battle;
using SepCore.CustomUtility;
using SepCore.Definition;
using UnityEngine;
@@ -145,9 +146,19 @@ namespace SepCore.Entity
return;
}
int damage = AIUtility.CalcDamageHP(AttackDamage, null, targetImpactData.DefenseStat,
targetImpactData.DodgeStat);
_targetableTarget.ApplyDamage(this, damage);
DamageContext context = new DamageContext
{
BaseDamage = AttackDamage,
DamageScalingStat = StatType.None,
ScalingRatio = 1.0f,
BaseCritChance = 0f,
CritMultiplier = 0f,
AttackerPipeline = selfImpactData.AttributeProvider,
DefenderPipeline = targetImpactData.AttributeProvider
};
DamageResult result = DamagePipeline.Calculate(in context);
_targetableTarget.ApplyDamage(this, in result, _targetableTarget.CachedTransform.position);
}
}
}
@@ -169,7 +169,7 @@ namespace SepCore.Entity
AddWeaponAsync(EntityDataFactory.Create(GameEntry.Entity.NextId(), WeaponType.Knife, Id, _playerData.Camp)).Forget();
// StatComponent
foreach (var modifier in role.Modifiers)
foreach (var modifier in role.StatModifierConfigs)
{
_statComponent.AddModifier(modifier);
}
@@ -189,7 +189,7 @@ namespace SepCore.Entity
public override ImpactData GetImpactData()
{
return new ImpactData(_playerData.Camp, 0, null, _healthComponent.DefenseStat, _healthComponent.DodgeStat);
return new ImpactData(_playerData.Camp, 0, _statComponent.Pipeline);
}
protected override void OnInit(object userData)
@@ -1,4 +1,5 @@
using SepCore.Components;
using SepCore.Battle;
using SepCore.Components;
using SepCore.Definition;
using SepCore.CustomUtility;
using UnityEngine;
@@ -18,22 +19,54 @@ namespace SepCore.Entity
public abstract ImpactData GetImpactData();
public void ApplyDamage(EntityBase attacker, int damageHP)
public void ApplyDamage(EntityBase attacker, in DamageResult damageResult, Vector3? hitPosition = null)
{
if (damageResult.IsDodge)
{
_healthComponent.TakeDamage(0, false, true, hitPosition);
return;
}
float fromHPRatio = _healthComponent.HealthRatio;
_healthComponent.TakeDamage(damageHP);
_healthComponent.TakeDamage(damageResult.Damage, damageResult.IsCrit, false, hitPosition);
float toHPRatio = _healthComponent.HealthRatio;
if (fromHPRatio > toHPRatio)
{
GameEntry.HPBar.ShowHPBar(this, fromHPRatio, toHPRatio);
}
if (damageResult.IsLifeSteal && attacker != null)
{
HealthComponent attackerHealth = attacker.GetComponent<HealthComponent>();
if (attackerHealth == null && attacker is WeaponBase weapon)
{
attackerHealth = weapon.GetComponentInParent<HealthComponent>();
}
if (attackerHealth != null)
{
attackerHealth.TryTriggerLifeSteal(damageResult.LifeStealAmount);
}
}
if (_healthComponent.CurrentHealth <= 0)
{
OnDead(attacker);
}
}
public void ApplyDamage(EntityBase attacker, int damageHP)
{
ApplyDamage(attacker, new DamageResult
{
Damage = damageHP,
IsDodge = false,
IsCrit = false,
IsLifeSteal = false,
LifeStealAmount = 0
});
}
protected override void OnInit(object userData)
{
base.OnInit(userData);
@@ -43,13 +43,7 @@ namespace SepCore.Entity
return false;
}
var simulationWorld = GameEntry.SimulationWorld;
if (simulationWorld == null)
{
return false;
}
if (!simulationWorld.TryGetNearestEnemyEntityId(weapon.CachedTransform.position, maxSqrRange,
if (!GameEntry.SimulationWorld.TryGetNearestEnemyEntityId(weapon.CachedTransform.position, maxSqrRange,
out int entityId))
{
return false;
@@ -27,16 +27,33 @@ namespace SepCore.Entity
protected EntityBase _target;
private float _sqrRange;
public EntityBase Target => _target;
public float SqrRange => _sqrRange;
public float CurrentCooldown
{
get
{
if (WeaponData == null) return 0f;
float attackSpeed = _ownerAttributeProvider?.GetValue(StatType.AttackSpeed) ?? 0f;
float speedFactor = Mathf.Max(0.2f, 1f + attackSpeed);
return Mathf.Max(0.02f, WeaponData.Cooldown / speedFactor);
}
}
protected StatProperty AttackStat { get; private set; } = new();
public float CurrentAttackRange
{
get
{
if (WeaponData == null) return 0f;
float rangeBonus = _ownerAttributeProvider?.GetValue(StatType.Range) ?? 0f;
float effectiveBonus = rangeBonus >= 50f ? rangeBonus / 1000f : rangeBonus;
return Mathf.Max(0.5f, WeaponData.AttackRange + effectiveBonus);
}
}
private StatComponent _attackStatComponent;
private System.Action<modifier, bool> _attackStatCallback;
public float SqrRange => CurrentAttackRange * CurrentAttackRange;
private IAttributeProvider _ownerAttributeProvider;
private static float _rotateSpeed => GameEntry.Luban.Global.WeaponRotateSpeed;
@@ -63,8 +80,7 @@ namespace SepCore.Entity
return;
}
StartAttackCooldown(WeaponData.Cooldown);
_sqrRange = WeaponData.AttackRange * WeaponData.AttackRange;
StartAttackCooldown(CurrentCooldown);
_cachedRotation = CachedTransform.rotation;
if (WeaponData.OwnerCamp == CampType.Player)
@@ -136,14 +152,33 @@ namespace SepCore.Entity
public virtual ImpactData GetImpactData()
{
return new ImpactData(WeaponData.OwnerCamp, WeaponData.Attack, AttackStat);
float baseCrit = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.WeaponBaseCrit
: 0.05f;
float critMult = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.CritMultiplier
: 1.5f;
return new ImpactData(
WeaponData.OwnerCamp,
WeaponData.Attack,
_ownerAttributeProvider,
WeaponData.DamageType,
1.0f,
baseCrit,
critMult);
}
public abstract void Attack();
public void ResetAttackCooldown()
{
ResetAttackCooldown(CurrentCooldown);
}
public virtual void Check()
{
_target = SelectTarget(_sqrRange);
_target = SelectTarget(SqrRange);
}
public virtual void RotateToTarget(float elapseSeconds)
@@ -272,30 +307,18 @@ namespace SepCore.Entity
protected void BindAttackStatFromOwner(EntityLogic parentEntity)
{
ReleaseAttackStatSubscription();
AttackStat = new StatProperty();
if (parentEntity is not Player player) return;
var statComponent = player.GetComponent<StatComponent>();
if (statComponent == null) return;
AttackStat = statComponent.GetStat(StatType.Attack);
_attackStatComponent = statComponent;
_attackStatCallback = (modifier, isApply) =>
statComponent.UpdateStat(AttackStat, modifier, isApply);
statComponent.Subscribe(StatType.Attack, _attackStatCallback);
_ownerAttributeProvider = statComponent.Pipeline;
}
protected void ReleaseAttackStatSubscription()
{
if (_attackStatComponent != null && _attackStatCallback != null)
{
_attackStatComponent.Unsubscribe(StatType.Attack, _attackStatCallback);
}
_attackStatComponent = null;
_attackStatCallback = null;
AttackStat = new StatProperty();
_ownerAttributeProvider = null;
}
}
}
@@ -55,7 +55,7 @@ namespace SepCore.Entity
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset);
Vector3 directionToTarget = targetPosition - fireOrigin;
float maxDistance = WeaponData.AttackRange;
float maxDistance = CurrentAttackRange;
if (directionToTarget.sqrMagnitude > Mathf.Epsilon &&
Physics.Raycast(fireOrigin, directionToTarget.normalized, out RaycastHit hit, maxDistance, _hitMask,
QueryTriggerInteraction.Collide))
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -42,7 +42,7 @@ namespace SepCore.Entity
_attackCenter = _target != null ? _target.CachedTransform.position : CachedTransform.position;
CachedTransform.SetParent(null);
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * WeaponData.AttackRange;
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * CurrentAttackRange;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(targetPos, _attackDuration).SetEase(Ease.OutQuad));
_attackSequence.AppendCallback(() =>
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -57,7 +57,7 @@ namespace SepCore.Entity
CachedTransform.DORotateQuaternion(endRotation, _attackDuration).SetEase(Ease.OutSine));
_attackSequence.InsertCallback(_attackDuration * 0.5f, () =>
{
_attackEffect.Play(this, _attackCenter, _target, WeaponData.AttackRange);
_attackEffect.Play(this, _attackCenter, _target, CurrentAttackRange);
ApplySectorDamage();
});
_attackSequence.Append(CachedTransform.DORotateQuaternion(_cachedAttackRotation, _returnDuration)
@@ -71,7 +71,7 @@ namespace SepCore.Entity
private void ApplySectorDamage()
{
float attackRadius = WeaponData.AttackRange;
float attackRadius = CurrentAttackRange;
if (attackRadius <= 0f) return;
Vector3 forward = CachedTransform.forward;
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -125,7 +125,7 @@ namespace SepCore.UI
private void ApplyReward(LevelUpRewardConfig reward)
{
_player.AddProp(new PropItem(reward.Modifiers, reward.Rarity, reward.Title, reward.Sprite));
_player.AddProp(new PropItem(reward.StatModifierConfigs, reward.Rarity, reward.Title, reward.Sprite));
}
private void BuildRewardRarityPools()
@@ -76,7 +76,7 @@ namespace SepCore.UI
selectedRoleData = new ShowAreaRawData
{
Name = selectedRole.Name,
InitialProperties = selectedRole.Modifiers
InitialProperties = selectedRole.StatModifierConfigs
};
}
}
@@ -6,6 +6,6 @@ namespace SepCore.UI
public class ShowAreaRawData
{
public string Name;
public List<modifier> InitialProperties;
public List<StatModifierConfig> InitialProperties;
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SepCore.Battle;
using SepCore.Definition;
using SepCore.Entity;
using UnityEngine;
@@ -209,10 +210,19 @@ namespace SepCore.CustomUtility
return;
}
int entityDamageHP = CalcDamageHP(projectileImpactData.AttackBase, projectileImpactData.AttackStat,
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
DamageContext context = new DamageContext
{
BaseDamage = projectileImpactData.AttackBase,
DamageScalingStat = projectileImpactData.DamageType.ToScalingStat(),
ScalingRatio = projectileImpactData.ScalingRatio,
BaseCritChance = projectileImpactData.BaseCritChance,
CritMultiplier = projectileImpactData.CritMultiplier,
AttackerPipeline = projectileImpactData.AttributeProvider,
DefenderPipeline = entityImpactData.AttributeProvider
};
entity.ApplyDamage(enemyProjectile, entityDamageHP);
DamageResult result = DamagePipeline.Calculate(in context);
entity.ApplyDamage(enemyProjectile, in result, enemyProjectile.CachedTransform.position);
enemyProjectile.Expire();
return;
}
@@ -228,43 +238,23 @@ namespace SepCore.CustomUtility
return;
}
int entityDamageHP = CalcDamageHP(weaponImpactData.AttackBase, weaponImpactData.AttackStat,
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
DamageContext context = new DamageContext
{
BaseDamage = weaponImpactData.AttackBase,
DamageScalingStat = weaponImpactData.DamageType.ToScalingStat(),
ScalingRatio = weaponImpactData.ScalingRatio,
BaseCritChance = weaponImpactData.BaseCritChance,
CritMultiplier = weaponImpactData.CritMultiplier,
AttackerPipeline = weaponImpactData.AttributeProvider,
DefenderPipeline = entityImpactData.AttributeProvider
};
entity.ApplyDamage(weapon, entityDamageHP);
DamageResult result = DamagePipeline.Calculate(in context);
entity.ApplyDamage(weapon, in result, entity.CachedTransform.position);
return;
}
}
public static int CalcDamageHP(int attack, StatProperty attackStat, StatProperty defenseStat,
StatProperty dodgeStat)
{
// 1. 处理闪避(从全局表读取闪避率上限)
float maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
if (dodgeStat != null)
{
if (Random.value < Mathf.Clamp(dodgeStat.Value, 0f, maxDodge)) return 0;
}
// 2. 处理攻击加成 最终伤害 = (基础伤害 + 伤害提升固定值) * 伤害提升率
float damage = attack;
if (attackStat != null)
{
damage = (attack + attackStat.Value) * attackStat.Percent;
}
// 3. 处理防御加成 最终伤害 = (基础伤害 - 防御提升固定值) / 防御提升率
if (defenseStat != null)
{
damage = (damage - defenseStat.Value) / defenseStat.Percent;
}
// 4. 将伤害限制为保底伤害,避免堆防御导致无法造成伤害
int minDamage = GameEntry.Luban.Global.MinDamage;
if (damage < minDamage) return minDamage;
return Mathf.CeilToInt(damage);
}
[StructLayout(LayoutKind.Auto)]
private struct CampPair
{
+3 -1
View File
@@ -726,7 +726,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_FrameRate
value: 240
value: 60
objectReference: {fileID: 0}
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EditorLanguage
@@ -1411,6 +1411,8 @@ MonoBehaviour:
_projectileHitMarkerColor: {r: 1, g: 0, b: 0, a: 0.95}
_projectileHitEffectEnabled: 0
_projectileHitEffectTypeId: 0
_fixedStepEnabled: 1
_fixedStepSeconds: 0.016666668
_collisionPipelineSettings:
ProjectileCollisionQueryRadius: 0.35
ProjectileMaxCandidatesPerQuery: 1
@@ -119,8 +119,7 @@ namespace SepCore.InputModule.Tests
[Test]
public void RegisterListener_AddsToInternalDictionary()
{
bool called = false;
Action<InputCommand> listener = _ => called = true;
Action<InputCommand> listener = _ => { };
_component.RegisterListener(InputActionId.Pause, listener);
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8388e50452a240f4da478e849742b711
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d80647c93ec8e7b47bef0eb7da8fd6ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More