服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
247 lines
9.0 KiB
Go
247 lines
9.0 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
dnfmonster "longheng.io/server/internal/modules/dnf/monster"
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
dnfquest "longheng.io/server/internal/modules/dnf/quest"
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func TestCurrentQuestDropMatchMonsterIncludesTypeOneEnemyRewardItem(t *testing.T) {
|
|
ctx := context.Background()
|
|
source := bridgePVFSource{
|
|
dnfquest.DefaultList: "3233 `enemy_reward.qst`\n3234 `monster_reward.qst`\n",
|
|
"n_quest/enemy_reward.qst": "[grade]\n" +
|
|
"`[epic]`\n" +
|
|
"[type]\n" +
|
|
"`[seeking]`\n" +
|
|
"[int data]\n" +
|
|
"10164861 30\n" +
|
|
"[enemy reward item]\n" +
|
|
"420 1 32 -1 10164861 1 100 30\n" +
|
|
"421 1 32 2 10164861 2 50 30\n" +
|
|
"420 2 32 -1 10164862 1 100 30\n" +
|
|
"16111 3 102 -1 10164801 1 100 15\n" +
|
|
"[/enemy reward item]\n",
|
|
"n_quest/monster_reward.qst": "[grade]\n" +
|
|
"`[normal]`\n" +
|
|
"[type]\n" +
|
|
"`[seeking]`\n" +
|
|
"[int data]\n" +
|
|
"10164863 1\n" +
|
|
"[monster reward item]\n" +
|
|
"420 32 -1 10164863 1 75 1\n" +
|
|
"[/monster reward item]\n",
|
|
}
|
|
index, err := dnfpvf.Build(ctx, source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := dnfquest.Load(ctx, index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
candidates := currentQuestDropMatchMonster(catalog, []int64{3233, 3234}, 32, 0, 420)
|
|
if len(candidates) != 2 {
|
|
t.Fatalf("candidates=%+v", candidates)
|
|
}
|
|
if candidates[0] != (currentQuestDropCandidate{QuestID: 3233, ItemID: 10164861, Count: 1, DropRate: 100, MaxStack: 30}) {
|
|
t.Fatalf("enemy reward candidate=%+v", candidates[0])
|
|
}
|
|
if candidates[1] != (currentQuestDropCandidate{QuestID: 3234, ItemID: 10164863, Count: 1, DropRate: 75, MaxStack: 1}) {
|
|
t.Fatalf("monster reward candidate=%+v", candidates[1])
|
|
}
|
|
if got := currentQuestDropMatchMonster(catalog, []int64{3233}, 32, 0, 421); len(got) != 0 {
|
|
t.Fatalf("difficulty-specific enemy reward matched wrong difficulty: %+v", got)
|
|
}
|
|
if got := currentQuestDropMatchMonster(catalog, []int64{3233}, 32, 2, 421); len(got) != 1 || got[0].Count != 2 || got[0].DropRate != 50 {
|
|
t.Fatalf("difficulty-specific enemy reward=%+v", got)
|
|
}
|
|
if got := currentQuestDropMatchMonster(catalog, []int64{3233}, 31, 0, 420); len(got) != 0 {
|
|
t.Fatalf("enemy reward matched wrong dungeon: %+v", got)
|
|
}
|
|
if got := currentQuestDropMatchMonster(catalog, []int64{3233}, 102, 0, 16111); len(got) != 0 {
|
|
t.Fatalf("type-3 passive object leaked into monster matching: %+v", got)
|
|
}
|
|
got := currentQuestDropMatchEnemy(catalog, []int64{3233}, 102, 3, 16111, 3)
|
|
if len(got) != 1 || got[0] != (currentQuestDropCandidate{QuestID: 3233, ItemID: 10164801, Count: 1, DropRate: 100, MaxStack: 15}) {
|
|
t.Fatalf("type-3 passive-object reward=%+v", got)
|
|
}
|
|
if got := currentQuestDropMatchEnemy(catalog, []int64{3233}, 102, 3, 16111, 1); len(got) != 0 {
|
|
t.Fatalf("type-3 passive object leaked into type-1 matching: %+v", got)
|
|
}
|
|
if got := currentQuestDropMatchEnemy(catalog, []int64{3233}, 101, 3, 16111, 3); len(got) != 0 {
|
|
t.Fatalf("type-3 passive object matched wrong dungeon: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestCurrentQuestDropMatchImplicitMonsterUsesPVFScopeAndHiddenBossExplanation(t *testing.T) {
|
|
ctx := context.Background()
|
|
source := bridgePVFSource{
|
|
dnfquest.DefaultList: "8557 `event.qst`\n13701 `regular.qst`\n12872 `hidden.qst`\n",
|
|
"n_quest/event.qst": "[name]\n`进攻准备`\n[type]\n`[seeking]`\n[int data]\n10094602 10\n",
|
|
"n_quest/regular.qst": "[name]\n`进攻准备`\n[type]\n`[seeking]`\n[int data]\n10094602 3\n[dungeon info]\n3200 -1\n[/dungeon info]\n",
|
|
"n_quest/hidden.qst": "[name]\n`异界的异变 (2/4)`\n[type]\n`[seeking]`\n[int data]\n10000204 1\n",
|
|
}
|
|
index, err := dnfpvf.Build(ctx, source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
questCatalog, err := dnfquest.Load(ctx, index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
itemCatalog := &pvfDungeonDropCatalog{
|
|
source: bridgePVFSource{},
|
|
itemCache: map[uint32]dungeonDropItemDefinition{
|
|
10094602: {ItemID: 10094602, Kind: dungeonDropItemStackable, StackableType: "[material]"},
|
|
10000204: {ItemID: 10000204, Kind: dungeonDropItemStackable, StackableType: "[quest]", Explain: "消灭异次元裂缝的依普利乌斯时,可以获得该道具"},
|
|
},
|
|
}
|
|
monsterCatalog := &pvfDungeonMonsterCatalog{
|
|
source: bridgePVFSource{},
|
|
cache: map[int64]dnfmonster.Monster{
|
|
420: {ID: 420, Path: "monster/ordinary.mob", Name: "普通怪物"},
|
|
107000256: {
|
|
ID: 107000256, Path: "monster/newmonsters/dimensioncrack/specialboss/iflius/iflius1.mob",
|
|
Name: "依普利乌斯", Sections: []string{"dimension hidden boss"},
|
|
},
|
|
},
|
|
}
|
|
|
|
material := currentQuestDropMatchImplicitMonster(
|
|
questCatalog, itemCatalog, monsterCatalog, []int64{8557}, 3200, 0, 420,
|
|
)
|
|
if len(material) != 1 || material[0] != (currentQuestDropCandidate{
|
|
QuestID: 8557, ItemID: 10094602, Count: 1, DropRate: 100, MaxStack: 10,
|
|
}) {
|
|
t.Fatalf("scoped material candidate=%+v", material)
|
|
}
|
|
if got := currentQuestDropMatchImplicitMonster(
|
|
questCatalog, itemCatalog, monsterCatalog, []int64{8557}, 3201, 0, 420,
|
|
); len(got) != 0 {
|
|
t.Fatalf("scoped material leaked into wrong dungeon: %+v", got)
|
|
}
|
|
|
|
hiddenBoss := currentQuestDropMatchImplicitMonster(
|
|
questCatalog, itemCatalog, monsterCatalog, []int64{12872}, 7177, 3, 107000256,
|
|
)
|
|
if len(hiddenBoss) != 1 || hiddenBoss[0] != (currentQuestDropCandidate{
|
|
QuestID: 12872, ItemID: 10000204, Count: 1, DropRate: 100, MaxStack: 1,
|
|
}) {
|
|
t.Fatalf("hidden boss candidate=%+v", hiddenBoss)
|
|
}
|
|
if got := currentQuestDropMatchImplicitMonster(
|
|
questCatalog, itemCatalog, monsterCatalog, []int64{12872}, 7177, 3, 420,
|
|
); len(got) != 0 {
|
|
t.Fatalf("quest item leaked to ordinary monster: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestRealScriptPVFImplicitSeekingQuestDrops(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify implicit seeking quest drops")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), archive, dnfpvf.BuildOptions{
|
|
Lists: []string{dnfquest.DefaultList}, Prefixes: []string{"n_quest/"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
questCatalog, err := dnfquest.Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scopes := questCatalog.SeekingDungeonScopes(8557)
|
|
if len(scopes) != 1 || scopes[0] != (dnfquest.DungeonScope{DungeonID: 3200, Difficulty: -1}) {
|
|
var related []dnfquest.Definition
|
|
for _, definition := range questCatalog.Definitions() {
|
|
for _, target := range questCatalog.SeekingItemTargets(definition.ID) {
|
|
if target.ItemID == 10094602 {
|
|
related = append(related, definition)
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("quest 8557 inherited scopes=%+v related=%+v", scopes, related)
|
|
}
|
|
itemCatalog, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
monsterCatalog, err := newPVFDungeonMonsterCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
material := currentQuestDropMatchImplicitMonster(
|
|
questCatalog, itemCatalog, monsterCatalog, []int64{8557}, 3200, 0, 107000256,
|
|
)
|
|
if len(material) != 1 || material[0].ItemID != 10094602 || material[0].MaxStack != 10 {
|
|
t.Fatalf("quest 8557 material candidate=%+v", material)
|
|
}
|
|
hiddenBoss := currentQuestDropMatchImplicitMonster(
|
|
questCatalog, itemCatalog, monsterCatalog, []int64{12872}, 7177, 3, 107000256,
|
|
)
|
|
if len(hiddenBoss) != 1 || hiddenBoss[0] != (currentQuestDropCandidate{
|
|
QuestID: 12872, ItemID: 10000204, Count: 1, DropRate: 100, MaxStack: 1,
|
|
}) {
|
|
t.Fatalf("quest 12872 hidden-boss candidate=%+v", hiddenBoss)
|
|
}
|
|
}
|
|
|
|
func TestCurrentQuestHelperDropRateRaisesOneFifthAndCaps(t *testing.T) {
|
|
tests := []struct {
|
|
base int64
|
|
active bool
|
|
want int64
|
|
}{
|
|
{base: 50, active: false, want: 50},
|
|
{base: 50, active: true, want: 60},
|
|
{base: 83, active: true, want: 100},
|
|
{base: 100, active: true, want: 100},
|
|
{base: 0, active: true, want: 0},
|
|
}
|
|
for _, test := range tests {
|
|
if got := currentQuestHelperDropRate(test.base, test.active); got != test.want {
|
|
t.Fatalf("base=%d active=%t got=%d want=%d", test.base, test.active, got, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCurrentQuestItemDropRateCombinesGrowthContractAndQuestHelper(t *testing.T) {
|
|
tests := []struct {
|
|
base int64
|
|
growthPercent int64
|
|
questHelper bool
|
|
want int64
|
|
}{
|
|
{base: 50, growthPercent: 0, questHelper: false, want: 50},
|
|
{base: 50, growthPercent: 20, questHelper: false, want: 60},
|
|
{base: 50, growthPercent: 20, questHelper: true, want: 70},
|
|
{base: 83, growthPercent: 20, questHelper: false, want: 100},
|
|
{base: 99, growthPercent: -1, questHelper: false, want: 99},
|
|
{base: 0, growthPercent: 20, questHelper: true, want: 0},
|
|
}
|
|
for _, test := range tests {
|
|
if got := currentQuestItemDropRate(test.base, test.growthPercent, test.questHelper); got != test.want {
|
|
t.Fatalf(
|
|
"base=%d growth=%d helper=%t got=%d want=%d",
|
|
test.base,
|
|
test.growthPercent,
|
|
test.questHelper,
|
|
got,
|
|
test.want,
|
|
)
|
|
}
|
|
}
|
|
}
|