服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
167 lines
5.6 KiB
Go
167 lines
5.6 KiB
Go
package drop
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func TestLoadClearRewardRulesPreservesPVFRowsAndResolvesCurrentFormula(t *testing.T) {
|
|
rules, err := LoadClearRewardRules(clearRewardRuleTestSource{ClearRewardRulePath: validClearRewardRuleText()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if snapshot := rules.Snapshot(); snapshot != (ClearRewardRuleSnapshot{
|
|
MapRateRows: 5, DifficultyBonusRows: 5, RarityThresholdRows: 7,
|
|
PartyBonusRows: 4, GoldDifficultyRows: 5, GoldCardCostRows: 2, ItemReferenceRows: 2,
|
|
}) {
|
|
t.Fatalf("snapshot=%+v", snapshot)
|
|
}
|
|
plan := rules.PlanSource(60, 1, 2, 5)
|
|
if !plan.ItemRateFound || !plan.DifficultyFound || !plan.PartyBonusFound ||
|
|
!plan.GoldDifficultyFound || !plan.GoldCardCostFound || !plan.ItemReferenceFound ||
|
|
plan.ItemRate != 6000 || plan.DifficultyBonus != 600 || plan.PartyBonus != 1.2 ||
|
|
plan.GoldDifficultyRate != 1.38 || plan.GoldCardCost != 7180 ||
|
|
plan.ItemReference != (ClearRewardItemReference{LookupKey: 60, ValueA: 7, ValueB: 3}) {
|
|
t.Fatalf("plan=%+v", plan)
|
|
}
|
|
missing := rules.PlanSource(59, 5, 5, 6)
|
|
if missing.ItemRateFound || missing.DifficultyFound || missing.PartyBonusFound ||
|
|
missing.GoldDifficultyFound || missing.GoldCardCostFound || missing.ItemReferenceFound {
|
|
t.Fatalf("missing lookup fabricated rows: %+v", missing)
|
|
}
|
|
raw := rules.RawTables()
|
|
if raw.ItemRateByMapCount[5] != 6000 || raw.DifficultyDropBonus != ([5]int64{0, 600, 1000, 1400, 1200}) ||
|
|
raw.RarityDecision != ([7]int64{500000, 799900, 999900, 1000000, 1000001, 1000002, 1000003}) ||
|
|
raw.PartyMemberDropBonus != ([4]float64{1, 1.2, 1.4, 1.6}) ||
|
|
raw.GoldDifficultyDropBonus != ([5]float64{1.02, 1.38, 1.6, 1.9, 2}) || raw.GoldCardCostByLevel[60] != 7180 {
|
|
t.Fatalf("raw=%+v", raw)
|
|
}
|
|
raw.ItemRateByMapCount[5] = 1
|
|
raw.GoldCardCostByLevel[60] = 1
|
|
again := rules.RawTables()
|
|
if again.ItemRateByMapCount[5] != 6000 || again.GoldCardCostByLevel[60] != 7180 {
|
|
t.Fatalf("rules leaked mutable tables: %+v", again)
|
|
}
|
|
resolved, err := rules.Resolve(60, 1, 2, 9)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resolved.EquipmentRate != 10_000 || resolved.GradeDown != 7 || resolved.GradeUp != 3 ||
|
|
resolved.RarityThresholds != ([7]uint32{500000, 799900, 999900, 1000000, 1000001, 1000002, 1000003}) ||
|
|
resolved.RarityScale != 1000003 || resolved.PaidCardCost != 7180 ||
|
|
resolved.GoldDifficultyRate != 1.38 || resolved.MapCount != 5 {
|
|
t.Fatalf("resolved=%+v", resolved)
|
|
}
|
|
if _, err := rules.Resolve(59, 0, 1, 1); !errors.Is(err, ErrClearRewardRuleLookupMissing) {
|
|
t.Fatalf("missing resolved lookup error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadClearRewardRulesRejectsMalformedPVFShapes(t *testing.T) {
|
|
if _, err := LoadClearRewardRules(nil); !errors.Is(err, ErrClearRewardRuleSourceRequired) {
|
|
t.Fatalf("nil source error=%v", err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(string) string
|
|
}{
|
|
{
|
|
name: "map counts must be complete and ordered",
|
|
mutate: func(text string) string {
|
|
return replaceOnce(t, text, "1 2000 2 3000", "1 2000 3 3000")
|
|
},
|
|
},
|
|
{
|
|
name: "difficulty rows must remain five",
|
|
mutate: func(text string) string {
|
|
return replaceOnce(t, text, "0 600 1000 1400 1200", "0 600 1000 1400")
|
|
},
|
|
},
|
|
{
|
|
name: "rarity thresholds cannot decrease",
|
|
mutate: func(text string) string {
|
|
return replaceOnce(t, text, "500000 799900 999900", "500000 400000 999900")
|
|
},
|
|
},
|
|
{
|
|
name: "party bonus must remain positive",
|
|
mutate: func(text string) string {
|
|
return replaceOnce(t, text, "1.00 1.20 1.40 1.60", "1.00 0 1.40 1.60")
|
|
},
|
|
},
|
|
{
|
|
name: "gold card level keys must increase",
|
|
mutate: func(text string) string {
|
|
return replaceOnce(t, text, "1 100 60 7180", "60 7180 1 100")
|
|
},
|
|
},
|
|
{
|
|
name: "item references require nonnegative values",
|
|
mutate: func(text string) string {
|
|
return replaceOnce(t, text, "60 7 3", "60 -1 3")
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := LoadClearRewardRules(clearRewardRuleTestSource{ClearRewardRulePath: test.mutate(validClearRewardRuleText())})
|
|
if !errors.Is(err, ErrClearRewardRuleSectionShape) {
|
|
t.Fatalf("error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRealScriptPVFClearRewardRulesPreserveCurrentSourceTables(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
pvfPath = os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
|
}
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to run real clear-reward PVF smoke")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rules, err := LoadClearRewardRules(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshot := rules.Snapshot()
|
|
if snapshot.MapRateRows != 5 || snapshot.DifficultyBonusRows != 5 || snapshot.RarityThresholdRows != 7 ||
|
|
snapshot.PartyBonusRows != 4 || snapshot.GoldDifficultyRows != 5 || snapshot.GoldCardCostRows == 0 || snapshot.ItemReferenceRows == 0 {
|
|
t.Fatalf("real snapshot=%+v", snapshot)
|
|
}
|
|
}
|
|
|
|
func validClearRewardRuleText() string {
|
|
return `[reward item rate per map max count]
|
|
1 2000 2 3000 3 4000 4 5000 5 6000
|
|
[dungeon difficulty drop bonusrate]
|
|
0 600 1000 1400 1200
|
|
[basis of rarity dicision]
|
|
500000 799900 999900 1000000 1000001 1000002 1000003
|
|
[party member drop bonusrate]
|
|
1.00 1.20 1.40 1.60
|
|
[dungeon difficulty gold drop bonusrate]
|
|
1.02 1.38 1.60 1.90 2.00
|
|
[gold card cost table]
|
|
1 100 60 7180
|
|
[item drop ref table]
|
|
1 0 3 60 7 3
|
|
`
|
|
}
|
|
|
|
type clearRewardRuleTestSource map[string]string
|
|
|
|
func (source clearRewardRuleTestSource) ReadText(path string) (string, error) {
|
|
if text, found := source[path]; found {
|
|
return text, nil
|
|
}
|
|
return "", errors.New("missing test PVF path: " + path)
|
|
}
|