服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
224 lines
9.4 KiB
Go
224 lines
9.4 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
dnfhellparty "longheng.io/server/internal/modules/dnf/hellparty"
|
||
|
||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||
appkit "longheng.io/server/internal/platform/app"
|
||
)
|
||
|
||
func TestGMUnlocksAllDungeonDifficultiesAndHellPermissions(t *testing.T) {
|
||
t.Parallel()
|
||
ctx := context.Background()
|
||
repositories := dnfrepomemory.NewMemoryGroup()
|
||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "77", AccountID: "dnf:gm-dungeon", Name: "Dungeon GM", Job: "0", Level: 90,
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
source := bridgeDungeonPVF(false)
|
||
source["dungeon/test.dgn"] = "[name]\n`Synthetic Dungeon`\n" +
|
||
"[difficulty level]\n1 2 3 4 5\n" +
|
||
"[maze info]\n[size]\n1 1\n[greed]\n`A`\n" +
|
||
"[map specification]\n`map` 0 0 100\n[start map]\n0 0\n[boss map]\n0 0\n"
|
||
source[worldmap.DefaultWorldMapList] = "39 `test.wdm`\n"
|
||
source["worldmap/test.wdm"] = "[name]\n`Synthetic Hell Area`\n" +
|
||
"[dungeon]\n700 -1\n[/dungeon]\n" +
|
||
"[hell dungeon]\n1\n" +
|
||
"[hell quest]\n2900 2901\n[/hell quest]\n"
|
||
table, resolver, _ := loadBridgeDungeonStaticData(t, source)
|
||
service := NewWithOptions(&appkit.Env{}, ServiceOptions{
|
||
RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||
})
|
||
service.worldMapTable = table
|
||
service.worldMapResolver = resolver
|
||
|
||
initial, err := service.GMDungeonAccess(ctx, "77")
|
||
if err != nil || initial.DifficultyDungeons != 1 || initial.DifficultyDungeonsUnlocked != 0 ||
|
||
initial.HellAreas != 1 || initial.HellPermissionQuests != 2 || initial.HellPermissionQuestsCleared != 0 {
|
||
t.Fatalf("initial dungeon access=%+v err=%v", initial, err)
|
||
}
|
||
difficulties, err := service.GMUnlockAllDungeonDifficulties(ctx, "77")
|
||
if err != nil || difficulties.DifficultyDungeonsUnlocked != 1 || difficulties.DifficultyDungeonsUpdated != 1 {
|
||
t.Fatalf("difficulty unlock=%+v err=%v", difficulties, err)
|
||
}
|
||
permission, found, err := repositories.DungeonPermission.Load(ctx, "77")
|
||
if err != nil || !found || len(permission.Entries) != 1 || permission.Entries[0].DungeonID != 700 || permission.Entries[0].ClearState != 4 {
|
||
t.Fatalf("permission=%+v found=%t err=%v", permission, found, err)
|
||
}
|
||
hell, err := service.GMUnlockAllHellChallenges(ctx, "77")
|
||
if err != nil || hell.HellPermissionQuestsCleared != 2 || hell.HellPermissionQuestsUpdated != 2 {
|
||
t.Fatalf("hell unlock=%+v err=%v", hell, err)
|
||
}
|
||
quests, found, err := repositories.Quest.Load(ctx, "77")
|
||
if err != nil || !found {
|
||
t.Fatalf("quest record found=%t err=%v", found, err)
|
||
}
|
||
for _, questID := range []int64{2900, 2901} {
|
||
state := quests.States[questID]
|
||
if state.Status != "completed" || state.Extra["gm_rewards_granted"] != "false" || state.Extra["gm_all_hell_unlocked"] != "true" {
|
||
t.Fatalf("quest %d state=%+v", questID, state)
|
||
}
|
||
}
|
||
hellPermission, found, err := repositories.HellPartyPermission.Load(ctx, "77")
|
||
if err != nil || !found || len(hellPermission.Entries) != 1 || hellPermission.Entries[0].AreaID != 39 || hellPermission.Entries[0].QuestID != 2900 {
|
||
t.Fatalf("hell permission=%+v found=%t err=%v", hellPermission, found, err)
|
||
}
|
||
session := &gameSession{selectedCharacterID: 77}
|
||
if err := service.authorizeCurrentDungeonHellParty(ctx, session, table, 700); err != nil {
|
||
t.Fatalf("hell entry after GM unlock error=%v", err)
|
||
}
|
||
repeated, err := service.GMUnlockAllHellChallenges(ctx, "77")
|
||
if err != nil || repeated.HellPermissionQuestsUpdated != 0 {
|
||
t.Fatalf("repeated hell unlock=%+v err=%v", repeated, err)
|
||
}
|
||
|
||
// A character whose quests were already completed by an earlier unlock that
|
||
// never wrote the permission row is repaired by running the unlock again.
|
||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||
CharacterID: "78", AccountID: "dnf:gm-dungeon", Name: "Legacy Unlock", Job: "0", Level: 90,
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||
CharacterID: "78",
|
||
States: map[int64]dnfrepo.QuestState{
|
||
2900: {Status: "completed"},
|
||
2901: {Status: "completed"},
|
||
},
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
legacy, err := service.GMUnlockAllHellChallenges(ctx, "78")
|
||
if err != nil || legacy.HellPermissionQuestsUpdated != 0 || legacy.HellPermissionQuestsCleared != 2 {
|
||
t.Fatalf("legacy hell unlock=%+v err=%v", legacy, err)
|
||
}
|
||
legacyPermission, found, err := repositories.HellPartyPermission.Load(ctx, "78")
|
||
if err != nil || !found || len(legacyPermission.Entries) != 1 || legacyPermission.Entries[0].AreaID != 39 {
|
||
t.Fatalf("legacy hell permission=%+v found=%t err=%v", legacyPermission, found, err)
|
||
}
|
||
}
|
||
|
||
func TestGMHellEpicDropRatePersistsAndScalesPVFRarityRate(t *testing.T) {
|
||
t.Parallel()
|
||
ctx := context.Background()
|
||
repositories := dnfrepomemory.NewMemoryGroup()
|
||
newService := func() *Service {
|
||
return NewWithOptions(&appkit.Env{}, ServiceOptions{
|
||
RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||
})
|
||
}
|
||
service := newService()
|
||
settings, err := service.GMGameplaySettings(ctx)
|
||
if err != nil || settings.HellEpicDropRatePercent != gmHellEpicDropRateDefaultPercent {
|
||
t.Fatalf("default settings=%+v err=%v", settings, err)
|
||
}
|
||
settings, err = service.GMSetHellEpicDropRate(ctx, 250)
|
||
if err != nil || settings.HellEpicDropRatePercent != 250 {
|
||
t.Fatalf("saved settings=%+v err=%v", settings, err)
|
||
}
|
||
persisted, err := newService().GMGameplaySettings(ctx)
|
||
if err != nil || persisted.HellEpicDropRatePercent != 250 {
|
||
t.Fatalf("persisted settings=%+v err=%v", persisted, err)
|
||
}
|
||
for _, test := range []struct {
|
||
base int64
|
||
percent int
|
||
want int64
|
||
}{
|
||
{base: 7_000, percent: 100, want: 7_000},
|
||
{base: 7_000, percent: 1_000, want: 70_000},
|
||
{base: 20_000, percent: 10_000, want: int64(dnfhellparty.RarityRollDenominator)},
|
||
{base: 7_000, percent: 0, want: 0},
|
||
{base: 0, percent: 10000, want: 0},
|
||
} {
|
||
if got := scaleGMHellEpicRarityRate(test.base, test.percent); got != test.want {
|
||
t.Fatalf("scale base=%d percent=%d got=%d want=%d", test.base, test.percent, got, test.want)
|
||
}
|
||
}
|
||
if _, err := service.GMSetHellEpicDropRate(ctx, gmHellEpicDropRateMaximumPercent+1); err == nil {
|
||
t.Fatal("out-of-range hell epic drop rate was accepted")
|
||
}
|
||
if maximum, err := service.GMSetHellEpicDropRate(ctx, gmHellEpicDropRateMaximumPercent); err != nil || maximum.HellEpicDropRatePercent != gmHellEpicDropRateMaximumPercent {
|
||
t.Fatalf("maximum setting=%+v err=%v", maximum, err)
|
||
}
|
||
}
|
||
|
||
func TestGMAdjustHellEpicRarity(t *testing.T) {
|
||
const base = int64(10_000)
|
||
if rarity, keep := gmAdjustHellEpicRarity(4, base, 0, 0); keep || rarity != 4 {
|
||
t.Fatalf("zero-percent epic = rarity %d keep %t", rarity, keep)
|
||
}
|
||
if rarity, keep := gmAdjustHellEpicRarity(3, base, 200, 0); !keep || rarity != 4 {
|
||
t.Fatalf("boosted non-epic = rarity %d keep %t", rarity, keep)
|
||
}
|
||
if rarity, keep := gmAdjustHellEpicRarity(3, base, 100, 0); !keep || rarity != 3 {
|
||
t.Fatalf("default non-epic = rarity %d keep %t", rarity, keep)
|
||
}
|
||
}
|
||
|
||
// TestGMEpicPotionTestMultiplierPersistsAndDoesNotDisturbTheHellRate:
|
||
// 服主要"先改一下倍率来测药剂是否真生效,后面再改回从 PVF 读取",所以这个旋钮必须
|
||
//
|
||
// ① 默认 0(= 关闭,按 PVF 表走);
|
||
// ② 能存能读(换一个 Service 实例也读得到);
|
||
// ③ 与 GM 全局掉落率**互不干扰**(改一个不覆盖另一个);
|
||
// ④ 越界要被拒绝。
|
||
func TestGMEpicPotionTestMultiplierPersistsAndDoesNotDisturbTheHellRate(t *testing.T) {
|
||
t.Parallel()
|
||
ctx := context.Background()
|
||
repositories := dnfrepomemory.NewMemoryGroup()
|
||
newService := func() *Service {
|
||
return NewWithOptions(&appkit.Env{}, ServiceOptions{
|
||
RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||
})
|
||
}
|
||
service := newService()
|
||
settings, err := service.GMGameplaySettings(ctx)
|
||
if err != nil || settings.EpicDropRatePotionTestMultiplierPercent != gmEpicDropRatePotionTestMultiplierDefaultPercent {
|
||
t.Fatalf("default potion test multiplier=%+v err=%v", settings, err)
|
||
}
|
||
|
||
if _, err := service.GMSetHellEpicDropRate(ctx, 250); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
settings, err = service.GMSetEpicDropRatePotionTestMultiplier(ctx, 2000)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if settings.EpicDropRatePotionTestMultiplierPercent != 2000 || settings.HellEpicDropRatePercent != 250 {
|
||
t.Fatalf("after potion set=%+v want potion 2000 + hell 250 kept", settings)
|
||
}
|
||
|
||
persisted, err := newService().GMGameplaySettings(ctx)
|
||
if err != nil || persisted.EpicDropRatePotionTestMultiplierPercent != 2000 ||
|
||
persisted.HellEpicDropRatePercent != 250 {
|
||
t.Fatalf("persisted=%+v err=%v", persisted, err)
|
||
}
|
||
|
||
// 改回全局掉落率不能把测试倍率冲掉。
|
||
if _, err := newService().GMSetHellEpicDropRate(ctx, 300); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
after, err := newService().GMGameplaySettings(ctx)
|
||
if err != nil || after.EpicDropRatePotionTestMultiplierPercent != 2000 || after.HellEpicDropRatePercent != 300 {
|
||
t.Fatalf("after hell set=%+v err=%v", after, err)
|
||
}
|
||
|
||
// 关掉旋钮 = 回到 PVF 表。
|
||
off, err := newService().GMSetEpicDropRatePotionTestMultiplier(ctx, 0)
|
||
if err != nil || off.EpicDropRatePotionTestMultiplierPercent != 0 {
|
||
t.Fatalf("disabled=%+v err=%v", off, err)
|
||
}
|
||
for _, bad := range []int{-1, gmEpicDropRatePotionTestMultiplierMaximumPercent + 1} {
|
||
if _, err := newService().GMSetEpicDropRatePotionTestMultiplier(ctx, bad); err == nil {
|
||
t.Fatalf("out-of-range potion test multiplier %d was accepted", bad)
|
||
}
|
||
}
|
||
}
|