188 行
5.5 KiB
Go
188 行
5.5 KiB
Go
|
|
package battle
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math/rand"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/honghuang-game/server/config"
|
||
|
|
)
|
||
|
|
|
||
|
|
// testConfig 返回带默认战斗参数的静态配置,保证单元测试不依赖 Nacos
|
||
|
|
func testConfig() *config.Config {
|
||
|
|
return config.NewStaticConfig(map[string]interface{}{
|
||
|
|
"combat.atb.gauge_max": 100,
|
||
|
|
"combat.atb.base_coefficient": 0.1,
|
||
|
|
"combat.battle.max_ticks": 3000,
|
||
|
|
"combat.battle.max_actions": 50,
|
||
|
|
"combat.mana.max": 100,
|
||
|
|
"combat.mana.regen_per_tick": 0.15,
|
||
|
|
"combat.mana.regen_per_action": 3,
|
||
|
|
"combat.mana.small_skill_cost": 20,
|
||
|
|
"combat.mana.big_skill_cost": 40,
|
||
|
|
"combat.skill.normal_attack_coef": 1.0,
|
||
|
|
"combat.skill.small_coef_max": 2.5,
|
||
|
|
"combat.skill.big_coef_min": 2.5,
|
||
|
|
"combat.skill.small_cd": 225,
|
||
|
|
"combat.skill.big_cd": 800,
|
||
|
|
"combat.skill.small_trigger_rate": 1.25,
|
||
|
|
"combat.skill.big_trigger_rate": 2.0,
|
||
|
|
"combat.crit.multiplier": 1.5,
|
||
|
|
"combat.crit.rate_cap": 0.6,
|
||
|
|
"combat.evade.rate_cap": 0.5,
|
||
|
|
"combat.defense.coefficient": 0.5,
|
||
|
|
"combat.atk.coefficient": 1.0,
|
||
|
|
"combat.magic_atk.coefficient": 1.0,
|
||
|
|
"combat.element.advantage": 1.2,
|
||
|
|
"combat.element.disadvantage": 0.85,
|
||
|
|
"combat.faction.cap": 0.15,
|
||
|
|
"combat.cc.slow_value": 0.2,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func makePlayer(id, name, side string, str, vit, agi, luk float64) *Fighter {
|
||
|
|
return NewFighter(id, name, "tiger_yao", side, 1, 1, BaseStats{
|
||
|
|
Str: str,
|
||
|
|
Vit: vit,
|
||
|
|
Wis: 10,
|
||
|
|
Agi: agi,
|
||
|
|
Spi: 10,
|
||
|
|
Luk: luk,
|
||
|
|
}, "neutral", "none", true)
|
||
|
|
}
|
||
|
|
|
||
|
|
// countSkillUses 统计某技能在战报中使用次数
|
||
|
|
func countSkillUses(report *BattleReport, skillID string) int {
|
||
|
|
cnt := 0
|
||
|
|
for _, r := range report.Rounds {
|
||
|
|
if r.SkillID == skillID {
|
||
|
|
cnt++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cnt
|
||
|
|
}
|
||
|
|
|
||
|
|
// countActorRounds 统计某方行动次数
|
||
|
|
func countActorRounds(report *BattleReport, side string) int {
|
||
|
|
cnt := 0
|
||
|
|
for _, r := range report.Rounds {
|
||
|
|
if r.Actor == side {
|
||
|
|
cnt++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cnt
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestHighSpeedCrush 验证高速方可以碾压低速目标
|
||
|
|
func TestHighSpeedCrush(t *testing.T) {
|
||
|
|
config.Global = testConfig()
|
||
|
|
|
||
|
|
player := makePlayer("c-001", "铁爪传", "attacker", 50, 30, 300, 30)
|
||
|
|
monster := makePlayer("npc-001", "幽魂", "defender", 30, 25, 80, 10)
|
||
|
|
monster.IsPlayer = false
|
||
|
|
monster.Side = "defender"
|
||
|
|
|
||
|
|
eng := NewEngine(player, monster, rand.New(rand.NewSource(42)), "expedition_pve", 1)
|
||
|
|
report := eng.Run()
|
||
|
|
|
||
|
|
if report.Result.Winner != "attacker" {
|
||
|
|
t.Fatalf("expected attacker win, got %s", report.Result.Winner)
|
||
|
|
}
|
||
|
|
|
||
|
|
pa := countActorRounds(report, "attacker")
|
||
|
|
da := countActorRounds(report, "defender")
|
||
|
|
if pa <= da {
|
||
|
|
t.Fatalf("expected attacker actions > defender, got attacker=%d defender=%d", pa, da)
|
||
|
|
}
|
||
|
|
if report.Result.FinalHP["attacker"] <= 0 {
|
||
|
|
t.Fatalf("expected attacker survive, final hp=%d", report.Result.FinalHP["attacker"])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLowSpeedTank 验证低速肉盾可凭 HP/防御耗死敌人
|
||
|
|
func TestLowSpeedTank(t *testing.T) {
|
||
|
|
config.Global = testConfig()
|
||
|
|
|
||
|
|
player := makePlayer("c-002", "石盾", "attacker", 35, 120, 80, 10)
|
||
|
|
monster := makePlayer("npc-002", "赤焰狼妖", "defender", 45, 40, 150, 15)
|
||
|
|
monster.IsPlayer = false
|
||
|
|
monster.Side = "defender"
|
||
|
|
|
||
|
|
// 给玩家一个中低速但高伤的简单技能,避免纯普攻超时
|
||
|
|
skill := &SkillInstance{
|
||
|
|
InstanceID: "s-001",
|
||
|
|
SkillID: "heavy_blow",
|
||
|
|
Name: "重击",
|
||
|
|
DamageType: DamageTypePhysical,
|
||
|
|
Element: "none",
|
||
|
|
Alignment: "neutral",
|
||
|
|
ScalingAttr: "str",
|
||
|
|
BaseCoef: 2.0,
|
||
|
|
CD: 300,
|
||
|
|
Cost: 25,
|
||
|
|
TriggerRate: 1.5,
|
||
|
|
TargetType: TargetSingle,
|
||
|
|
}
|
||
|
|
player.AddSkill(skill)
|
||
|
|
|
||
|
|
eng := NewEngine(player, monster, rand.New(rand.NewSource(7)), "expedition_pve", 1)
|
||
|
|
report := eng.Run()
|
||
|
|
|
||
|
|
if report.Result.Winner != "attacker" {
|
||
|
|
t.Fatalf("expected attacker win, got %s", report.Result.Winner)
|
||
|
|
}
|
||
|
|
if report.Result.FinalHP["attacker"] <= 0 {
|
||
|
|
t.Fatalf("expected tank survive, final hp=%d", report.Result.FinalHP["attacker"])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSkillCDConstraint 验证技能受 CD 与内力双重约束,不能无限连发
|
||
|
|
func TestSkillCDConstraint(t *testing.T) {
|
||
|
|
config.Global = testConfig()
|
||
|
|
|
||
|
|
player := makePlayer("c-003", "剑修", "attacker", 60, 30, 150, 20)
|
||
|
|
monster := makePlayer("npc-003", "山魈", "defender", 30, 40, 60, 10)
|
||
|
|
monster.IsPlayer = false
|
||
|
|
monster.Side = "defender"
|
||
|
|
|
||
|
|
// 高伤大招,CD 225,消耗 40
|
||
|
|
bigSkill := &SkillInstance{
|
||
|
|
InstanceID: "s-002",
|
||
|
|
SkillID: "sky_sword",
|
||
|
|
Name: "天剑斩",
|
||
|
|
DamageType: DamageTypePhysical,
|
||
|
|
Element: "none",
|
||
|
|
Alignment: "neutral",
|
||
|
|
ScalingAttr: "str",
|
||
|
|
BaseCoef: 3.0,
|
||
|
|
CD: 225,
|
||
|
|
Cost: 40,
|
||
|
|
TriggerRate: 2.0,
|
||
|
|
TargetType: TargetSingle,
|
||
|
|
}
|
||
|
|
player.AddSkill(bigSkill)
|
||
|
|
|
||
|
|
eng := NewEngine(player, monster, rand.New(rand.NewSource(13)), "expedition_pve", 1)
|
||
|
|
report := eng.Run()
|
||
|
|
|
||
|
|
uses := countSkillUses(report, "sky_sword")
|
||
|
|
if uses == 0 {
|
||
|
|
t.Fatalf("expected big skill used at least once")
|
||
|
|
}
|
||
|
|
|
||
|
|
// 总 tick 数应远小于上限,理论上最多使用次数受 CD+回蓝限制
|
||
|
|
// 放宽判断:使用次数不超过 15,且大招不会连续出现
|
||
|
|
if uses > 15 {
|
||
|
|
t.Fatalf("expected skill uses <= 15 due to CD/mana, got %d", uses)
|
||
|
|
}
|
||
|
|
|
||
|
|
lastTick := -9999
|
||
|
|
for _, r := range report.Rounds {
|
||
|
|
if r.SkillID == "sky_sword" {
|
||
|
|
if r.Tick-lastTick < 225 {
|
||
|
|
t.Fatalf("skill reused too soon: last=%d current=%d", lastTick, r.Tick)
|
||
|
|
}
|
||
|
|
lastTick = r.Tick
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|