服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
449 lines
14 KiB
Go
449 lines
14 KiB
Go
package cargo
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"testing"
|
||
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||
)
|
||
|
||
func TestOwnerPlanAccountReadsCargoMetadata(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-1",
|
||
Metadata: map[string]string{
|
||
"account_cargo_gold": "1200",
|
||
"account_cargo_level": "3",
|
||
"account_cargo_created": "1",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.PlanAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: " acc-1 "})
|
||
if err != nil {
|
||
t.Fatalf("PlanAccount() error = %v", err)
|
||
}
|
||
if got.AccountID != "acc-1" || got.CargoGold != 1200 || got.CargoLevel != 3 || !got.CargoCreated {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestOwnerPlanMoneyDepositChecksCharacterGold(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-2",
|
||
Metadata: map[string]string{"account_cargo_gold": "50"},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "77",
|
||
AccountID: "acc-2",
|
||
Stats: map[string]int64{"gold": 500},
|
||
}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.PlanMoney(ctx, MoneyCommand{
|
||
AccountID: "acc-2",
|
||
SelectedCharacterID: 77,
|
||
Direction: MoneyDeposit,
|
||
Amount: 300,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PlanMoney() error = %v", err)
|
||
}
|
||
if got.CharacterGold != 500 || got.CargoGold != 50 || got.Amount != 300 || got.Direction != MoneyDeposit {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestOwnerPlanMoneyWithdrawChecksCargoGold(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-3",
|
||
Metadata: map[string]string{"account_cargo_gold": "900"},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "78",
|
||
AccountID: "acc-3",
|
||
Stats: map[string]int64{"gold": 20},
|
||
}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.PlanMoney(ctx, MoneyCommand{
|
||
AccountID: "acc-3",
|
||
SelectedCharacterID: 78,
|
||
Direction: MoneyWithdraw,
|
||
Amount: 700,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PlanMoney() error = %v", err)
|
||
}
|
||
if got.CharacterGold != 20 || got.CargoGold != 900 || got.Amount != 700 || got.Direction != MoneyWithdraw {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestOwnerPlanMoneyRejectsInsufficientGold(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-4",
|
||
Metadata: map[string]string{"account_cargo_gold": "5"},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "79",
|
||
AccountID: "acc-4",
|
||
Stats: map[string]int64{"gold": 10},
|
||
}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
_, err = owner.PlanMoney(ctx, MoneyCommand{
|
||
AccountID: "acc-4",
|
||
SelectedCharacterID: 79,
|
||
Direction: MoneyWithdraw,
|
||
Amount: 6,
|
||
})
|
||
if !errors.Is(err, ErrGoldInsufficient) {
|
||
t.Fatalf("PlanMoney() error = %v, want ErrGoldInsufficient", err)
|
||
}
|
||
}
|
||
|
||
func TestOwnerApplyCreateCargoChargesGold(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: "acc-5"}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "80",
|
||
AccountID: "acc-5",
|
||
Stats: map[string]int64{"gold": 500000},
|
||
}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.ApplyAccount(ctx, "create_account_cargo", AccountCommand{
|
||
AccountID: "acc-5",
|
||
SelectedCharacterID: 80,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ApplyAccount() error = %v", err)
|
||
}
|
||
if !got.Changed || got.CharacterGold != 400000 || got.CargoLevel != 1 || !got.CargoCreated {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
|
||
account, ok, err := repos.Account.Load(ctx, "acc-5")
|
||
if err != nil || !ok {
|
||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||
}
|
||
if account.Metadata["account_cargo_level"] != "1" || account.Metadata["account_cargo_created"] != "true" {
|
||
t.Fatalf("account metadata = %+v", account.Metadata)
|
||
}
|
||
character, ok, err := repos.Character.Load(ctx, "80")
|
||
if err != nil || !ok {
|
||
t.Fatalf("load character ok=%t err=%v", ok, err)
|
||
}
|
||
if got := character.Stats["gold"]; got != 400000 {
|
||
t.Fatalf("character gold = %d, want 400000", got)
|
||
}
|
||
}
|
||
|
||
func TestOwnerApplyAccountUpgradeDebitsAccountSharedCera(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-cera",
|
||
Metadata: map[string]string{
|
||
"account_cargo_created": "true",
|
||
"account_cargo_level": "8",
|
||
"account_cargo_gold": "0",
|
||
"account_cera": "3000",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "87", AccountID: "acc-cera", Stats: map[string]int64{"gold": 1}}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-cera", SelectedCharacterID: 87})
|
||
if err != nil {
|
||
t.Fatalf("ApplyAccount() error = %v", err)
|
||
}
|
||
if !got.Changed || got.Cost.Kind != CostCera || got.Cost.Amount != 2000 || got.CharacterCera != 1000 || got.CargoLevel != 16 {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
account, ok, err := repos.Account.Load(ctx, "acc-cera")
|
||
if err != nil || !ok {
|
||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||
}
|
||
if account.Metadata["account_cera"] != "1000" || account.Metadata["account_cargo_level"] != "16" {
|
||
t.Fatalf("account metadata = %+v", account.Metadata)
|
||
}
|
||
}
|
||
|
||
func TestOwnerApplyAccountUpgradeRejectsInsufficientAccountCera(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-cera-poor",
|
||
Metadata: map[string]string{
|
||
"account_cargo_created": "true",
|
||
"account_cargo_level": "8",
|
||
"account_cargo_gold": "0",
|
||
"account_cera": "100",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "88", AccountID: "acc-cera-poor", Stats: map[string]int64{"gold": 1}}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
if _, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-cera-poor", SelectedCharacterID: 88}); !errors.Is(err, ErrGoldInsufficient) {
|
||
t.Fatalf("ApplyAccount() error = %v, want ErrGoldInsufficient", err)
|
||
}
|
||
account, _, _ := repos.Account.Load(ctx, "acc-cera-poor")
|
||
if account.Metadata["account_cera"] != "100" || account.Metadata["account_cargo_level"] != "8" {
|
||
t.Fatalf("insufficient-cera attempt mutated account: %+v", account.Metadata)
|
||
}
|
||
}
|
||
|
||
func TestOwnerApplyMoneyDepositPersistsBothSides(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-6",
|
||
Metadata: map[string]string{
|
||
"account_cargo_gold": "50",
|
||
"account_cargo_level": "1",
|
||
"account_cargo_created": "true",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "81",
|
||
AccountID: "acc-6",
|
||
Stats: map[string]int64{"gold": 500},
|
||
}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.ApplyMoney(ctx, MoneyCommand{
|
||
AccountID: "acc-6",
|
||
SelectedCharacterID: 81,
|
||
Direction: MoneyDeposit,
|
||
Amount: 300,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ApplyMoney() error = %v", err)
|
||
}
|
||
if !got.Changed || got.CharacterGold != 200 || got.CargoGold != 350 {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
|
||
account, ok, err := repos.Account.Load(ctx, "acc-6")
|
||
if err != nil || !ok {
|
||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||
}
|
||
if got := account.Metadata["account_cargo_gold"]; got != "350" {
|
||
t.Fatalf("cargo gold = %q, want 350", got)
|
||
}
|
||
character, ok, err := repos.Character.Load(ctx, "81")
|
||
if err != nil || !ok {
|
||
t.Fatalf("load character ok=%t err=%v", ok, err)
|
||
}
|
||
if got := character.Stats["gold"]; got != 200 {
|
||
t.Fatalf("character gold = %d, want 200", got)
|
||
}
|
||
}
|
||
|
||
func TestOwnerApplyAccountUpgradeConsumesVoidMagicStoneForMiddleTiers(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-void",
|
||
Metadata: map[string]string{
|
||
"account_cargo_created": "true",
|
||
"account_cargo_level": "16",
|
||
"account_cargo_gold": "0",
|
||
},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "86", AccountID: "acc-void", Stats: map[string]int64{"gold": 1, "cera": 1}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "86", Slots: map[string]dnfrepo.ItemStack{
|
||
"0:10": {ItemID: accountCargoVoidMagicStoneItemID, Count: 200},
|
||
"0:11": {ItemID: accountCargoVoidMagicStoneItemID, Count: 100},
|
||
}}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-void", SelectedCharacterID: 86})
|
||
if err != nil {
|
||
t.Fatalf("upgrade account cargo: %v", err)
|
||
}
|
||
if !got.Changed || got.CargoLevel != 24 || got.Cost.Kind != CostMaterial || got.Cost.Amount != 250 {
|
||
t.Fatalf("upgrade result=%+v", got)
|
||
}
|
||
items, found, err := repos.Inventory.Load(ctx, "86")
|
||
if err != nil || !found {
|
||
t.Fatalf("load inventory found=%t err=%v", found, err)
|
||
}
|
||
if _, exists := items.Slots["0:10"]; exists {
|
||
t.Fatalf("first material stack not consumed: %+v", items.Slots)
|
||
}
|
||
if got := items.Slots["0:11"].Count; got != 50 {
|
||
t.Fatalf("remaining void magic stones=%d want=50", got)
|
||
}
|
||
}
|
||
|
||
func TestOwnerApplyMoneyWithdrawPersistsBothSides(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-7",
|
||
Metadata: map[string]string{
|
||
"account_cargo_gold": "900",
|
||
"account_cargo_level": "1",
|
||
"account_cargo_created": "true",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "82",
|
||
AccountID: "acc-7",
|
||
Stats: map[string]int64{"gold": 20},
|
||
}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
got, err := owner.ApplyMoney(ctx, MoneyCommand{
|
||
AccountID: "acc-7",
|
||
SelectedCharacterID: 82,
|
||
Direction: MoneyWithdraw,
|
||
Amount: 700,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ApplyMoney() error = %v", err)
|
||
}
|
||
if !got.Changed || got.CharacterGold != 720 || got.CargoGold != 200 {
|
||
t.Fatalf("result = %+v", got)
|
||
}
|
||
}
|
||
|
||
// TestNextAccountCargoTierCoversEveryCeraShopTool 锁住完整阶梯。2026-08-25 现网
|
||
// 抓包证明客户端按顺序消耗 12 个点券工具,容量 64 时请求 tool6(64->72);阶梯
|
||
// 只写到 64 时那次购买被拒,客户端显示"物品栏空间不足"。
|
||
func TestNextAccountCargoTierCoversEveryCeraShopTool(t *testing.T) {
|
||
want := []int64{1, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120}
|
||
tier := want[0]
|
||
for _, expected := range want[1:] {
|
||
next, ok := NextAccountCargoTier(tier)
|
||
if !ok || next != expected {
|
||
t.Fatalf("NextAccountCargoTier(%d) = %d, %t; want %d, true", tier, next, ok, expected)
|
||
}
|
||
tier = next
|
||
}
|
||
if next, ok := NextAccountCargoTier(tier); ok {
|
||
t.Fatalf("NextAccountCargoTier(%d) = %d, true; want max level", tier, next)
|
||
}
|
||
if next, ok := NextAccountCargoTier(5); ok {
|
||
t.Fatalf("NextAccountCargoTier(5) = %d, true; want an unknown tier to fail closed", next)
|
||
}
|
||
}
|
||
|
||
// TestOwnerApplyAccountUpgradeRequiresToolAboveMaterialTiers 保证新接上的高档位
|
||
// 不会被金库窗口按钮(op306)白送——那些档只有商城工具能付费。
|
||
func TestOwnerApplyAccountUpgradeRequiresToolAboveMaterialTiers(t *testing.T) {
|
||
ctx := context.Background()
|
||
repos := dnfrepomemory.NewMemoryGroup()
|
||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||
AccountID: "acc-tool",
|
||
Metadata: map[string]string{
|
||
"account_cargo_created": "true",
|
||
"account_cargo_level": "64",
|
||
"account_cargo_gold": "0",
|
||
"account_cera": "9000000",
|
||
},
|
||
}); err != nil {
|
||
t.Fatalf("save account: %v", err)
|
||
}
|
||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "89", AccountID: "acc-tool", Stats: map[string]int64{"gold": 900000000}}); err != nil {
|
||
t.Fatalf("save character: %v", err)
|
||
}
|
||
|
||
owner, err := NewOwner(repos)
|
||
if err != nil {
|
||
t.Fatalf("NewOwner() error = %v", err)
|
||
}
|
||
if _, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-tool", SelectedCharacterID: 89}); !errors.Is(err, ErrCargoUpgradeToolRequired) {
|
||
t.Fatalf("ApplyAccount() error = %v, want ErrCargoUpgradeToolRequired", err)
|
||
}
|
||
account, ok, err := repos.Account.Load(ctx, "acc-tool")
|
||
if err != nil || !ok {
|
||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||
}
|
||
if account.Metadata["account_cargo_level"] != "64" || account.Metadata["account_cera"] != "9000000" {
|
||
t.Fatalf("blocked op306 upgrade mutated account: %+v", account.Metadata)
|
||
}
|
||
}
|