Files
90CNServer/server/internal/modules/dnf/raid/anton_pvf_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

789 lines
24 KiB
Go

package raid
import (
"errors"
"fmt"
"os"
"reflect"
"strings"
"testing"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestLoadAntonConfigReadsOnlyTargetAndPreservesSourceOrder(t *testing.T) {
source := &antonRecordingSource{text: antonTestDocument()}
config, err := LoadAntonConfig(source)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(source.paths, []string{AntonPVFPath}) {
t.Fatalf("read paths=%v want only %q", source.paths, AntonPVFPath)
}
want := &AntonConfig{
PhaseMax: 2,
MemberMax: 20,
StartMinimumMembers: 10,
SingleStartMinimumMembers: 1,
StartDelaySeconds: 3,
DisconnMemberWaitTime: 300,
PhaseTimeoutSeconds: []int{2400, 2400},
PhaseBreakSeconds: 600,
MovieEndSeconds: []int{20, 21, 22, 23},
EntryCost: AntonEntryCost{ItemID: 10096296, Count: 1},
WeeklyEnterCount: [2]int{30, 3},
Phases: []AntonPhase{
{
Name: "phase one", MinimalDungeonClearCount: 1,
Rewards: []AntonStateReward{
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10094735, Flag: 0},
{Kind: "squad_item", Rank: -1, Weight: 50, DefinitionItemID: 10157773, Flag: 1},
},
Dungeons: []AntonDungeonInfo{
{Index: 210, InitState: AntonDungeonOpen, HP: 4, PartyMax: 5},
{Index: 211, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
},
RankConditions: antonTestRankConditions(),
},
{
Name: "phase two", MinimalDungeonClearCount: 1,
Rewards: []AntonStateReward{
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10157778, Flag: 0},
{Kind: "squad_item", Rank: -1, Weight: 775, DefinitionItemID: 10161936, Flag: 1},
},
Dungeons: []AntonDungeonInfo{
{Index: 218, InitState: AntonDungeonOpen, HP: 1, PartyMax: 5},
{Index: 220, InitState: AntonDungeonHide, HP: 5, PartyMax: 5},
},
RankConditions: antonTestRankConditions(),
},
},
SingleMode: &AntonSingleModeConfig{
Name: "single phase", TimeLimitSeconds: 2100,
Phase: AntonPhase{
Name: "single phase", MinimalDungeonClearCount: 1,
Rewards: []AntonStateReward{{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10094735, Flag: 0}},
Dungeons: []AntonDungeonInfo{{Index: 9999, InitState: AntonDungeonOpen, HP: 1, PartyMax: 0}},
RankConditions: antonTestRankConditions(),
},
},
}
if !reflect.DeepEqual(config, want) {
t.Fatalf("config=%+v\nwant=%+v", config, want)
}
}
func TestAntonConfigModeChoicesExposeBothModesWithoutSharingSlices(t *testing.T) {
config, err := LoadAntonConfig(&antonRecordingSource{text: antonTestDocument()})
if err != nil {
t.Fatal(err)
}
want := []AntonRaidModeChoice{
{
Kind: AntonRaidModeTeam, PhaseNames: []string{"phase one", "phase two"},
StartMinimumMembers: 10, TimeLimitSeconds: []int{2400, 2400},
},
{
Kind: AntonRaidModeSingle, PhaseNames: []string{"single phase"},
StartMinimumMembers: 1, TimeLimitSeconds: []int{2100},
},
}
choices := config.ModeChoices()
if !reflect.DeepEqual(choices, want) {
t.Fatalf("choices=%+v\nwant=%+v", choices, want)
}
choices[0].PhaseNames[0] = "mutated"
choices[0].TimeLimitSeconds[0] = 1
choices[1].PhaseNames[0] = "mutated"
choices[1].TimeLimitSeconds[0] = 1
if !reflect.DeepEqual(config.ModeChoices(), want) {
t.Fatalf("mode choices share mutable slices with caller or config: %+v", config.ModeChoices())
}
var nilConfig *AntonConfig
if choices := nilConfig.ModeChoices(); choices != nil {
t.Fatalf("nil config choices=%+v want nil", choices)
}
}
func TestLoadAntonConfigRejectsNilSourceAndPropagatesTargetReadFailure(t *testing.T) {
if _, err := LoadAntonConfig(nil); !errors.Is(err, ErrAntonPVFSourceRequired) {
t.Fatalf("nil source error=%v", err)
}
source := &antonRecordingSource{err: os.ErrNotExist}
if _, err := LoadAntonConfig(source); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("read error=%v", err)
}
if !reflect.DeepEqual(source.paths, []string{AntonPVFPath}) {
t.Fatalf("failed read paths=%v", source.paths)
}
}
func TestParseAntonConfigRejectsWrongDocumentPath(t *testing.T) {
document, err := dnfpvf.Parse("etc/raid/luke.etc", antonTestDocument())
if err != nil {
t.Fatal(err)
}
if _, err := ParseAntonConfig(document); !errors.Is(err, ErrAntonPVFDocumentInvalid) {
t.Fatalf("wrong path error=%v", err)
}
if _, err := ParseAntonConfig(nil); !errors.Is(err, ErrAntonPVFDocumentInvalid) {
t.Fatalf("nil document error=%v", err)
}
}
func TestAntonConfigRejectsMissingDuplicateAndInvalidCurrentFields(t *testing.T) {
tests := []struct {
name string
mutate func(string) string
want error
}{
{
name: "missing global field",
mutate: func(text string) string {
return strings.Replace(text, "[PHASE BREAK TIME]\n600\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "duplicate global field",
mutate: func(text string) string {
return strings.Replace(text, "[PHASE MAX]\n2", "[PHASE MAX]\n2\n[PHASE MAX]\n2", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "unsupported ordinary phase max",
mutate: func(text string) string {
return strings.Replace(text, "[PHASE MAX]\n2", "[PHASE MAX]\n3", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "member max exceeds protocol roster",
mutate: func(text string) string {
return strings.Replace(text, "[RAID MEMBER MAX]\n20", "[RAID MEMBER MAX]\n21", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "minimum members exceeds member max",
mutate: func(text string) string {
return strings.Replace(text, "[RAID START MINIMUM MEMBER]\n10", "[RAID START MINIMUM MEMBER]\n21", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "phase timeout row is partial",
mutate: func(text string) string {
return strings.Replace(text, "[PHASE TIME OVER]\n2400 2400", "[PHASE TIME OVER]\n2400", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "movie end value is nonpositive",
mutate: func(text string) string {
return strings.Replace(text, "[MOVIE END TIME]\n20 21 22 23", "[MOVIE END TIME]\n20 21 0 23", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "entry cost block is unclosed",
mutate: func(text string) string {
return strings.Replace(text, "[/ENTRY COST]\n", "", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "missing weekly enter count",
mutate: func(text string) string {
return strings.Replace(text, "[WEEKLY ENTER COUNT]\n30 3\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "weekly enter count row is partial",
mutate: func(text string) string {
return strings.Replace(text, "[WEEKLY ENTER COUNT]\n30 3", "[WEEKLY ENTER COUNT]\n30", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "weekly enter count value is nonpositive",
mutate: func(text string) string {
return strings.Replace(text, "[WEEKLY ENTER COUNT]\n30 3", "[WEEKLY ENTER COUNT]\n30 0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "missing phase rank table",
mutate: func(text string) string {
return strings.Replace(text, "[CONDITION FOR RANK]\n[DEAD COUNT]\n0 1 0\n[DEAD COUNT]\n2 5 1\n[/CONDITION FOR RANK]\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "rank table carries a foreign section",
mutate: func(text string) string {
return strings.Replace(text, "[DEAD COUNT]\n0 1 0", "[COIN COUNT]\n1", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "dead count row is partial",
mutate: func(text string) string {
return strings.Replace(text, "[DEAD COUNT]\n0 1 0", "[DEAD COUNT]\n0 1", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "dead count range is reversed",
mutate: func(text string) string {
return strings.Replace(text, "[DEAD COUNT]\n2 5 1", "[DEAD COUNT]\n5 2 1", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "phase coin limit is nonpositive",
mutate: func(text string) string {
return strings.Replace(text, "[MINIMAL DUNGEON CLEAR COUNT]\n1", "[PHASE COIN LIMIT]\n0\n[MINIMAL DUNGEON CLEAR COUNT]\n1", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "lockout type row is partial",
mutate: func(text string) string {
return strings.Replace(text, "[210 DGN HP]\n4\n[PARTY MAX]\n5", "[210 DGN HP]\n4\n[PARTY MAX]\n5\n[LOCKOUT TYPE]\n4", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "lockout type value is negative",
mutate: func(text string) string {
return strings.Replace(text, "[210 DGN HP]\n4\n[PARTY MAX]\n5", "[210 DGN HP]\n4\n[PARTY MAX]\n5\n[LOCKOUT TYPE]\n-1 0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "destroy object info precedes party max",
mutate: func(text string) string {
return strings.Replace(text, "[210 DGN HP]\n4\n[PARTY MAX]\n5", "[210 DGN HP]\n4\n[DESTROY OBJECT INFO]\n[PARTY MAX]\n5", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "destroy object info declares no group",
mutate: func(text string) string {
return strings.Replace(text, "[210 DGN HP]\n4\n[PARTY MAX]\n5", "[210 DGN HP]\n4\n[PARTY MAX]\n5\n[DESTROY OBJECT INFO]", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "destroy object group carries values",
mutate: func(text string) string {
return strings.Replace(text, "[210 DGN HP]\n4\n[PARTY MAX]\n5",
"[210 DGN HP]\n4\n[PARTY MAX]\n5\n[DESTROY OBJECT INFO]\n[BIRTH SANCTUM OBJECT]\n1", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "entry cost count is invalid",
mutate: func(text string) string {
return strings.Replace(text, "10096296 1", "10096296 0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "ordinary phase missing",
mutate: func(text string) string {
start := strings.Index(text, "[PHASE]\n[PHASE NAME]\n`phase two`")
end := strings.Index(text[start:], "[/PHASE]") + start + len("[/PHASE]\n")
return text[:start] + text[end:]
},
want: ErrAntonPVFSectionShape,
},
{
name: "ordinary phase is unclosed",
mutate: func(text string) string {
return strings.Replace(text, "[/PHASE]\n[PHASE]\n[PHASE NAME]\n`phase two`", "[PHASE]\n[PHASE NAME]\n`phase two`", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "phase name missing",
mutate: func(text string) string {
return strings.Replace(text, "[PHASE NAME]\n`phase one`\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "reward block missing close",
mutate: func(text string) string {
return strings.Replace(text, "[/CLEAR REWARD ITEM]\n[MINIMAL DUNGEON CLEAR COUNT]", "[MINIMAL DUNGEON CLEAR COUNT]", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "reward row is partial",
mutate: func(text string) string {
return strings.Replace(text, "`party_card` -1 100 10094735 0", "`party_card` -1 100 10094735", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "reward rank below native sentinel",
mutate: func(text string) string {
return strings.Replace(text, "`party_card` -1 100 10094735 0", "`party_card` -2 100 10094735 0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "reward weight is zero",
mutate: func(text string) string {
return strings.Replace(text, "`party_card` -1 100 10094735 0", "`party_card` -1 0 10094735 0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "reward flag is outside boolean domain",
mutate: func(text string) string {
return strings.Replace(text, "`party_card` -1 100 10094735 0", "`party_card` -1 100 10094735 2", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "minimum clear count exceeds dungeon count",
mutate: func(text string) string {
return strings.Replace(text, "[MINIMAL DUNGEON CLEAR COUNT]\n1", "[MINIMAL DUNGEON CLEAR COUNT]\n3", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "dungeon repeats across phases",
mutate: func(text string) string {
text = strings.Replace(text, "[INDEX]\n218", "[INDEX]\n210", 1)
return strings.Replace(text, "[218 DGN HP]", "[210 DGN HP]", 1)
},
want: ErrAntonPVFDuplicateDungeon,
},
{
name: "dungeon index is zero",
mutate: func(text string) string {
text = strings.Replace(text, "[INDEX]\n210", "[INDEX]\n0", 1)
return strings.Replace(text, "[210 DGN HP]", "[0 DGN HP]", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "dungeon state is unknown",
mutate: func(text string) string {
return strings.Replace(text, "[INIT STATE]\n`open`", "[INIT STATE]\n`ready`", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "dungeon hp is zero",
mutate: func(text string) string {
return strings.Replace(text, "[210 DGN HP]\n4", "[210 DGN HP]\n0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "dungeon party max exceeds raid member max",
mutate: func(text string) string {
return strings.Replace(text, "[PARTY MAX]\n5", "[PARTY MAX]\n21", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "dungeon field is duplicated",
mutate: func(text string) string {
return strings.Replace(text, "[INDEX]\n210", "[INDEX]\n210\n[INDEX]\n210", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "dungeon fields are out of order",
mutate: func(text string) string {
return strings.Replace(text, "[HP INFO]\n[210 DGN HP]\n4\n[PARTY MAX]\n5", "[PARTY MAX]\n5\n[HP INFO]\n[210 DGN HP]\n4", 1)
},
want: ErrAntonPVFSectionShape,
},
{
name: "dungeon block has unsupported field",
mutate: func(text string) string {
return strings.Replace(text, "[PARTY MAX]\n5\n[/DUNGEON INFO]", "[PARTY MAX]\n5\n[UNKNOWN]\n1\n[/DUNGEON INFO]", 1)
},
want: ErrAntonPVFSectionShape,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := &antonRecordingSource{text: test.mutate(antonTestDocument())}
_, err := LoadAntonConfig(source)
if !errors.Is(err, test.want) {
t.Fatalf("error=%v want errors.Is(%v)", err, test.want)
}
if !reflect.DeepEqual(source.paths, []string{AntonPVFPath}) {
t.Fatalf("read paths=%v", source.paths)
}
})
}
}
func TestAntonConfigSingleModeValidation(t *testing.T) {
tests := []struct {
name string
mutate func(string) string
want error
}{
{
name: "missing single minimum members",
mutate: func(text string) string {
return strings.Replace(text, "[SINGLE RAID START MINIUM MEMBER]\n1\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "missing single timeout",
mutate: func(text string) string {
return strings.Replace(text, "[SINGLE RAID TIME OVER]\n2100\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
{
name: "nonpositive single timeout",
mutate: func(text string) string {
return strings.Replace(text, "[SINGLE RAID TIME OVER]\n2100", "[SINGLE RAID TIME OVER]\n0", 1)
},
want: ErrAntonPVFValueInvalid,
},
{
name: "duplicate single phase",
mutate: func(text string) string {
start := strings.Index(text, "[SINGLE RAID PHASE]\n")
end := strings.Index(text[start:], "[/SINGLE RAID PHASE]") + start + len("[/SINGLE RAID PHASE]\n")
return text + text[start:end]
},
want: ErrAntonPVFSectionShape,
},
{
name: "single phase name missing",
mutate: func(text string) string {
return strings.Replace(text, "[PHASE NAME]\n`single phase`\n", "", 1)
},
want: ErrAntonPVFSectionMissing,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := LoadAntonConfig(&antonRecordingSource{text: test.mutate(antonTestDocument())})
if !errors.Is(err, test.want) {
t.Fatalf("error=%v want errors.Is(%v)", err, test.want)
}
})
}
}
func TestAntonConfigWithoutSinglePhaseKeepsOrdinaryTeamMode(t *testing.T) {
text := antonTestDocument()
start := strings.Index(text, "[SINGLE RAID TIME OVER]\n")
end := strings.Index(text, "[/SINGLE RAID PHASE]") + len("[/SINGLE RAID PHASE]\n")
text = text[:start] + text[end:]
text = strings.Replace(text, "[SINGLE RAID START MINIUM MEMBER]\n1\n", "", 1)
config, err := LoadAntonConfig(&antonRecordingSource{text: text})
if err != nil {
t.Fatal(err)
}
if config.SingleMode != nil {
t.Fatalf("single config=%+v", config)
}
if config.SingleStartMinimumMembers != 0 {
t.Fatalf("single minimum members=%d want 0", config.SingleStartMinimumMembers)
}
choices := config.ModeChoices()
if len(choices) != 1 || choices[0].Kind != AntonRaidModeTeam {
t.Fatalf("choices=%+v want team-only", choices)
}
}
func TestRealScriptPVFAntonConfigMatchesCurrentTwoPhaseCatalog(t *testing.T) {
pvfPath := os.Getenv("DNF_ANTON_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNF_ANTON_REAL_PVF_SMOKE to verify etc/raid/anton.etc")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
config, err := LoadAntonConfig(archive)
if err != nil {
t.Fatal(err)
}
if config.PhaseMax != 2 || config.MemberMax != 20 || config.StartMinimumMembers != 10 ||
config.SingleStartMinimumMembers != 1 || config.StartDelaySeconds != 3 ||
// [DISCONN MEMBER WAIT TIME] is the drop grace the raid runtime reads
// (dnfbridge runtimeRaidDisconnectGraceForState). Pinned here because the
// runtime used to carry the same 300 as a bare constant: if this ever
// stops being 300, the grace silently changes with it.
config.DisconnMemberWaitTime != 300 ||
config.PhaseBreakSeconds != 600 || config.EntryCost != (AntonEntryCost{ItemID: 10096296, Count: 1}) ||
!reflect.DeepEqual(config.PhaseTimeoutSeconds, []int{2400, 2400}) ||
!reflect.DeepEqual(config.MovieEndSeconds, []int{20, 20, 20, 20}) ||
config.WeeklyEnterCount != [2]int{30, 3} {
t.Fatalf("real global config=%+v", config)
}
// Both ordinary phases and the single-mode phase share one rank table, and
// no Anton phase declares a revive-coin cap.
wantRank := []AntonRankCondition{
{MinimumDeaths: 0, MaximumDeaths: 1, Rank: 0},
{MinimumDeaths: 2, MaximumDeaths: 5, Rank: 1},
{MinimumDeaths: 6, MaximumDeaths: 9, Rank: 2},
{MinimumDeaths: 10, MaximumDeaths: 14, Rank: 3},
}
wantPhases := []AntonPhase{
{
Name: "\u963b\u622a\u5b89\u5f92\u6069", MinimalDungeonClearCount: 1,
Rewards: []AntonStateReward{
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10094735, Flag: 0},
{Kind: "pcroom_card", Rank: -1, Weight: 100, DefinitionItemID: 10094733, Flag: 0},
{Kind: "squad_item", Rank: -1, Weight: 50, DefinitionItemID: 10157773, Flag: 0},
{Kind: "squad_item", Rank: -1, Weight: 1, DefinitionItemID: 10157775, Flag: 1},
{Kind: "squad_item", Rank: -1, Weight: 1, DefinitionItemID: 10157776, Flag: 1},
{Kind: "squad_item", Rank: -1, Weight: 48, DefinitionItemID: 10157777, Flag: 1},
},
Dungeons: []AntonDungeonInfo{
{Index: 210, InitState: AntonDungeonOpen, HP: 4, PartyMax: 5},
{Index: 211, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
{Index: 212, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
{Index: 213, InitState: AntonDungeonHide, HP: 2, PartyMax: 5},
{Index: 214, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
{Index: 215, InitState: AntonDungeonHide, HP: 2, PartyMax: 5},
{Index: 216, InitState: AntonDungeonHide, HP: 1, PartyMax: 1},
},
RankConditions: wantRank,
},
{
Name: "\u706d\u6740\u5b89\u5f92\u6069", MinimalDungeonClearCount: 1,
Rewards: []AntonStateReward{
{Kind: "party_card", Rank: -1, Weight: 100, DefinitionItemID: 10157778, Flag: 0},
{Kind: "pcroom_card", Rank: -1, Weight: 100, DefinitionItemID: 10094733, Flag: 0},
{Kind: "squad_item", Rank: -1, Weight: 775, DefinitionItemID: 10161936, Flag: 0},
{Kind: "squad_item", Rank: -1, Weight: 145, DefinitionItemID: 10157783, Flag: 1},
{Kind: "squad_item", Rank: -1, Weight: 50, DefinitionItemID: 10157784, Flag: 1},
{Kind: "squad_item", Rank: -1, Weight: 25, DefinitionItemID: 10157785, Flag: 1},
{Kind: "squad_item", Rank: -1, Weight: 5, DefinitionItemID: 10157786, Flag: 1},
},
Dungeons: []AntonDungeonInfo{
{Index: 218, InitState: AntonDungeonOpen, HP: 1, PartyMax: 5},
{Index: 219, InitState: AntonDungeonOpen, HP: 1, PartyMax: 5},
{Index: 220, InitState: AntonDungeonHide, HP: 5, PartyMax: 5},
{Index: 221, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
{Index: 222, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
{Index: 223, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
{Index: 224, InitState: AntonDungeonHide, HP: 1, PartyMax: 5},
},
RankConditions: wantRank,
},
}
if !reflect.DeepEqual(config.Phases, wantPhases) {
t.Fatalf("real phases=%+v\nwant=%+v", config.Phases, wantPhases)
}
}
func TestRealScriptPVFAntonConfigExposesNativeModeChoices(t *testing.T) {
pvfPath := os.Getenv("DNF_ANTON_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNF_ANTON_REAL_PVF_SMOKE to verify native Anton mode choices")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
config, err := LoadAntonConfig(archive)
if err != nil {
t.Fatal(err)
}
if config.SingleMode == nil || config.SingleMode.Name != "\u5355\u4eba\u5b89\u5f92\u6069" || config.SingleMode.TimeLimitSeconds != 2100 {
t.Fatalf("single mode=%+v", config.SingleMode)
}
choices := config.ModeChoices()
if len(choices) != 2 {
t.Fatalf("mode choices=%+v want two native modes", choices)
}
if choices[0].Kind != AntonRaidModeTeam || choices[0].StartMinimumMembers != 10 ||
!reflect.DeepEqual(choices[0].PhaseNames, []string{"\u963b\u622a\u5b89\u5f92\u6069", "\u706d\u6740\u5b89\u5f92\u6069"}) ||
!reflect.DeepEqual(choices[0].TimeLimitSeconds, []int{2400, 2400}) {
t.Fatalf("team choice=%+v", choices[0])
}
if choices[1].Kind != AntonRaidModeSingle || choices[1].StartMinimumMembers != 1 ||
!reflect.DeepEqual(choices[1].PhaseNames, []string{config.SingleMode.Name}) ||
!reflect.DeepEqual(choices[1].TimeLimitSeconds, []int{2100}) {
t.Fatalf("single choice=%+v", choices[1])
}
}
type antonRecordingSource struct {
text string
err error
paths []string
}
func (source *antonRecordingSource) ReadText(path string) (string, error) {
source.paths = append(source.paths, path)
if path != AntonPVFPath {
return "", fmt.Errorf("unexpected path %q", path)
}
if source.err != nil {
return "", source.err
}
return source.text, nil
}
// antonTestRankConditions mirrors the [CONDITION FOR RANK] block every phase of
// antonTestDocument declares.
func antonTestRankConditions() []AntonRankCondition {
return []AntonRankCondition{
{MinimumDeaths: 0, MaximumDeaths: 1, Rank: 0},
{MinimumDeaths: 2, MaximumDeaths: 5, Rank: 1},
}
}
func antonTestDocument() string {
return `[PHASE MAX]
2
[RAID MEMBER MAX]
20
[RAID START MINIMUM MEMBER]
10
[SINGLE RAID START MINIUM MEMBER]
1
[START DELAY TIME]
3
[PHASE TIME OVER]
2400 2400
[PHASE BREAK TIME]
600
[MOVIE END TIME]
20 21 22 23
[DISCONN MEMBER WAIT TIME]
300
[ENTRY COST]
10096296 1
[/ENTRY COST]
[WEEKLY ENTER COUNT]
30 3
[PHASE]
[PHASE NAME]
` + "`phase one`" + `
[CONDITION FOR RANK]
[DEAD COUNT]
0 1 0
[DEAD COUNT]
2 5 1
[/CONDITION FOR RANK]
[CLEAR REWARD ITEM]
[STATE REWARD]
` + "`party_card`" + ` -1 100 10094735 0
[STATE REWARD]
` + "`squad_item`" + ` -1 50 10157773 1
[/CLEAR REWARD ITEM]
[MINIMAL DUNGEON CLEAR COUNT]
1
[DUNGEON INFO]
[INDEX]
210
[INIT STATE]
` + "`open`" + `
[HP INFO]
[210 DGN HP]
4
[PARTY MAX]
5
[/DUNGEON INFO]
[DUNGEON INFO]
[INDEX]
211
[INIT STATE]
` + "`hide`" + `
[HP INFO]
[211 DGN HP]
1
[PARTY MAX]
5
[/DUNGEON INFO]
[/PHASE]
[PHASE]
[PHASE NAME]
` + "`phase two`" + `
[CONDITION FOR RANK]
[DEAD COUNT]
0 1 0
[DEAD COUNT]
2 5 1
[/CONDITION FOR RANK]
[CLEAR REWARD ITEM]
[STATE REWARD]
` + "`party_card`" + ` -1 100 10157778 0
[STATE REWARD]
` + "`squad_item`" + ` -1 775 10161936 1
[/CLEAR REWARD ITEM]
[MINIMAL DUNGEON CLEAR COUNT]
1
[DUNGEON INFO]
[INDEX]
218
[INIT STATE]
` + "`open`" + `
[HP INFO]
[218 DGN HP]
1
[PARTY MAX]
5
[/DUNGEON INFO]
[DUNGEON INFO]
[INDEX]
220
[INIT STATE]
` + "`hide`" + `
[HP INFO]
[220 DGN HP]
5
[PARTY MAX]
5
[/DUNGEON INFO]
[/PHASE]
[SINGLE RAID TIME OVER]
2100
[SINGLE RAID PHASE]
[PHASE NAME]
` + "`single phase`" + `
[CONDITION FOR RANK]
[DEAD COUNT]
0 1 0
[DEAD COUNT]
2 5 1
[/CONDITION FOR RANK]
[CLEAR REWARD ITEM]
[STATE REWARD]
` + "`party_card`" + ` -1 100 10094735 0
[/CLEAR REWARD ITEM]
[DUNGEON INFO]
[INDEX]
9999
[INIT STATE]
` + "`open`" + `
[HP INFO]
[9999 DGN HP]
1
[PARTY MAX]
0
[/DUNGEON INFO]
[/SINGLE RAID PHASE]
`
}