DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,587 @@
|
||||
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,
|
||||
}},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user