fix(server): separate story visuals from battle parties and preserve deck choices across quests
This commit is contained in:
@@ -15,13 +15,9 @@ type StoryCharacterCatalog struct {
|
||||
}
|
||||
|
||||
func LoadStoryCharacterCatalog(root, version string, packs []int, placeholderCostumes ...uint64) (*StoryCharacterCatalog, error) {
|
||||
var placeholder uint64
|
||||
if len(placeholderCostumes) > 1 {
|
||||
return nil, fmt.Errorf("gamedata: multiple story placeholders")
|
||||
}
|
||||
if len(placeholderCostumes) == 1 {
|
||||
placeholder = placeholderCostumes[0]
|
||||
}
|
||||
db, cleanup, err := openStatDatabase(root, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -85,12 +81,8 @@ func LoadStoryCharacterCatalog(root, version string, packs []int, placeholderCos
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, story := range formation.StoryCostumes {
|
||||
if story.UniqueCharacterID == unique && (placeholder == 0 || story.CostumeID != placeholder) {
|
||||
costume = story.CostumeID
|
||||
break
|
||||
}
|
||||
}
|
||||
// CharGroup supplies battle instances. StoryCharGroup supplies
|
||||
// cosmetic field actors and cannot override a battle costume.
|
||||
base, err := stats.BaseStats(row.CharacterID, row.Level)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,8 +7,13 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ResolveStoryParty combines the authored temporary CharGroup members with
|
||||
// actual account instances named by StoryCharGroup costume rows.
|
||||
// A normal battle deck has five character slots (CommonPacket and DeckSave).
|
||||
const storyBattlePartySize = 5
|
||||
|
||||
// ResolveStoryParty applies the authored temporary CharGroup members to the
|
||||
// saved battle party. StoryCharGroup costume designs only describe the field
|
||||
// story cast: FieldDeckPacket builds their cosmetic actors without inventory
|
||||
// identities, so they do not reserve battle slots, including placeholders.
|
||||
func (s *Service) ResolveStoryParty(packID, questID int) ([]player.Character, error) {
|
||||
if s.storyRoster == nil {
|
||||
return nil, nil
|
||||
@@ -26,6 +31,7 @@ func (s *Service) ResolveStoryParty(packID, questID int) ([]player.Character, er
|
||||
}
|
||||
temporary := make([]player.Character, 0, len(designs))
|
||||
reused := make([]player.Character, 0, len(designs))
|
||||
var authoredOrder []uint64
|
||||
for _, d := range designs {
|
||||
var existing player.Character
|
||||
for _, c := range s.characters.RawAll() {
|
||||
@@ -36,119 +42,87 @@ func (s *Service) ResolveStoryParty(packID, questID int) ([]player.Character, er
|
||||
}
|
||||
if existing.InvenIndex != 0 {
|
||||
reused = append(reused, existing)
|
||||
authoredOrder = append(authoredOrder, existing.InvenIndex)
|
||||
continue
|
||||
}
|
||||
if packID <= 0 || packID >= 65536 || d.CharacterID == 0 || d.CharacterID >= 1<<32 || d.Level == 0 || d.Level >= 256 {
|
||||
return nil, fmt.Errorf("world: story instance namespace overflow")
|
||||
}
|
||||
index := player.StoryCharacterIndexBase | uint64(packID)<<40 | d.CharacterID<<8 | d.Level
|
||||
authoredOrder = append(authoredOrder, index)
|
||||
temporary = append(temporary, player.Character{InvenIndex: index, ID: d.CharacterID, HP: d.HP, Level: d.Level, CostumeID: d.CostumeID, TalentLevel: d.InitialTalentLevel})
|
||||
}
|
||||
if s.characters == nil {
|
||||
return nil, fmt.Errorf("world: story character store unavailable")
|
||||
if len(designs) > storyBattlePartySize {
|
||||
return nil, fmt.Errorf("world: pack%d quest%d character group%d exceeds battle capacity: %d", packID, questID, formation.CharGroupID, len(designs))
|
||||
}
|
||||
if err := s.characters.EnsureStoryCharacters(temporary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]player.Character, 0, len(temporary)+len(formation.StoryCostumes))
|
||||
used := map[uint64]bool{}
|
||||
authored := make(map[uint64]player.Character, len(designs))
|
||||
for _, c := range reused {
|
||||
current, ok := s.characters.Find(c.InvenIndex)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("world: saved story instance unavailable")
|
||||
}
|
||||
result = append(result, current)
|
||||
used[current.InvenIndex] = true
|
||||
authored[current.InvenIndex] = current
|
||||
}
|
||||
for _, c := range temporary {
|
||||
current, ok := s.characters.Find(c.InvenIndex)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("world: story character was not saved")
|
||||
}
|
||||
result = append(result, current)
|
||||
used[current.InvenIndex] = true
|
||||
authored[current.InvenIndex] = current
|
||||
}
|
||||
// Placeholder entries are player-controlled party slots. Their costume
|
||||
// and character cannot be inferred from the design placeholder itself.
|
||||
for _, costume := range formation.StoryCostumes {
|
||||
if s.seed.PlaceholderCostumeID != 0 && costume.CostumeID == s.seed.PlaceholderCostumeID {
|
||||
continue
|
||||
}
|
||||
temporaryMember := false
|
||||
for _, design := range designs {
|
||||
if design.UniqueCharacterID == costume.UniqueCharacterID {
|
||||
temporaryMember = true
|
||||
owned := map[uint64]player.Character{}
|
||||
for _, c := range s.visibleOwnedCharacters(s.characters.All()) {
|
||||
banned := false
|
||||
for _, row := range formation.Characters {
|
||||
// PackManager.CalcBanCharUniqueId groups growth rows by ID/10.
|
||||
if row.Banned && row.CharacterID/10 == c.ID/10 {
|
||||
banned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if temporaryMember {
|
||||
if banned {
|
||||
continue
|
||||
}
|
||||
var ownedCostumes []player.Costume
|
||||
if s.starter != nil {
|
||||
ownedCostumes = append(ownedCostumes, s.starter.Costumes...)
|
||||
}
|
||||
if s.collection != nil {
|
||||
ownedCostumes = append(ownedCostumes, s.collection.Costumes()...)
|
||||
}
|
||||
for _, c := range s.visibleOwnedCharacters(s.characters.All()) {
|
||||
matched := c.CostumeID == costume.CostumeID
|
||||
for _, owned := range ownedCostumes {
|
||||
if owned.ID == costume.CostumeID && owned.UseChar == c.InvenIndex {
|
||||
matched = true
|
||||
c.CostumeID = owned.ID
|
||||
c.UseCostume = owned.InvenIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
if player.IsStoryCharacter(c) || used[c.InvenIndex] || !matched {
|
||||
continue
|
||||
}
|
||||
result = append(result, c)
|
||||
used[c.InvenIndex] = true
|
||||
break
|
||||
}
|
||||
owned[c.InvenIndex] = c
|
||||
}
|
||||
// Fill authored player-controlled slots from the persisted field party,
|
||||
// then the persisted battle party. Never synthesize the placeholder ID.
|
||||
for _, costume := range formation.StoryCostumes {
|
||||
if (s.seed.PlaceholderCostumeID == 0 || costume.CostumeID != s.seed.PlaceholderCostumeID) || s.decks == nil {
|
||||
continue
|
||||
}
|
||||
var candidates []uint64
|
||||
for _, entry := range s.decks.CurrentFieldDeck() {
|
||||
candidates = append(candidates, entry.CharacterInvenIndex)
|
||||
}
|
||||
var candidates []uint64
|
||||
if s.decks != nil {
|
||||
for _, entry := range s.decks.CurrentDeck() {
|
||||
candidates = append(candidates, entry.CharacterInvenIndex)
|
||||
}
|
||||
for _, index := range candidates {
|
||||
if used[index] {
|
||||
continue
|
||||
// With no saved battle choice, the account's field party supplies its
|
||||
// initial controlled members. A saved battle choice takes precedence.
|
||||
if len(candidates) == 0 {
|
||||
for _, entry := range s.decks.CurrentFieldDeck() {
|
||||
candidates = append(candidates, entry.CharacterInvenIndex)
|
||||
}
|
||||
var controlled player.Character
|
||||
for _, c := range s.visibleOwnedCharacters(s.characters.All()) {
|
||||
if c.InvenIndex == index {
|
||||
controlled = c
|
||||
break
|
||||
}
|
||||
}
|
||||
if controlled.InvenIndex == 0 {
|
||||
continue
|
||||
}
|
||||
temporaryDesign := false
|
||||
for _, d := range designs {
|
||||
if d.CharacterID == controlled.ID {
|
||||
temporaryDesign = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if temporaryDesign {
|
||||
continue
|
||||
}
|
||||
result = append(result, controlled)
|
||||
}
|
||||
}
|
||||
result := make([]player.Character, 0, storyBattlePartySize)
|
||||
used := map[uint64]bool{}
|
||||
controlledSlots := storyBattlePartySize - len(authored)
|
||||
for _, index := range candidates {
|
||||
if used[index] {
|
||||
continue
|
||||
}
|
||||
if c, ok := authored[index]; ok {
|
||||
result = append(result, c)
|
||||
used[index] = true
|
||||
continue
|
||||
}
|
||||
if c, ok := owned[index]; ok && controlledSlots > 0 {
|
||||
result = append(result, c)
|
||||
used[index] = true
|
||||
controlledSlots--
|
||||
}
|
||||
}
|
||||
for _, index := range authoredOrder {
|
||||
if !used[index] {
|
||||
result = append(result, authored[index])
|
||||
used[index] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
@@ -166,10 +140,20 @@ func (s *Service) resolveActivePartyWires(packID, questID int) ([][]byte, [][]by
|
||||
entries := make([]deck.DeckEntry, 0, len(party))
|
||||
characters := make([][]byte, 0, len(party))
|
||||
wires := make([][]byte, 0, len(party))
|
||||
savedPositions := map[uint64]uint64{}
|
||||
if s.decks != nil {
|
||||
for _, entry := range s.decks.CurrentDeck() {
|
||||
savedPositions[entry.CharacterInvenIndex] = entry.CostumeInvenIndex
|
||||
}
|
||||
}
|
||||
for i, c := range party {
|
||||
position := ^uint64(0)
|
||||
if i < len(formation.DeckList) {
|
||||
position = formation.DeckList[i]
|
||||
} else if len(formation.DeckList) == 0 {
|
||||
if saved, ok := savedPositions[c.InvenIndex]; ok {
|
||||
position = saved
|
||||
}
|
||||
}
|
||||
entry := deck.DeckEntry{CharacterInvenIndex: c.InvenIndex, CostumeInvenIndex: position, Slot: uint64(i + 1)}
|
||||
entries = append(entries, entry)
|
||||
@@ -179,7 +163,7 @@ func (s *Service) resolveActivePartyWires(packID, questID int) ([][]byte, [][]by
|
||||
}
|
||||
if s.decks != nil {
|
||||
if err := s.decks.SetStoryParty(entries); err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("world: save battle party pack%d quest%d charGroup%d storyCharGroup%d members%d: %w", packID, questID, formation.CharGroupID, formation.StoryCharGroupID, len(entries), err)
|
||||
}
|
||||
}
|
||||
return characters, wires, nil
|
||||
|
||||
@@ -2,8 +2,10 @@ package world
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/accountstate"
|
||||
"bd2server/internal/server/deck"
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/progress"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
"os"
|
||||
@@ -93,6 +95,139 @@ func TestInstalledStoryPartyInitialTalentPersistsAndEncodes(t *testing.T) {
|
||||
t.Fatal("temporary blacksmith omitted from party CharDBInfo")
|
||||
}
|
||||
|
||||
// Quest13 and quest14 share CharGroup102, but their field StoryCharGroup grows
|
||||
// from three to four costume designs. Owning the whole field cast used to add
|
||||
// a sixth battle member and roll back this otherwise completed quest clear.
|
||||
func TestInstalledQuest13ClearKeepsSavedBattlePartyAcrossReopen(t *testing.T) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
t.Skip("BD2_REAL_GAMEDATA not configured")
|
||||
}
|
||||
roster, err := gamedata.LoadStoryCharacterCatalog(root, "20260923193640", []int{1}, 996000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := gamedata.LoadStoryCatalog(root, "20260923193640")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seed := &player.Starter{Version: "2.35.10", Characters: []player.Character{
|
||||
{InvenIndex: 101, ID: 10, HP: 71, Level: 30, CostumeID: 101, UseCostume: 1001, TalentLevel: 1, TalentExp: 13, Exp: 123},
|
||||
{InvenIndex: 102, ID: 20, HP: 72, Level: 30, CostumeID: 202, UseCostume: 1002, TalentLevel: 1, TalentExp: 14, Exp: 124},
|
||||
{InvenIndex: 103, ID: 130, HP: 73, Level: 30, CostumeID: 1301, UseCostume: 1003, TalentLevel: 1, TalentExp: 15, Exp: 125},
|
||||
{InvenIndex: 104, ID: 140, HP: 74, Level: 30, CostumeID: 1401, UseCostume: 1004, TalentLevel: 1},
|
||||
}, Costumes: []player.Costume{
|
||||
{InvenIndex: 1001, ID: 101, UseChar: 101}, {InvenIndex: 1002, ID: 202, UseChar: 102},
|
||||
{InvenIndex: 1003, ID: 1301, UseChar: 103}, {InvenIndex: 1004, ID: 1401, UseChar: 104},
|
||||
}}
|
||||
path := filepath.Join(t.TempDir(), "state.db")
|
||||
open := func() (*accountstate.Repository, *Service) {
|
||||
t.Helper()
|
||||
repo, err := accountstate.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repo.Close() })
|
||||
s := testService()
|
||||
s.seed.PackID, s.seed.StartQuestID, s.startingPackID = 1, 1, 1
|
||||
s.storyRoster, s.storyCatalog, s.starter = roster, catalog, seed
|
||||
s.quests = catalog.Packs[1].Quests
|
||||
s.state, err = progress.OpenStore(repo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.inventory, err = player.OpenInventory(repo, seed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.characters, err = player.OpenCharacterStore(repo, seed.Characters, s.inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = s.characters.EnsurePersisted(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.decks, err = deck.OpenStore(repo, deck.Seed{Version: "2.35.10", FieldDeck: []deck.FieldEntry{{Slot: 1, CharacterInvenIndex: 104}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return repo, s
|
||||
}
|
||||
repo, s := open()
|
||||
for id := 1; id < 13; id++ {
|
||||
if err := s.state.ClearQuest(id, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
update := wire.AppendVarint(selectionRequest(13, 1, 0), 4, 1)
|
||||
if _, err := s.state.UpdateQuest(update); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
party, err := s.ResolveStoryParty(1, 13)
|
||||
if err != nil || len(party) != 3 { // Two temporaries and the only initial field choice.
|
||||
t.Fatalf("initial story party=%+v err=%v", party, err)
|
||||
}
|
||||
var saved []deck.DeckEntry
|
||||
for _, c := range party {
|
||||
if !player.IsStoryCharacter(c) {
|
||||
continue
|
||||
}
|
||||
if err := s.characters.SetCurrentHealth(c.InvenIndex, 7+uint64(len(saved))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved = append(saved, deck.DeckEntry{CharacterInvenIndex: c.InvenIndex, CostumeInvenIndex: uint64(len(saved) * 2), Slot: uint64(len(saved) + 1)})
|
||||
}
|
||||
for _, c := range seed.Characters[:3] {
|
||||
saved = append(saved, deck.DeckEntry{CharacterInvenIndex: c.InvenIndex, CostumeInvenIndex: uint64(len(saved) * 2), Slot: uint64(len(saved) + 1)})
|
||||
}
|
||||
if err := s.decks.SetStoryParty(saved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := s.characters.RawAll()
|
||||
op, err := repo.BeginOperation()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, response, handled, err := s.Handle("/QuestClear", selectionRequest(13, 1, 0))
|
||||
if err != nil || !handled {
|
||||
_ = op.Rollback()
|
||||
t.Fatalf("quest13 clear handled=%v err=%v", handled, err)
|
||||
}
|
||||
if err := op.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var echoed []deck.DeckEntry
|
||||
var joins int
|
||||
if err := wire.Walk(response, func(f wire.Field) error {
|
||||
if f.Number == 5 {
|
||||
joins++
|
||||
}
|
||||
if f.Number == 4 {
|
||||
index, _, _ := wire.Varint(f.Value, 1)
|
||||
position, _, _ := wire.Varint(f.Value, 2)
|
||||
slot, _, _ := wire.Varint(f.Value, 3)
|
||||
echoed = append(echoed, deck.DeckEntry{CharacterInvenIndex: index, CostumeInvenIndex: position, Slot: slot})
|
||||
}
|
||||
return nil
|
||||
}); err != nil || !reflect.DeepEqual(echoed, saved) || joins != 0 {
|
||||
t.Fatalf("clear changed saved battle choices/positions or repeated join UI: deck=%+v want=%+v joins=%d err=%v", echoed, saved, joins, err)
|
||||
}
|
||||
if err := repo.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, s = open()
|
||||
if !s.state.QuestCleared(13, 1) || s.firstUnclearedQuestFor(1) != 14 {
|
||||
t.Fatal("quest13 clear did not survive SQLite reopen")
|
||||
}
|
||||
if !reflect.DeepEqual(s.decks.CurrentDeck(), saved) || !reflect.DeepEqual(s.characters.RawAll(), before) {
|
||||
t.Fatal("quest transition or reopen changed party, positions, character stats, talents, or health")
|
||||
}
|
||||
party, err = s.ResolveStoryParty(1, 14)
|
||||
if err != nil || len(party) != 5 {
|
||||
t.Fatalf("reopened quest14 battle party=%+v err=%v", party, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstalledAdjacentStoryPartyRetainsHealth(t *testing.T) {
|
||||
root := os.Getenv("BD2_REAL_GAMEDATA")
|
||||
if root == "" {
|
||||
|
||||
@@ -24,23 +24,31 @@ func TestInstalledQuestClearProjectsPartyChangesOnly(t *testing.T) {
|
||||
if root == "" {
|
||||
t.Skip("set BD2_REAL_GAMEDATA for authored party transition regression")
|
||||
}
|
||||
roster, err := gamedata.LoadStoryCharacterCatalog(root, "20260923193640", []int{1, 21}, 996000)
|
||||
catalog, err := gamedata.LoadStoryCatalog(root, "20260923193640")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pack := range []int{1, 21} {
|
||||
var packs []int
|
||||
for id := range catalog.Packs {
|
||||
packs = append(packs, id)
|
||||
}
|
||||
sort.Ints(packs)
|
||||
roster, err := gamedata.LoadStoryCharacterCatalog(root, "20260923193640", packs, 996000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pack := range packs {
|
||||
t.Run(fmt.Sprintf("pack%d", pack), func(t *testing.T) {
|
||||
formations, err := gamedata.LoadQuestFormations(root, "20260923193640", pack)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ids []int
|
||||
for id := range formations {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Ints(ids)
|
||||
design := catalog.Packs[pack]
|
||||
ids := append([]int(nil), design.MainQuestIDs...)
|
||||
path := filepath.Join(t.TempDir(), "state.db")
|
||||
seed := &player.Starter{Version: "2.35.10", Characters: []player.Character{{InvenIndex: 77, ID: 10, Level: 1, HP: 7, CostumeID: 101}}}
|
||||
seed := &player.Starter{Version: "2.35.10", Characters: []player.Character{
|
||||
{InvenIndex: 77, ID: 10, Level: 1, HP: 7, CostumeID: 101, TalentLevel: 1},
|
||||
{InvenIndex: 78, ID: 20, Level: 1, HP: 7, CostumeID: 202, TalentLevel: 1},
|
||||
{InvenIndex: 79, ID: 130, Level: 1, HP: 7, CostumeID: 1301, TalentLevel: 1},
|
||||
{InvenIndex: 80, ID: 140, Level: 1, HP: 7, CostumeID: 1401, TalentLevel: 1},
|
||||
{InvenIndex: 81, ID: 350, Level: 1, HP: 7, CostumeID: 3501, TalentLevel: 1},
|
||||
}}
|
||||
open := func() (*accountstate.Repository, *Service) {
|
||||
t.Helper()
|
||||
repo, err := accountstate.Open(path)
|
||||
@@ -66,20 +74,35 @@ func TestInstalledQuestClearProjectsPartyChangesOnly(t *testing.T) {
|
||||
if err = s.characters.EnsurePersisted(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var chosen []deck.DeckEntry
|
||||
for i, c := range seed.Characters {
|
||||
chosen = append(chosen, deck.DeckEntry{CharacterInvenIndex: c.InvenIndex, CostumeInvenIndex: uint64(i), Slot: uint64(i + 1)})
|
||||
}
|
||||
s.decks, err = deck.OpenStore(repo, deck.Seed{Version: "2.35.10", FieldDeck: []deck.FieldEntry{{Slot: 1, CharacterInvenIndex: 77}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(s.decks.CurrentDeck()) == 0 {
|
||||
if err = s.decks.SetStoryParty(chosen); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
s.quests = map[int]gamedata.QuestDesign{}
|
||||
for _, id := range ids {
|
||||
s.quests[id] = gamedata.QuestDesign{ID: id}
|
||||
q := design.Quests[id]
|
||||
s.quests[id] = gamedata.QuestDesign{ID: id, Type: q.Type, NextQuestID: q.NextQuestID, PriorQuestID: q.PriorQuestID}
|
||||
}
|
||||
attachTestStoryCatalog(s)
|
||||
s.storyCatalog = &gamedata.StoryCatalog{Packs: map[int]gamedata.StoryPack{pack: {ID: pack, Quests: s.quests, MainQuestIDs: ids}}}
|
||||
s.packs = map[int]map[int]gamedata.QuestDesign{pack: s.quests}
|
||||
return repo, s
|
||||
}
|
||||
repo, s := open()
|
||||
unchanged, joined := false, false
|
||||
for i, id := range ids[:len(ids)-1] {
|
||||
unchanged := false
|
||||
for _, id := range ids {
|
||||
next := design.Quests[id].NextQuestID
|
||||
if next == 0 {
|
||||
continue
|
||||
}
|
||||
before, err := s.ResolveStoryParty(pack, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -96,7 +119,7 @@ func TestInstalledQuestClearProjectsPartyChangesOnly(t *testing.T) {
|
||||
if err = op.Commit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, err := s.ResolveStoryParty(pack, ids[i+1])
|
||||
after, err := s.ResolveStoryParty(pack, next)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -122,11 +145,10 @@ func TestInstalledQuestClearProjectsPartyChangesOnly(t *testing.T) {
|
||||
decks++
|
||||
}
|
||||
return nil
|
||||
}); err != nil || !reflect.DeepEqual(got, want) || len(after) > 0 && decks != len(after) {
|
||||
t.Fatalf("pack%d quest%d→%d joins=%v want=%v formation=%d/%d err=%v", pack, id, ids[i+1], got, want, decks, len(after), err)
|
||||
}); err != nil || !reflect.DeepEqual(got, want) || len(after) > 0 && decks != len(after) || decks > 5 {
|
||||
t.Fatalf("pack%d quest%d→%d joins=%v want=%v formation=%d/%d err=%v", pack, id, next, got, want, decks, len(after), err)
|
||||
}
|
||||
unchanged = unchanged || len(before) > 0 && len(want) == 0
|
||||
joined = joined || len(want) > 0
|
||||
if pack == 1 && id == 2 {
|
||||
if len(got) != 0 {
|
||||
t.Fatal("pack-1 gate quest repeated character acquisition")
|
||||
@@ -140,12 +162,9 @@ func TestInstalledQuestClearProjectsPartyChangesOnly(t *testing.T) {
|
||||
t.Fatal("quest transition healed or replaced existing character")
|
||||
}
|
||||
}
|
||||
if unchanged && (pack == 1 && id >= 3 || pack == 21 && joined) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !unchanged || pack == 21 && !joined {
|
||||
t.Fatal("authored regression did not exercise both stable and joining party transitions")
|
||||
if !unchanged {
|
||||
t.Fatal("authored regression did not exercise stable party transitions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user