服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
260 lines
11 KiB
Go
260 lines
11 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func newUpgradeTicketTestCatalog(t *testing.T) (*pvfDungeonDropCatalog, dungeonDropCatalogTestSource) {
|
|
t.Helper()
|
|
source := dungeonDropCatalogTestSource{
|
|
"monster/monster.lst": "",
|
|
"stackable/stackable.lst": "9100 `ticket/plus10.stk`\n9101 `ticket/amplify12.stk`\n9102 `ticket/random.stk`\n9103 `ticket/plain.stk`\n9104 `ticket/portable_amplify.stk`\n9105 `ticket/portable_upgrade.stk`\n",
|
|
"equipment/equipment.lst": "700 `weapon/test_sword.equ`\n701 `weapon/sealed.equ`\n",
|
|
"stackable/ticket/plus10.stk": "[name] `+10 Ticket`\n[stackable type] `[material]`\n[equipment reinforcement ticket] 10 100\n",
|
|
"stackable/ticket/amplify12.stk": "[name] `+12 Amplify Ticket`\n[stackable type] `[material]`\n[equipment amplify reinforcement ticket] 12 50\n",
|
|
"stackable/ticket/random.stk": "[name] `Random Ticket`\n[stackable type] `[material]`\n[enchant random]\n[er_grade]\n0 1 2 3 5 6\n[er_slot]\n1\n[er_seal]\n2\n[er_enchant]\n2 10 60000 12 40000\n[/er_enchant]\n[/enchant random]\n",
|
|
"stackable/ticket/plain.stk": "[name] `Plain Material`\n[stackable type] `[material]`\n",
|
|
"stackable/ticket/portable_amplify.stk": "[name] `Portable Amplifier`\n[stackable type] `[waste]`\n[action type] `[portable amplify]` 0 50\n[/action type]\n[check usable itemlevel] 1 85\n",
|
|
"stackable/ticket/portable_upgrade.stk": "[name] `Portable Reinforcer`\n[stackable type] `[town and dungeon]`\n[action type] `[portable upgrade]` 10 30\n[/action type]\n[check usable itemlevel] 85 90\n",
|
|
"equipment/weapon/test_sword.equ": "[name] `Test Sword`\n[equipment type] `[weapon]`\n[rarity] 3\n[minimum level] 85\n[durability] 57\n",
|
|
"equipment/weapon/sealed.equ": "[name] `Sealed`\n[equipment type] `[weapon]`\n[impossible contents] `[upgrade]`\n",
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return catalog, source
|
|
}
|
|
|
|
func TestResolveCurrentUpgradeTicketMetadata(t *testing.T) {
|
|
catalog, source := newUpgradeTicketTestCatalog(t)
|
|
|
|
resolution, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9100, 700)
|
|
if err != nil {
|
|
t.Fatalf("resolve error = %v", err)
|
|
}
|
|
if resolution.TicketMode != "reinforce" || resolution.TargetLevel != 10 || resolution.SuccessWeight != 100000 {
|
|
t.Fatalf("reinforce ticket = %+v", resolution)
|
|
}
|
|
if resolution.TicketRandom {
|
|
t.Fatalf("regular ticket marked random")
|
|
}
|
|
if resolution.TargetKind != "equipment" || resolution.TargetEquipmentType != "[weapon]" || resolution.TargetUpgradeForbidden {
|
|
t.Fatalf("target = %+v", resolution)
|
|
}
|
|
if resolution.TargetRarity != 3 {
|
|
t.Fatalf("target rarity = %d, want 3", resolution.TargetRarity)
|
|
}
|
|
|
|
amplify, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9101, 700)
|
|
if err != nil {
|
|
t.Fatalf("resolve error = %v", err)
|
|
}
|
|
if amplify.TicketMode != "amplify" || amplify.TargetLevel != 12 || amplify.SuccessWeight != 50000 {
|
|
t.Fatalf("amplify ticket = %+v", amplify)
|
|
}
|
|
|
|
random, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9102, 700)
|
|
if err != nil {
|
|
t.Fatalf("resolve error = %v", err)
|
|
}
|
|
if !random.TicketRandom || random.TicketMode != "reinforce" {
|
|
t.Fatalf("random ticket = %+v", random)
|
|
}
|
|
if random.RandomTicket.SlotRestriction != 1 || random.RandomTicket.SealRestriction != 2 {
|
|
t.Fatalf("random restrictions = %+v", random.RandomTicket)
|
|
}
|
|
if len(random.RandomTicket.Rarities) != 6 || random.RandomTicket.Rarities[0] != 0 || random.RandomTicket.Rarities[5] != 6 {
|
|
t.Fatalf("random rarities = %+v", random.RandomTicket.Rarities)
|
|
}
|
|
wantEntries := []alignedcmd.UpgradeTicketRandomEntry{{TargetLevel: 10, Weight: 60000}, {TargetLevel: 12, Weight: 40000}}
|
|
if len(random.RandomTicket.Entries) != len(wantEntries) {
|
|
t.Fatalf("random entries = %+v, want leading count skipped", random.RandomTicket.Entries)
|
|
}
|
|
for index, want := range wantEntries {
|
|
if random.RandomTicket.Entries[index] != want {
|
|
t.Fatalf("random entry %d = %+v, want %+v", index, random.RandomTicket.Entries[index], want)
|
|
}
|
|
}
|
|
|
|
forbidden, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9100, 701)
|
|
if err != nil {
|
|
t.Fatalf("resolve error = %v", err)
|
|
}
|
|
if !forbidden.TargetUpgradeForbidden {
|
|
t.Fatalf("forbidden target not flagged: %+v", forbidden)
|
|
}
|
|
|
|
portable, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9104, 700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !portable.PortableAmplifier || portable.TicketMode != "" || portable.PortableMinimumLevel != 0 || portable.PortableMaximumLevel != 50 ||
|
|
portable.TargetMinimumLevel != 85 || portable.TargetLevelMinimum != 1 || portable.TargetLevelMaximum != 85 || portable.TargetKind != "equipment" {
|
|
t.Fatalf("portable amplifier = %+v", portable)
|
|
}
|
|
|
|
reinforcer, err := resolveCurrentUpgradeTicketMetadata(catalog, source, 9105, 700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reinforcer.PortableReinforcer || reinforcer.TicketMode != "" || reinforcer.PortableMinimumLevel != 10 || reinforcer.PortableMaximumLevel != 30 ||
|
|
reinforcer.TargetMinimumLevel != 85 || reinforcer.TargetLevelMinimum != 85 || reinforcer.TargetLevelMaximum != 90 || reinforcer.TargetKind != "equipment" {
|
|
t.Fatalf("portable reinforcer = %+v", reinforcer)
|
|
}
|
|
}
|
|
|
|
func TestResolveCurrentUpgradeTicketMetadataNonTickets(t *testing.T) {
|
|
catalog, source := newUpgradeTicketTestCatalog(t)
|
|
tests := []struct {
|
|
name string
|
|
material int64
|
|
}{
|
|
{name: "plain stackable", material: 9103},
|
|
{name: "unknown material", material: 9999},
|
|
{name: "equipment as material", material: 700},
|
|
{name: "zero material", material: 0},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
resolution, err := resolveCurrentUpgradeTicketMetadata(catalog, source, tc.material, 700)
|
|
if err != nil {
|
|
t.Fatalf("resolve error = %v", err)
|
|
}
|
|
if resolution.TicketMode != "" {
|
|
t.Fatalf("ticket mode = %q, want empty for non-ticket", resolution.TicketMode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRealPVFBlessedEnchantRandomTickets pins the shipped definitions of
|
|
// 祝福强化券 (武器) 2681908 and 祝福强化券 (防具) 2681909: both carry an
|
|
// [enchant random] block with the same eight er_enchant candidates and differ
|
|
// only in er_slot (1 weapon vs 2 armor).
|
|
func TestRealPVFBlessedEnchantRandomTickets(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the blessed enchant tickets")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantEntries := []alignedcmd.UpgradeTicketRandomEntry{
|
|
{TargetLevel: 6, Weight: 15285},
|
|
{TargetLevel: 7, Weight: 30805},
|
|
{TargetLevel: 8, Weight: 32150},
|
|
{TargetLevel: 9, Weight: 11201},
|
|
{TargetLevel: 10, Weight: 8600},
|
|
{TargetLevel: 11, Weight: 1440},
|
|
{TargetLevel: 12, Weight: 437},
|
|
{TargetLevel: 13, Weight: 82},
|
|
}
|
|
for _, test := range []struct {
|
|
itemID int64
|
|
wantSlot int64
|
|
}{
|
|
{itemID: 2681908, wantSlot: 1},
|
|
{itemID: 2681909, wantSlot: 2},
|
|
} {
|
|
got, resolveErr := resolveCurrentUpgradeTicketMetadata(catalog, archive, test.itemID, 100240061)
|
|
if resolveErr != nil {
|
|
t.Fatalf("item=%d: %v", test.itemID, resolveErr)
|
|
}
|
|
if !got.TicketRandom || got.TicketMode != "reinforce" {
|
|
t.Fatalf("item=%d resolution=%+v, want a reinforce random ticket", test.itemID, got)
|
|
}
|
|
if got.RandomTicket.SlotRestriction != test.wantSlot || got.RandomTicket.SealRestriction != 2 ||
|
|
len(got.RandomTicket.Rarities) != 6 {
|
|
t.Fatalf("item=%d restrictions=%+v", test.itemID, got.RandomTicket)
|
|
}
|
|
if len(got.RandomTicket.Entries) != len(wantEntries) {
|
|
t.Fatalf("item=%d entries=%+v", test.itemID, got.RandomTicket.Entries)
|
|
}
|
|
for index, want := range wantEntries {
|
|
if got.RandomTicket.Entries[index] != want {
|
|
t.Fatalf("item=%d entry %d = %+v, want %+v", test.itemID, index, got.RandomTicket.Entries[index], want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRealPVFPortableAmplifiersUseTheirOwnMaterialContract(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify the portable amplifier")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct {
|
|
itemID int64
|
|
currentMax int64
|
|
targetMinimum int64
|
|
targetMaximum int64
|
|
}{
|
|
{itemID: 10005313, currentMax: 50, targetMinimum: 1, targetMaximum: 85},
|
|
{itemID: 490702022, currentMax: 50, targetMinimum: 55, targetMaximum: 90},
|
|
{itemID: 490702253, currentMax: 30, targetMinimum: 55, targetMaximum: 90},
|
|
{itemID: 490702334, currentMax: 30, targetMinimum: 55, targetMaximum: 90},
|
|
} {
|
|
got, resolveErr := resolveCurrentUpgradeTicketMetadata(catalog, archive, test.itemID, 100240061)
|
|
if resolveErr != nil {
|
|
t.Fatalf("item=%d: %v", test.itemID, resolveErr)
|
|
}
|
|
if !got.PortableAmplifier || got.PortableMinimumLevel != 0 || got.PortableMaximumLevel != test.currentMax ||
|
|
got.TargetLevelMinimum != test.targetMinimum || got.TargetLevelMaximum != test.targetMaximum || got.TargetMinimumLevel != 85 ||
|
|
got.TargetKind != "equipment" {
|
|
t.Fatalf("portable amplifier item=%d resolution=%+v", test.itemID, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRealPVFPortableReinforcersUseTheirOwnMaterialContract(t *testing.T) {
|
|
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify portable reinforcers")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := newPVFDungeonDropCatalog(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct {
|
|
itemID int64
|
|
minimumLevel int64
|
|
maximumLevel int64
|
|
targetMin int64
|
|
targetMax int64
|
|
}{
|
|
{itemID: 490000362, minimumLevel: 0, maximumLevel: 9, targetMin: 1, targetMax: 86},
|
|
{itemID: 490702163, minimumLevel: 10, maximumLevel: 30, targetMin: 85, targetMax: 90},
|
|
} {
|
|
got, resolveErr := resolveCurrentUpgradeTicketMetadata(catalog, archive, test.itemID, 100240061)
|
|
if resolveErr != nil {
|
|
t.Fatalf("item=%d: %v", test.itemID, resolveErr)
|
|
}
|
|
if !got.PortableReinforcer || got.PortableMinimumLevel != test.minimumLevel || got.PortableMaximumLevel != test.maximumLevel ||
|
|
got.TargetLevelMinimum != test.targetMin || got.TargetLevelMaximum != test.targetMax || got.TargetKind != "equipment" {
|
|
t.Fatalf("portable reinforcer item=%d resolution=%+v", test.itemID, got)
|
|
}
|
|
}
|
|
}
|