diff --git a/Assets/GameMain/DataTables/tbglobalconfig.bytes b/Assets/GameMain/DataTables/tbglobalconfig.bytes
index 2020384..60f8db9 100644
Binary files a/Assets/GameMain/DataTables/tbglobalconfig.bytes and b/Assets/GameMain/DataTables/tbglobalconfig.bytes differ
diff --git a/Assets/GameMain/DataTables/tbglobalconfig.json b/Assets/GameMain/DataTables/tbglobalconfig.json
index 1b726f7..d97e382 100644
--- a/Assets/GameMain/DataTables/tbglobalconfig.json
+++ b/Assets/GameMain/DataTables/tbglobalconfig.json
@@ -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
}
diff --git a/Assets/GameMain/DataTables/tbleveluprewardconfig.bytes b/Assets/GameMain/DataTables/tbleveluprewardconfig.bytes
index c2ad59d..d94b33a 100644
--- a/Assets/GameMain/DataTables/tbleveluprewardconfig.bytes
+++ b/Assets/GameMain/DataTables/tbleveluprewardconfig.bytes
@@ -1 +1 @@
-ƒé肌肉2肌肉ƒê牙齿2牙齿ƒë脑2脑ƒì鼻子 2鼻子ƒí腿2腿ƒî眼睛2眼睛ƒï肌肉€–肌肉ƒð牙齿€–牙齿ƒñ脑€–脑ƒò鼻子 €–鼻子ƒó腿€–腿ƒô眼睛€–眼睛ƒõ肌肉�,肌肉ƒö牙齿�,牙齿ƒ÷脑�,脑ƒø鼻子 �,鼻子ƒù腿�,腿ƒú眼睛�,眼睛
\ No newline at end of file
+ƒé肌肉ÍÌL=€È肌肉ƒê牙齿ÍÌL=€È牙齿ƒë脑 ÍÌL=€È脑ƒì鼻子ÍÌL=€È鼻子ƒí腿ÍÌL=€È腿ƒî眼睛ÍÌL=€È眼睛ƒï肌肉š™>€È肌肉ƒð牙齿š™>€È牙齿ƒñ脑 š™>€È脑ƒò鼻子š™>€È鼻子ƒó腿š™>€È腿ƒô眼睛š™>€È眼睛ƒõ肌肉š™™>€È肌肉ƒö牙齿š™™>€È牙齿ƒ÷脑 š™™>€È脑ƒø鼻子š™™>€È鼻子ƒù腿š™™>€È腿ƒú眼睛š™™>€È眼睛
\ No newline at end of file
diff --git a/Assets/GameMain/DataTables/tbleveluprewardconfig.json b/Assets/GameMain/DataTables/tbleveluprewardconfig.json
index 1fcb8c4..98dba9b 100644
--- a/Assets/GameMain/DataTables/tbleveluprewardconfig.json
+++ b/Assets/GameMain/DataTables/tbleveluprewardconfig.json
@@ -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": "眼睛"
diff --git a/Assets/GameMain/DataTables/tbpropconfig.bytes b/Assets/GameMain/DataTables/tbpropconfig.bytes
index 4f35c7f..570fd8e 100644
Binary files a/Assets/GameMain/DataTables/tbpropconfig.bytes and b/Assets/GameMain/DataTables/tbpropconfig.bytes differ
diff --git a/Assets/GameMain/DataTables/tbpropconfig.json b/Assets/GameMain/DataTables/tbpropconfig.json
index dd623c7..837f278 100644
--- a/Assets/GameMain/DataTables/tbpropconfig.json
+++ b/Assets/GameMain/DataTables/tbpropconfig.json
@@ -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": "战神勋章"
diff --git a/Assets/GameMain/DataTables/tbroleconfig.bytes b/Assets/GameMain/DataTables/tbroleconfig.bytes
index 753589a..14cdde0 100644
Binary files a/Assets/GameMain/DataTables/tbroleconfig.bytes and b/Assets/GameMain/DataTables/tbroleconfig.bytes differ
diff --git a/Assets/GameMain/DataTables/tbroleconfig.json b/Assets/GameMain/DataTables/tbroleconfig.json
index 66a0e0a..acc56b3 100644
--- a/Assets/GameMain/DataTables/tbroleconfig.json
+++ b/Assets/GameMain/DataTables/tbroleconfig.json
@@ -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": "老人"
diff --git a/Assets/GameMain/DataTables/tbstatconfig.bytes b/Assets/GameMain/DataTables/tbstatconfig.bytes
new file mode 100644
index 0000000..bed3ac6
Binary files /dev/null and b/Assets/GameMain/DataTables/tbstatconfig.bytes differ
diff --git a/Assets/GameMain/DataTables/tbstatconfig.bytes.meta b/Assets/GameMain/DataTables/tbstatconfig.bytes.meta
new file mode 100644
index 0000000..16cf85b
--- /dev/null
+++ b/Assets/GameMain/DataTables/tbstatconfig.bytes.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0ee410edd089fef4d91515cb4a75589d
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/DataTables/tbstatconfig.json b/Assets/GameMain/DataTables/tbstatconfig.json
new file mode 100644
index 0000000..5f3a0cd
--- /dev/null
+++ b/Assets/GameMain/DataTables/tbstatconfig.json
@@ -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
+ }
+]
\ No newline at end of file
diff --git a/Assets/GameMain/DataTables/tbstatconfig.json.meta b/Assets/GameMain/DataTables/tbstatconfig.json.meta
new file mode 100644
index 0000000..78e16ab
--- /dev/null
+++ b/Assets/GameMain/DataTables/tbstatconfig.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b66a5cb55d1374a45b0f74f7baba5283
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/DataTables/tbweaponconfig.bytes b/Assets/GameMain/DataTables/tbweaponconfig.bytes
index ec2c8f2..f8ea49e 100644
Binary files a/Assets/GameMain/DataTables/tbweaponconfig.bytes and b/Assets/GameMain/DataTables/tbweaponconfig.bytes differ
diff --git a/Assets/GameMain/DataTables/tbweaponconfig.json b/Assets/GameMain/DataTables/tbweaponconfig.json
index ef22236..b2a0c0e 100644
--- a/Assets/GameMain/DataTables/tbweaponconfig.json
+++ b/Assets/GameMain/DataTables/tbweaponconfig.json
@@ -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": "长枪"
}
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/DamageTypeExtensions.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/DamageTypeExtensions.cs
new file mode 100644
index 0000000..51a51ae
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/DamageTypeExtensions.cs
@@ -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;
+ }
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/DamageTypeExtensions.cs.meta b/Assets/GameMain/Scripts/Base/Definition/DataStruct/DamageTypeExtensions.cs.meta
new file mode 100644
index 0000000..18fcb88
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/DamageTypeExtensions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5e6128fa910e4a778c187123490b4119
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/IAttributeProvider.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/IAttributeProvider.cs
new file mode 100644
index 0000000..bfe4420
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/IAttributeProvider.cs
@@ -0,0 +1,11 @@
+namespace SepCore.Definition
+{
+ ///
+ /// 属性查询接口。
+ /// 供战斗碰撞(ImpactData)与伤害管线解耦读取属性值。
+ ///
+ public interface IAttributeProvider
+ {
+ float GetValue(StatType statType);
+ }
+}
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/IAttributeProvider.cs.meta b/Assets/GameMain/Scripts/Base/Definition/DataStruct/IAttributeProvider.cs.meta
new file mode 100644
index 0000000..b8e6979
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/IAttributeProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b483f12479014631bb5914285260aa31
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/ImpactData.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/ImpactData.cs
index 1eac60c..72460b6 100644
--- a/Assets/GameMain/Scripts/Base/Definition/DataStruct/ImpactData.cs
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/ImpactData.cs
@@ -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)
+ {
+ }
}
}
\ No newline at end of file
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/PropItem.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/PropItem.cs
index fd3902b..a99f16b 100644
--- a/Assets/GameMain/Scripts/Base/Definition/DataStruct/PropItem.cs
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/PropItem.cs
@@ -4,24 +4,24 @@ namespace SepCore.Definition
{
public class PropItem
{
- private readonly List _modifiers = new();
+ private readonly List _modifiers = new();
public string Title { get; private set; }
public string IconAssetName { get; private set; }
public Rarity Rarity { get; private set; }
- public IReadOnlyList Modifiers => _modifiers;
+ public IReadOnlyList 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 modifiers)
+ public PropItem(IEnumerable modifiers)
{
if (modifiers != null)
{
@@ -29,7 +29,7 @@ namespace SepCore.Definition
}
}
- public PropItem(IEnumerable modifiers, Rarity rarity, string title, string iconAssetName)
+ public PropItem(IEnumerable modifiers, Rarity rarity, string title, string iconAssetName)
{
if (modifiers != null)
{
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatConfig.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatConfig.cs
new file mode 100644
index 0000000..8a0c5b0
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatConfig.cs
@@ -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;
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatConfig.cs.meta b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatConfig.cs.meta
new file mode 100644
index 0000000..71e45a7
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7b910e53a2454b5f903517865c36ad18
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifier.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifier.cs
new file mode 100644
index 0000000..cb3ec69
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifier.cs
@@ -0,0 +1,44 @@
+using System;
+using UnityEngine;
+
+namespace SepCore.Definition
+{
+ ///
+ /// 属性修饰器。
+ /// 包含修饰数值、修饰类型(固定值/百分比加算/独立乘区)以及来源对象(道具/武器/羁绊/特质/Buff)。
+ ///
+ 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;
+ }
+
+ ///
+ /// 从 Luban 导出的 modifier 数据结构转换。
+ ///
+ /// Luban modifier 数据实例
+ /// 来源对象引用
+ 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}]";
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifier.cs.meta b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifier.cs.meta
new file mode 100644
index 0000000..2ba2c26
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifier.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 887b4b22fce86cb43b95cfdf3c6b967c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifierConfig.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifierConfig.cs
new file mode 100644
index 0000000..720f42e
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifierConfig.cs
@@ -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;
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/modifier.cs.meta b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifierConfig.cs.meta
similarity index 100%
rename from Assets/GameMain/Scripts/Base/Definition/DataStruct/modifier.cs.meta
rename to Assets/GameMain/Scripts/Base/Definition/DataStruct/StatModifierConfig.cs.meta
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatProperty.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatProperty.cs
deleted file mode 100644
index 4f8fddf..0000000
--- a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatProperty.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatProperty.cs.meta b/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatProperty.cs.meta
deleted file mode 100644
index 28eeef3..0000000
--- a/Assets/GameMain/Scripts/Base/Definition/DataStruct/StatProperty.cs.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: 18e7bc9587114ae4a91c70f829543de8
-timeCreated: 1770353735
\ No newline at end of file
diff --git a/Assets/GameMain/Scripts/Base/Definition/DataStruct/modifier.cs b/Assets/GameMain/Scripts/Base/Definition/DataStruct/modifier.cs
deleted file mode 100644
index b3fb3e1..0000000
--- a/Assets/GameMain/Scripts/Base/Definition/DataStruct/modifier.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using UnityEngine;
-
-namespace SepCore.Definition
-{
- public sealed partial class modifier
- {
- public StatType StatType => Key;
-
- ///
- /// 实际参与数值计算的浮点数值。
- /// 1. 百分比加成(IsPercent == true):表内千分比(如 200 代表 20%),转换为 0.2f。
- /// 2. 非百分比加成(IsPercent == false):移动速度原为 float 放大了 1000 倍(如 800 代表 0.8f),除以 1000f;其余(如生命+53、攻击+10)直接使用原值。
- ///
- 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}";
- }
- }
-}
diff --git a/Assets/GameMain/Scripts/Base/EntityData/Weapon/WeaponData.cs b/Assets/GameMain/Scripts/Base/EntityData/Weapon/WeaponData.cs
index c799e46..c447648 100644
--- a/Assets/GameMain/Scripts/Base/EntityData/Weapon/WeaponData.cs
+++ b/Assets/GameMain/Scripts/Base/EntityData/Weapon/WeaponData.cs
@@ -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 Parameters => Config.Parameters;
- public IReadOnlyList Modifiers => Config.Modifiers;
+ public IReadOnlyList Modifiers => Config.StatModifierConfigs;
public int GetParam(ParamType paramType)
{
diff --git a/Assets/GameMain/Scripts/Base/Gen/DamageType.cs b/Assets/GameMain/Scripts/Base/Gen/DamageType.cs
new file mode 100644
index 0000000..5ff448a
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/DamageType.cs
@@ -0,0 +1,39 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+
+namespace SepCore.Definition
+{
+ ///
+ /// 伤害类型
+ ///
+ [System.Flags]
+ public enum DamageType : int
+ {
+ ///
+ /// 无
+ ///
+ None = 0,
+ ///
+ /// 近战伤害
+ ///
+ Melee = 1,
+ ///
+ /// 远程伤害
+ ///
+ Ranged = 2,
+ ///
+ /// 元素伤害
+ ///
+ Element = 4,
+ }
+
+}
+
diff --git a/Assets/GameMain/Scripts/Base/Gen/DamageType.cs.meta b/Assets/GameMain/Scripts/Base/Gen/DamageType.cs.meta
new file mode 100644
index 0000000..27fe3cb
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/DamageType.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 178341689d25049458e8460d398db2dc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Gen/GlobalConfig.cs b/Assets/GameMain/Scripts/Base/Gen/GlobalConfig.cs
index 198781e..fe91619 100644
--- a/Assets/GameMain/Scripts/Base/Gen/GlobalConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/GlobalConfig.cs
@@ -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
///
public readonly int WeaponRotateSpeed;
///
+ /// 武器基本暴击率
+ ///
+ public readonly float WeaponBaseCrit;
+ ///
/// 玩家对象组名
///
public readonly string PlayerGroupName;
@@ -180,6 +186,10 @@ public sealed partial class GlobalConfig : Luban.BeanBase
///
public readonly int EntityAsyncTimeout;
///
+ /// 暴击伤害倍率
+ ///
+ public readonly float CritMultiplier;
+ ///
/// 敌人碰撞半径
///
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 + ","
+ "}";
diff --git a/Assets/GameMain/Scripts/Base/Gen/LevelConfig.cs b/Assets/GameMain/Scripts/Base/Gen/LevelConfig.cs
index 555b16d..1229c97 100644
--- a/Assets/GameMain/Scripts/Base/Gen/LevelConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/LevelConfig.cs
@@ -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(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(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
///
public readonly int Id;
- public readonly System.Collections.Generic.List EnemySpawnConfigs;
+ public readonly System.Collections.Generic.List EnemySpawnConfigs;
///
/// 关卡时间
///
diff --git a/Assets/GameMain/Scripts/Base/Gen/LevelUpRewardConfig.cs b/Assets/GameMain/Scripts/Base/Gen/LevelUpRewardConfig.cs
index d1412b5..7e5b487 100644
--- a/Assets/GameMain/Scripts/Base/Gen/LevelUpRewardConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/LevelUpRewardConfig.cs
@@ -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(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(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
/// 道具品质
///
public readonly Rarity Rarity;
- public readonly System.Collections.Generic.List Modifiers;
+ public readonly System.Collections.Generic.List StatModifierConfigs;
///
/// 图标
///
@@ -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 + ","
+ "}";
}
diff --git a/Assets/GameMain/Scripts/Base/Gen/ModifierType.cs b/Assets/GameMain/Scripts/Base/Gen/ModifierType.cs
new file mode 100644
index 0000000..18dfbdf
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/ModifierType.cs
@@ -0,0 +1,38 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+
+namespace SepCore.Definition
+{
+ ///
+ /// 修饰器类型
+ ///
+ public enum ModifierType : int
+ {
+ ///
+ /// 无
+ ///
+ None = 0,
+ ///
+ /// 固定值
+ ///
+ Flat = 100,
+ ///
+ /// 百分比累加
+ ///
+ PercentAdd = 200,
+ ///
+ /// 最终百分比独立乘区
+ ///
+ PercentMult = 300,
+ }
+
+}
+
diff --git a/Assets/GameMain/Scripts/Base/Gen/ModifierType.cs.meta b/Assets/GameMain/Scripts/Base/Gen/ModifierType.cs.meta
new file mode 100644
index 0000000..a393ccd
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/ModifierType.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6a95015722b22de41a817f1d17e7d560
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Gen/PropConfig.cs b/Assets/GameMain/Scripts/Base/Gen/PropConfig.cs
index 54ddf2e..5bca30f 100644
--- a/Assets/GameMain/Scripts/Base/Gen/PropConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/PropConfig.cs
@@ -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(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(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
/// 道具价格
///
public readonly int Price;
- public readonly System.Collections.Generic.List Modifiers;
+ public readonly System.Collections.Generic.List StatModifierConfigs;
///
/// 图标
///
@@ -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 + ","
+ "}";
}
diff --git a/Assets/GameMain/Scripts/Base/Gen/RoleConfig.cs b/Assets/GameMain/Scripts/Base/Gen/RoleConfig.cs
index 9e792c1..9bd501d 100644
--- a/Assets/GameMain/Scripts/Base/Gen/RoleConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/RoleConfig.cs
@@ -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(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(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(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;
///
- /// key
+ /// statType
///
- public readonly System.Collections.Generic.List Modifiers;
+ public readonly System.Collections.Generic.List StatModifierConfigs;
public readonly System.Collections.Generic.List 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 + ","
diff --git a/Assets/GameMain/Scripts/Base/Gen/StatConfig.cs b/Assets/GameMain/Scripts/Base/Gen/StatConfig.cs
new file mode 100644
index 0000000..80ec55a
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/StatConfig.cs
@@ -0,0 +1,97 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+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);
+ }
+
+ ///
+ /// 属性类型
+ ///
+ public readonly StatType StatType;
+ ///
+ /// 名称
+ ///
+ public readonly string Name;
+ ///
+ /// 默认值
+ ///
+ public readonly float DefaultBaseValue;
+ ///
+ /// 是否有最小值
+ ///
+ public readonly bool HasMin;
+ ///
+ /// 最小值
+ ///
+ public readonly float MinValue;
+ ///
+ /// 是否有最大值
+ ///
+ public readonly bool HasMax;
+ ///
+ /// 最大值
+ ///
+ public readonly float MaxValue;
+ ///
+ /// UI 文本是否百分比
+ ///
+ 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 + ","
+ + "}";
+ }
+}
+
+}
diff --git a/Assets/GameMain/Scripts/Base/Gen/StatConfig.cs.meta b/Assets/GameMain/Scripts/Base/Gen/StatConfig.cs.meta
new file mode 100644
index 0000000..566246a
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/StatConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2dfe101008e1ded4ea35681a5a656d2f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Gen/modifier.cs b/Assets/GameMain/Scripts/Base/Gen/StatModifierConfig.cs
similarity index 57%
rename from Assets/GameMain/Scripts/Base/Gen/modifier.cs
rename to Assets/GameMain/Scripts/Base/Gen/StatModifierConfig.cs
index e935b0c..7a6c957 100644
--- a/Assets/GameMain/Scripts/Base/Gen/modifier.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/StatModifierConfig.cs
@@ -15,34 +15,34 @@ namespace SepCore.Definition
///
/// 修饰器
///
-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);
}
///
/// 属性类型
///
- public readonly StatType Key;
+ public readonly StatType StatType;
///
/// 值
///
- public readonly int Value;
+ public readonly float Value;
///
- /// 是否为百分比
+ /// 修饰器类型
///
- 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 + ","
+ "}";
}
}
diff --git a/Assets/GameMain/Scripts/Base/Gen/modifier.cs.meta b/Assets/GameMain/Scripts/Base/Gen/StatModifierConfig.cs.meta
similarity index 100%
rename from Assets/GameMain/Scripts/Base/Gen/modifier.cs.meta
rename to Assets/GameMain/Scripts/Base/Gen/StatModifierConfig.cs.meta
diff --git a/Assets/GameMain/Scripts/Base/Gen/StatType.cs b/Assets/GameMain/Scripts/Base/Gen/StatType.cs
index e4ad1ca..91c49d0 100644
--- a/Assets/GameMain/Scripts/Base/Gen/StatType.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/StatType.cs
@@ -23,39 +23,67 @@ namespace SepCore.Definition
///
/// 最大生命
///
- MaxHealth = 1,
+ MaxHp = 1,
///
- /// 移动速度
+ /// 生命恢复
///
- MovementSpeed = 2,
+ HpRegen = 2,
///
- /// 攻击力
+ /// 生命窃取
///
- Attack = 3,
+ LifeSteal = 3,
///
- /// 防御力
+ /// 伤害
///
- Defense = 4,
+ DamagePercent = 4,
+ ///
+ /// 近战伤害
+ ///
+ MeleeDamage = 5,
+ ///
+ /// 远程伤害
+ ///
+ RangedDamage = 6,
+ ///
+ /// 元素伤害
+ ///
+ ElementalDamage = 7,
///
/// 攻击速度
///
- AttackSpeed = 5,
+ AttackSpeed = 8,
///
/// 暴击率
///
- Critical = 6,
+ CritChance = 9,
///
- /// 暴击伤害
+ /// 工程学
///
- CriticalDamage = 7,
+ Engineering = 10,
+ ///
+ /// 攻击范围
+ ///
+ Range = 11,
+ ///
+ /// 护甲
+ ///
+ Armor = 12,
///
/// 闪避率
///
- Dodge = 8,
+ Dodge = 13,
///
- /// 自动吸收范围
+ /// 移动速度
///
- AbsorbRange = 9,
+ MoveSpeed = 14,
+ ///
+ /// 收获
+ ///
+ Harvesting = 15,
+ ///
+ /// 幸运
+ ///
+ Luck = 16,
}
}
diff --git a/Assets/GameMain/Scripts/Base/Gen/Tables.cs b/Assets/GameMain/Scripts/Base/Gen/Tables.cs
index 32336e2..43015bb 100644
--- a/Assets/GameMain/Scripts/Base/Gen/Tables.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/Tables.cs
@@ -61,6 +61,10 @@ public partial class Tables
/// 界面配置
///
public TbWeaponConfig TbWeaponConfig {get; }
+ ///
+ /// 角色属性配置
+ ///
+ public TbStatConfig TbStatConfig {get; }
public Tables(System.Func 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);
}
}
diff --git a/Assets/GameMain/Scripts/Base/Gen/TbGlobalConfig.cs b/Assets/GameMain/Scripts/Base/Gen/TbGlobalConfig.cs
index 20f6ffd..1bbdda1 100644
--- a/Assets/GameMain/Scripts/Base/Gen/TbGlobalConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/TbGlobalConfig.cs
@@ -95,6 +95,10 @@ public partial class TbGlobalConfig
///
public int WeaponRotateSpeed => _data.WeaponRotateSpeed;
///
+ /// 武器基本暴击率
+ ///
+ public float WeaponBaseCrit => _data.WeaponBaseCrit;
+ ///
/// 玩家对象组名
///
public string PlayerGroupName => _data.PlayerGroupName;
@@ -151,6 +155,10 @@ public partial class TbGlobalConfig
///
public int EntityAsyncTimeout => _data.EntityAsyncTimeout;
///
+ /// 暴击伤害倍率
+ ///
+ public float CritMultiplier => _data.CritMultiplier;
+ ///
/// 敌人碰撞半径
///
public int EnemyBodyRadius => _data.EnemyBodyRadius;
diff --git a/Assets/GameMain/Scripts/Base/Gen/TbStatConfig.cs b/Assets/GameMain/Scripts/Base/Gen/TbStatConfig.cs
new file mode 100644
index 0000000..b02aec6
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/TbStatConfig.cs
@@ -0,0 +1,55 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using Luban;
+
+
+namespace SepCore.Definition
+{
+///
+/// 角色属性配置
+///
+public partial class TbStatConfig
+{
+ private readonly System.Collections.Generic.Dictionary _dataMap;
+ private readonly System.Collections.Generic.List _dataList;
+
+ public TbStatConfig(ByteBuf _buf)
+ {
+ _dataMap = new System.Collections.Generic.Dictionary();
+ _dataList = new System.Collections.Generic.List();
+
+ 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 DataMap => _dataMap;
+ public System.Collections.Generic.List 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);
+ }
+ }
+
+}
+
+}
+
diff --git a/Assets/GameMain/Scripts/Base/Gen/TbStatConfig.cs.meta b/Assets/GameMain/Scripts/Base/Gen/TbStatConfig.cs.meta
new file mode 100644
index 0000000..a3b2c25
--- /dev/null
+++ b/Assets/GameMain/Scripts/Base/Gen/TbStatConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 54c58e47ce828d246ae57a00a9704563
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Base/Gen/WeaponConfig.cs b/Assets/GameMain/Scripts/Base/Gen/WeaponConfig.cs
index 792637f..2a99935 100644
--- a/Assets/GameMain/Scripts/Base/Gen/WeaponConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/WeaponConfig.cs
@@ -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(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(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(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
///
public readonly int Cooldown;
///
+ /// 伤害类型
+ ///
+ public readonly DamageType DamageType;
+ ///
/// 攻击范围
///
public readonly int AttackRange;
@@ -73,7 +78,7 @@ public sealed partial class WeaponConfig : Luban.BeanBase
/// 参数类型
///
public readonly System.Collections.Generic.Dictionary Parameters;
- public readonly System.Collections.Generic.List Modifiers;
+ public readonly System.Collections.Generic.List StatModifierConfigs;
///
/// 预制体
///
@@ -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 + ","
+ "}";
diff --git a/Assets/GameMain/Scripts/Base/Gen/enemySpawnConfig.cs b/Assets/GameMain/Scripts/Base/Gen/enemySpawnConfig.cs
index 1e4558f..06fbfe7 100644
--- a/Assets/GameMain/Scripts/Base/Gen/enemySpawnConfig.cs
+++ b/Assets/GameMain/Scripts/Base/Gen/enemySpawnConfig.cs
@@ -15,18 +15,18 @@ namespace SepCore.Definition
///
/// 出怪配置
///
-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);
}
///
@@ -42,7 +42,7 @@ public sealed partial class enemySpawnConfig : Luban.BeanBase
///
public readonly int Interval;
- public const int __ID__ = -1182208523;
+ public const int __ID__ = -2024984043;
public override int GetTypeId() => __ID__;
public void ResolveRef(Tables tables)
diff --git a/Assets/GameMain/Scripts/Editor/PlayerBuffConsoleWindow.cs b/Assets/GameMain/Scripts/Editor/PlayerBuffConsoleWindow.cs
index cbd0421..a8a7dd0 100644
--- a/Assets/GameMain/Scripts/Editor/PlayerBuffConsoleWindow.cs
+++ b/Assets/GameMain/Scripts/Editor/PlayerBuffConsoleWindow.cs
@@ -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();
}
diff --git a/Assets/GameMain/Scripts/Presentation/LevelUp/LevelUpController.cs b/Assets/GameMain/Scripts/Presentation/LevelUp/LevelUpController.cs
index 3079359..256e97a 100644
--- a/Assets/GameMain/Scripts/Presentation/LevelUp/LevelUpController.cs
+++ b/Assets/GameMain/Scripts/Presentation/LevelUp/LevelUpController.cs
@@ -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
});
}
diff --git a/Assets/GameMain/Scripts/Presentation/Shop/GoodsItemContext.cs b/Assets/GameMain/Scripts/Presentation/Shop/GoodsItemContext.cs
index db25ad4..98221d0 100644
--- a/Assets/GameMain/Scripts/Presentation/Shop/GoodsItemContext.cs
+++ b/Assets/GameMain/Scripts/Presentation/Shop/GoodsItemContext.cs
@@ -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);
}
}
}
diff --git a/Assets/GameMain/Scripts/Presentation/Utility/ItemDescUtility.cs b/Assets/GameMain/Scripts/Presentation/Utility/ItemDescUtility.cs
index 5fcc220..fb533a9 100644
--- a/Assets/GameMain/Scripts/Presentation/Utility/ItemDescUtility.cs
+++ b/Assets/GameMain/Scripts/Presentation/Utility/ItemDescUtility.cs
@@ -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}: {sign}{displayPercent:0.##}%";
}
- return $"{name}: +{displayValue:0.##}{suffix}";
+ if (statModifierConfig.ModifierType == ModifierType.PercentMult)
+ {
+ return $"{name}: x{statModifierConfig.Value:0.##}";
+ }
+
+ return $"{name}: {sign}{statModifierConfig.Value:0.##}";
}
- public static string CreatePropDescription(IEnumerable modifiers)
+ public static string CreatePropDescription(IEnumerable 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 modifiers, IReadOnlyDictionary parameters)
+ IEnumerable modifiers, IReadOnlyDictionary parameters)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"攻击: {attack}");
diff --git a/Assets/GameMain/Scripts/Procedure/DebugPanel/RuntimeDebugPanelComponent.cs b/Assets/GameMain/Scripts/Procedure/DebugPanel/RuntimeDebugPanelComponent.cs
index 7adb0f0..f6d2137 100644
--- a/Assets/GameMain/Scripts/Procedure/DebugPanel/RuntimeDebugPanelComponent.cs
+++ b/Assets/GameMain/Scripts/Procedure/DebugPanel/RuntimeDebugPanelComponent.cs
@@ -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)
diff --git a/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs b/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs
index 1c418a6..5a696ab 100644
--- a/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs
+++ b/Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs
@@ -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));
diff --git a/Assets/GameMain/Scripts/Runtime/AssemblyInfo.cs b/Assets/GameMain/Scripts/Runtime/AssemblyInfo.cs
new file mode 100644
index 0000000..e8996be
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Simulation.EditModeTests")]
+[assembly: InternalsVisibleTo("Simulation.PlayModeTests")]
diff --git a/Assets/GameMain/Scripts/Runtime/AssemblyInfo.cs.meta b/Assets/GameMain/Scripts/Runtime/AssemblyInfo.cs.meta
new file mode 100644
index 0000000..9c18c13
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/AssemblyInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2d68a67948722984392ea42a1cec3594
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Base/GameEntry.Custom.cs b/Assets/GameMain/Scripts/Runtime/Base/GameEntry.Custom.cs
index adc86d0..5f65df8 100644
--- a/Assets/GameMain/Scripts/Runtime/Base/GameEntry.Custom.cs
+++ b/Assets/GameMain/Scripts/Runtime/Base/GameEntry.Custom.cs
@@ -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; }
diff --git a/Assets/GameMain/Scripts/Runtime/Battle.meta b/Assets/GameMain/Scripts/Runtime/Battle.meta
new file mode 100644
index 0000000..cb248fa
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 715771c24b633ee49b200700baec66de
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/AttributePipeline.cs b/Assets/GameMain/Scripts/Runtime/Battle/AttributePipeline.cs
new file mode 100644
index 0000000..d550ec3
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/AttributePipeline.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using SepCore.Definition;
+
+namespace SepCore.Battle
+{
+ ///
+ /// 多维属性修饰器管线(Attribute Pipeline)。
+ /// 管理角色所有属性维度,提供三段修饰求值、脏标记缓存与按 Source 批量移除机制。
+ /// 纯 C# 类,无 MonoBehaviour 依赖。
+ ///
+ public sealed class AttributePipeline : IAttributeProvider
+ {
+ private readonly Dictionary _stats = new();
+
+ ///
+ /// 当某个属性的值发生变动(添加/移除修饰器、修改基础值)时触发。
+ /// 参数:(StatType 属性类型, float 新的当前值)。
+ ///
+ public event Action 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);
+ }
+ }
+
+ ///
+ /// 获取指定类型的属性容器。
+ ///
+ public CharacterStat Get(StatType statType)
+ {
+ if (!_stats.TryGetValue(statType, out CharacterStat stat))
+ {
+ stat = new CharacterStat(statType, 0f);
+ _stats.Add(statType, stat);
+ }
+
+ return stat;
+ }
+
+ ///
+ /// 获取指定属性的当前最终计算值(带脏标记缓存与边界钳制)。
+ ///
+ public float GetValue(StatType statType)
+ {
+ return Get(statType).Value;
+ }
+
+ ///
+ /// 设置指定属性的基础值。
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 添加属性修饰器。
+ ///
+ 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);
+ }
+
+ ///
+ /// 移除属性修饰器。
+ ///
+ 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;
+ }
+
+ ///
+ /// 添加 Luban 导出的 modifier(自动转换为 StatModifier)。
+ ///
+ 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);
+ }
+
+ ///
+ /// 移除由 Luban 导出的 modifier 所生成的对应修饰器。
+ ///
+ 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 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;
+ }
+
+ ///
+ /// 一键移除所有属性中指定来源(道具、武器、羁绊、Buff 等)的所有修饰器。
+ ///
+ /// 来源对象引用
+ /// 移除的修饰器总数
+ public int RemoveAllModifiersFromSource(object source)
+ {
+ if (source == null)
+ {
+ return 0;
+ }
+
+ int totalRemoved = 0;
+ foreach (KeyValuePair pair in _stats)
+ {
+ int removed = pair.Value.RemoveAllModifiersFromSource(source);
+ if (removed > 0)
+ {
+ totalRemoved += removed;
+ OnStatChanged?.Invoke(pair.Key, pair.Value.Value);
+ }
+ }
+
+ return totalRemoved;
+ }
+
+ ///
+ /// 清空所有属性的修饰器并标记为脏。
+ ///
+ public void Clear()
+ {
+ foreach (KeyValuePair pair in _stats)
+ {
+ pair.Value.ClearModifiers();
+ OnStatChanged?.Invoke(pair.Key, pair.Value.Value);
+ }
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/AttributePipeline.cs.meta b/Assets/GameMain/Scripts/Runtime/Battle/AttributePipeline.cs.meta
new file mode 100644
index 0000000..4aa038f
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/AttributePipeline.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 312b0c5905f501449b3dc700f4ac0e5b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/CharacterStat.cs b/Assets/GameMain/Scripts/Runtime/Battle/CharacterStat.cs
new file mode 100644
index 0000000..64b414f
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/CharacterStat.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections.Generic;
+using SepCore.Definition;
+using UnityEngine;
+
+namespace SepCore.Battle
+{
+ ///
+ /// 单个属性实例容器。
+ /// 维护基础值、修饰器列表,通过脏标记(Dirty Flag)缓存计算结果,并基于 StatConfig 进行边界钳制。
+ ///
+ public sealed class CharacterStat
+ {
+ private readonly List _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((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 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;
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/CharacterStat.cs.meta b/Assets/GameMain/Scripts/Runtime/Battle/CharacterStat.cs.meta
new file mode 100644
index 0000000..7617053
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/CharacterStat.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 67040be8b42ae744a81f961aa4ec5333
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/DamageContext.cs b/Assets/GameMain/Scripts/Runtime/Battle/DamageContext.cs
new file mode 100644
index 0000000..709326f
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/DamageContext.cs
@@ -0,0 +1,56 @@
+using SepCore.Definition;
+
+namespace SepCore.Battle
+{
+ ///
+ /// 伤害结算输入上下文。
+ /// 封装攻击方与受击方的属性管线、武器基础伤害、加成属性、暴击等参数。
+ ///
+ public struct DamageContext
+ {
+ ///
+ /// 基础伤害(武器攻击力或敌人基础伤害)。
+ ///
+ public int BaseDamage;
+
+ ///
+ /// 伤害加成属性(近战伤害/远程伤害/元素伤害/None)。
+ ///
+ public StatType DamageScalingStat;
+
+ ///
+ /// 伤害加成系数,默认 1.0f。
+ ///
+ public float ScalingRatio;
+
+ ///
+ /// 基础暴击率(例如来自武器或全局配置)。
+ ///
+ public float BaseCritChance;
+
+ ///
+ /// 暴击伤害倍率(例如 1.5f 或 2.0f)。
+ ///
+ public float CritMultiplier;
+
+ ///
+ /// 攻击方属性修饰管线(可为 null,如普通怪物未挂载管线)。
+ ///
+ public IAttributeProvider AttackerPipeline;
+
+ ///
+ /// 防御方属性修饰管线(可为 null,如普通怪物未挂载管线)。
+ ///
+ public IAttributeProvider DefenderPipeline;
+
+ ///
+ /// 防御方备用护甲(当 DefenderPipeline 为 null 时生效)。
+ ///
+ public float FallbackDefenderArmor;
+
+ ///
+ /// 防御方备用闪避率(当 DefenderPipeline 为 null 时生效)。
+ ///
+ public float FallbackDefenderDodge;
+ }
+}
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/DamageContext.cs.meta b/Assets/GameMain/Scripts/Runtime/Battle/DamageContext.cs.meta
new file mode 100644
index 0000000..727af2d
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/DamageContext.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8192a0b127344919ba1812836248cc01
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/DamagePipeline.cs b/Assets/GameMain/Scripts/Runtime/Battle/DamagePipeline.cs
new file mode 100644
index 0000000..d32e790
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/DamagePipeline.cs
@@ -0,0 +1,134 @@
+using SepCore.Definition;
+using UnityEngine;
+
+namespace SepCore.Battle
+{
+ ///
+ /// 伤害结算管线(纯 C# 静态计算器)。
+ /// 实现武器类型加成、百分比放大、闪避判定、暴击判定、双曲护甲折减、保底伤害与生命偷取判定。
+ ///
+ public static class DamagePipeline
+ {
+ public const float ArmorDivisor = 15f; // Brotato 双曲护甲基数
+ public const float DefaultCritMultiplier = 1.5f;
+ public const float DefaultBaseCrit = 0.05f;
+
+ ///
+ /// 计算最终伤害结果。
+ ///
+ 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
+ };
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/DamagePipeline.cs.meta b/Assets/GameMain/Scripts/Runtime/Battle/DamagePipeline.cs.meta
new file mode 100644
index 0000000..bd1369c
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/DamagePipeline.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a394c2d349564121dc3834058460ee23
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/DamageResult.cs b/Assets/GameMain/Scripts/Runtime/Battle/DamageResult.cs
new file mode 100644
index 0000000..6461524
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/DamageResult.cs
@@ -0,0 +1,33 @@
+namespace SepCore.Battle
+{
+ ///
+ /// 伤害计算输出结果。
+ ///
+ public struct DamageResult
+ {
+ ///
+ /// 最终扣除的生命值(若闪避则为 0)。
+ ///
+ public int Damage;
+
+ ///
+ /// 是否闪避。
+ ///
+ public bool IsDodge;
+
+ ///
+ /// 是否暴击。
+ ///
+ public bool IsCrit;
+
+ ///
+ /// 是否触发生命偷取。
+ ///
+ public bool IsLifeSteal;
+
+ ///
+ /// 偷取回复的生命值(通常为 1)。
+ ///
+ public int LifeStealAmount;
+ }
+}
diff --git a/Assets/GameMain/Scripts/Runtime/Battle/DamageResult.cs.meta b/Assets/GameMain/Scripts/Runtime/Battle/DamageResult.cs.meta
new file mode 100644
index 0000000..1e15195
--- /dev/null
+++ b/Assets/GameMain/Scripts/Runtime/Battle/DamageResult.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9283b1c238454920cb2923947359dd12
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Runtime/Components/AbsorbComponent.cs b/Assets/GameMain/Scripts/Runtime/Components/AbsorbComponent.cs
index 8ca25ae..693bfbe 100644
--- a/Assets/GameMain/Scripts/Runtime/Components/AbsorbComponent.cs
+++ b/Assets/GameMain/Scripts/Runtime/Components/AbsorbComponent.cs
@@ -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 _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();
- if (coin != null && coin.Available)
- {
- AbsorbDrop(coin.CachedTransform, elapseSeconds);
- coin.TryCollect(_player, CollectDistance);
- continue;
- }
-
- ExpEntity exp = hit.GetComponent();
- 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);
}
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/Components/HealthComponent.cs b/Assets/GameMain/Scripts/Runtime/Components/HealthComponent.cs
index 39adc68..cd42297 100644
--- a/Assets/GameMain/Scripts/Runtime/Components/HealthComponent.cs
+++ b/Assets/GameMain/Scripts/Runtime/Components/HealthComponent.cs
@@ -13,23 +13,17 @@ namespace SepCore.Components
private StatComponent _statComponent;
- private StatProperty _maxHealthStat;
- private Action _maxHealthStatCallback;
- public StatProperty MaxHealthStat => _maxHealthStat;
-
- private StatProperty _defenseStat;
- private Action _defenseStatCallback;
- public StatProperty DefenseStat => _defenseStat;
-
- private StatProperty _dodgeStat;
- private Action _dodgeStatCallback;
- public StatProperty DodgeStat => _dodgeStat;
+ private Action _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;
- }
- 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;
+ 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);
+ }
+ 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);
+ }
}
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/Components/MovementComponent.cs b/Assets/GameMain/Scripts/Runtime/Components/MovementComponent.cs
index 862ec88..1cc7b55 100644
--- a/Assets/GameMain/Scripts/Runtime/Components/MovementComponent.cs
+++ b/Assets/GameMain/Scripts/Runtime/Components/MovementComponent.cs
@@ -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 _movementStatCallback;
+ private Action _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();
+ }
+ }
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/Components/StatComponent.cs b/Assets/GameMain/Scripts/Runtime/Components/StatComponent.cs
index 82f522b..1fc7144 100644
--- a/Assets/GameMain/Scripts/Runtime/Components/StatComponent.cs
+++ b/Assets/GameMain/Scripts/Runtime/Components/StatComponent.cs
@@ -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> _statModifiers = new();
- private readonly Dictionary> _statModifierCallbacks = new();
+ private readonly AttributePipeline _pipeline = new();
+
+ ///
+ /// 纯逻辑属性修饰器管线。
+ ///
+ 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();
- _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);
- }
-
- if (_statModifierCallbacks.TryGetValue(modifier.StatType, out var callbacks))
- callbacks?.Invoke(modifier, false);
+ return _pipeline.RemoveModifier(statType, modifier);
}
- public void Subscribe(StatType statType, Action callback)
+ public void AddModifier(StatModifierConfig statModifierConfig, object source = null)
{
- if (!_statModifierCallbacks.TryAdd(statType, callback))
- {
- _statModifierCallbacks[statType] += callback;
- }
+ _pipeline.AddModifier(statModifierConfig, source);
}
- public void Unsubscribe(StatType statType, Action callback)
+ public void RemoveModifier(StatModifierConfig statModifierConfig, object source = null)
{
- if (!_statModifierCallbacks.TryGetValue(statType, out var callbacks))
- {
- return;
- }
-
- callbacks -= callback;
- if (callbacks == null)
- {
- _statModifierCallbacks.Remove(statType);
- }
+ _pipeline.RemoveModifier(statModifierConfig, source);
}
- public StatProperty GetStat(StatType statType)
+ public int RemoveAllModifiersFromSource(object source)
{
- 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;
+ return _pipeline.RemoveAllModifiersFromSource(source);
}
- public void UpdateStat(StatProperty stat, modifier modifier, bool isApply)
+ public float GetStatValue(StatType statType)
{
- float coef = isApply ? 1f : -1f;
- if (modifier.IsPercent)
- {
- stat.Percent += modifier.RealValue * coef;
- }
- else
- {
- stat.Value += modifier.RealValue * coef;
- }
+ return _pipeline.GetValue(statType);
+ }
+
+ public CharacterStat GetCharacterStat(StatType statType)
+ {
+ return _pipeline.Get(statType);
}
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextComponent.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextComponent.cs
index 91bef96..2a4707e 100644
--- a/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextComponent.cs
+++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextComponent.cs
@@ -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);
}
diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextItem.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextItem.cs
index fad4e53..a5790c7 100644
--- a/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextItem.cs
+++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/DamageText/DamageTextItem.cs
@@ -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 _onComplete;
- public void Show(Vector3 worldPosition, int damage, Canvas canvas, Action onComplete)
+ public void Show(Vector3 worldPosition, int damage, bool isCrit, bool isDodge, Canvas canvas, Action onComplete)
{
_onComplete = onComplete;
gameObject.SetActive(true);
@@ -30,8 +32,27 @@ namespace SepCore.DamageText
_canvasGroup.alpha = 1f;
_cachedTransform.localScale = Vector3.one;
- _text.color = NormalColor;
- _text.text = damage.ToString();
+
+ 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 onComplete)
+ {
+ Show(worldPosition, damage, false, false, canvas, onComplete);
+ }
+
public void ResetItem()
{
KillSequence();
diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/Luban/LubanComponent.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/Luban/LubanComponent.cs
index 6e94364..300cadb 100644
--- a/Assets/GameMain/Scripts/Runtime/CustomComponent/Luban/LubanComponent.cs
+++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/Luban/LubanComponent.cs
@@ -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(Func> rowGetterFactory,
diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Jobs/SimulationWorld.CollisionResolve.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Jobs/SimulationWorld.CollisionResolve.cs
index 64ef671..ead2dcd 100644
--- a/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Jobs/SimulationWorld.CollisionResolve.cs
+++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Jobs/SimulationWorld.CollisionResolve.cs
@@ -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;
}
diff --git a/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Presentation/SimulationWorld.HitPresentation.cs b/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Presentation/SimulationWorld.HitPresentation.cs
index 76e0f6d..370a914 100644
--- a/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Presentation/SimulationWorld.HitPresentation.cs
+++ b/Assets/GameMain/Scripts/Runtime/CustomComponent/Simulation/Presentation/SimulationWorld.HitPresentation.cs
@@ -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);
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Enemy/MeleeEnemy.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Enemy/MeleeEnemy.cs
index 904200a..335eafd 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Enemy/MeleeEnemy.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Enemy/MeleeEnemy.cs
@@ -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);
}
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Player.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Player.cs
index 1fcb494..8088095 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Player.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Player.cs
@@ -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)
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/TargetableObject.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/TargetableObject.cs
index 0e94ab5..9ce9f5f 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/TargetableObject.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/TargetableObject.cs
@@ -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();
+ if (attackerHealth == null && attacker is WeaponBase weapon)
+ {
+ attackerHealth = weapon.GetComponentInParent();
+ }
+
+ 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);
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs
index 4b9788c..bf8083d 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/TargetSelector/NearestTargetSelector.cs
@@ -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;
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponBase.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponBase.cs
index c376e6b..b43dabe 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponBase.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponBase.cs
@@ -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 _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();
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;
}
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgun.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgun.cs
index 482c1cf..36c80f2 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgun.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgun.cs
@@ -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))
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgunAttackState.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgunAttackState.cs
index fcf5bbe..9c56c0c 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgunAttackState.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponHandgun/WeaponHandgunAttackState.cs
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
- _weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
+ _weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs
index 632674c..e731926 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs
@@ -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(() =>
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnifeAttackState.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnifeAttackState.cs
index c9c8db1..2a8da36 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnifeAttackState.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponKnife/WeaponKnifeAttackState.cs
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
- _weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
+ _weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLance/WeaponLanceAttackState.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLance/WeaponLanceAttackState.cs
index d8955b3..c45b182 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLance/WeaponLanceAttackState.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLance/WeaponLanceAttackState.cs
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
- _weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
+ _weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLightning/WeaponLightningAttackState.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLightning/WeaponLightningAttackState.cs
index aee7a0a..20eb9c8 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLightning/WeaponLightningAttackState.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponLightning/WeaponLightningAttackState.cs
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
- _weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
+ _weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs
index 96b61d0..571f8ad 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs
@@ -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;
diff --git a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlashAttackState.cs b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlashAttackState.cs
index 98e3645..5e2edbf 100644
--- a/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlashAttackState.cs
+++ b/Assets/GameMain/Scripts/Runtime/EntityLogic/Weapon/WeaponSlash/WeaponSlashAttackState.cs
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
- _weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
+ _weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
diff --git a/Assets/GameMain/Scripts/Runtime/UIBase/LevelUp/LevelUpUseCase.cs b/Assets/GameMain/Scripts/Runtime/UIBase/LevelUp/LevelUpUseCase.cs
index 4347fb2..a358074 100644
--- a/Assets/GameMain/Scripts/Runtime/UIBase/LevelUp/LevelUpUseCase.cs
+++ b/Assets/GameMain/Scripts/Runtime/UIBase/LevelUp/LevelUpUseCase.cs
@@ -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()
diff --git a/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/SelectRoleUseCase.cs b/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/SelectRoleUseCase.cs
index 4fdf11a..e0bee14 100644
--- a/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/SelectRoleUseCase.cs
+++ b/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/SelectRoleUseCase.cs
@@ -76,7 +76,7 @@ namespace SepCore.UI
selectedRoleData = new ShowAreaRawData
{
Name = selectedRole.Name,
- InitialProperties = selectedRole.Modifiers
+ InitialProperties = selectedRole.StatModifierConfigs
};
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/ShowAreaRawData.cs b/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/ShowAreaRawData.cs
index 0ac1e65..8229bf8 100644
--- a/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/ShowAreaRawData.cs
+++ b/Assets/GameMain/Scripts/Runtime/UIBase/SelectRole/ShowAreaRawData.cs
@@ -6,6 +6,6 @@ namespace SepCore.UI
public class ShowAreaRawData
{
public string Name;
- public List InitialProperties;
+ public List InitialProperties;
}
}
diff --git a/Assets/GameMain/Scripts/Runtime/Utility/AIUtility.cs b/Assets/GameMain/Scripts/Runtime/Utility/AIUtility.cs
index 7fdc539..3c79853 100644
--- a/Assets/GameMain/Scripts/Runtime/Utility/AIUtility.cs
+++ b/Assets/GameMain/Scripts/Runtime/Utility/AIUtility.cs
@@ -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
{
diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity
index cf47dca..6253dab 100644
--- a/Assets/Launcher.unity
+++ b/Assets/Launcher.unity
@@ -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
diff --git a/Assets/Plugins/InputModule/Tests/PlayMode/InputModule/InputModuleComponentEventTests.cs b/Assets/Plugins/InputModule/Tests/PlayMode/InputModule/InputModuleComponentEventTests.cs
index f4d2f7e..51f53ad 100644
--- a/Assets/Plugins/InputModule/Tests/PlayMode/InputModule/InputModuleComponentEventTests.cs
+++ b/Assets/Plugins/InputModule/Tests/PlayMode/InputModule/InputModuleComponentEventTests.cs
@@ -119,8 +119,7 @@ namespace SepCore.InputModule.Tests
[Test]
public void RegisterListener_AddsToInternalDictionary()
{
- bool called = false;
- Action listener = _ => called = true;
+ Action listener = _ => { };
_component.RegisterListener(InputActionId.Pause, listener);
diff --git a/Assets/Tests/Gameplay.meta b/Assets/Tests/Gameplay.meta
new file mode 100644
index 0000000..8aff4f2
--- /dev/null
+++ b/Assets/Tests/Gameplay.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8388e50452a240f4da478e849742b711
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Gameplay/EditMode.meta b/Assets/Tests/Gameplay/EditMode.meta
new file mode 100644
index 0000000..c2a7648
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d80647c93ec8e7b47bef0eb7da8fd6ea
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Gameplay/EditMode/AttributePipelineTests.cs b/Assets/Tests/Gameplay/EditMode/AttributePipelineTests.cs
new file mode 100644
index 0000000..b935274
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode/AttributePipelineTests.cs
@@ -0,0 +1,173 @@
+using System;
+using NUnit.Framework;
+using SepCore.Battle;
+using SepCore.Definition;
+
+namespace VampireLike.Tests.Gameplay.EditMode
+{
+ [TestFixture]
+ public class AttributePipelineTests
+ {
+ [Test]
+ public void FormulaOrder_CalculatesFlatPercentAddAndPercentMultCorrectly()
+ {
+ var stat = new CharacterStat(StatType.MeleeDamage, baseValue: 10f);
+
+ // Formula: ((Base + Sum(Flat)) * (1 + Sum(PercentAdd))) * Prod(PercentMult)
+ // Flat: +5, +3 -> Sum = +8 -> Base + Flat = 18
+ stat.AddModifier(new StatModifier(5f, ModifierType.Flat));
+ stat.AddModifier(new StatModifier(3f, ModifierType.Flat));
+
+ // PercentAdd: +20% (0.2), +30% (0.3) -> Sum = +50% -> 1 + 0.5 = 1.5
+ stat.AddModifier(new StatModifier(0.2f, ModifierType.PercentAdd));
+ stat.AddModifier(new StatModifier(0.3f, ModifierType.PercentAdd));
+
+ // PercentMult: 1.2x, 1.5x -> Prod = 1.8
+ stat.AddModifier(new StatModifier(1.2f, ModifierType.PercentMult));
+ stat.AddModifier(new StatModifier(1.5f, ModifierType.PercentMult));
+
+ // Expected: 18 * 1.5 * 1.8 = 48.6
+ Assert.AreEqual(48.6f, stat.Value, 1e-4f);
+ }
+
+ [Test]
+ public void DirtyFlag_CachesValueUntilModified()
+ {
+ var stat = new CharacterStat(StatType.MaxHp, baseValue: 100f);
+ var mod = new StatModifier(20f, ModifierType.Flat);
+
+ stat.AddModifier(mod);
+ float firstRead = stat.Value;
+ Assert.AreEqual(120f, firstRead, 1e-4f);
+
+ // 读取多次,验证缓存值一致
+ float secondRead = stat.Value;
+ Assert.AreEqual(120f, secondRead, 1e-4f);
+
+ // 移除修饰器后脏标记生效,值重新计算
+ stat.RemoveModifier(mod);
+ Assert.AreEqual(100f, stat.Value, 1e-4f);
+
+ // 修改 BaseValue 后脏标记生效
+ stat.BaseValue = 150f;
+ Assert.AreEqual(150f, stat.Value, 1e-4f);
+ }
+
+ [Test]
+ public void RemoveAllModifiersFromSource_RemovesOnlyMatchingSource()
+ {
+ var pipeline = new AttributePipeline();
+ var sourceA = new object();
+ var sourceB = new object();
+
+ pipeline.SetBaseValue(StatType.MeleeDamage, 10f);
+ pipeline.SetBaseValue(StatType.MoveSpeed, 5f);
+
+ // sourceA 附加加成到近战伤害与移速
+ pipeline.AddModifier(StatType.MeleeDamage, new StatModifier(10f, ModifierType.Flat, sourceA));
+ pipeline.AddModifier(StatType.MoveSpeed, new StatModifier(0.2f, ModifierType.PercentAdd, sourceA));
+
+ // sourceB 附加加成到近战伤害
+ pipeline.AddModifier(StatType.MeleeDamage, new StatModifier(5f, ModifierType.Flat, sourceB));
+
+ Assert.AreEqual(25f, pipeline.GetValue(StatType.MeleeDamage), 1e-4f);
+ Assert.AreEqual(6f, pipeline.GetValue(StatType.MoveSpeed), 1e-4f);
+
+ // 清理 sourceA
+ int removedCount = pipeline.RemoveAllModifiersFromSource(sourceA);
+ Assert.AreEqual(2, removedCount);
+
+ // 验证 sourceA 被清除,sourceB 依然生效
+ Assert.AreEqual(15f, pipeline.GetValue(StatType.MeleeDamage), 1e-4f);
+ Assert.AreEqual(5f, pipeline.GetValue(StatType.MoveSpeed), 1e-4f);
+ }
+
+ [Test]
+ public void StatConfig_ClampsMinAndMaxCorrectly()
+ {
+ // 闪避率硬上限 80% (0.8f) 来自配置表
+ var dodgeConfig = new StatConfig(StatType.Dodge, hasMin: true, minValue: 0f, hasMax: true, maxValue: 0.80f);
+ var dodgeStat = new CharacterStat(StatType.Dodge, baseValue: 0.50f, dodgeConfig);
+ dodgeStat.AddModifier(new StatModifier(0.40f, ModifierType.Flat));
+ Assert.AreEqual(0.80f, dodgeStat.Value, 1e-4f);
+
+ // 移速保底 0.5f 来自配置表
+ var speedConfig = new StatConfig(StatType.MoveSpeed, hasMin: true, minValue: 0.5f);
+ var speedStat = new CharacterStat(StatType.MoveSpeed, baseValue: 3f, speedConfig);
+ speedStat.AddModifier(new StatModifier(-10f, ModifierType.Flat));
+ Assert.AreEqual(0.5f, speedStat.Value, 1e-4f);
+
+ // 暴击率硬上限 90% (0.9f) 来自配置表
+ var critConfig = new StatConfig(StatType.CritChance, hasMin: true, minValue: 0f, hasMax: true, maxValue: 0.90f);
+ var critStat = new CharacterStat(StatType.CritChance, baseValue: 0.50f, critConfig);
+ critStat.AddModifier(new StatModifier(0.60f, ModifierType.Flat));
+ Assert.AreEqual(0.90f, critStat.Value, 1e-4f);
+ }
+
+ [Test]
+ public void FromLuban_ConvertsFlatAndPercentModifiersCorrectly()
+ {
+ // 1. 固定值 (Flat): 比如近战伤害 +15
+ var flatConfig = new StatModifierConfig(StatType.MeleeDamage, 15f, ModifierType.Flat);
+ var flatStatMod = StatModifier.FromLuban(flatConfig);
+ Assert.AreEqual(ModifierType.Flat, flatStatMod.Type);
+ Assert.AreEqual(15f, flatStatMod.Value, 1e-4f);
+
+ // 2. 百分比累加 (PercentAdd): 比如伤害 +20% (0.2f)
+ var percentConfig = new StatModifierConfig(StatType.DamagePercent, 0.2f, ModifierType.PercentAdd);
+ var percentStatMod = StatModifier.FromLuban(percentConfig);
+ Assert.AreEqual(ModifierType.PercentAdd, percentStatMod.Type);
+ Assert.AreEqual(0.2f, percentStatMod.Value, 1e-4f);
+
+ // 3. 独立乘区 (PercentMult): 比如伤害 1.5 倍
+ var multConfig = new StatModifierConfig(StatType.DamagePercent, 1.5f, ModifierType.PercentMult);
+ var multStatMod = StatModifier.FromLuban(multConfig);
+ Assert.AreEqual(ModifierType.PercentMult, multStatMod.Type);
+ Assert.AreEqual(1.5f, multStatMod.Value, 1e-4f);
+ }
+
+ [Test]
+ public void OnStatChanged_FiresOnModifications()
+ {
+ var pipeline = new AttributePipeline();
+ StatType changedStat = StatType.None;
+ float changedValue = 0f;
+ int eventCount = 0;
+
+ pipeline.OnStatChanged += (type, val) =>
+ {
+ changedStat = type;
+ changedValue = val;
+ eventCount++;
+ };
+
+ // 设置基础值
+ pipeline.SetBaseValue(StatType.Armor, 10f);
+ Assert.AreEqual(StatType.Armor, changedStat);
+ Assert.AreEqual(10f, changedValue, 1e-4f);
+ Assert.AreEqual(1, eventCount);
+
+ // 添加修饰器
+ var mod = new StatModifier(5f, ModifierType.Flat);
+ pipeline.AddModifier(StatType.Armor, mod);
+ Assert.AreEqual(15f, changedValue, 1e-4f);
+ Assert.AreEqual(2, eventCount);
+
+ // 移除修饰器
+ pipeline.RemoveModifier(StatType.Armor, mod);
+ Assert.AreEqual(10f, changedValue, 1e-4f);
+ Assert.AreEqual(3, eventCount);
+ }
+
+ [Test]
+ public void DynamicConfig_MarksDirtyAndUpdatesClamp()
+ {
+ var stat = new CharacterStat(StatType.Dodge, baseValue: 0.90f);
+ Assert.AreEqual(0.90f, stat.Value, 1e-4f); // 未注入配置时不钳制
+
+ // 动态注入配置,硬上限 80%
+ stat.Config = new StatConfig(StatType.Dodge, hasMax: true, maxValue: 0.80f);
+ Assert.AreEqual(0.80f, stat.Value, 1e-4f); // 自动标记脏并重新钳制
+ }
+ }
+}
diff --git a/Assets/Tests/Gameplay/EditMode/AttributePipelineTests.cs.meta b/Assets/Tests/Gameplay/EditMode/AttributePipelineTests.cs.meta
new file mode 100644
index 0000000..f77c0e5
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode/AttributePipelineTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d4e6bd8e676b316468d43ad23e20e917
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Gameplay/EditMode/DamagePipelineTests.cs b/Assets/Tests/Gameplay/EditMode/DamagePipelineTests.cs
new file mode 100644
index 0000000..5be5839
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode/DamagePipelineTests.cs
@@ -0,0 +1,316 @@
+using NUnit.Framework;
+using SepCore.Battle;
+using SepCore.Definition;
+
+namespace VampireLike.Tests.Gameplay.EditMode
+{
+ [TestFixture]
+ public class DamagePipelineTests
+ {
+ [Test]
+ public void DamageScaling_AddsMeleeDamageBeforePercentMultiplier()
+ {
+ var attackerPipeline = new AttributePipeline();
+ attackerPipeline.SetBaseValue(StatType.MeleeDamage, 20f);
+ attackerPipeline.SetBaseValue(StatType.DamagePercent, 0.50f); // +50%
+
+ var context = new DamageContext
+ {
+ BaseDamage = 100,
+ DamageScalingStat = StatType.MeleeDamage,
+ ScalingRatio = 1.0f,
+ BaseCritChance = 0f,
+ CritMultiplier = 1.5f,
+ AttackerPipeline = attackerPipeline,
+ DefenderPipeline = null,
+ FallbackDefenderArmor = 0f,
+ FallbackDefenderDodge = 0f
+ };
+
+ // Raw = 100 + 20 = 120
+ // Amplified = 120 * (1 + 0.5) = 180
+ // Armor = 0 -> Mitigation = 1.0 -> 180
+ DamageResult result = DamagePipeline.Calculate(in context);
+ Assert.AreEqual(180, result.Damage);
+ Assert.IsFalse(result.IsDodge);
+ Assert.IsFalse(result.IsCrit);
+ }
+
+ [Test]
+ public void DamageScaling_RangedAndElemental_ApplyToRespectiveTypes()
+ {
+ var attackerPipeline = new AttributePipeline();
+ attackerPipeline.SetBaseValue(StatType.RangedDamage, 30f);
+ attackerPipeline.SetBaseValue(StatType.ElementalDamage, 50f);
+
+ // 1. 远程伤害加成
+ var rangedContext = new DamageContext
+ {
+ BaseDamage = 50,
+ DamageScalingStat = StatType.RangedDamage,
+ ScalingRatio = 1.0f,
+ AttackerPipeline = attackerPipeline
+ };
+ DamageResult rangedResult = DamagePipeline.Calculate(in rangedContext);
+ Assert.AreEqual(80, rangedResult.Damage); // 50 + 30 = 80
+
+ // 2. 元素伤害加成(带 0.8 加成系数)
+ var elementalContext = new DamageContext
+ {
+ BaseDamage = 50,
+ DamageScalingStat = StatType.ElementalDamage,
+ ScalingRatio = 0.8f,
+ AttackerPipeline = attackerPipeline
+ };
+ DamageResult elementalResult = DamagePipeline.Calculate(in elementalContext);
+ Assert.AreEqual(90, elementalResult.Damage); // 50 + 50 * 0.8 = 90
+ }
+
+ [Test]
+ public void ArmorMitigation_PositiveArmor_AppliesHyperbolicReduction()
+ {
+ // 基础伤害 100,测试双曲公式 1 / (1 + Armor / 15)
+ // 1. Armor = 15 -> 减伤 50% -> 50
+ var context15 = new DamageContext
+ {
+ BaseDamage = 100,
+ FallbackDefenderArmor = 15f
+ };
+ DamageResult result15 = DamagePipeline.Calculate(in context15);
+ Assert.AreEqual(50, result15.Damage);
+
+ // 2. Armor = 30 -> 减伤 66.7% -> 100 / 3 = 33.33 -> 向上取整 34
+ var context30 = new DamageContext
+ {
+ BaseDamage = 100,
+ FallbackDefenderArmor = 30f
+ };
+ DamageResult result30 = DamagePipeline.Calculate(in context30);
+ Assert.AreEqual(34, result30.Damage);
+ }
+
+ [Test]
+ public void ArmorMitigation_NegativeArmor_IncreasesDamageTaken()
+ {
+ // 基础伤害 100,负护甲增伤公式: 2 - 1 / (1 - Armor / 15)
+ // Armor = -15 -> 2 - 1 / (1 - (-1)) = 2 - 0.5 = 1.5 -> 150
+ var context = new DamageContext
+ {
+ BaseDamage = 100,
+ FallbackDefenderArmor = -15f
+ };
+ DamageResult result = DamagePipeline.Calculate(in context);
+ Assert.AreEqual(150, result.Damage);
+ }
+
+ [Test]
+ public void Crit_WhenGuaranteed_MultipliesDamageCorrectly()
+ {
+ var context = new DamageContext
+ {
+ BaseDamage = 100,
+ BaseCritChance = 1.0f, // 100% 暴击率(触发 Clamp 90% 上限,但若直接传入高暴击率测试倍率)
+ CritMultiplier = 2.0f,
+ FallbackDefenderArmor = 0f
+ };
+
+ // 测试 100 次计算,由于暴击率最高 90%,我们设置 BaseCritChance = 0.9f 并循环验证暴击时的数值
+ var guaranteedContext = new DamageContext
+ {
+ BaseDamage = 100,
+ BaseCritChance = 0.9f,
+ CritMultiplier = 2.0f
+ };
+
+ bool sawCrit = false;
+ for (int i = 0; i < 50; i++)
+ {
+ DamageResult result = DamagePipeline.Calculate(in guaranteedContext);
+ if (result.IsCrit)
+ {
+ Assert.AreEqual(200, result.Damage);
+ sawCrit = true;
+ break;
+ }
+ }
+
+ Assert.IsTrue(sawCrit, "At 90% crit chance, at least one crit should occur across 50 trials.");
+ }
+
+ [Test]
+ public void Dodge_WhenGuaranteed_ReturnsZeroDamageAndIsDodgeTrue()
+ {
+ var defenderPipeline = new AttributePipeline();
+ defenderPipeline.SetBaseValue(StatType.Dodge, 0.80f); // 80% 闪避率上限
+
+ var context = new DamageContext
+ {
+ BaseDamage = 100,
+ DefenderPipeline = defenderPipeline
+ };
+
+ bool sawDodge = false;
+ for (int i = 0; i < 50; i++)
+ {
+ DamageResult result = DamagePipeline.Calculate(in context);
+ if (result.IsDodge)
+ {
+ Assert.AreEqual(0, result.Damage);
+ sawDodge = true;
+ break;
+ }
+ }
+
+ Assert.IsTrue(sawDodge, "At 80% dodge chance, at least one dodge should occur across 50 trials.");
+ }
+
+ [Test]
+ public void MinDamage_EnforcesFloor_WhenMitigationReducesDamageTooLow()
+ {
+ var context = new DamageContext
+ {
+ BaseDamage = 1,
+ FallbackDefenderArmor = 9999f
+ };
+
+ DamageResult result = DamagePipeline.Calculate(in context);
+ Assert.AreEqual(1, result.Damage);
+ }
+
+ [Test]
+ public void LifeSteal_TriggersWhenDamageDealtAndRateSucceeds()
+ {
+ var attackerPipeline = new AttributePipeline();
+ attackerPipeline.SetBaseValue(StatType.LifeSteal, 0.99f); // 99% 吸血概率
+
+ var context = new DamageContext
+ {
+ BaseDamage = 50,
+ AttackerPipeline = attackerPipeline
+ };
+
+ bool sawLifeSteal = false;
+ for (int i = 0; i < 20; i++)
+ {
+ DamageResult result = DamagePipeline.Calculate(in context);
+ if (result.IsLifeSteal)
+ {
+ Assert.AreEqual(1, result.LifeStealAmount);
+ sawLifeSteal = true;
+ break;
+ }
+ }
+
+ Assert.IsTrue(sawLifeSteal, "At 99% life steal rate, life steal should trigger.");
+ }
+
+ [Test]
+ public void DamageTypeExtensions_MapsCorrectlyToStatType()
+ {
+ Assert.AreEqual(StatType.MeleeDamage, DamageType.Melee.ToScalingStat());
+ Assert.AreEqual(StatType.RangedDamage, DamageType.Ranged.ToScalingStat());
+ Assert.AreEqual(StatType.ElementalDamage, DamageType.Element.ToScalingStat());
+ Assert.AreEqual(StatType.None, DamageType.None.ToScalingStat());
+ }
+
+ [Test]
+ public void AttackSpeed_ScalesCooldownCorrectly()
+ {
+ float baseCooldown = 1.0f;
+
+ // 1. +50% 攻速 -> 1.0 / (1 + 0.5) = 0.6667s
+ float speedBonus1 = 0.5f;
+ float factor1 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus1);
+ float cd1 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor1);
+ Assert.AreEqual(1f / 1.5f, cd1, 1e-4f);
+
+ // 2. -50% 攻速 -> 1.0 / (1 - 0.5) = 2.0s
+ float speedBonus2 = -0.5f;
+ float factor2 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus2);
+ float cd2 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor2);
+ Assert.AreEqual(2.0f, cd2, 1e-4f);
+
+ // 3. -80% 攻速(配置表下限)-> 1.0 / (1 - 0.8) = 5.0s
+ float speedBonus3 = -0.8f;
+ float factor3 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus3);
+ float cd3 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor3);
+ Assert.AreEqual(5.0f, cd3, 1e-4f);
+
+ // 4. -95% 攻速(受 0.2 保护下限钳制)-> 1.0 / 0.2 = 5.0s
+ float speedBonus4 = -0.95f;
+ float factor4 = UnityEngine.Mathf.Max(0.2f, 1f + speedBonus4);
+ float cd4 = UnityEngine.Mathf.Max(0.02f, baseCooldown / factor4);
+ Assert.AreEqual(5.0f, cd4, 1e-4f);
+ }
+
+ [Test]
+ public void RangeBonus_CalculatesAttackRangeCorrectly()
+ {
+ float baseRange = 5.0f; // 5m
+
+ // 1. 增加 2m (direct units)
+ float bonus1 = 2.0f;
+ float eff1 = bonus1 >= 50f ? bonus1 / 1000f : bonus1;
+ float range1 = UnityEngine.Mathf.Max(0.5f, baseRange + eff1);
+ Assert.AreEqual(7.0f, range1, 1e-4f);
+
+ // 2. 增加 1500 (millimeters)
+ float bonus2 = 1500f;
+ float eff2 = bonus2 >= 50f ? bonus2 / 1000f : bonus2;
+ float range2 = UnityEngine.Mathf.Max(0.5f, baseRange + eff2);
+ Assert.AreEqual(6.5f, range2, 1e-4f);
+ }
+
+ [Test]
+ public void LifeStealICD_ThrottlesRepeatedTriggersWithin100ms()
+ {
+ var go = new UnityEngine.GameObject("TestHealthEntity");
+ var health = go.AddComponent();
+ health.OnInit(100);
+ health.CurrentHealth = 50;
+
+ // 第一次吸血成功 (t = 1.00s)
+ bool first = health.TryTriggerLifeSteal(1, customTime: 1.00f);
+ Assert.IsTrue(first);
+ Assert.AreEqual(51, health.CurrentHealth);
+
+ // 50ms 后再次触发,小于 0.1s ICD,应被节流拦截
+ bool second = health.TryTriggerLifeSteal(1, customTime: 1.05f);
+ Assert.IsFalse(second);
+ Assert.AreEqual(51, health.CurrentHealth);
+
+ // 110ms 后触发 (t = 1.11s),超过 0.1s ICD,应成功
+ bool third = health.TryTriggerLifeSteal(1, customTime: 1.11f);
+ Assert.IsTrue(third);
+ Assert.AreEqual(52, health.CurrentHealth);
+
+ UnityEngine.Object.DestroyImmediate(go);
+ }
+
+ [Test]
+ public void MovementAndHealthComponents_EvaluateViaAttributePipeline()
+ {
+ var go = new UnityEngine.GameObject("TestEntity");
+ var statComp = go.AddComponent();
+ var moveComp = go.AddComponent();
+ var healthComp = go.AddComponent();
+
+ // 初始化:基础速度 5m/s,基础生命 100
+ moveComp.OnInit(5.0f, go.transform, statComp, false, 0.5f, 4);
+ healthComp.OnInit(100, statComp);
+
+ Assert.AreEqual(5.0f, moveComp.Speed, 1e-4f);
+ Assert.AreEqual(100, healthComp.MaxHealth);
+
+ // 添加 +20% 移速修饰器
+ statComp.AddModifier(StatType.MoveSpeed, new StatModifier(0.20f, ModifierType.PercentAdd));
+ Assert.AreEqual(6.0f, moveComp.Speed, 1e-4f); // 5 * 1.2 = 6.0
+
+ // 添加 +50 生命上限修饰器
+ statComp.AddModifier(StatType.MaxHp, new StatModifier(50f, ModifierType.Flat));
+ Assert.AreEqual(150, healthComp.MaxHealth);
+
+ UnityEngine.Object.DestroyImmediate(go);
+ }
+ }
+}
diff --git a/Assets/Tests/Gameplay/EditMode/DamagePipelineTests.cs.meta b/Assets/Tests/Gameplay/EditMode/DamagePipelineTests.cs.meta
new file mode 100644
index 0000000..02220f0
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode/DamagePipelineTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6f8492019a4b419c8d3091728490bb15
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Gameplay/EditMode/Gameplay.EditModeTests.asmdef b/Assets/Tests/Gameplay/EditMode/Gameplay.EditModeTests.asmdef
new file mode 100644
index 0000000..f8988a7
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode/Gameplay.EditModeTests.asmdef
@@ -0,0 +1,27 @@
+{
+ "name": "Gameplay.EditModeTests",
+ "rootNamespace": "",
+ "references": [
+ "UnityGameFramework.Runtime",
+ "UnityEngine.TestRunner",
+ "UnityEditor.TestRunner",
+ "SepCore.Runtime",
+ "SepCore.Base"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": true,
+ "precompiledReferences": [
+ "nunit.framework.dll",
+ "GameFramework.dll"
+ ],
+ "autoReferenced": false,
+ "defineConstraints": [
+ "UNITY_INCLUDE_TESTS"
+ ],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Assets/Tests/Gameplay/EditMode/Gameplay.EditModeTests.asmdef.meta b/Assets/Tests/Gameplay/EditMode/Gameplay.EditModeTests.asmdef.meta
new file mode 100644
index 0000000..9846fbb
--- /dev/null
+++ b/Assets/Tests/Gameplay/EditMode/Gameplay.EditModeTests.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 98acb13808fbbff4c83e548d62c5119d
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/docs/P5 Gameplay 玩法体系落地规划.md b/docs/P5 Gameplay 玩法体系落地规划.md
new file mode 100644
index 0000000..d9d9eb7
--- /dev/null
+++ b/docs/P5 Gameplay 玩法体系落地规划.md
@@ -0,0 +1,318 @@
+# P5 Gameplay 玩法体系落地规划(类土豆兄弟 Brotato)
+
+> **文档定位**:针对当前项目“性能优化/架构重,Gameplay 较少”在校招面试中的痛点,将项目从单纯的割草压测 Demo 升级为具备深度系统设计、复杂公式结算、规则可插拔的商业级“类土豆兄弟(Brotato)”完整玩法原型。
+>
+> 结合现有技术栈:**UGF + Luban 配置表 + 5层UI架构 + SimulationWorld(Burst + Job)**。
+
+---
+
+## 目录
+1. [背景与核心设计目标](#1-背景与核心设计目标)
+2. [总体架构分层与工程边界](#2-总体架构分层与工程边界)
+3. [Phase 1: 复合属性修饰器管线与伤害结算系统](#3-phase-1-复合属性修饰器管线与伤害结算系统)
+4. [Phase 2: 武器标签、羁绊与合成升阶系统](#4-phase-2-武器标签羁绊与合成升阶系统)
+5. [Phase 3: 战斗触发器总线与状态/Buff系统](#5-phase-3-战斗触发器总线与状态buff系统)
+6. [Phase 4: 局内波次经济循环与商店系统实战](#6-phase-4-局内波次经济循环与商店系统实战)
+7. [Phase 5: 差异化角色特质系统(规则颠覆)](#7-phase-5-差异化角色特质系统规则颠覆)
+8. [Phase 6: 机制化敌人与波次节奏导演](#8-phase-6-机制化敌人与波次节奏导演)
+9. [阶段里程碑与推进检查表](#9-阶段里程碑与推进检查表)
+10. [校招简历重塑与面试对线指南](#10-校招简历重塑与面试对线指南)
+
+---
+
+## 1. 背景与核心设计目标
+
+### 1.1 面试痛点诊断
+- **现状**:简历重构了 `SimulationWorld`,落地了 Burst + Job System,Android 压测 2k 敌人降耗 56.4%;但面试官全在问 Gameplay,因缺乏深度玩法而陷入被动。
+- **面试官心理**:
+ 1. 校招 Gameplay 岗核心看重:**能否承接复杂策划需求、业务抽象能力、数据结构与算法应用、面向对象与开闭原则(OCP)**。
+ 2. 对单纯纯移动/无复杂状态的 Job 压测持防御态度(担心逻辑复杂后无法扩展)。
+- **破局方案**:
+ - **保留底层性能作为技术壁垒**,展示“不仅能写底层高性能 Job,还能在上层支撑复杂多状态 Gameplay”。
+ - 落地土豆兄弟最具有代表性的核心机制:**深度属性网络、武器羁绊与合成、规则颠覆型角色特质、事件触发器与 Buff、波次经济商店**。
+
+---
+
+## 2. 总体架构分层与工程边界
+
+为防止业务逻辑与现有的 Job/Burst 模拟层产生混乱耦合,确立以下分层原则:
+
+```mermaid
+flowchart TD
+ subgraph UI_Presentation ["表现与界面层 (Presentation)"]
+ UI["5层UI架构 (RawData/UseCase/Controller/Context/View)"]
+ FX["表现层 (Animator / Audio / VFX / DamageText)"]
+ end
+
+ subgraph Gameplay_Logic ["Gameplay 逻辑主控层 (Managed C#)"]
+ RoleTrait["角色特质系统 (Rule Interceptor)"]
+ AttrSys["属性修饰器管线 (Attribute Pipeline)"]
+ WeaponSys["武器槽位/羁绊/合成 (Weapon Synergy & Combine)"]
+ BuffSys["Buff与状态触发器总线 (Event Dispatcher & Buffs)"]
+ ShopSys["商店事务与波次经济 (Economy & Wave Director)"]
+ end
+
+ subgraph Sim_Layer ["高性能物理与移动层 (Burst + Job System)"]
+ SimWorld["SimulationWorld"]
+ Jobs["移动 / 分离 / 空间哈希分桶 / 碰撞候选生成 (Broad Phase)"]
+ end
+
+ UI --> Gameplay_Logic
+ Gameplay_Logic --> SimWorld
+ SimWorld -->|命中事件缓冲区 / 坐标输出| Gameplay_Logic
+ Gameplay_Logic --> FX
+```
+
+1. **SimulationWorld 保持纯粹**:
+ - 负责:空间哈希、宽相碰撞筛选、集群移动分离、投射物飞行轨迹。
+ - 绝不引入:托管对象、UI 回调、复杂 Buff 状态机。
+2. **Gameplay 逻辑层(托管)掌管结算**:
+ - 消费 Simulation 输出的命中对(Collision Pair),执行伤害公式计算、Buff 挂载、击杀判定、事件派发。
+3. **表现层与 UI 解耦**:
+ - UI 严格遵守现有的 5 层架构与 UGF 事件通信。
+
+---
+
+## 3. Phase 1: 复合属性修饰器管线与伤害结算系统
+
+### 3.1 属性定义(15+ 维核心属性)
+土豆兄弟的核心乐趣在于属性间的互斥与权衡取舍:
+- **基础输出**:近战伤害(`MeleeDamage`)、远程伤害(`RangedDamage`)、元素伤害(`ElementalDamage`)、百分比增伤(`DamagePercent`)、攻击速度(`AttackSpeed`)、暴击率(`CritChance`)、攻击范围(`Range`)。
+- **生存与机动**:最大生命(`MaxHp`)、生命回复(`HpRegen`)、生命偷取(`LifeSteal`)、护甲(`Armor`)、闪避(`Dodge`)、移动速度(`MoveSpeed`)。
+- **特殊与经济**:工程学(`Engineering`,影响炮台/地雷)、收获(`Harvesting`,波次利息)、幸运(`Luck`,箱子掉率与商店品质)。
+
+### 3.2 属性修饰器管线(Attribute Modifier Pipeline)
+采用 **Base + Modifiers** 结构,结合 **Dirty Flag(脏标记缓存)** 避免高频 GC 与重算:
+
+```csharp
+public enum StatType
+{
+ MaxHp, HpRegen, LifeSteal, DamagePercent, MeleeDamage, RangedDamage,
+ ElementalDamage, AttackSpeed, CritChance, Engineering, Range,
+ Armor, Dodge, MoveSpeed, Harvesting, Luck
+}
+
+public enum ModifierType
+{
+ Flat = 100, // 固定值累加 (e.g. +5 近战伤害)
+ PercentAdd = 200, // 百分比累加 (e.g. +10% 伤害, 与其他同类相加)
+ PercentMult = 300 // 最终百分比独立乘区 (e.g. 伤害变为 1.5 倍)
+}
+
+public class StatModifier
+{
+ public float Value { get; }
+ public ModifierType Type { get; }
+ public object Source { get; } // 来源:道具/武器/羁绊/特质
+}
+```
+
+- **求值公式**:
+ $$\text{CurrentValue} = \left( (\text{Base} + \sum \text{Flat}) \times (1 + \sum \text{PercentAdd}) \right) \times \prod \text{PercentMult}$$
+- **边界钳制(Clamping)**:
+ - 闪避硬上限:`Mathf.Min(Dodge, 0.60f)`(最高 60%)。
+ - 护甲减伤公式:$$\text{DamageMultiplier} = \frac{1}{1 + \frac{\text{Armor}}{15}} \quad (\text{若 } \text{Armor} \ge 0)$$
+ - 生命偷取:按次命中概率触发,内置全局 CD(如最多每秒偷取 10 次,防止霰弹枪/加特林瞬间回满)。
+
+### 3.3 伤害结算流水线
+```mermaid
+flowchart LR
+ A[武器基础伤害] --> B[类型加成: 近战/远程/元素]
+ B --> C[百分比伤害放大: 1 + DamagePercent]
+ C --> D[暴击判定: 暴击倍率 1.5x ~ 2.0x]
+ D --> E[目标护甲折减]
+ E --> F[伤害飘字 & 扣血 & 吸血触发]
+```
+
+---
+
+## 4. Phase 2: 武器标签、羁绊与合成升阶系统
+
+### 4.1 核心机制规范
+1. **6 槽位限制**:玩家最多同时装备 6 把武器,槽位满时不可拾取/购买新武器(除非替换或合成)。
+2. **武器标签(Weapon Tags)**:
+ - 每种武器配置 1~2 个标签,如:
+ - 小刀:`[Blade, Precise]`
+ - 霰弹枪:`[Gun]`
+ - 扳手:`[Engineering, Blunt]`
+ - 火把:`[Primitive, Elemental]`
+3. **阶梯羁绊(Set Bonuses)**:
+ - 当装备中同标签武器数量达到阈值(如 2 / 3 / 4 / 6)时,自动挂载全队属性修饰器:
+ - *原始(Primitive)*:2件 +3 HP / 3件 +6 HP / 6件 +15 HP。
+ - *刀剑(Blade)*:2件 +1 近战伤害 / 4件 +3 近战伤害 / 6件 +8 近战伤害 + 5% 吸血。
+ - *枪械(Gun)*:2件 +10 射程 / 4件 +25 射程 / 6件 +50 射程且自带穿透+1。
+4. **武器合成(Tier Synthesis)**:
+ - 武器分 4 阶:白(Tier 1)/ 蓝(Tier 2)/ 紫(Tier 3)/ 红(Tier 4)。
+ - **合成规则**:在商店或背包中,两把同名、同品质的武器可点击合成,升阶为高一阶武器,空出一个槽位并增强属性(CD更短、射程更远、新增弹道或穿透)。
+
+### 4.2 数据表设计(Luban)
+- **`WeaponConfig_武器配置.xlsx` 扩展**:
+ - `Tags`:`list,int`(标签枚举列表)。
+ - `Tier`:`int`(品质 1~4)。
+ - `NextTierWeaponId`:`int`(下一级武器 ID)。
+ - `ScalingStat`:`int`(加成主属性:近战/远程/元素/工程)。
+ - `ScalingRatio`:`float`(加成系数,如 0.8)。
+- **新建 `WeaponSynergyConfig_武器羁绊配置.xlsx`**:
+ - `TagId`:标签 ID。
+ - `RequiredCounts`:`list,int`(阶梯数量,如 `[2, 3, 4, 6]`)。
+ - `Modifiers`:`list,bean:StatModifierConfig`(对应各阶梯触发的属性修正)。
+
+---
+
+## 5. Phase 3: 战斗触发器总线与状态/Buff系统
+
+### 5.1 战斗事件总线(Battle Event Bus)
+建立高性能的直接委托事件驱动,覆盖战斗高频节点:
+- `OnHit(HitContext ctx)`:命中敌人时(参数含受击者、伤害、是否暴击、伤害类型)。
+- `OnDamaged(DamageContext ctx)`:自身受到伤害时。
+- `OnKill(KillContext ctx)`:击杀敌人时。
+- `OnWaveStart()` / `OnWaveEnd()`:波次起止。
+
+### 5.2 经典 Buff 框架
+```csharp
+public abstract class BuffBase
+{
+ public int BuffId { get; set; }
+ public float Duration { get; set; }
+ public int CurrentStacks { get; set; }
+ public int MaxStacks { get; set; }
+
+ public virtual void OnApply(Entity owner) { }
+ public virtual void OnTick(Entity owner, float deltaTime) { }
+ public virtual void OnRemove(Entity owner) { }
+ public virtual void OnStackAdded(Entity owner) { }
+}
+```
+- **堆叠策略**:
+ 1. `RefreshDuration`:刷新持续时间,层数 +1(如点燃、流血)。
+ 2. `Additive`:独立计时器(复杂 Buff)。
+ 3. `Override`:高强度替换低强度。
+- **代表性状态实装**:
+ - **点燃(Burning)**:每 0.5s 造成一次伤害,伤害受玩家 `ElementalDamage` 加成,受击者死亡时向周围 2m 内敌人传播点燃。
+ - **击退(Knockback)**:击退位移,接入 `SimulationWorld` 的速度冲量或阻尼位移。
+
+---
+
+## 6. Phase 4: 局内波次经济循环与商店系统实战
+
+### 6.1 核心流转与双重货币(Materials)
+土豆兄弟没有将经验和金币分开,掉落的**绿晶(材料 Material)**同时兼具两种职能:
+1. **拾取即获得局内经验**:达到阈值触发升级弹窗(局末加点或即时加点)。
+2. **拾取存入临时金库**:作为波次结束后在商店消费的唯一货币。
+3. **吸附清场**:波次计时结束(如 20s~60s)时,地图上未拾取的材料自动全图飞向玩家并结算入库。
+
+### 6.2 收获机制(Harvesting)
+- 每波次结束时:玩家获得等同于 `Harvesting` 点数的材料奖励。
+- **复合增长(利息)**:若 `Harvesting > 0`,每波结束其自身永久增长 5%(向上取整),形成后期经济滚雪球。
+
+### 6.3 商店系统落地(实战验证 5 层 UI 架构)
+- **商品生成**:每轮提供 4 个展示槽(武器 + 道具混合抽取,概率受 `Luck` 属性加成)。
+- **锁定槽位(Lock)**:玩家资金不足时可锁定某格商品,波次刷新时不被替换。
+- **刷新(Reroll)与通胀**:
+ $$\text{RerollCost} = \text{BaseCost} + (\text{CurrentWave} \times 1) + (\text{RollTimesInThisShop} \times 2)$$
+- **属性升级四选一(Level-up Draft)**:
+ - 每升 1 级积累 1 次加点机会。
+ - 随机抽取 4 项属性,并按概率随机赋予品质(普通/稀有/史诗/传奇,加成数值成倍放大)。
+
+---
+
+## 7. Phase 5: 差异化角色特质系统(规则颠覆)
+
+### 7.1 为什么不做简单的数值增减?
+如果只是“角色 A 攻击力 +10%,角色 B 生命 +20”,毫无架构含金量。土豆兄弟的精髓在于**打破既有规则**。
+
+### 7.2 规则拦截器架构(Rule Interceptor Pattern)
+通过接口/抽象组件挂载,在主流程核心逻辑的关键判断点注入拦截逻辑,遵循**开闭原则(OCP)**,拒绝在全局战斗代码中堆砌 `if (role == Bull)`:
+
+```csharp
+public interface IRuleInterceptor
+{
+ // 是否允许装备指定类型武器
+ bool CanEquipWeapon(WeaponConfig weapon, int currentWeaponCount);
+ // 受到伤害时的特殊结算(公牛自爆)
+ void OnTakeDamageIntercept(ref DamageInfo damage);
+ // 波次结算时的特殊收益(和平主义者)
+ void OnWaveEndIntercept(WaveResult result);
+}
+```
+
+### 7.3 实装 4 个极具代表性的角色
+1. **独臂(One-Armed)**:
+ - 规则:`MaxWeaponSlots = 1`。
+ - 补偿:常驻 `AttackSpeed +200%`,`DamagePercent +100%`。
+2. **公牛(Bull)**:
+ - 规则:`MaxWeaponSlots = 0`(不可携带任何武器)。
+ - 特质:受击时立即触发 360° 巨额范围爆炸,基础护甲 +15,生命秒回 +10。
+3. **和平主义者(Pacifist)**:
+ - 规则:常驻 `DamagePercent = -100%`(击杀收益极低)。
+ - 特质:波次倒计时结束时,场上存活的敌人每有 1 只,转化为 1 份额外材料与经验。
+4. **法师(Mage)**:
+ - 规则:所有攻击附带点燃效果,元素伤害加成翻倍,但近战/远程基础伤害扣减。
+
+---
+
+## 8. Phase 6: 机制化敌人与波次节奏导演
+
+### 8.1 典型敌人机制(区别于纯移速僵尸)
+1. **冲锋怪(Charger)**:进入玩家一定距离后停步蓄力闪红(Telegraph),随后进行高移速直线冲撞。
+2. **图腾增益怪(Buffer)**:自身不攻击,环绕光环为其半径 4m 内的其他敌人提供移速与减伤 Buff。
+3. **远程射击怪(Shooter)**:与玩家保持放风筝距离,周期性射出直线投射物。
+4. **第 11 / 20 波 精英/Boss**:
+ - 拥有多阶段行为(Phase 1 环形弹幕,Phase 2 召唤小怪 + 追踪地雷),击杀必掉稀有道具宝箱。
+
+---
+
+## 9. 阶段里程碑与推进检查表
+
+| 阶段 | 核心任务 | 涉及代码与模块 | 交付标准与回归 |
+| :--- | :--- | :--- | :--- |
+| **Phase 1** | **属性修饰器与伤害管线** | `StatModifier`, `AttributePipeline`, `PlayerData` 扩展, 伤害公式重构 | 单元测试覆盖所有修饰器加法/乘法与护甲折减公式,无浮点截断异常 |
+| **Phase 2** | **武器标签羁绊与合成** | `WeaponSynergyManager`, `WeaponConfig` 扩展, 背包/槽位管理 | 6槽位限制生效,同标签激活被动,2把同名同阶武器合成升阶无资源泄露 |
+| **Phase 3** | **事件触发器与Buff** | `BattleEventBus`, `BuffManager`, 点燃/击退/吸血实装 | 点燃每秒跳字传火正常,击退对接 Simulation 冲量,吸血有内置 CD 控制 |
+| **Phase 4** | **波次经济与商店实战** | `ShopController` (5层UI), `Harvesting` 利息公式, 锁定与加点 | 倒计时清场自动吸附材料,商店刷新递增、锁定跨波次保存、4选1加点正常 |
+| **Phase 5** | **规则颠覆角色特质** | `IRuleInterceptor`, 实装独臂/公牛/和平主义者 | 零硬编码支持独臂只能拿1把枪、公牛无法拿武器且受击自爆 |
+| **Phase 6** | **机制敌人与波次配置** | 冲锋怪、图腾怪、Boss 弹幕、`WaveDirector` 表驱动 | 1~20 波次节奏平滑,精英怪出场带有危险警示圈与阶段切换 |
+
+---
+
+## 10. 校招简历重塑与面试对线指南
+
+完成上述系统后,简历专业度将实现质的飞跃。
+
+### 10.1 简历 Bullet Points 建议模板
+
+- **主流程与 UI 架构**:
+ > 基于 Unity GameFramework 的 Procedure/FSM 搭建波次流转与选卡循环;为 Shop / LevelUp 设计落地 **RawData-UseCase-Controller-Context-View 五层 UI 架构**,解耦商品生成、锁定事务与展示渲染,提升高频迭代模块的复用性。
+- **Gameplay 核心系统架构(重点突出)**:
+ > - **多维属性与伤害流水线**:设计支持 15+ 维派生属性的 **Attribute Modifier Pipeline**,通过脏标记(Dirty Flag)缓存避免频繁重算;落地包含基础增伤、暴击、护甲减免与内置 CD 吸血的复合伤害公式。
+ > - **武器羁绊与合成升阶**:实现基于**标签(Tags)聚合驱动的武器多维羁绊系统**与 6 槽位限制,支持同品质武器 2 合 1 升阶,解耦武器生命周期、模型挂点与发射器复用。
+ > - **事件驱动触发器与状态机**:搭建基于**观察者模式的战斗事件总线(OnHit / OnDamaged / OnKill)**与通用 Buff 状态机,实现点燃(DoT与传火扩散)、击退与状态互斥堆叠。
+ > - **开闭原则特质系统**:采用**规则拦截器(Rule Interceptor)模式**实现差异化角色特质,在零修改战斗主链路的前提下无缝支持“独臂(限制单武器/高攻速)”、“公牛(禁止武器/受击自爆)”等规则颠覆型玩法。
+- **底层性能保障(底牌)**:
+ > - **Job System + Burst 并行模拟**:重构底层 `SimulationWorld`,将海量敌人移动分离、投射物模拟与网格空间哈希候选筛选接入 Burst 并行计算;在实机 2k 敌人同屏下 Tick 热路径耗时由 21.68 ms 降至 9.44 ms,为上层复杂 Gameplay 提供了宽裕的性能预算。
+
+---
+
+### 10.2 高频面试真题与最佳应答套路
+
+#### Q1: “如果策划要加一个新角色‘受到伤害时分裂出小型炸弹,且无法使用枪械’,你的代码要改哪些地方?”
+* **雷区回答**:在受到伤害的地方写 `if (role == BombMan)`,在穿装备的地方写 `if (weapon == Gun && role == BombMan) return false;`。
+* **满分回答**:
+ > “在我的架构里完全不需要改动任何战斗核心代码。我设计了 `IRuleInterceptor` 特质机制:
+ > 1. 新建 `BombManTrait`,在 `CanEquipWeapon` 中检查武器标签若包含 `Gun` 则拦截返回 `false`。
+ > 2. 实现 `OnDamaged` 接口,订阅受击事件,在自身坐标实例化炸弹实体。
+ > 3. 在 Luban 的 `RoleConfig` 中为该角色绑定此特质 ID,角色进入战斗时自动挂载到上下文。核心逻辑完全对扩展开放、对修改关闭。”
+
+#### Q2: “武器羁绊系统如果有 6 件套加成,玩家在战斗或商店里频繁换装/合成,属性怎么计算才不会错乱或产生高开销?”
+* **满分回答**:
+ > “我采用了 **Tag 计数器 + 属性修饰器句柄 + 脏标记** 的设计:
+ > 1. 玩家持有 `Dictionary`。每次穿卸或合成武器,只对对应 Tag 计数做增减。
+ > 2. 羁绊触发器监听计数变更,当跨越 2/3/4/6 阈值时,向角色属性池派发或撤销带有唯一来源标记(Source)的 `StatModifier`。
+ > 3. 属性系统自身不每帧循环遍历修饰器,而是通过 `_isDirty` 标记在下次读取(如计算攻击伤害或生命最大值)时惰性重算并缓存结果,零 GC 且保证数据唯一真相源。”
+
+#### Q3: “你的底层用 Burst 和 Job 处理了 2k 敌人,但上面这套复杂的 Buff、点燃传火、触发器全在托管堆,这两者怎么高效协同而不打破 Burst 约束?”
+* **满分回答**:
+ > “这是我专门设计的**逻辑分层(Simulation vs Gameplay)边界**:
+ > 1. Job 和 Burst 只做高密度的**纯数据计算与宽相筛选(Broad Phase)**——如空间分桶、距离剔除、包围盒相交,并将发生碰撞的实体 ID 成对输出到 `NativeQueue` 中。
+ > 2. 在主线程 Tick 阶段,由托管层的 `BattleManager` 一次性消费队列事件,执行复杂的 Gameplay 逻辑(判断防御、暴击、挂载 Buff、派发 C# 事件)。
+ > 3. 这样既享受了 Burst 处理大批量空间遍历与移动物理的高性能,又保住了上层 Gameplay 编写复杂业务时的灵活性与面向对象表达能力。”
diff --git a/docs/Route.md b/docs/Route.md
index 4e89e34..834c9f4 100644
--- a/docs/Route.md
+++ b/docs/Route.md
@@ -1,4 +1,4 @@
-# 3D 类吸血鬼幸存者项目 Todo(GameMain 侧规划)
+# 3D 类吸血鬼幸存者项目 Todo(GameMain 侧规划)
> 范围说明:本清单基于当前 `Assets/GameMain` 代码现状制定,未涉及 `Assets/GameFramework` 底层实现。
@@ -199,6 +199,8 @@
- 热更失败可自动回退,启动不中断。
## 6. P5 玩法目标对齐(与技术栈并行)
+> 详细落地规划已输出至独立文档:[P5 Gameplay 玩法体系落地规划.md](P5%20Gameplay%20玩法体系落地规划.md)
+
- [ ] 武器系统补完:
- 自动攻击、多武器并存、同武器升级/进化。
- `Shop` 武器购买流程补完(当前已有 TODO)。