From d58517251459005a965132ab800520817289e21f Mon Sep 17 00:00:00 2001 From: Flechazo <2558755403@qq.com> Date: Sun, 4 Oct 2026 11:26:05 +0800 Subject: [PATCH] fix(server): allow ranked equipment with constant health options --- .../server/gamedata/equipment_stats.go | 4 +- .../server/gamedata/equipment_stats_test.go | 17 ++++++++ .../server/player/equipment_stats_test.go | 39 +++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/go/internal/server/gamedata/equipment_stats.go b/go/internal/server/gamedata/equipment_stats.go index 18f589c..8006cea 100644 --- a/go/internal/server/gamedata/equipment_stats.go +++ b/go/internal/server/gamedata/equipment_stats.go @@ -118,7 +118,9 @@ func equipmentStatContribution(rule EquipmentStatRule, option EquipmentOption, s } var ranks float32 for i, rank := range option.Rank { - if rank == 0 { + // EquipmentInfo.GetEquipOptionByRank returns zero for an empty + // RankValueN curve, even when the equipment has a rank there. + if rank == 0 || len(rule.Ranks[i]) == 0 { continue } if rank < 1 || rank > len(rule.Ranks[i]) { diff --git a/go/internal/server/gamedata/equipment_stats_test.go b/go/internal/server/gamedata/equipment_stats_test.go index 5b40d44..54243b1 100644 --- a/go/internal/server/gamedata/equipment_stats_test.go +++ b/go/internal/server/gamedata/equipment_stats_test.go @@ -75,3 +75,20 @@ func TestEquipmentOptionContributionTruncatesRatherThanRounds(t *testing.T) { t.Fatalf("fraction percent=%+v err=%v", percent, err) } } + +func TestEquipmentHealthStatsAllowRankedItemsWithEmptyOptionRankCurves(t *testing.T) { + // EquipmentInfo.GetEquipOptionByRank (0x06003145) returns zero when + // RankValue1/2/3 is empty. Rank belongs to the equipment, not each option. + d := &EquipmentStatDesign{Options: map[[2]uint64]EquipmentStatRule{ + {10, 1}: {Default: 10, Growth: 2, Levels: []float64{0, 1}}, + {20, 2}: {Default: 0.1, Growth: 0.01, Levels: []float64{0, 1}, Ranks: [3][]float64{nil, {0.5, 1}, nil}}, + }} + flat, err := d.HealthContribution(EquipmentOption{GroupID: 10, ID: 1, Level: 1, Rank: [3]int{2, 3, 4}}, false) + if err != nil || flat.Flat != 12 { + t.Fatalf("empty rank curve contribution=%+v err=%v", flat, err) + } + percent, err := d.HealthContribution(EquipmentOption{GroupID: 20, ID: 2, Level: 1, Rank: [3]int{2, 2, 4}}, false) + if err != nil || percent.Percent != 0.12 { + t.Fatalf("mixed rank curve contribution=%+v err=%v", percent, err) + } +} diff --git a/go/internal/server/player/equipment_stats_test.go b/go/internal/server/player/equipment_stats_test.go index b2a53ff..ee78d24 100644 --- a/go/internal/server/player/equipment_stats_test.go +++ b/go/internal/server/player/equipment_stats_test.go @@ -46,3 +46,42 @@ func TestEquipmentStatContributionsUseOnlyEquippedOwnerAndAllHealthOptions(t *te t.Fatal("Find deadlocked calculating equipped health") } } + +func TestEatFoodWithRankedEquipmentAndEmptyHealthOptionRankCurves(t *testing.T) { + food, inventory, characters := foodTestService(t, stateio.NewMemory()) + equipment := &EquipmentInventory{owned: equipmentSnapshot{Equipment: []Equipment{{ + InvenIndex: 1, UseChar: 77, Level: 1, Rank: []uint64{2, 2, 2}, + MainOption: []EquipmentOption{{GroupID: 10, ID: 1}}, + }}}} + if err := equipment.AttachStatDesign(&gamedata.EquipmentStatDesign{Options: map[[2]uint64]gamedata.EquipmentStatRule{ + {10, 1}: {Default: 10, Growth: 2, Levels: []float64{0, 1}}, + }}); err != nil { + t.Fatal(err) + } + if err := characters.AttachMaxHealth(func(c Character) (uint64, error) { + contributions, err := equipment.StatContributions(c) + if err != nil { + return 0, err + } + return uint64(gamedata.AggregateStats(gamedata.BaseStats{Health: 100}, contributions).Health), nil + }); err != nil { + t.Fatal(err) + } + if err := characters.SetCurrentHealth(77, 20); err != nil { + t.Fatal(err) + } + stacks, err := inventory.GrantOnce("ranked-equipment-food", []gamedata.BattleReward{{Type: 5, ID: 105, Count: 1}}) + if err != nil { + t.Fatal(err) + } + if _, _, _, err := food.Handle("/EatFood", foodRequest(1, 77, 21, stacks[0])); err != nil { + t.Fatal(err) + } + // Base 100 + equipment 12 = 112 maximum; the 50% dish heals 56. + if hp, err := characters.CurrentHealth(77); err != nil || hp != 76 { + t.Fatalf("healed health=%d err=%v", hp, err) + } + if len(inventory.All()) != 0 { + t.Fatal("successful recovery did not consume dish") + } +}