服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
196 lines
8.0 KiB
Go
196 lines
8.0 KiB
Go
package fatigue
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/premium"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
|
)
|
|
|
|
func TestReconcileDailyStatsCrossesBeijingSixAndResetsEveryPotion(t *testing.T) {
|
|
before := time.Date(2026, 8, 30, 21, 59, 0, 0, time.UTC)
|
|
after := time.Date(2026, 8, 30, 22, 1, 0, 0, time.UTC)
|
|
stats := map[string]int64{
|
|
"fatigue": 0, "fatigue_limit": BlackDiamondLimit, "fatigue_display_update": BlackDiamondLimit,
|
|
DailyRecoveryDayStatKey: premium.ServiceDayID(before),
|
|
PotionUsageDayStatKey: premium.ServiceDayID(before),
|
|
PotionUsageStatPrefix + "490702338": 3,
|
|
PotionUsageStatPrefix + "490702339": 2,
|
|
// The generic 每天可用次数 counters of [total usable count] items move
|
|
// with the same boundary, so they must be reset by this call too.
|
|
ItemUsageDayStatKey: premium.ServiceDayID(before),
|
|
ItemUsageStatPrefix + "490006906": 3,
|
|
ItemUsageStatPrefix + "10153501": 1,
|
|
}
|
|
result := ReconcileDailyStats(stats, after)
|
|
if !result.Changed || !result.FatigueRestored || !result.PotionUsageReset || !result.ItemUsageReset {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
if stats["fatigue"] != BlackDiamondLimit || stats["fatigue_limit"] != BlackDiamondLimit ||
|
|
stats["fatigue_display_update"] != 0 ||
|
|
stats[PotionUsageStatPrefix+"490702338"] != 0 || stats[PotionUsageStatPrefix+"490702339"] != 0 ||
|
|
stats[ItemUsageStatPrefix+"490006906"] != 0 || stats[ItemUsageStatPrefix+"10153501"] != 0 {
|
|
t.Fatalf("stats=%+v", stats)
|
|
}
|
|
wantDay := premium.ServiceDayID(after)
|
|
if stats[DailyRecoveryDayStatKey] != wantDay || stats[PotionUsageDayStatKey] != wantDay ||
|
|
stats[ItemUsageDayStatKey] != wantDay {
|
|
t.Fatalf("service days recovery=%d potion=%d item=%d want=%d",
|
|
stats[DailyRecoveryDayStatKey], stats[PotionUsageDayStatKey], stats[ItemUsageDayStatKey], wantDay)
|
|
}
|
|
}
|
|
|
|
func TestReconcileDailyStatsDoesNotRepeatWithinServiceDay(t *testing.T) {
|
|
now := time.Date(2026, 8, 30, 23, 0, 0, 0, time.UTC)
|
|
dayID := premium.ServiceDayID(now)
|
|
stats := map[string]int64{
|
|
"fatigue": 77, "fatigue_limit": BlackDiamondLimit, "fatigue_display_update": 111,
|
|
DailyRecoveryDayStatKey: dayID,
|
|
PotionUsageDayStatKey: dayID,
|
|
PotionUsageStatPrefix + "490702338": 2,
|
|
ItemUsageDayStatKey: dayID,
|
|
ItemUsageStatPrefix + "490006906": 2,
|
|
}
|
|
result := ReconcileDailyStats(stats, now.Add(2*time.Hour))
|
|
if result.Changed || result.FatigueRestored || result.PotionUsageReset || result.ItemUsageReset {
|
|
t.Fatalf("result=%+v", result)
|
|
}
|
|
if stats["fatigue"] != 77 || stats["fatigue_display_update"] != 111 ||
|
|
stats[PotionUsageStatPrefix+"490702338"] != 2 || stats[ItemUsageStatPrefix+"490006906"] != 2 {
|
|
t.Fatalf("stats=%+v", stats)
|
|
}
|
|
}
|
|
|
|
func TestReconcileDailyStatsUsesDefaultLimitWhenMissing(t *testing.T) {
|
|
now := time.Date(2026, 8, 30, 22, 1, 0, 0, time.UTC)
|
|
stats := map[string]int64{"fatigue": 0}
|
|
result := ReconcileDailyStats(stats, now)
|
|
if !result.FatigueRestored || stats["fatigue"] != DefaultLimit || stats["fatigue_limit"] != DefaultLimit {
|
|
t.Fatalf("result=%+v stats=%+v", result, stats)
|
|
}
|
|
}
|
|
|
|
func TestBatteryCapacityMatchesServerLevelBands(t *testing.T) {
|
|
for _, test := range []struct {
|
|
level int
|
|
want int64
|
|
}{
|
|
{1, 78}, {19, 78}, {20, 117}, {29, 117}, {30, 156}, {39, 156}, {40, 234}, {90, 234},
|
|
} {
|
|
if got := BatteryCapacity(test.level); got != test.want {
|
|
t.Fatalf("BatteryCapacity(%d)=%d, want %d", test.level, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReconcileDailyStatsStoresPreviousServiceDayFatigueInBattery(t *testing.T) {
|
|
before := time.Date(2026, 8, 30, 21, 59, 0, 0, time.UTC)
|
|
after := time.Date(2026, 8, 30, 22, 1, 0, 0, time.UTC)
|
|
stats := map[string]int64{
|
|
"fatigue": 150, "fatigue_limit": 188,
|
|
DailyRecoveryDayStatKey: premium.ServiceDayID(before),
|
|
}
|
|
result := ReconcileDailyStats(stats, after, 90)
|
|
if !result.Changed || stats["fatigue"] != 188 || stats[FatigueBatteryChargeStatKey] != 150 || stats[FatigueBatteryStatKey] != 0 {
|
|
t.Fatalf("result=%+v stats=%+v", result, stats)
|
|
}
|
|
}
|
|
|
|
func TestReconcileDailyStatsPreservesBatteryAfterSkippedServiceDay(t *testing.T) {
|
|
now := time.Date(2026, 8, 30, 22, 1, 0, 0, time.UTC)
|
|
stats := map[string]int64{
|
|
"fatigue": 80, "fatigue_limit": 156, FatigueBatteryStatKey: 20,
|
|
DailyRecoveryDayStatKey: premium.ServiceDayID(now.Add(-48 * time.Hour)),
|
|
}
|
|
ReconcileDailyStats(stats, now, 30)
|
|
if stats[FatigueBatteryStatKey] != 20 || stats[FatigueBatteryChargeStatKey] != 0 {
|
|
t.Fatalf("stats=%+v, want existing battery retained without invented skipped-day fatigue", stats)
|
|
}
|
|
}
|
|
|
|
func TestReconcileDailyStatsDoesNotChargeCreationServiceDay(t *testing.T) {
|
|
createdDay := time.Date(2026, 8, 29, 22, 1, 0, 0, time.UTC)
|
|
nextDay := createdDay.Add(24 * time.Hour)
|
|
stats := map[string]int64{
|
|
"fatigue": 156, "fatigue_limit": 156,
|
|
DailyRecoveryDayStatKey: premium.ServiceDayID(createdDay),
|
|
FatigueCreatedDayStatKey: premium.ServiceDayID(createdDay),
|
|
FatigueBatteryStatKey: 0,
|
|
}
|
|
ReconcileDailyStats(stats, nextDay, 1)
|
|
if stats[FatigueBatteryStatKey] != 0 {
|
|
t.Fatalf("battery=%d, want 0 on first day after creation", stats[FatigueBatteryStatKey])
|
|
}
|
|
ReconcileDailyStats(stats, nextDay.Add(24*time.Hour), 1)
|
|
if stats[FatigueBatteryStatKey] != 31 || stats[FatigueBatteryChargeStatKey] != 1 {
|
|
t.Fatalf("stats=%+v, want 31 charges and 1 raw point after a complete service day", stats)
|
|
}
|
|
}
|
|
|
|
func TestReconcileDailyStatsAccumulatesAndConvertsBattery(t *testing.T) {
|
|
now := time.Date(2026, 9, 5, 0, 0, 0, 0, time.UTC)
|
|
for _, test := range []struct {
|
|
name string
|
|
level int
|
|
stored, charge, unused, wantStored, wantCharge int64
|
|
}{
|
|
{"below threshold", 40, 0, 100, 55, 0, 155},
|
|
{"at threshold", 40, 0, 100, 56, 31, 1},
|
|
{"retains existing blessing", 40, 20, 100, 56, 51, 1},
|
|
{"low level cap", 19, 70, 0, 156, 78, 116},
|
|
{"full cap retains raw charge", 40, 234, 100, 100, 234, 200},
|
|
{"raw storage cap", 40, 234, 19999, 188, 234, 20000},
|
|
{"negative values", 40, -1, -1, -1, 0, 0},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
stats := map[string]int64{
|
|
"fatigue": test.unused, "fatigue_limit": 188,
|
|
FatigueBatteryStatKey: test.stored, FatigueBatteryChargeStatKey: test.charge,
|
|
DailyRecoveryDayStatKey: premium.ServiceDayID(now) - 1,
|
|
}
|
|
ReconcileDailyStats(stats, now, test.level)
|
|
if stats[FatigueBatteryStatKey] != test.wantStored || stats[FatigueBatteryChargeStatKey] != test.wantCharge {
|
|
t.Fatalf("stats=%+v, want battery=%d charge=%d", stats, test.wantStored, test.wantCharge)
|
|
}
|
|
if result := ReconcileDailyStats(stats, now.Add(time.Hour), test.level); result.Changed {
|
|
t.Fatalf("same day changed again: %+v", stats)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOwnerReconcileDailyPersistsOnce(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
before := time.Date(2026, 8, 30, 21, 59, 0, 0, time.UTC)
|
|
after := time.Date(2026, 8, 30, 22, 1, 0, 0, time.UTC)
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "19", AccountID: "account-1", Stats: map[string]int64{
|
|
"fatigue": 0, "fatigue_limit": DefaultLimit,
|
|
DailyRecoveryDayStatKey: premium.ServiceDayID(before),
|
|
PotionUsageDayStatKey: premium.ServiceDayID(before),
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owner, err := NewOwner(repositories)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, err := owner.ReconcileDaily(ctx, ReconcileDailyCommand{
|
|
AccountID: "account-1", CharacterID: "19", UpdatedAt: after,
|
|
})
|
|
if err != nil || !first.Changed || first.Character.Stats["fatigue"] != DefaultLimit || !first.Character.UpdatedAt.Equal(after) {
|
|
t.Fatalf("first=%+v err=%v", first, err)
|
|
}
|
|
second, err := owner.ReconcileDaily(ctx, ReconcileDailyCommand{
|
|
AccountID: "account-1", CharacterID: "19", UpdatedAt: after.Add(time.Hour),
|
|
})
|
|
if err != nil || second.Changed || !second.Character.UpdatedAt.Equal(after) {
|
|
t.Fatalf("second=%+v err=%v", second, err)
|
|
}
|
|
}
|