服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
324 lines
9.0 KiB
Go
324 lines
9.0 KiB
Go
package guild
|
|
|
|
import "testing"
|
|
|
|
// The layout file nests [dungeon] blocks with their own subsections, and the
|
|
// section names span several whitespace-separated fields, so the scanner is the
|
|
// risky part. This fixture is a trimmed copy of the real
|
|
// etc/guild/guilddungeon/0_2Way6Dungeon.etc.
|
|
const dungeonCatalogFixture = `
|
|
[weekly reward]
|
|
|
|
[final dungeon]
|
|
8508
|
|
[/weekly reward]
|
|
|
|
[dungeon]
|
|
|
|
[dungeon index]
|
|
8500
|
|
[gauge limit]
|
|
10000
|
|
[gauge on clear]
|
|
150
|
|
[conquest gauge ratio]
|
|
50
|
|
[color]
|
|
` + "`black`" + `
|
|
[type]
|
|
` + "`normal`" + `
|
|
[reward]
|
|
|
|
[gauge ratio]
|
|
99
|
|
[item]
|
|
10154439 3
|
|
[contribute]
|
|
20
|
|
[/gauge ratio]
|
|
|
|
[/reward]
|
|
|
|
[/dungeon]
|
|
|
|
[dungeon]
|
|
|
|
[dungeon index]
|
|
8504
|
|
[gauge limit]
|
|
10000
|
|
[gauge on clear]
|
|
75
|
|
[conquest gauge ratio]
|
|
50
|
|
[color]
|
|
` + "`red`" + `
|
|
[type]
|
|
` + "`normal`" + `
|
|
[required dungeon conquest]
|
|
8500
|
|
[/required dungeon conquest]
|
|
|
|
[/dungeon]
|
|
|
|
[dungeon]
|
|
|
|
[dungeon index]
|
|
8503
|
|
[gauge limit]
|
|
10000
|
|
[gauge on clear]
|
|
75
|
|
[conquest gauge ratio]
|
|
50
|
|
[color]
|
|
` + "`blue`" + `
|
|
[type]
|
|
` + "`normal`" + `
|
|
|
|
[/dungeon]
|
|
|
|
[dungeon]
|
|
|
|
[dungeon index]
|
|
8505
|
|
[gauge limit]
|
|
10000
|
|
[gauge on clear]
|
|
50
|
|
[conquest gauge ratio]
|
|
50
|
|
[color]
|
|
` + "`red`" + `
|
|
[type]
|
|
` + "`hard`" + `
|
|
|
|
[/dungeon]
|
|
|
|
[dungeon]
|
|
|
|
[dungeon index]
|
|
8508
|
|
[gauge limit]
|
|
10000
|
|
[gauge on clear]
|
|
30
|
|
[conquest gauge ratio]
|
|
100
|
|
[color]
|
|
` + "`red`" + `
|
|
[type]
|
|
` + "`final`" + `
|
|
[required dungeon conquest]
|
|
8503 8505
|
|
[/required dungeon conquest]
|
|
|
|
[/dungeon]
|
|
`
|
|
|
|
func TestParseDungeonCatalogReadsStagesAndPrerequisites(t *testing.T) {
|
|
catalog, err := parseDungeonCatalog(dungeonCatalogFixture)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if catalog.FinalDungeon != 8508 {
|
|
t.Fatalf("final dungeon=%d want 8508", catalog.FinalDungeon)
|
|
}
|
|
if len(catalog.Stages) != 5 {
|
|
t.Fatalf("stages=%d want 5", len(catalog.Stages))
|
|
}
|
|
|
|
first, ok := catalog.Stage(8500)
|
|
if !ok {
|
|
t.Fatal("8500 missing")
|
|
}
|
|
if first.GaugeLimit != 10000 || first.GaugeOnClear != 150 || first.ConquestGaugeRatio != 50 {
|
|
t.Fatalf("8500=%+v", first)
|
|
}
|
|
if first.Color != "black" || first.Type != "normal" {
|
|
t.Fatalf("8500 color=%q type=%q", first.Color, first.Type)
|
|
}
|
|
// The reward block's own numbers (99, 10154439, 3, 20) must not leak into
|
|
// the stage: they sit inside [reward] and belong to a different scope.
|
|
if len(first.RequiredConquest) != 0 {
|
|
t.Fatalf("8500 must have no prerequisite, got %v", first.RequiredConquest)
|
|
}
|
|
// 50% of 10000.
|
|
if got := first.ConquestGauge(); got != 5000 {
|
|
t.Fatalf("8500 conquest gauge=%d want 5000", got)
|
|
}
|
|
|
|
second, _ := catalog.Stage(8504)
|
|
if len(second.RequiredConquest) != 1 || second.RequiredConquest[0] != 8500 {
|
|
t.Fatalf("8504 prerequisites=%v want [8500]", second.RequiredConquest)
|
|
}
|
|
final, _ := catalog.Stage(8508)
|
|
if len(final.RequiredConquest) != 2 || final.RequiredConquest[0] != 8503 || final.RequiredConquest[1] != 8505 {
|
|
t.Fatalf("8508 prerequisites=%v want [8503 8505]", final.RequiredConquest)
|
|
}
|
|
if got := final.ConquestGauge(); got != 10000 {
|
|
t.Fatalf("8508 conquest gauge=%d want 10000 (100%%)", got)
|
|
}
|
|
}
|
|
|
|
// The unlock rule is what the client's own gate mirrors: a stage opens only
|
|
// once every prerequisite has reached its conquest threshold.
|
|
func TestDungeonCatalogUnlockFollowsConquest(t *testing.T) {
|
|
catalog, err := parseDungeonCatalog(dungeonCatalogFixture)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gauges := map[uint32]uint32{}
|
|
|
|
// No prerequisites: always open, which is why an empty guild still sees 伤城.
|
|
if !catalog.Unlocked(gauges, 8500) {
|
|
t.Fatal("8500 must be open with no gauges")
|
|
}
|
|
if catalog.Unlocked(gauges, 8504) {
|
|
t.Fatal("8504 must be shut before 8500 is conquered")
|
|
}
|
|
// One clear is not a conquest: 150 of the 5000 needed.
|
|
gauges[8500] = 150
|
|
if catalog.Unlocked(gauges, 8504) {
|
|
t.Fatal("8504 opened after a single clear of 8500")
|
|
}
|
|
gauges[8500] = 4999
|
|
if catalog.Unlocked(gauges, 8504) {
|
|
t.Fatal("8504 opened one point short of the threshold")
|
|
}
|
|
gauges[8500] = 5000
|
|
if !catalog.Unlocked(gauges, 8504) {
|
|
t.Fatal("8504 still shut at exactly the threshold")
|
|
}
|
|
|
|
// The final stage needs both prerequisites, and at 100%.
|
|
gauges[8503] = 10000
|
|
if catalog.Unlocked(gauges, 8508) {
|
|
t.Fatal("8508 opened with only one prerequisite conquered")
|
|
}
|
|
// 8505 conquers at 50% of 10000, so one point short must still block.
|
|
gauges[8505] = 4999
|
|
if catalog.Unlocked(gauges, 8508) {
|
|
t.Fatal("8508 opened while 8505 was below its own threshold")
|
|
}
|
|
gauges[8505] = 5000
|
|
if !catalog.Unlocked(gauges, 8508) {
|
|
t.Fatal("8508 shut with both prerequisites conquered")
|
|
}
|
|
}
|
|
|
|
func TestAddDungeonGaugeClampsToTheStageLimit(t *testing.T) {
|
|
registry := NewRegistry()
|
|
registry.Guilds[1] = &Guild{ID: 1}
|
|
// The pair is (before, after): the caller needs both to tell which reward
|
|
// thresholds this clear actually crossed.
|
|
if previous, gauge, err := registry.AddDungeonGauge(1, 8500, 150, 10000); err != nil || previous != 0 || gauge != 150 {
|
|
t.Fatalf("first clear %d->%d err=%v want 0->150", previous, gauge, err)
|
|
}
|
|
if previous, gauge, err := registry.AddDungeonGauge(1, 8500, 150, 10000); err != nil || previous != 150 || gauge != 300 {
|
|
t.Fatalf("second clear %d->%d err=%v want 150->300", previous, gauge, err)
|
|
}
|
|
if _, gauge, err := registry.AddDungeonGauge(1, 8500, 100_000, 10000); err != nil || gauge != 10000 {
|
|
t.Fatalf("clamped gauge=%d err=%v want 10000", gauge, err)
|
|
}
|
|
if _, _, err := registry.AddDungeonGauge(99, 8500, 1, 10000); err == nil {
|
|
t.Fatal("an unknown guild must not accumulate a gauge")
|
|
}
|
|
}
|
|
|
|
// Rewards are paid on crossing a [gauge ratio] line, once. A repeat clear past
|
|
// the line must not pay again, and a clear that jumps several lines at once
|
|
// pays each of them.
|
|
func TestRewardsCrossedPaysEachThresholdOnce(t *testing.T) {
|
|
stage := DungeonStage{
|
|
GaugeLimit: 10000,
|
|
GaugeOnClear: 150,
|
|
Rewards: []DungeonReward{
|
|
{GaugeRatio: 99, ItemID: 10154439, ItemCount: 3, Contribute: 20},
|
|
{GaugeRatio: 100, ItemID: 10154439, ItemCount: 3, Contribute: 20},
|
|
},
|
|
}
|
|
if got := stage.RewardsCrossed(0, 150); len(got) != 0 {
|
|
t.Fatalf("an early clear paid %d rewards", len(got))
|
|
}
|
|
// 99% of 10000 is 9900.
|
|
if got := stage.RewardsCrossed(9800, 9900); len(got) != 1 || got[0].GaugeRatio != 99 {
|
|
t.Fatalf("crossing 99%% paid %+v", got)
|
|
}
|
|
// Already past it: nothing more until 100%.
|
|
if got := stage.RewardsCrossed(9900, 9950); len(got) != 0 {
|
|
t.Fatalf("staying above 99%% paid again: %+v", got)
|
|
}
|
|
if got := stage.RewardsCrossed(9950, 10000); len(got) != 1 || got[0].GaugeRatio != 100 {
|
|
t.Fatalf("crossing 100%% paid %+v", got)
|
|
}
|
|
// One clear that jumps both lines pays both.
|
|
if got := stage.RewardsCrossed(0, 10000); len(got) != 2 {
|
|
t.Fatalf("a full jump paid %d rewards, want 2", len(got))
|
|
}
|
|
}
|
|
|
|
// The claim marker is guild-wide and survives within the week, so a second
|
|
// member cannot collect the same threshold.
|
|
func TestClaimDungeonRewardIsOncePerGuildPerThreshold(t *testing.T) {
|
|
registry := NewRegistry()
|
|
registry.Guilds[1] = &Guild{ID: 1}
|
|
claimed, err := registry.ClaimDungeonReward(1, 8500, 99)
|
|
if err != nil || !claimed {
|
|
t.Fatalf("first claim=%t err=%v", claimed, err)
|
|
}
|
|
claimed, err = registry.ClaimDungeonReward(1, 8500, 99)
|
|
if err != nil || claimed {
|
|
t.Fatalf("second claim=%t err=%v, must not pay twice", claimed, err)
|
|
}
|
|
// A different threshold, and a different stage, are separate claims.
|
|
if claimed, _ := registry.ClaimDungeonReward(1, 8500, 100); !claimed {
|
|
t.Fatal("the 100%% threshold was blocked by the 99%% claim")
|
|
}
|
|
if claimed, _ := registry.ClaimDungeonReward(1, 8504, 99); !claimed {
|
|
t.Fatal("another stage was blocked by the first stage's claim")
|
|
}
|
|
}
|
|
|
|
// A new week wipes gauges and claims so the cycle restarts; the same week is a
|
|
// no-op, which is what keeps a clear from resetting the bar it just raised.
|
|
func TestResetDungeonWeekClearsProgressOnlyOnANewWeek(t *testing.T) {
|
|
registry := NewRegistry()
|
|
registry.Guilds[1] = &Guild{ID: 1}
|
|
if _, _, err := registry.AddDungeonGauge(1, 8500, 9000, 10000); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := registry.ClaimDungeonReward(1, 8500, 99); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// First settle of a fresh guild stamps the week without wiping anything.
|
|
if reset, err := registry.ResetDungeonWeek(1, 100); err != nil || !reset {
|
|
t.Fatalf("first settle reset=%t err=%v", reset, err)
|
|
}
|
|
if len(registry.DungeonGauges(1)) != 0 {
|
|
t.Fatal("the first settle should have cleared the pre-existing progress")
|
|
}
|
|
|
|
if _, _, err := registry.AddDungeonGauge(1, 8500, 600, 10000); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Same week: progress must survive.
|
|
if reset, err := registry.ResetDungeonWeek(1, 100); err != nil || reset {
|
|
t.Fatalf("same-week reset=%t err=%v", reset, err)
|
|
}
|
|
if registry.DungeonGauges(1)[8500] != 600 {
|
|
t.Fatalf("same-week settle wiped the gauge: %v", registry.DungeonGauges(1))
|
|
}
|
|
// Next week: wiped, claims too.
|
|
if reset, err := registry.ResetDungeonWeek(1, 101); err != nil || !reset {
|
|
t.Fatalf("new-week reset=%t err=%v", reset, err)
|
|
}
|
|
if len(registry.DungeonGauges(1)) != 0 {
|
|
t.Fatalf("new week kept gauges: %v", registry.DungeonGauges(1))
|
|
}
|
|
if claimed, _ := registry.ClaimDungeonReward(1, 8500, 99); !claimed {
|
|
t.Fatal("new week kept last week's reward claim")
|
|
}
|
|
}
|