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)。