服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
818 lines
33 KiB
Go
818 lines
33 KiB
Go
package quest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
)
|
|
|
|
type catalogTestSource map[string]string
|
|
|
|
func (s catalogTestSource) ReadText(relativePath string) (string, error) {
|
|
value, ok := s[relativePath]
|
|
if !ok {
|
|
return "", errors.New("missing test PVF path: " + relativePath)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func TestCatalogAcceptableUsesPVFCharacterAndQuestState(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "1 `main.qst`\n2 `next.qst`\n3 `active.qst`\n4 `event.qst`\n5 `other_job.qst`\n6 `answer.qst`\n7 `collision.qst`\n",
|
|
"n_quest/main.qst": questCatalogTestDefinition("[epic]", 1, 10, "[gunner]", ""),
|
|
"n_quest/next.qst": questCatalogTestDefinition("[epic]", 2, 10, "[gunner]", "[pre required quest]\n1\n"),
|
|
"n_quest/active.qst": questCatalogTestDefinition("[normal]", 1, 10, "[gunner]", ""),
|
|
"n_quest/event.qst": questCatalogTestDefinition("[epic]", 1, 10, "[gunner]", "[event]\n1\n"),
|
|
"n_quest/other_job.qst": questCatalogTestDefinition("[epic]", 1, 10, "[fighter]", ""),
|
|
"n_quest/answer.qst": questCatalogTestDefinition("[epic]", 1, 10, "[gunner]", "[pre required quest answer]\n1\n"),
|
|
"n_quest/collision.qst": questCatalogTestDefinition("[epic]", 1, 10, "[gunner]", "[collision quest]\n1\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
record := dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
3: {Status: "active"},
|
|
}}
|
|
fresh := catalog.Acceptable(CharacterEligibility{Level: 1, Job: 2, GrowType: 0}, record)
|
|
if !reflect.DeepEqual(fresh.IDs, []int32{1, 7}) || fresh.EpicCount != 2 {
|
|
t.Fatalf("fresh acceptable=%v epic=%d, want [1 7] epic=2", fresh.IDs, fresh.EpicCount)
|
|
}
|
|
listed := catalog.QuestList(CharacterEligibility{Level: 1, Job: 2, GrowType: 0}, record)
|
|
if !reflect.DeepEqual(listed.IDs, []int32{1, 3, 7}) || listed.EpicCount != 2 || listed.ActiveCount != 1 {
|
|
t.Fatalf("fresh quest list=%v epic=%d active=%d, want [1 3 7] epic=2 active=1", listed.IDs, listed.EpicCount, listed.ActiveCount)
|
|
}
|
|
|
|
record.States[1] = dnfrepo.QuestState{Status: "completed"}
|
|
advanced := catalog.Acceptable(CharacterEligibility{Level: 2, Job: 2, GrowType: 0}, record)
|
|
if !reflect.DeepEqual(advanced.IDs, []int32{2}) || advanced.EpicCount != 1 {
|
|
t.Fatalf("advanced acceptable=%v epic=%d, want [2] epic=1", advanced.IDs, advanced.EpicCount)
|
|
}
|
|
advancedList := catalog.QuestList(CharacterEligibility{Level: 2, Job: 2, GrowType: 0}, record)
|
|
if !reflect.DeepEqual(advancedList.IDs, []int32{2, 3}) || advancedList.EpicCount != 1 || advancedList.ActiveCount != 1 {
|
|
t.Fatalf("advanced quest list=%v epic=%d active=%d, want [2 3] epic=1 active=1", advancedList.IDs, advancedList.EpicCount, advancedList.ActiveCount)
|
|
}
|
|
}
|
|
|
|
func TestCatalogSeekingDungeonScopesInheritMatchingPVFSibling(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "8557 `event.qst`\n13702 `other.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/other.qst": "[name]\n`进攻准备`\n[type]\n`[seeking]`\n[int data]\n10094699 10\n[dungeon info]\n9999 -1\n[/dungeon info]\n",
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{
|
|
Lists: []string{DefaultList}, Paths: []string{"n_quest/regular.qst"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := catalog.SeekingItemTargets(8557); !reflect.DeepEqual(got, []SeekingItemTarget{{ItemID: 10094602, Count: 10}}) {
|
|
t.Fatalf("event seeking targets=%+v", got)
|
|
}
|
|
want := []DungeonScope{{DungeonID: 3200, Difficulty: -1}}
|
|
if got := catalog.SeekingDungeonScopes(8557); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("event inherited scopes=%+v want=%+v", got, want)
|
|
}
|
|
if snapshot := catalog.Snapshot(); snapshot.ScopeHints != 1 {
|
|
t.Fatalf("scope hints=%d want=1", snapshot.ScopeHints)
|
|
}
|
|
}
|
|
|
|
func TestCatalogAcceptableMissionUsesRaidPhaseClearCounts(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "12829 `anton_enter.qst`\n12830 `anton_mission.qst`\n",
|
|
"n_quest/anton_enter.qst": questCatalogTestDefinition("[side]", 85, 99, "[all]", ""),
|
|
"n_quest/anton_mission.qst": questCatalogTestDefinition("[mission]", 85, 99, "[all]",
|
|
"[type]\n`[raid phase clear]`\n[pre required quest]\n12829\n[int data]\n0 0 5 -1 1 0 5 -1\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
character := CharacterEligibility{Level: 90, Job: 2}
|
|
if got := catalog.Acceptable(character, dnfrepo.QuestRecord{}).IDs; !reflect.DeepEqual(got, []int32{12829}) {
|
|
t.Fatalf("before prerequisite acceptable=%v want [12829]", got)
|
|
}
|
|
record := dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{12829: {Status: "completed"}}}
|
|
if got := catalog.Acceptable(character, record).IDs; !reflect.DeepEqual(got, []int32{12830}) {
|
|
t.Fatalf("after prerequisite acceptable=%v want [12830]", got)
|
|
}
|
|
plan, err := catalog.PlanAccept(character, record, 12830)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := packTriggerChannels(5, 5); plan.InitTrigger != want {
|
|
t.Fatalf("raid mission init trigger=%#x want=%#x", plan.InitTrigger, want)
|
|
}
|
|
}
|
|
|
|
func TestExpertJobTransitionQuestDirectoryPreservesChainAndTerminals(t *testing.T) {
|
|
catalog := &Catalog{ordered: []Definition{
|
|
{ID: 2702, JobChangeQuest: 20},
|
|
{ID: 10020, JobChangeQuest: 20, PreRequiredGroups: [][]int64{{2702}}},
|
|
{ID: 10021, JobChangeQuest: 20, PreRequiredGroups: [][]int64{{10020}}},
|
|
{ID: 9000, JobChangeQuest: 2},
|
|
}}
|
|
if got := catalog.ExpertJobTransitionQuestIDs(); !reflect.DeepEqual(got, []int64{2702, 10020, 10021}) {
|
|
t.Fatalf("transition quests=%v", got)
|
|
}
|
|
if got := catalog.ExpertJobTransitionTerminalQuestIDs(); !reflect.DeepEqual(got, []int64{10021}) {
|
|
t.Fatalf("terminal transition quests=%v", got)
|
|
}
|
|
}
|
|
|
|
func TestCatalogActiveMeetingNPCTriggersUsesPVFTargetAndCompoundOrder(t *testing.T) {
|
|
catalog := &Catalog{byID: map[int64]Definition{
|
|
100: {ID: 100, Type: "[meet npc]", IntData: []int64{347}},
|
|
101: {ID: 101, Type: "[seek n meet npc]", IntData: []int64{10158964, 5, 347}},
|
|
102: {ID: 102, Type: "[meet npc]", IntData: []int64{348}},
|
|
103: {ID: 103, Type: "[meet secret npc]", IntData: []int64{347}},
|
|
}}
|
|
record := dnfrepo.QuestRecord{
|
|
States: map[int64]dnfrepo.QuestState{
|
|
101: {Status: "active", ProgressValue: 517},
|
|
102: {Status: "active", ProgressValue: 1},
|
|
103: {Status: "completed"},
|
|
},
|
|
Progress: map[int64]dnfrepo.QuestState{
|
|
100: {Status: "in_progress", ProgressValue: 1},
|
|
101: {Status: "active", ProgressValue: 517},
|
|
},
|
|
}
|
|
want := []MeetingNPCTrigger{
|
|
{QuestID: 100, TriggerType: 0},
|
|
{QuestID: 101, TriggerType: 0x10},
|
|
{QuestID: 101, TriggerType: 0x20},
|
|
}
|
|
if got := catalog.ActiveMeetingNPCTriggers(record, 347); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("meeting triggers=%+v want=%+v", got, want)
|
|
}
|
|
if got := catalog.ActiveMeetingNPCTriggers(record, 999); len(got) != 0 {
|
|
t.Fatalf("unknown NPC triggers=%+v", got)
|
|
}
|
|
record.States[101] = dnfrepo.QuestState{Status: "active", ProgressValue: 5}
|
|
if got := catalog.ActiveMeetingNPCTriggers(record, 347); !reflect.DeepEqual(got, []MeetingNPCTrigger{
|
|
{QuestID: 100, TriggerType: 0},
|
|
{QuestID: 101, TriggerType: 0x10},
|
|
}) {
|
|
t.Fatalf("already-met compound triggers=%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestCatalogAcceptableAppliesGrowType(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "10 `grow.qst`\n11 `job_change.qst`\n12 `second_awaken.qst`\n",
|
|
"n_quest/grow.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]", "[grow type]\n2\n"),
|
|
"n_quest/job_change.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]", "[grow type]\n2\n[job change quest]\n2\n"),
|
|
"n_quest/second_awaken.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]", "[grow type]\n2\n[job change quest]\n3\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := catalog.Acceptable(CharacterEligibility{Level: 20, Job: 2, GrowType: 0x12}, dnfrepo.QuestRecord{})
|
|
if !reflect.DeepEqual(got.IDs, []int32{11, 12}) {
|
|
t.Fatalf("grow acceptable=%v, want [11 12]", got.IDs)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanAcceptUsesCSharpInitialTriggerRulesAndProjectsEventItems(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "3145 `clear_map.qst`\n3146 `event_item.qst`\n3147 `seek_meet.qst`\n3148 `quest_clear.qst`\n3149 `fallback.qst`\n",
|
|
"n_quest/clear_map.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
|
"[type]\n`[clear map]`\n[int data]\n76126\n"),
|
|
"n_quest/event_item.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
|
"[type]\n`[clear map]`\n[int data]\n76126\n[depend give item]\n1001 1\n"),
|
|
"n_quest/seek_meet.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
|
"[type]\n`[seek n meet npc]`\n[int data]\n1001 1 1\n"),
|
|
"n_quest/quest_clear.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
|
"[type]\n`[quest clear]`\n[int data]\n1 2 3\n"),
|
|
"n_quest/fallback.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
|
"[type]\n`[custom client only type]`\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
character := CharacterEligibility{Level: 1, Job: 2, GrowType: 0}
|
|
plan, err := catalog.PlanAccept(character, dnfrepo.QuestRecord{}, 3145)
|
|
if err != nil {
|
|
t.Fatalf("PlanAccept clear-map error = %v", err)
|
|
}
|
|
if plan.QuestID != 3145 || normalizeQuestTag(plan.Type) != "clear map" || plan.InitTrigger != 1 {
|
|
t.Fatalf("clear-map plan = %+v", plan)
|
|
}
|
|
eventPlan, err := catalog.PlanAccept(character, dnfrepo.QuestRecord{}, 3146)
|
|
if err != nil || len(eventPlan.EventItems) != 1 || eventPlan.EventItems[0].ItemID != 1001 || eventPlan.EventItems[0].Count != 1 {
|
|
t.Fatalf("event-item PlanAccept = %+v, %v", eventPlan, err)
|
|
}
|
|
seekPlan, err := catalog.PlanAccept(character, dnfrepo.QuestRecord{}, 3147)
|
|
if err != nil || seekPlan.InitTrigger != 1|(1<<9) {
|
|
t.Fatalf("seek-and-meet PlanAccept = %+v, %v", seekPlan, err)
|
|
}
|
|
questClearPlan, err := catalog.PlanAccept(character, dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
1: {Status: "completed"},
|
|
}}, 3148)
|
|
if err != nil || questClearPlan.InitTrigger != 2 {
|
|
t.Fatalf("quest-clear PlanAccept = %+v, %v", questClearPlan, err)
|
|
}
|
|
fallbackPlan, err := catalog.PlanAccept(character, dnfrepo.QuestRecord{}, 3149)
|
|
if err != nil || fallbackPlan.InitTrigger != 1 {
|
|
t.Fatalf("fallback PlanAccept = %+v, %v", fallbackPlan, err)
|
|
}
|
|
active := dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{3145: {Status: "active"}}}
|
|
if _, err := catalog.PlanAccept(character, active, 3145); !errors.Is(err, ErrQuestNotAcceptable) {
|
|
t.Fatalf("active PlanAccept error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanAcceptLinksNoRewardMainSubQuests(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "3145 `pre.qst`\n3146 `parent.qst`\n3157 `hunt.qst`\n3054 `clear.qst`\n",
|
|
"n_quest/pre.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
|
"[type]\n`[clear map]`\n[int data]\n76126\n"),
|
|
"n_quest/parent.qst": questCatalogTestDefinition("[epic]", 3, 99, "[gunner]",
|
|
"[type]\n`[quest clear]`\n[pre required quest]\n3145\n[int data]\n3157 3054\n"),
|
|
"n_quest/hunt.qst": questCatalogTestDefinition("[sub]", 3, 99, "[gunner]",
|
|
"[type]\n`[hunt enemy]`\n[main quest]\n3146\n[int data]\n3 -1 13099 3 1\n"),
|
|
"n_quest/clear.qst": questCatalogTestDefinition("[sub]", 1, 99, "[gunner]",
|
|
"[type]\n`[clear map]`\n[main quest]\n3146\n[int data]\n76136\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if definition, ok := catalog.Find(3157); !ok || definition.MainQuestID != 3146 {
|
|
t.Fatalf("main quest parse = %+v ok=%t, want 3146", definition, ok)
|
|
}
|
|
plan, err := catalog.PlanAccept(CharacterEligibility{Level: 3, Job: 2, GrowType: 0}, dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
3145: {Status: "completed"},
|
|
}}, 3146)
|
|
if err != nil {
|
|
t.Fatalf("PlanAccept parent error = %v", err)
|
|
}
|
|
if plan.InitTrigger != 2 || len(plan.LinkedSubQuests) != 2 {
|
|
t.Fatalf("parent plan = %+v, want trigger 2 with two linked subtasks", plan)
|
|
}
|
|
if plan.LinkedSubQuests[0].QuestID != 3157 || plan.LinkedSubQuests[0].InitTrigger != 1 ||
|
|
plan.LinkedSubQuests[1].QuestID != 3054 || plan.LinkedSubQuests[1].InitTrigger != 1 {
|
|
t.Fatalf("linked subtasks = %+v", plan.LinkedSubQuests)
|
|
}
|
|
|
|
withoutParent := catalog.QuestList(CharacterEligibility{Level: 3, Job: 2, GrowType: 0}, dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
3145: {Status: "completed"},
|
|
}})
|
|
if reflect.DeepEqual(withoutParent.IDs, []int32{3146, 3157, 3054}) || containsCatalogQuestID(withoutParent.IDs, 3157) || containsCatalogQuestID(withoutParent.IDs, 3054) {
|
|
t.Fatalf("unowned linked subtasks leaked into quest list: %v", withoutParent.IDs)
|
|
}
|
|
withParent := catalog.QuestList(CharacterEligibility{Level: 3, Job: 2, GrowType: 0}, dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
3145: {Status: "completed"},
|
|
3146: {Status: "active"},
|
|
3157: {Status: "active"},
|
|
3054: {Status: "active"},
|
|
}})
|
|
if !containsCatalogQuestID(withParent.IDs, 3157) || !containsCatalogQuestID(withParent.IDs, 3054) {
|
|
t.Fatalf("active parent's linked subtasks missing from quest list: %v", withParent.IDs)
|
|
}
|
|
}
|
|
|
|
func TestTriggerFromIntDataPacksThreeNineBitChannels(t *testing.T) {
|
|
values := []int64{10, 0, 0, 5, 20, 0, 0, 3, 30, 0, 0, 2}
|
|
if got, want := triggerFromIntData(values, 4), uint32(5|(3<<9)|(2<<18)); got != want {
|
|
t.Fatalf("trigger = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardFiltersPVFJobAndSelection(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "3145 `finish.qst`\n",
|
|
"n_quest/finish.qst": questCatalogTestDefinition("[epic]", 1, 99, "[all]", `
|
|
[difficulty]
|
|
`+"`1`"+`
|
|
[reward type]
|
|
`+"`[item]`"+`
|
|
[reward int data]
|
|
10165055 1 104010230 `+"`[job]`"+` 2 -1 1 999 `+"`[job]`"+` 1 -1 1
|
|
[reward selection int data]
|
|
2001 `+"`[job]`"+` 2 -1 1 2002 `+"`[job]`"+` 2 -1 1
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := catalog.PlanFinishReward(CharacterEligibility{Level: 1, Job: 2, GrowType: 0}, 3145, 1, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if plan.Difficulty != '1' || !plan.RewardSelectionUsed || len(plan.Items) != 3 ||
|
|
plan.Items[0].ItemID != 10165055 || plan.Items[1].ItemID != 104010230 || plan.Items[2].ItemID != 2002 {
|
|
t.Fatalf("finish reward plan = %+v", plan)
|
|
}
|
|
if _, err := catalog.PlanFinishReward(CharacterEligibility{Level: 1, Job: 2, GrowType: 0}, 3145, 2, true); !errors.Is(err, ErrQuestRewardSelectionInvalid) {
|
|
t.Fatalf("out-of-range selection error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardPreservesGoldMarkerOutOfItemSlots(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "3145 `finish.qst`\n",
|
|
"n_quest/finish.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]", `
|
|
[difficulty]
|
|
`+"`N`"+`
|
|
[reward type]
|
|
`+"`[item]`"+`
|
|
[gold multiple]
|
|
150
|
|
[reward int data]
|
|
0 1 10403 2
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := catalog.PlanFinishReward(CharacterEligibility{Level: 1, Job: 2, GrowType: 0}, 3145, 0, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !plan.HasGoldReward || plan.GoldMultiple != 150 {
|
|
t.Fatalf("gold reward marker not preserved: %+v", plan)
|
|
}
|
|
if len(plan.Items) != 1 || plan.Items[0].ItemID != 10403 || plan.Items[0].Count != 2 {
|
|
t.Fatalf("gold marker leaked into item rewards: %+v", plan.Items)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardRejectsMalformedPVF(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "7 `bad.qst`\n",
|
|
"n_quest/bad.qst": questCatalogTestDefinition("[epic]", 1, 99, "[all]", `
|
|
[reward type]
|
|
`+"`[item]`"+`
|
|
[reward int data]
|
|
101 `+"`[job]`"+` 2
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := catalog.PlanFinishReward(CharacterEligibility{Level: 1, Job: 2}, 7, 0, false); !errors.Is(err, ErrQuestRewardMalformed) {
|
|
t.Fatalf("malformed reward error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardPlansPVFProfessionTransitions(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "100 `change.qst`\n101 `awaken.qst`\n102 `second.qst`\n",
|
|
"n_quest/change.qst": questCatalogTestDefinition("[epic]", 15, 99, "[gunner]", `
|
|
[job change quest]
|
|
1
|
|
[reward type]
|
|
`+"`[grow type]`"+`
|
|
[reward int data]
|
|
2
|
|
`),
|
|
"n_quest/awaken.qst": questCatalogTestDefinition("[epic]", 50, 99, "[gunner]", `
|
|
[job change quest]
|
|
2
|
|
[grow type]
|
|
2
|
|
[reward type]
|
|
`+"`[awakening type]`"+`
|
|
[reward int data]
|
|
1
|
|
`),
|
|
"n_quest/second.qst": questCatalogTestDefinition("[epic]", 75, 99, "[gunner]", `
|
|
[job change quest]
|
|
3
|
|
[grow type]
|
|
2
|
|
[reward type]
|
|
`+"`[awakening type]`"+`
|
|
[reward int data]
|
|
2
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changed, err := catalog.PlanFinishReward(CharacterEligibility{Level: 15, Job: 2, GrowType: 0}, 100, 0, false)
|
|
if err != nil || !changed.HasProfession || changed.ProfessionRequest.GrowNumber != 2 || changed.ProfessionRequest.ChainType != 1 || len(changed.Items) != 0 {
|
|
t.Fatalf("change plan = %+v, %v", changed, err)
|
|
}
|
|
awakened, err := catalog.PlanFinishReward(CharacterEligibility{Level: 50, Job: 2, GrowType: 0x02}, 101, 0, false)
|
|
if err != nil || awakened.ProfessionRequest.GrowNumber != 1 || awakened.ProfessionRequest.ChainType != 2 {
|
|
t.Fatalf("awakening plan = %+v, %v", awakened, err)
|
|
}
|
|
second, err := catalog.PlanFinishReward(CharacterEligibility{Level: 75, Job: 2, GrowType: 0x12}, 102, 0, false)
|
|
if err != nil || second.ProfessionRequest.GrowNumber != 2 || second.ProfessionRequest.JobChangeQuest != 3 {
|
|
t.Fatalf("second awakening plan = %+v, %v", second, err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardPlansExpertJobChain20(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "2710 `expert.qst`\n",
|
|
"n_quest/expert.qst": questCatalogTestDefinition("[common unique]", 20, 99, "[all]", `
|
|
[job change quest]
|
|
20
|
|
[reward type]
|
|
`+"`[expert job]`"+`
|
|
[reward int data]
|
|
3
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := catalog.PlanFinishReward(CharacterEligibility{Level: 20, Job: 2}, 2710, 0, false)
|
|
if err != nil || !plan.HasExpertJob || plan.ExpertJobType != 3 || plan.HasProfession || len(plan.Items) != 0 {
|
|
t.Fatalf("expert-job plan = %+v, %v", plan, err)
|
|
}
|
|
if _, err := catalog.PlanFinishReward(CharacterEligibility{Level: 20, Job: 2}, 2710, 0, true); !errors.Is(err, ErrQuestRewardSelectionInvalid) {
|
|
t.Fatalf("expert-job selectable plan error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardDerivesSlotExpansionFromRuntimePVFIndex(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "649 `support.qst`\n650 `magic.qst`\n2636 `earring.qst`\n",
|
|
"n_quest/support.qst": questCatalogTestDefinition("[common unique]", 1, 99, "[all]", `
|
|
[reward type]
|
|
`+"`[slot expansion]`"+`
|
|
[reward int data]
|
|
0
|
|
`),
|
|
"n_quest/magic.qst": questCatalogTestDefinition("[common unique]", 1, 99, "[all]", `
|
|
[reward type]
|
|
`+"`[slot expansion]`"+`
|
|
[reward int data]
|
|
1
|
|
`),
|
|
"n_quest/earring.qst": questCatalogTestDefinition("[common unique]", 1, 99, "[all]", `
|
|
[reward type]
|
|
`+"`[slot expansion]`"+`
|
|
[reward int data]
|
|
2
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct {
|
|
questID int64
|
|
index uint32
|
|
bit byte
|
|
}{
|
|
{questID: 649, index: 0, bit: 0x01},
|
|
{questID: 650, index: 1, bit: 0x02},
|
|
{questID: 2636, index: 2, bit: 0x10},
|
|
} {
|
|
plan, planErr := catalog.PlanFinishReward(CharacterEligibility{Level: 90, Job: 2}, test.questID, 0, false)
|
|
if planErr != nil {
|
|
t.Fatalf("quest %d plan: %v", test.questID, planErr)
|
|
}
|
|
if !plan.HasSlotExpansion || plan.SlotExpansionIndex != test.index || plan.SlotExpansionBit != test.bit {
|
|
t.Fatalf("quest %d slot expansion plan=%+v", test.questID, plan)
|
|
}
|
|
}
|
|
if ExEquipSlotAll != 0x13 {
|
|
t.Fatalf("current NoPack extra-equipment-slot mask=%#x want=0x13", ExEquipSlotAll)
|
|
}
|
|
|
|
for _, values := range [][]int64{nil, {}, {0, 1}, {-1}, {3}} {
|
|
definition := Definition{
|
|
ID: 9000, Path: "n_quest/bad_slot.qst", LevelMin: 1, LevelMax: 99,
|
|
Difficulty: "N", RewardType: "[slot expansion]", RewardIntData: values,
|
|
}
|
|
badCatalog := &Catalog{ordered: []Definition{definition}, byID: map[int64]Definition{definition.ID: definition}}
|
|
if _, planErr := badCatalog.PlanFinishReward(CharacterEligibility{Level: 90, Job: 2}, definition.ID, 0, false); !errors.Is(planErr, ErrQuestRewardMalformed) {
|
|
t.Fatalf("slot expansion values=%v error=%v", values, planErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardAcceptsPVFLegitimateEmptyReward(t *testing.T) {
|
|
definition := Definition{
|
|
ID: 943, Path: "n_quest/job2_mid.qst", LevelMin: 1, LevelMax: 99,
|
|
Difficulty: "N", JobChangeQuest: 2, RewardIntData: []int64{0, 0},
|
|
}
|
|
catalog := &Catalog{ordered: []Definition{definition}, byID: map[int64]Definition{definition.ID: definition}}
|
|
plan, err := catalog.PlanFinishReward(CharacterEligibility{Level: 50, Job: 2, GrowType: 0x01}, definition.ID, 0, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if plan.HasProfession || len(plan.Items) != 0 || plan.QuestID != definition.ID {
|
|
t.Fatalf("empty PVF reward plan = %+v", plan)
|
|
}
|
|
if _, err := catalog.PlanFinishReward(CharacterEligibility{Level: 50, Job: 2, GrowType: 0x01}, definition.ID, 0, true); !errors.Is(err, ErrQuestRewardSelectionInvalid) {
|
|
t.Fatalf("empty reward selection error = %v", err)
|
|
}
|
|
malformed := definition
|
|
malformed.ID = 944
|
|
malformed.RewardIntData = []int64{0, 1}
|
|
catalog.byID[malformed.ID] = malformed
|
|
if _, err := catalog.PlanFinishReward(CharacterEligibility{Level: 50, Job: 2, GrowType: 0x01}, malformed.ID, 0, false); !errors.Is(err, ErrQuestRewardMalformed) {
|
|
t.Fatalf("nonzero empty-type reward error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardParsesHellChallengeStrictly(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
data []int64
|
|
want bool
|
|
}{
|
|
{name: "valid", data: []int64{1}, want: true},
|
|
{name: "zero", data: []int64{0}},
|
|
{name: "extra", data: []int64{1, 2}},
|
|
{name: "negative", data: []int64{-1}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
definition := Definition{ID: 2900, Path: "metrocenter/hell.qst", LevelMin: 1, LevelMax: 99, Difficulty: "N", RewardType: "[hell challenge]", RewardIntData: test.data}
|
|
catalog := &Catalog{ordered: []Definition{definition}, byID: map[int64]Definition{definition.ID: definition}}
|
|
plan, err := catalog.PlanFinishReward(CharacterEligibility{Level: 90, Job: 2}, definition.ID, 0, false)
|
|
if test.want {
|
|
if err != nil || !plan.HasHellPartyUnlock {
|
|
t.Fatalf("valid hell challenge plan=%+v err=%v", plan, err)
|
|
}
|
|
return
|
|
}
|
|
if !errors.Is(err, ErrQuestRewardMalformed) {
|
|
t.Fatalf("hell challenge data=%v error=%v", test.data, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCatalogPlanFinishRewardParsesInfernoChallengeStrictly(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
data []int64
|
|
want int64
|
|
}{
|
|
{name: "valid", data: []int64{7527}, want: 7527},
|
|
{name: "zero", data: []int64{0}},
|
|
{name: "extra", data: []int64{7527, 1}},
|
|
{name: "negative", data: []int64{-1}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
definition := Definition{ID: 10104, Path: "Hell/achievement_70_Hell_inferno_2.qst", LevelMin: 77, LevelMax: 99, Difficulty: "G", RewardType: "[inferno challenge]", RewardIntData: test.data}
|
|
catalog := &Catalog{ordered: []Definition{definition}, byID: map[int64]Definition{definition.ID: definition}}
|
|
plan, err := catalog.PlanFinishReward(CharacterEligibility{Level: 90, Job: 2}, definition.ID, 0, false)
|
|
if test.want > 0 {
|
|
if err != nil || plan.InfernoChallengeDungeonID != test.want {
|
|
t.Fatalf("valid inferno challenge plan=%+v err=%v", plan, err)
|
|
}
|
|
return
|
|
}
|
|
if !errors.Is(err, ErrQuestRewardMalformed) {
|
|
t.Fatalf("inferno challenge data=%v error=%v", test.data, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCatalogAcceptablePreservesTargetCharacterGrowAndAwakeningTuple(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "2680 `branch_zero_first.qst`\n5135 `branch_zero_second.qst`\n",
|
|
"n_quest/branch_zero_first.qst": questCatalogTestDefinition("[realization]", 50, 99, "[all]", `
|
|
[target character]
|
|
`+"`[demonic swordman]`"+` 0 0 `+"`[creator mage]`"+` 0 0
|
|
[job change quest]
|
|
2
|
|
`),
|
|
"n_quest/branch_zero_second.qst": questCatalogTestDefinition("[realization]", 75, 99, "[all]", `
|
|
[target character]
|
|
`+"`[demonic swordman]`"+` 0 1
|
|
[job change quest]
|
|
3
|
|
`),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := catalog.Acceptable(CharacterEligibility{Level: 50, Job: 9, GrowType: 0x00}, dnfrepo.QuestRecord{}).IDs; !reflect.DeepEqual(got, []int32{2680}) {
|
|
t.Fatalf("branch-zero first-awakening acceptable=%v", got)
|
|
}
|
|
if got := catalog.Acceptable(CharacterEligibility{Level: 75, Job: 9, GrowType: 0x10}, dnfrepo.QuestRecord{}).IDs; !reflect.DeepEqual(got, []int32{5135}) {
|
|
t.Fatalf("branch-zero second-awakening acceptable=%v", got)
|
|
}
|
|
if got := catalog.Acceptable(CharacterEligibility{Level: 75, Job: 2, GrowType: 0x10}, dnfrepo.QuestRecord{}).IDs; len(got) != 0 {
|
|
t.Fatalf("target tuple leaked to ordinary job: %v", got)
|
|
}
|
|
}
|
|
|
|
func questCatalogTestDefinition(grade string, levelMin, levelMax int, job, extra string) string {
|
|
return "[grade]\n`" + grade + "`\n[level]\n" + integerText(levelMin) + " " + integerText(levelMax) + "\n[job]\n`" + job + "`\n[exposed by npc]\n1\n" + extra
|
|
}
|
|
|
|
func integerText(value int) string {
|
|
if value == 0 {
|
|
return "0"
|
|
}
|
|
digits := make([]byte, 0, 8)
|
|
for value > 0 {
|
|
digits = append(digits, byte('0'+value%10))
|
|
value /= 10
|
|
}
|
|
for left, right := 0, len(digits)-1; left < right; left, right = left+1, right-1 {
|
|
digits[left], digits[right] = digits[right], digits[left]
|
|
}
|
|
return string(digits)
|
|
}
|
|
|
|
func TestCatalogSuccessorsFollowsPrerequisiteGroupsInOrder(t *testing.T) {
|
|
source := catalogTestSource{
|
|
DefaultList: "1 `first.qst`\n2 `second.qst`\n3 `branch.qst`\n4 `unrelated.qst`\n",
|
|
"n_quest/first.qst": questCatalogTestDefinition("[epic]", 1, 99, "[all]", ""),
|
|
"n_quest/second.qst": questCatalogTestDefinition("[epic]", 1, 99, "[all]",
|
|
"[pre required quest]\n1\n"),
|
|
"n_quest/branch.qst": questCatalogTestDefinition("[normal]", 1, 99, "[all]",
|
|
"[pre required quest]\n1 9\n"),
|
|
"n_quest/unrelated.qst": questCatalogTestDefinition("[epic]", 1, 99, "[all]",
|
|
"[pre required quest]\n2\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
successors := catalog.Successors(1)
|
|
if len(successors) != 2 || successors[0].ID != 2 || successors[1].ID != 3 {
|
|
t.Fatalf("successors(1) = %+v, want [2 3] in catalog order", successors)
|
|
}
|
|
if none := catalog.Successors(99); len(none) != 0 {
|
|
t.Fatalf("successors(99) = %+v, want none", none)
|
|
}
|
|
if successors[0].PreRequiredGroups[0][0] != 1 {
|
|
t.Fatalf("successor clone shares prerequisite state: %+v", successors[0].PreRequiredGroups)
|
|
}
|
|
character := CharacterEligibility{Level: 1, Job: 2, GrowType: 0}
|
|
partial := catalog.EligibleSuccessors(1, character, dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
1: {Status: "completed"},
|
|
}})
|
|
if !reflect.DeepEqual(definitionIDs(partial), []int64{2}) {
|
|
t.Fatalf("eligible successors with partial prerequisites=%v, want [2]", definitionIDs(partial))
|
|
}
|
|
complete := catalog.EligibleSuccessors(1, character, dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
1: {Status: "completed"},
|
|
9: {Status: "completed"},
|
|
}})
|
|
if !reflect.DeepEqual(definitionIDs(complete), []int64{2, 3}) {
|
|
t.Fatalf("eligible successors with complete prerequisites=%v, want [2 3]", definitionIDs(complete))
|
|
}
|
|
}
|
|
|
|
func containsCatalogQuestID(ids []int32, want int32) bool {
|
|
for _, id := range ids {
|
|
if id == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestCatalogStoryAwakeningCompanionProjectsCanonicalFinish(t *testing.T) {
|
|
canonical := Definition{
|
|
ID: 3367, Grade: "[epic]", Type: "[clear map]", LevelMin: 50, LevelMax: 99,
|
|
RewardType: "[item]", RewardIntData: []int64{0, 0}, ExposedByNPC: -1,
|
|
TargetCharacterRules: []TargetCharacterRule{
|
|
{JobTag: "[swordman]", FirstGrowType: 4, AwakeningStage: 0},
|
|
{JobTag: "[swordman]", FirstGrowType: 4, AwakeningStage: 1},
|
|
}, CollisionQuests: []int64{3371}, IntData: []int64{95264},
|
|
}
|
|
variant := Definition{
|
|
ID: 3371, Grade: "[epic]", Type: "[clear map]", LevelMin: 50, LevelMax: 99,
|
|
Job: "[swordman]", GrowType: 4, HasGrowType: true, JobChangeQuest: 2,
|
|
RewardType: "[awakening type]", RewardIntData: []int64{1}, CollisionQuests: []int64{3367},
|
|
IntData: []int64{95264},
|
|
}
|
|
catalog := &Catalog{ordered: []Definition{canonical, variant}, byID: map[int64]Definition{3367: canonical, 3371: variant}, storyAwakeningByCanonical: map[int64][]int64{}, storyAwakeningCanonicalFor: map[int64]int64{}}
|
|
catalog.buildStoryAwakeningAliases()
|
|
character := CharacterEligibility{Level: 56, Job: 0, GrowType: 4}
|
|
if got := catalog.eligibility(character, dnfrepo.QuestRecord{}, false); !containsCatalogQuestID(got.IDs, 3367) || containsCatalogQuestID(got.IDs, 3371) {
|
|
t.Fatalf("story task projection=%v, want canonical only", got.IDs)
|
|
}
|
|
plan, err := catalog.PlanFinishReward(character, 3367, 0, false)
|
|
if err != nil || !plan.HasProfession || plan.ProfessionRequest.ChainType != 2 || plan.ProfessionRequest.GrowNumber != 1 {
|
|
t.Fatalf("canonical finish plan=%+v err=%v", plan, err)
|
|
}
|
|
if !catalog.FinishRewardNeedsProfessionResources(dnfrepo.QuestRecord{}, character, 3367, 0, false) {
|
|
t.Fatal("canonical story awakening did not require profession resources")
|
|
}
|
|
}
|
|
|
|
func TestCatalogFinishRewardDefinitionProjectsFinalRewardChild(t *testing.T) {
|
|
parent := Definition{ID: 5000, Grade: "[epic]", Type: "[quest clear]", SubType: 2, RewardType: "[item]", RewardIntData: []int64{0, 0}, IntData: []int64{5001, 5002}}
|
|
first := Definition{ID: 5001, Grade: "[sub]", Type: "[meet npc]", MainQuestID: 5000, NoExperience: true}
|
|
final := Definition{ID: 5002, Grade: "[sub]", Type: "[seek n meet npc]", MainQuestID: 5000, NoExperience: true, RewardType: "[item]", RewardIntData: []int64{123, 2}}
|
|
catalog := &Catalog{ordered: []Definition{parent, first, final}, byID: map[int64]Definition{5000: parent, 5001: first, 5002: final}}
|
|
record := dnfrepo.QuestRecord{States: map[int64]dnfrepo.QuestState{
|
|
5000: {Status: "active", ProgressValue: 0},
|
|
5001: {Status: "completed", ProgressValue: 0},
|
|
5002: {Status: "active", ProgressValue: 0},
|
|
}}
|
|
source, additional, ok := catalog.FinishRewardDefinition(record, CharacterEligibility{Level: 90, Job: 0}, 5000)
|
|
if !ok || source.ID != 5002 || !reflect.DeepEqual(additional, []int64{5002}) {
|
|
t.Fatalf("final child source=%+v additional=%v ok=%t", source, additional, ok)
|
|
}
|
|
}
|
|
|
|
func TestCatalogTitleAchievementDefinitionsStayOutsideOrdinaryQuestAcceptance(t *testing.T) {
|
|
definitions := []Definition{
|
|
{ID: 6501, Grade: "[title]", LevelMin: 10, LevelMax: 99},
|
|
{ID: 6502, Grade: "[title]", LevelMin: 10, LevelMax: 49},
|
|
{ID: 6503, Grade: "[title]", LevelMin: 10, LevelMax: 99, Job: "[fighter]"},
|
|
{ID: 1, Grade: "[epic]", LevelMin: 1, LevelMax: 99},
|
|
}
|
|
catalog := &Catalog{ordered: definitions, byID: map[int64]Definition{}}
|
|
for _, definition := range definitions {
|
|
catalog.byID[definition.ID] = definition
|
|
}
|
|
character := CharacterEligibility{Level: 50, Job: 0}
|
|
if got := definitionIDs(catalog.TitleAchievementDefinitions(character)); !reflect.DeepEqual(got, []int64{6501}) {
|
|
t.Fatalf("title achievement definitions=%v, want [6501]", got)
|
|
}
|
|
if got := catalog.QuestList(character, dnfrepo.QuestRecord{}).IDs; containsCatalogQuestID(got, 6501) {
|
|
t.Fatalf("title achievement leaked into ordinary acceptable quests: %v", got)
|
|
}
|
|
}
|
|
|
|
func definitionIDs(definitions []Definition) []int64 {
|
|
ids := make([]int64, 0, len(definitions))
|
|
for _, definition := range definitions {
|
|
ids = append(ids, definition.ID)
|
|
}
|
|
return ids
|
|
}
|