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,245 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user