服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
588 lines
23 KiB
Go
588 lines
23 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
dnfdungeon "longheng.io/server/internal/modules/dnf/dungeon"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
"longheng.io/server/internal/modules/dnf/worldmap"
|
|
)
|
|
|
|
func TestCurrentDungeonEndlessAltarOwnsFiveRoundsPerRoomAndFifteenGlobally(t *testing.T) {
|
|
dungeon := currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15)
|
|
runtime, err := newCurrentDungeonAltarRuntime(dungeon, currentDungeonEndlessAltarScene(0, 17020))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := make(map[uint16]struct{})
|
|
var firstRoomSequence uint16
|
|
for room := 0; room < 3; room++ {
|
|
if room != 0 {
|
|
if err := runtime.bindRoom(currentDungeonEndlessAltarScene(room, int64(17020+room))); err != nil {
|
|
t.Fatalf("bind room %d: %v", room, err)
|
|
}
|
|
}
|
|
schedule, accepted, err := runtime.acceptReady(runtime.Room.Key, time.Unix(int64(room+1), 0))
|
|
if err != nil || !accepted || schedule == nil {
|
|
t.Fatalf("room %d ready accepted=%t schedule=%+v err=%v", room, accepted, schedule, err)
|
|
}
|
|
if duplicate, accepted, err := runtime.acceptReady(runtime.Room.Key, time.Now()); err != nil || accepted || duplicate != nil {
|
|
t.Fatalf("room %d duplicate ready accepted=%t schedule=%+v err=%v", room, accepted, duplicate, err)
|
|
}
|
|
for localRound := 0; localRound < 5; localRound++ {
|
|
if schedule.GlobalRound != room*5+localRound || schedule.RoundIndex != localRound {
|
|
t.Fatalf("room=%d local=%d schedule global=%d local=%d", room, localRound, schedule.GlobalRound, schedule.RoundIndex)
|
|
}
|
|
progress, sequence := completeCurrentDungeonAltarTestRound(t, runtime, schedule)
|
|
if sequence == 0 {
|
|
t.Fatal("server allocated sequence zero")
|
|
}
|
|
if _, duplicate := seen[sequence]; duplicate {
|
|
t.Fatalf("sequence %d was reused", sequence)
|
|
}
|
|
seen[sequence] = struct{}{}
|
|
if firstRoomSequence == 0 {
|
|
firstRoomSequence = sequence
|
|
}
|
|
lastLocal := localRound == 4
|
|
lastDungeon := room == 2 && lastLocal
|
|
switch {
|
|
case lastDungeon && progress != currentDungeonAltarProgressDungeonComplete:
|
|
t.Fatalf("final progress=%d", progress)
|
|
case lastLocal && !lastDungeon && progress != currentDungeonAltarProgressRoomComplete:
|
|
t.Fatalf("room %d final progress=%d", room, progress)
|
|
case !lastLocal && progress != currentDungeonAltarProgressRoundActive:
|
|
t.Fatalf("room=%d local=%d progress=%d", room, localRound, progress)
|
|
}
|
|
if !lastLocal {
|
|
schedule, err = runtime.beginRound(time.Unix(int64(room*5+localRound+2), 0))
|
|
if err != nil || schedule == nil {
|
|
t.Fatalf("begin next room=%d local=%d schedule=%+v err=%v", room, localRound, schedule, err)
|
|
}
|
|
}
|
|
}
|
|
if room == 0 {
|
|
nextScene := currentDungeonEndlessAltarScene(1, 17021)
|
|
if err := runtime.previewBindRoom(nextScene); err != nil {
|
|
t.Fatalf("preview next room: %v", err)
|
|
}
|
|
if _, err := runtime.applyDeaths(runtimeDungeonRoomKeyFromScene(nextScene), []uint16{firstRoomSequence}); !errors.Is(err, errCurrentDungeonAltarRoomMismatch) {
|
|
t.Fatalf("unbound cross-room death error=%v", err)
|
|
}
|
|
}
|
|
if room == 1 {
|
|
completedRounds := runtime.completedRounds
|
|
currentRoom := runtime.Room
|
|
completedScene := currentDungeonEndlessAltarScene(0, 17020)
|
|
if err := runtime.previewBindRoom(completedScene); !errors.Is(err, errCurrentDungeonAltarRoomComplete) {
|
|
t.Fatalf("completed-room preview error=%v", err)
|
|
}
|
|
if err := runtime.bindRoom(completedScene); !errors.Is(err, errCurrentDungeonAltarRoomComplete) {
|
|
t.Fatalf("completed-room bind error=%v", err)
|
|
}
|
|
if runtime.completedRounds != completedRounds || runtime.Room != currentRoom {
|
|
t.Fatalf("completed-room replay mutated state: completed=%d room=%+v", runtime.completedRounds, runtime.Room)
|
|
}
|
|
}
|
|
}
|
|
if runtime.completedRounds != 15 || len(runtime.completedRooms) != 3 || runtime.progress != currentDungeonAltarProgressDungeonComplete {
|
|
t.Fatalf("completed=%d rooms=%d progress=%d", runtime.completedRounds, len(runtime.completedRooms), runtime.progress)
|
|
}
|
|
if _, err := runtime.applyDeaths(runtime.Room.Key, []uint16{firstRoomSequence}); !errors.Is(err, errCurrentDungeonAltarSequenceCrossRoom) {
|
|
t.Fatalf("old-room sequence error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonUltimateAltarDifficultySelectionAndTimeoutRace(t *testing.T) {
|
|
dungeon := currentDungeonAltarTestDungeon(11007, worldmap.BloodAltarDungeonKindUltimate, 7)
|
|
runtime, err := newCurrentDungeonAltarRuntime(dungeon, currentDungeonUltimateAltarScene())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schedule, accepted, err := runtime.acceptReady(runtime.Room.Key, time.Unix(1, 0))
|
|
if err != nil || !accepted || schedule.Difficulty != worldmap.BloodAltarDifficultyA {
|
|
t.Fatalf("first schedule=%+v accepted=%t err=%v", schedule, accepted, err)
|
|
}
|
|
for round := 0; round < 7; round++ {
|
|
progress, _ := completeCurrentDungeonAltarTestRound(t, runtime, schedule)
|
|
if round == 6 {
|
|
if progress != currentDungeonAltarProgressDungeonComplete {
|
|
t.Fatalf("final progress=%d", progress)
|
|
}
|
|
break
|
|
}
|
|
if round == 5 {
|
|
if progress != currentDungeonAltarProgressFinalRoundDelay {
|
|
t.Fatalf("penultimate progress=%d", progress)
|
|
}
|
|
runtime.progress = currentDungeonAltarProgressRoundActive
|
|
schedule, err = runtime.beginRound(time.Unix(int64(round+2), 0))
|
|
if err != nil || schedule == nil || schedule.Difficulty != worldmap.BloodAltarDifficultyA {
|
|
t.Fatalf("final fixed-A schedule=%+v err=%v", schedule, err)
|
|
}
|
|
continue
|
|
}
|
|
if progress != currentDungeonAltarProgressAwaitingDifficulty {
|
|
t.Fatalf("round=%d progress=%d", round, progress)
|
|
}
|
|
roomGeneration, promptVersion := runtime.Room.Generation, runtime.difficultyVersion
|
|
chosen := worldmap.BloodAltarDifficultyB
|
|
if round%2 != 0 {
|
|
chosen = worldmap.BloodAltarDifficultyA
|
|
}
|
|
if !runtime.resolveDifficulty(roomGeneration, promptVersion, chosen) {
|
|
t.Fatalf("round=%d first difficulty lost", round)
|
|
}
|
|
other := worldmap.BloodAltarDifficultyA
|
|
if chosen == other {
|
|
other = worldmap.BloodAltarDifficultyB
|
|
}
|
|
if runtime.resolveDifficulty(roomGeneration, promptVersion, other) {
|
|
t.Fatalf("round=%d stale difficulty won race", round)
|
|
}
|
|
schedule, err = runtime.beginRound(time.Unix(int64(round+2), 0))
|
|
if err != nil || schedule == nil || schedule.Difficulty != chosen {
|
|
t.Fatalf("round=%d schedule=%+v chosen=%q err=%v", round, schedule, chosen, err)
|
|
}
|
|
}
|
|
if runtime.completedRounds != 7 || runtime.progress != currentDungeonAltarProgressDungeonComplete {
|
|
t.Fatalf("completed=%d progress=%d", runtime.completedRounds, runtime.progress)
|
|
}
|
|
if len(runtime.completedDifficulties) != 7 || runtime.completedDifficulties[0] != worldmap.BloodAltarDifficultyA ||
|
|
runtime.completedDifficulties[6] != worldmap.BloodAltarDifficultyA {
|
|
t.Fatalf("completed difficulties=%v", runtime.completedDifficulties)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonEndlessAltarCrazyMonsterSelectionAndVariant(t *testing.T) {
|
|
dungeon := currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15)
|
|
scene := currentDungeonEndlessAltarScene(2, 17022)
|
|
scene.Map.Map.Type = "[boss]"
|
|
runtime, err := newCurrentDungeonAltarRuntime(dungeon, scene)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := runtime.configureRunSeed(0x12345678); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if runtime.crazyMonster == nil || runtime.crazyMonster.RoundIndex < 0 || runtime.crazyMonster.RoundIndex >= 4 ||
|
|
runtime.crazyMonster.PhaseIndex != 0 || runtime.crazyMonster.MonsterCode != 61010 ||
|
|
runtime.crazyMonster.Variant > 18 {
|
|
t.Fatalf("boss-map crazy monster=%+v", runtime.crazyMonster)
|
|
}
|
|
|
|
runtime.nextRoundIndex = runtime.crazyMonster.RoundIndex
|
|
runtime.completedRounds = runtime.crazyMonster.RoundIndex
|
|
schedule, err := runtime.beginRound(time.Unix(1, 0))
|
|
if err != nil || schedule == nil || len(schedule.Waves) == 0 {
|
|
t.Fatalf("schedule=%+v err=%v", schedule, err)
|
|
}
|
|
wave, err := runtime.reserveWave(schedule.Generation, 0)
|
|
if err != nil || len(wave.Spawns) == 0 || wave.Spawns[0].Variant != runtime.crazyMonster.Variant {
|
|
t.Fatalf("crazy wave=%+v err=%v crazy=%+v", wave, err, runtime.crazyMonster)
|
|
}
|
|
|
|
ultimate, err := newCurrentDungeonAltarRuntime(
|
|
currentDungeonAltarTestDungeon(11007, worldmap.BloodAltarDungeonKindUltimate, 7),
|
|
currentDungeonUltimateAltarScene(),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ultimate.configureRunSeed(0x12345678); err != nil || ultimate.crazyMonster != nil {
|
|
t.Fatalf("ultimate crazy=%+v err=%v", ultimate.crazyMonster, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarRejectsNonContiguousOrWrongRoundCount(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dungeon worldmap.Dungeon
|
|
scene worldmap.DungeonRoomScene
|
|
}{
|
|
{
|
|
name: "endless_missing_round",
|
|
dungeon: currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
scene: func() worldmap.DungeonRoomScene {
|
|
scene := currentDungeonEndlessAltarScene(0, 17020)
|
|
scene.Map.Map.BloodPhases = scene.Map.Map.BloodPhases[:4]
|
|
return scene
|
|
}(),
|
|
},
|
|
{
|
|
name: "endless_round_gap",
|
|
dungeon: currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
scene: func() worldmap.DungeonRoomScene {
|
|
scene := currentDungeonEndlessAltarScene(0, 17020)
|
|
scene.Map.Map.BloodPhases[4].Round = 5
|
|
return scene
|
|
}(),
|
|
},
|
|
{
|
|
name: "ultimate_missing_round",
|
|
dungeon: currentDungeonAltarTestDungeon(11007, worldmap.BloodAltarDungeonKindUltimate, 7),
|
|
scene: func() worldmap.DungeonRoomScene {
|
|
scene := currentDungeonUltimateAltarScene()
|
|
scene.Map.Map.UltimatePhases = scene.Map.Map.UltimatePhases[:12]
|
|
return scene
|
|
}(),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, err := newCurrentDungeonAltarRuntime(test.dungeon, test.scene); !errors.Is(err, errCurrentDungeonAltarDefinitionInvalid) {
|
|
t.Fatalf("definition error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarKnownIDsCannotDowngradeToOrdinaryDungeon(t *testing.T) {
|
|
for _, dungeonID := range []int64{11006, 11007} {
|
|
dungeon := currentDungeonAltarTestDungeon(dungeonID, 0, 0)
|
|
runtime, err := newCurrentDungeonAltarRuntime(dungeon, worldmap.DungeonRoomScene{})
|
|
if runtime != nil || !errors.Is(err, errCurrentDungeonAltarDefinitionInvalid) {
|
|
t.Fatalf("dungeon=%d runtime=%+v error=%v", dungeonID, runtime, err)
|
|
}
|
|
}
|
|
runtime, err := newCurrentDungeonAltarRuntime(worldmap.Dungeon{ID: 100}, worldmap.DungeonRoomScene{})
|
|
if err != nil || runtime != nil {
|
|
t.Fatalf("ordinary dungeon runtime=%+v error=%v", runtime, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarRejectsOrdinaryBlockingHostiles(t *testing.T) {
|
|
scene := currentDungeonEndlessAltarScene(0, 17020)
|
|
scene.BlockingHostiles = []worldmap.HostileReference{{Kind: worldmap.HostileMonster, Index: 0}}
|
|
runtime, err := newCurrentDungeonAltarRuntime(
|
|
currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
scene,
|
|
)
|
|
if runtime != nil || !errors.Is(err, errCurrentDungeonAltarDefinitionInvalid) {
|
|
t.Fatalf("runtime=%+v error=%v", runtime, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarEntryIsSoloOnlyWithoutChangingOrdinaryDungeonAdmission(t *testing.T) {
|
|
character := dnfrepo.CharacterRecord{Level: 70, Stats: map[string]int64{"fatigue": 100}}
|
|
party := &gameSession{}
|
|
party.party.state = alignedcmd.PartyState{Members: []alignedcmd.PartyMemberState{
|
|
{UserID: 99},
|
|
{UserID: 100},
|
|
}}
|
|
altar := currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15)
|
|
if err := validateDungeonEntry(character, altar, party, false); !errors.Is(err, dnfdungeon.ErrEntryPartyLimit) {
|
|
t.Fatalf("party altar error=%v", err)
|
|
} else if reason, reply := selectDungeonFailureReason(err); !reply || reason != currentSelectDungeonPreparationRejectedReason {
|
|
t.Fatalf("party altar rejection reason=%d reply=%t", reason, reply)
|
|
}
|
|
|
|
solo := &gameSession{}
|
|
solo.party.state = alignedcmd.PartyState{Members: []alignedcmd.PartyMemberState{{UserID: 99}}}
|
|
if err := validateDungeonEntry(character, altar, solo, false); err != nil {
|
|
t.Fatalf("solo altar rejected: %v", err)
|
|
}
|
|
|
|
ordinary := worldmap.Dungeon{ID: 700}
|
|
if err := validateDungeonEntry(character, ordinary, party, false); err != nil {
|
|
t.Fatalf("ordinary dungeon admission changed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarPracticeEntryIsRejectedWithoutChangingOrdinaryPractice(t *testing.T) {
|
|
for _, dungeon := range []worldmap.Dungeon{
|
|
currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
currentDungeonAltarTestDungeon(11007, worldmap.BloodAltarDungeonKindUltimate, 7),
|
|
{ID: 99999, BloodAltarKind: worldmap.BloodAltarDungeonKindEndless},
|
|
} {
|
|
err := validatePracticeDungeonRoute(dungeon)
|
|
if !errors.Is(err, errDungeonEntryModeUnsupported) {
|
|
t.Fatalf("altar dungeon=%d kind=%d practice error=%v", dungeon.ID, dungeon.BloodAltarKind, err)
|
|
}
|
|
if reason, reply := selectDungeonFailureReason(err); !reply || reason != currentSelectDungeonPreparationRejectedReason {
|
|
t.Fatalf("altar dungeon=%d practice rejection reason=%d reply=%t", dungeon.ID, reason, reply)
|
|
}
|
|
}
|
|
if err := validatePracticeDungeonRoute(worldmap.Dungeon{ID: 700}); err != nil {
|
|
t.Fatalf("ordinary practice route changed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarDeathIsAtomicAndDuplicateReportIsIdempotent(t *testing.T) {
|
|
runtime, err := newCurrentDungeonAltarRuntime(
|
|
currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
currentDungeonEndlessAltarScene(0, 17020),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schedule, _, err := runtime.acceptReady(runtime.Room.Key, time.Unix(1, 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wave, err := runtime.reserveWave(schedule.Generation, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.commitWave(wave); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sequence := wave.Spawns[0].Sequence
|
|
if _, err := runtime.applyDeaths(runtime.Room.Key, []uint16{sequence, 65000}); !errors.Is(err, errCurrentDungeonAltarSequenceUnknown) {
|
|
t.Fatalf("mixed death error=%v", err)
|
|
}
|
|
if _, active := runtime.activeSequences[sequence]; !active {
|
|
t.Fatal("partially applied mixed valid/unknown death report")
|
|
}
|
|
result, err := runtime.applyDeaths(runtime.Room.Key, []uint16{sequence})
|
|
if err != nil || result.Accepted != 1 {
|
|
t.Fatalf("accepted result=%+v err=%v", result, err)
|
|
}
|
|
duplicate, err := runtime.applyDeaths(runtime.Room.Key, []uint16{sequence})
|
|
if err != nil || duplicate.Accepted != 0 || duplicate.Duplicate != 1 || runtime.completedRounds != 1 {
|
|
t.Fatalf("duplicate result=%+v completed=%d err=%v", duplicate, runtime.completedRounds, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonAltarPVFDurationExpiresUnreportedActorsAtPhaseBoundary(t *testing.T) {
|
|
scene := currentDungeonEndlessAltarScene(0, 17020)
|
|
scene.Map.Map.BloodMonsters = []worldmap.BloodAltarMonster{
|
|
{
|
|
MonsterCode: 61171,
|
|
Position: worldmap.Point3{X: 295, Y: 184},
|
|
DurationMilliseconds: 3000,
|
|
SpawnIntervalMilliseconds: 1000,
|
|
BaseSpawnCount: 1,
|
|
SpawnCountIncrement: 1,
|
|
BatchCount: 2,
|
|
},
|
|
{
|
|
MonsterCode: 61172,
|
|
Position: worldmap.Point3{X: 300, Y: 184},
|
|
DurationMilliseconds: 1000,
|
|
BaseSpawnCount: 1,
|
|
BatchCount: 1,
|
|
},
|
|
}
|
|
scene.Map.Map.BloodPhases = []worldmap.BloodAltarPhase{
|
|
{Round: 0, MonsterTemplateIndex: 0, DelayMilliseconds: 1000, Scale: 1.2},
|
|
{Round: 0, MonsterTemplateIndex: 1, DelayMilliseconds: 1000, Scale: 1.2},
|
|
{Round: 1, MonsterTemplateIndex: 1, Scale: 1},
|
|
{Round: 2, MonsterTemplateIndex: 1, Scale: 1},
|
|
{Round: 3, MonsterTemplateIndex: 1, Scale: 1},
|
|
{Round: 4, MonsterTemplateIndex: 1, Scale: 1},
|
|
}
|
|
runtime, err := newCurrentDungeonAltarRuntime(
|
|
currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
scene,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schedule, accepted, err := runtime.acceptReady(runtime.Room.Key, time.Unix(1, 0))
|
|
if err != nil || !accepted || len(schedule.Waves) != 3 {
|
|
t.Fatalf("schedule=%+v accepted=%t err=%v", schedule, accepted, err)
|
|
}
|
|
if schedule.Waves[0].Delay != time.Second || schedule.Waves[1].Delay != 2*time.Second ||
|
|
schedule.Waves[0].ExpireDelay != 3*time.Second || schedule.Waves[1].ExpireDelay != 3*time.Second ||
|
|
schedule.Waves[2].Delay != 4*time.Second || schedule.Waves[2].ExpireDelay != 5*time.Second {
|
|
t.Fatalf("unexpected duration schedule=%+v", schedule.Waves)
|
|
}
|
|
|
|
first, err := runtime.reserveWave(schedule.Generation, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.commitWave(first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := runtime.reserveWave(schedule.Generation, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(first.Spawns) != 1 || len(second.Spawns) != 2 {
|
|
t.Fatalf("phase spawn counts=%d/%d", len(first.Spawns), len(second.Spawns))
|
|
}
|
|
if _, err := runtime.commitWave(second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
phaseSequences := []uint16{first.Spawns[0].Sequence, second.Spawns[0].Sequence, second.Spawns[1].Sequence}
|
|
expired, progress, err := runtime.expireWave(runtime.Room, schedule.Generation, phaseSequences)
|
|
if err != nil || expired != 3 || progress != currentDungeonAltarProgressNone || len(runtime.activeSequences) != 0 {
|
|
t.Fatalf("first phase expiry expired=%d progress=%d active=%d err=%v", expired, progress, len(runtime.activeSequences), err)
|
|
}
|
|
duplicate, err := runtime.applyDeaths(runtime.Room.Key, []uint16{phaseSequences[0]})
|
|
if err != nil || duplicate.Accepted != 0 || duplicate.Duplicate != 1 {
|
|
t.Fatalf("late death after expiry result=%+v err=%v", duplicate, err)
|
|
}
|
|
|
|
third, err := runtime.reserveWave(schedule.Generation, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.commitWave(third); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expired, progress, err = runtime.expireWave(runtime.Room, schedule.Generation, []uint16{third.Spawns[0].Sequence})
|
|
if err != nil || expired != 1 || progress != currentDungeonAltarProgressRoundActive || runtime.completedRounds != 1 {
|
|
t.Fatalf("second phase expiry expired=%d progress=%d completed=%d err=%v", expired, progress, runtime.completedRounds, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonEndlessAltarFinalRoomWaveReleasesEarlierSurvivors(t *testing.T) {
|
|
scene := currentDungeonEndlessAltarScene(0, 17020)
|
|
scene.Map.Map.BloodMonsters = append(scene.Map.Map.BloodMonsters, worldmap.BloodAltarMonster{
|
|
MonsterCode: 61011,
|
|
Position: worldmap.Point3{X: 200, Y: 200},
|
|
BaseSpawnCount: 2,
|
|
BatchCount: 1,
|
|
})
|
|
scene.Map.Map.BloodPhases = append(scene.Map.Map.BloodPhases, worldmap.BloodAltarPhase{
|
|
Round: 4, MonsterTemplateIndex: 1, Scale: 1,
|
|
})
|
|
runtime, err := newCurrentDungeonAltarRuntime(
|
|
currentDungeonAltarTestDungeon(11006, worldmap.BloodAltarDungeonKindEndless, 15),
|
|
scene,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schedule, _, err := runtime.acceptReady(runtime.Room.Key, time.Unix(1, 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for round := 0; round < 4; round++ {
|
|
progress, _ := completeCurrentDungeonAltarTestRound(t, runtime, schedule)
|
|
if progress != currentDungeonAltarProgressRoundActive {
|
|
t.Fatalf("round=%d progress=%d", round, progress)
|
|
}
|
|
schedule, err = runtime.beginRound(time.Unix(int64(round+2), 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if len(schedule.Waves) != 2 {
|
|
t.Fatalf("final local round waves=%d", len(schedule.Waves))
|
|
}
|
|
oldWave, err := runtime.reserveWave(schedule.Generation, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.commitWave(oldWave); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
finalWave, err := runtime.reserveWave(schedule.Generation, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(finalWave.Spawns) != 2 || !finalWave.Spawns[0].FinalRoomWave || !finalWave.Spawns[1].FinalRoomWave {
|
|
t.Fatalf("final wave=%+v", finalWave)
|
|
}
|
|
if _, err := runtime.commitWave(finalWave); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
firstFinal := finalWave.Spawns[0].Sequence
|
|
partial, err := runtime.applyDeaths(runtime.Room.Key, []uint16{firstFinal})
|
|
if err != nil || partial.Accepted != 1 || partial.AutoReleased != 0 || partial.Progress != currentDungeonAltarProgressNone {
|
|
t.Fatalf("partial final-wave death result=%+v err=%v", partial, err)
|
|
}
|
|
if _, active := runtime.activeSequences[oldWave.Spawns[0].Sequence]; !active {
|
|
t.Fatal("earlier wave released before every final-wave sequence died")
|
|
}
|
|
|
|
lastFinal := finalWave.Spawns[1].Sequence
|
|
completed, err := runtime.applyDeaths(runtime.Room.Key, []uint16{lastFinal})
|
|
if err != nil || completed.Accepted != 1 || completed.AutoReleased != 1 || completed.Progress != currentDungeonAltarProgressRoomComplete {
|
|
t.Fatalf("last final-wave death result=%+v err=%v", completed, err)
|
|
}
|
|
oldOwner, retired := runtime.retiredSequences[oldWave.Spawns[0].Sequence]
|
|
if !retired || oldOwner.FinalRoomWave {
|
|
t.Fatalf("earlier survivor retirement owner=%+v retired=%t", oldOwner, retired)
|
|
}
|
|
if runtime.completedRounds != 5 || len(runtime.activeSequences) != 0 {
|
|
t.Fatalf("completed rounds=%d active=%d", runtime.completedRounds, len(runtime.activeSequences))
|
|
}
|
|
}
|
|
|
|
func completeCurrentDungeonAltarTestRound(
|
|
t *testing.T,
|
|
runtime *currentDungeonAltarRuntime,
|
|
schedule *currentDungeonAltarRoundSchedule,
|
|
) (currentDungeonAltarProgress, uint16) {
|
|
t.Helper()
|
|
if len(schedule.Waves) != 1 {
|
|
t.Fatalf("test schedule waves=%d", len(schedule.Waves))
|
|
}
|
|
wave, err := runtime.reserveWave(schedule.Generation, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(wave.Spawns) != 1 {
|
|
t.Fatalf("test wave spawns=%d", len(wave.Spawns))
|
|
}
|
|
if progress, err := runtime.commitWave(wave); err != nil || progress != currentDungeonAltarProgressNone {
|
|
t.Fatalf("commit progress=%d err=%v", progress, err)
|
|
}
|
|
result, err := runtime.applyDeaths(runtime.Room.Key, []uint16{wave.Spawns[0].Sequence})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result.Progress, wave.Spawns[0].Sequence
|
|
}
|
|
|
|
func currentDungeonAltarTestDungeon(
|
|
id int64,
|
|
kind worldmap.BloodAltarDungeonKind,
|
|
maxRounds int64,
|
|
) worldmap.Dungeon {
|
|
return worldmap.Dungeon{
|
|
ID: id,
|
|
BloodAltarKind: kind,
|
|
BloodMaxRounds: maxRounds,
|
|
Metadata: worldmap.DungeonMetadata{
|
|
BasisLevel: worldmap.OptionalInt{Value: 70, Set: true},
|
|
},
|
|
}
|
|
}
|
|
|
|
func currentDungeonEndlessAltarScene(room int, mapID int64) worldmap.DungeonRoomScene {
|
|
phases := make([]worldmap.BloodAltarPhase, 5)
|
|
for index := range phases {
|
|
phases[index] = worldmap.BloodAltarPhase{Round: int64(index), MonsterTemplateIndex: 0, Scale: 1}
|
|
}
|
|
return worldmap.DungeonRoomScene{
|
|
Coordinate: worldmap.RoomCoordinate{X: int64(room), Y: 0},
|
|
Map: worldmap.ResolvedMap{Map: worldmap.Map{
|
|
ID: mapID,
|
|
BloodMonsters: []worldmap.BloodAltarMonster{{
|
|
MonsterCode: 61010, Position: worldmap.Point3{X: 100, Y: 200}, BaseSpawnCount: 1, BatchCount: 1,
|
|
}},
|
|
BloodPhases: phases,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func currentDungeonUltimateAltarScene() worldmap.DungeonRoomScene {
|
|
phases := make([]worldmap.BloodAltarPhase, 0, 14)
|
|
for round := 0; round < 7; round++ {
|
|
phases = append(phases,
|
|
worldmap.BloodAltarPhase{Round: int64(round), MonsterTemplateIndex: 0, Scale: 1, Difficulty: worldmap.BloodAltarDifficultyA},
|
|
worldmap.BloodAltarPhase{Round: int64(round), MonsterTemplateIndex: 0, Scale: 1, Difficulty: worldmap.BloodAltarDifficultyB},
|
|
)
|
|
}
|
|
return worldmap.DungeonRoomScene{
|
|
Coordinate: worldmap.RoomCoordinate{X: 0, Y: 0},
|
|
Map: worldmap.ResolvedMap{Map: worldmap.Map{
|
|
ID: 17030,
|
|
UltimateMonsters: []worldmap.BloodAltarMonster{{
|
|
MonsterCode: 56017, Position: worldmap.Point3{X: 440, Y: 222}, BaseSpawnCount: 1, BatchCount: 1,
|
|
}},
|
|
UltimatePhases: phases,
|
|
}},
|
|
}
|
|
}
|