Files
90CNServer/server/internal/modules/dnf/fatigue/daily_reset_test.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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)
}
}