服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
246 lines
9.3 KiB
Go
246 lines
9.3 KiB
Go
package worldmap
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseTournamentPVFShape(t *testing.T) {
|
|
dungeonDoc := parseTestDocument(t, "dungeon/tournament.dgn", `
|
|
[basis level]
|
|
72
|
|
[maze info]
|
|
[size]
|
|
1 1
|
|
[greed]
|
|
`+"`BB`"+`
|
|
[start map]
|
|
0 0
|
|
[boss map]
|
|
0 0
|
|
[tournament dungeon]
|
|
1
|
|
[required item]
|
|
4726 1 1
|
|
[/required item]
|
|
[tournament round fatigue]
|
|
2
|
|
[result card]
|
|
[reward item rate]
|
|
3329 60 1 3329 35 2 3329 5 3
|
|
[/reward item rate]
|
|
0 0 0 100 1 70 0 30 2 80 0 20 3 90 0 10 4 100 0 0 5 0 100 0
|
|
[/result card]
|
|
[tournament clear reward exp]
|
|
0 0 1 30000 2 50000 3 100000 4 200000
|
|
[/tournament clear reward exp]
|
|
[limit party count]
|
|
1
|
|
[tournament clear reward gold rate]
|
|
16
|
|
`)
|
|
dungeon := ParseDungeon(120, "dungeon/tournament.dgn", dungeonDoc)
|
|
if !dungeon.Metadata.Flags["tournament dungeon"] || dungeon.Metadata.TournamentRoundFatigue != 2 || dungeon.Metadata.TournamentGoldRate != 16 {
|
|
t.Fatalf("tournament scalar metadata=%+v", dungeon.Metadata)
|
|
}
|
|
if len(dungeon.Metadata.RequiredItems) != 1 || dungeon.Metadata.RequiredItems[0] != (TicketItem{ItemID: 4726, Count: 1}) {
|
|
t.Fatalf("required items=%+v", dungeon.Metadata.RequiredItems)
|
|
}
|
|
if len(dungeon.Metadata.TournamentRewardItemRates) != 3 || dungeon.Metadata.TournamentRewardItemRates[2].Count != 3 {
|
|
t.Fatalf("reward item rates=%+v", dungeon.Metadata.TournamentRewardItemRates)
|
|
}
|
|
if len(dungeon.Metadata.TournamentResultCards) != 6 || dungeon.Metadata.TournamentResultCards[5].ItemWeight != 100 {
|
|
t.Fatalf("result cards=%+v", dungeon.Metadata.TournamentResultCards)
|
|
}
|
|
if len(dungeon.Metadata.TournamentClearRewardExp) != 5 || dungeon.Metadata.TournamentClearRewardExp[4].Experience != 200000 {
|
|
t.Fatalf("clear reward experience=%+v", dungeon.Metadata.TournamentClearRewardExp)
|
|
}
|
|
|
|
mapDoc := parseTestDocument(t, "map/tournament.map", `
|
|
[dungeon]
|
|
120
|
|
[/dungeon]
|
|
[tournament start area]
|
|
1 360 270 1
|
|
[tournament start area]
|
|
1 760 270 2
|
|
[tournament enemies]
|
|
1 `+"`[monster]`"+` 56048 900 `+"`A`"+` 56047 300 `+"`B`"+`
|
|
[/tournament enemies]
|
|
`)
|
|
parsedMap := ParseMap(24001, "map/tournament.map", mapDoc)
|
|
if parsedMap.TournamentMalformed || len(parsedMap.TournamentStartAreas) != 2 || len(parsedMap.TournamentEnemies) != 2 {
|
|
t.Fatalf("tournament map=%+v", parsedMap)
|
|
}
|
|
if parsedMap.TournamentEnemies[0].Kind != "[monster]" || parsedMap.TournamentEnemies[1].Strength != 300 {
|
|
t.Fatalf("tournament enemies=%+v", parsedMap.TournamentEnemies)
|
|
}
|
|
}
|
|
|
|
func TestTournamentRuntimeBuildsBracketAndRejectsStaleDeaths(t *testing.T) {
|
|
definition := tournamentRuntimeTestDefinition()
|
|
runtime, err := NewTournamentRuntime(definition, func(maximum int) (int, error) { return 0, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(runtime.Rounds) != 4 || len(runtime.Rounds[0].Teams) != 16 || len(runtime.Rounds[3].Teams) != 2 || len(runtime.PathActors) != 4 {
|
|
t.Fatalf("rounds=%+v path=%+v", runtime.Rounds, runtime.PathActors)
|
|
}
|
|
if !runtime.BindFirstActorSequence(100) || !runtime.BindFirstActorSequence(100) || runtime.BindFirstActorSequence(101) {
|
|
t.Fatal("first actor sequence binding is not idempotent")
|
|
}
|
|
if got := runtime.ApplyActorDeath("future", 101); got.Disposition != TournamentDeathRejected {
|
|
t.Fatalf("future death=%+v", got)
|
|
}
|
|
for round := byte(1); round <= 4; round++ {
|
|
sequence := uint16(99 + round)
|
|
transition := runtime.ApplyActorDeath(string([]byte{'e', '0' + round}), sequence)
|
|
want := TournamentRoundCompleted
|
|
if round == 4 {
|
|
want = TournamentCompleted
|
|
}
|
|
if transition.Disposition != want || transition.CompletedRound != round {
|
|
t.Fatalf("round=%d transition=%+v want=%d", round, transition, want)
|
|
}
|
|
if duplicate := runtime.ApplyActorDeath(string([]byte{'e', '0' + round}), sequence); duplicate.Disposition != TournamentDeathDuplicate {
|
|
t.Fatalf("round=%d duplicate=%+v", round, duplicate)
|
|
}
|
|
}
|
|
current, completed, terminal := runtime.Snapshot()
|
|
if current != 4 || completed != 4 || terminal != TournamentChampion {
|
|
t.Fatalf("terminal snapshot=%d/%d/%d", current, completed, terminal)
|
|
}
|
|
}
|
|
|
|
func TestTournamentRuntimeRejectsInvalidRandomIndex(t *testing.T) {
|
|
_, err := NewTournamentRuntime(tournamentRuntimeTestDefinition(), func(maximum int) (int, error) { return maximum, nil })
|
|
if !errors.Is(err, ErrTournamentRandomInvalid) {
|
|
t.Fatalf("error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestTournamentRuntimeEliminationIsTerminalAndIdempotent(t *testing.T) {
|
|
runtime, err := NewTournamentRuntime(tournamentRuntimeTestDefinition(), func(maximum int) (int, error) { return 0, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !runtime.BindFirstActorSequence(100) {
|
|
t.Fatal("bind sequence")
|
|
}
|
|
if !runtime.Eliminate() || runtime.Eliminate() {
|
|
t.Fatal("elimination was not a one-way transition")
|
|
}
|
|
current, completed, terminal := runtime.Snapshot()
|
|
if current != 1 || completed != 0 || terminal != TournamentEliminated {
|
|
t.Fatalf("eliminated snapshot=%d/%d/%d", current, completed, terminal)
|
|
}
|
|
if transition := runtime.ApplyActorDeath("after-elimination", 100); transition.Disposition != TournamentDeathRejected {
|
|
t.Fatalf("post-elimination death=%+v", transition)
|
|
}
|
|
}
|
|
|
|
func tournamentRuntimeTestDefinition() TournamentDefinition {
|
|
candidates := make([]TournamentActor, 15)
|
|
for index := range candidates {
|
|
candidates[index] = TournamentActor{Code: uint32(56000 + index), Strength: uint16(100 + index), Name: "actor", Level: 72}
|
|
}
|
|
return TournamentDefinition{DungeonID: 120, MapID: 24001, DifficultyLevel: 72, PartyCount: 1, Candidates: candidates}
|
|
}
|
|
|
|
func TestTournamentRewardPlanUsesUpperCompletedRoundsAndChampionLowerKey(t *testing.T) {
|
|
definition := TournamentDefinition{
|
|
PartyCount: 1,
|
|
ResultCards: map[byte]TournamentResultCard{
|
|
0: {ResultKey: 0, EmptyWeight: 1}, 1: {ResultKey: 1, EmptyWeight: 1},
|
|
2: {ResultKey: 2, EmptyWeight: 1}, 3: {ResultKey: 3, EmptyWeight: 1},
|
|
4: {ResultKey: 4, GoldWeight: 1}, 5: {ResultKey: 5, ItemWeight: 1},
|
|
},
|
|
RewardItems: []TournamentRewardItemRate{{ItemID: 3329, Weight: 1, Count: 3}},
|
|
}
|
|
runtime := &TournamentRuntime{Definition: definition, terminal: TournamentChampion, completedRounds: tournamentRoundCount}
|
|
plan, created, err := runtime.PrepareRewardPlan(200000, 777, func(maximum int) (int, error) { return 0, nil })
|
|
if err != nil || !created {
|
|
t.Fatalf("prepare plan created=%t err=%v", created, err)
|
|
}
|
|
for index, reward := range plan.Upper {
|
|
if !reward.Gold || reward.Count != 777 || reward.Empty {
|
|
t.Fatalf("upper[%d]=%+v, want frozen gold", index, reward)
|
|
}
|
|
}
|
|
for index, reward := range plan.Lower {
|
|
if reward.ItemID != 3329 || reward.Count != 3 || reward.Gold || reward.Empty {
|
|
t.Fatalf("lower[%d]=%+v, want item", index, reward)
|
|
}
|
|
}
|
|
second, created, err := runtime.PrepareRewardPlan(1, 2, func(maximum int) (int, error) { return 0, nil })
|
|
if err != nil || created || second.Experience != 200000 || second.Upper[0].Count != 777 {
|
|
t.Fatalf("second prepare created=%t plan=%+v err=%v", created, second, err)
|
|
}
|
|
}
|
|
|
|
func TestTournamentRewardPlanSelectionIsTwoPhaseAndIdempotent(t *testing.T) {
|
|
plan := TournamentRewardPlan{Champion: false, Upper: [2]TournamentReward{{ItemID: 3329, Count: 3}}}
|
|
plan.initSelection()
|
|
reward, reserved, replay := plan.ReserveSelection(0, 0)
|
|
if !reserved || replay || reward.ItemID != 3329 {
|
|
t.Fatalf("reserve=%+v reserved=%t replay=%t", reward, reserved, replay)
|
|
}
|
|
if _, second, _ := plan.ReserveSelection(0, 1); second {
|
|
t.Fatal("second card reservation unexpectedly accepted")
|
|
}
|
|
if !plan.CommitSelection(0, 0) || !plan.IsSelectionComplete() {
|
|
t.Fatal("committed non-champion selection is not complete")
|
|
}
|
|
replayReward, replayReserved, replay := plan.ReserveSelection(0, 0)
|
|
if replayReserved || !replay || replayReward.ItemID != 3329 {
|
|
t.Fatalf("replay=%+v reserved=%t replay=%t", replayReward, replayReserved, replay)
|
|
}
|
|
if _, differentReserved, differentReplay := plan.ReserveSelection(0, 1); differentReserved || differentReplay {
|
|
t.Fatalf("different card was accepted after commit: reserved=%t replay=%t", differentReserved, differentReplay)
|
|
}
|
|
}
|
|
|
|
func TestTournamentRewardPlanSelectedCardOnlyReportsCommittedSelection(t *testing.T) {
|
|
plan := TournamentRewardPlan{Champion: true}
|
|
plan.initSelection()
|
|
if _, ok := plan.SelectedCard(0); ok {
|
|
t.Fatal("unselected card was projected")
|
|
}
|
|
if _, reserved, _ := plan.ReserveSelection(0, 1); !reserved {
|
|
t.Fatal("selection was not reserved")
|
|
}
|
|
if _, ok := plan.SelectedCard(0); ok {
|
|
t.Fatal("uncommitted card was projected")
|
|
}
|
|
if !plan.CommitSelection(0, 1) {
|
|
t.Fatal("selection was not committed")
|
|
}
|
|
if index, ok := plan.SelectedCard(0); !ok || index != 1 {
|
|
t.Fatalf("selected card index=%d ok=%t, want 1/true", index, ok)
|
|
}
|
|
}
|
|
|
|
func TestTournamentCardlessRewardPlanCommitsWithoutSelectionRequest(t *testing.T) {
|
|
runtime := &TournamentRuntime{
|
|
Definition: TournamentDefinition{ResultCards: map[byte]TournamentResultCard{
|
|
4: {ResultKey: 4, EmptyWeight: 1},
|
|
5: {ResultKey: 5, EmptyWeight: 1},
|
|
}},
|
|
terminal: TournamentChampion,
|
|
completedRounds: tournamentRoundCount,
|
|
}
|
|
plan, _, err := runtime.PrepareRewardPlan(123, 456, func(maximum int) (int, error) { return 0, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !plan.Cardless() || runtime.RewardSelectionComplete() {
|
|
t.Fatalf("initial cardless plan=%+v complete=%t", plan, runtime.RewardSelectionComplete())
|
|
}
|
|
if !runtime.CommitCardlessRewardPlan() || !runtime.RewardSelectionComplete() {
|
|
t.Fatal("cardless reward plan did not cross the delivery gate")
|
|
}
|
|
if !runtime.CommitCardlessRewardPlan() {
|
|
t.Fatal("cardless reward completion replay was not idempotent")
|
|
}
|
|
}
|