服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
111 lines
4.5 KiB
Go
111 lines
4.5 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"testing"
|
|
|
|
dnfguild "longheng.io/server/internal/modules/dnf/guild"
|
|
)
|
|
|
|
// 运营规则(2026-09-15 起):每通关 10 次公会地下城发 1 个公会地下城奖励礼盒,
|
|
// 没有上限,也就是每通关一次涨 10%。(此前是 20 次 / 5%。)
|
|
func TestCurrentGuildDungeonRepeatBonusOwed(t *testing.T) {
|
|
for _, row := range []struct {
|
|
name string
|
|
before int64
|
|
after int64
|
|
want int64
|
|
}{
|
|
{name: "第一次通关", before: 0, after: 1, want: 0},
|
|
{name: "第 9 次仍未满", before: 8, after: 9, want: 0},
|
|
{name: "第 10 次发一个", before: 9, after: 10, want: 1},
|
|
{name: "第 11 次不再发", before: 10, after: 11, want: 0},
|
|
{name: "第 20 次再发一个(无上限)", before: 19, after: 20, want: 1},
|
|
{name: "一次跨过两个整块就补两个", before: 9, after: 21, want: 2},
|
|
{name: "计数没动就不发", before: 10, after: 10, want: 0},
|
|
{name: "重放(after 落后)不发", before: 40, after: 20, want: 0},
|
|
{name: "负数计数按 0 起算", before: -5, after: 1, want: 0},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
if got := currentGuildDungeonRepeatBonusOwed(row.before, row.after); got != row.want {
|
|
t.Fatalf("owed(%d -> %d) = %d, want %d", row.before, row.after, got, row.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCurrentGuildDungeonRepeatBonusPercent(t *testing.T) {
|
|
for _, row := range []struct {
|
|
count int64
|
|
want int64
|
|
}{
|
|
{count: 0, want: 0},
|
|
{count: 1, want: 10},
|
|
{count: 2, want: 20},
|
|
{count: 5, want: 50},
|
|
{count: 9, want: 90},
|
|
{count: 10, want: 0},
|
|
{count: 11, want: 10},
|
|
{count: 21, want: 10},
|
|
} {
|
|
if got := currentGuildDungeonRepeatBonusPercent(row.count); got != row.want {
|
|
t.Fatalf("percent(%d) = %d, want %d", row.count, got, row.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 百分比必须是从次数阈值算出来的,不是另写的一个常量:改阈值时两者不能脱节。
|
|
func TestCurrentGuildDungeonRepeatBonusPercentTracksTheThreshold(t *testing.T) {
|
|
step := 100 / currentGuildDungeonRepeatClearsPerBonus
|
|
for clears := int64(1); clears < currentGuildDungeonRepeatClearsPerBonus; clears++ {
|
|
if got, want := currentGuildDungeonRepeatBonusPercent(clears), clears*step; got != want {
|
|
t.Fatalf("percent(%d) = %d, want %d", clears, got, want)
|
|
}
|
|
}
|
|
if got := currentGuildDungeonRepeatBonusPercent(currentGuildDungeonRepeatClearsPerBonus); got != 0 {
|
|
t.Fatalf("a completed block reads %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
// 礼盒与净化槽必须是两套独立的东西:槽只在 99%/100% 发公会贡献证书。
|
|
func TestCurrentGuildDungeonRepeatBonusIsNotThePurificationGauge(t *testing.T) {
|
|
if currentGuildDungeonRepeatClearBonusItem == 10154439 {
|
|
t.Fatal("repeat-clear bonus must not be the contribution certificate")
|
|
}
|
|
if currentGuildDungeonRepeatClearBonusItem != 10156320 {
|
|
t.Fatalf("repeat-clear bonus item = %d, want 10156320 公会地下城奖励礼盒",
|
|
currentGuildDungeonRepeatClearBonusItem)
|
|
}
|
|
if currentGuildDungeonRepeatClearsPerBonus != 10 {
|
|
t.Fatalf("clears per bonus = %d, want 10", currentGuildDungeonRepeatClearsPerBonus)
|
|
}
|
|
}
|
|
|
|
// 净化率翻倍只能在入账侧做:客户端的 clears * [gauge on clear] / [gauge limit]
|
|
// 在它自己的 PVF 里,服务端改不动。一次通关记 2 份槽,除回来就是 2 次,8500 的
|
|
// 1.5% 因此显示成 3%。
|
|
func TestCurrentGuildDungeonGaugeOnClearIsDoubled(t *testing.T) {
|
|
stage := dnfguild.DungeonStage{GaugeOnClear: 150, GaugeLimit: 10000}
|
|
if got := currentGuildDungeonGaugeOnClear(stage); got != 300 {
|
|
t.Fatalf("gauge on clear = %d, want 300 (原版 150 的两倍)", got)
|
|
}
|
|
// 除回来给客户端的次数必须跟着翻倍,否则显示不会变。
|
|
if got := currentGuildDungeonClears(stage, currentGuildDungeonGaugeOnClear(stage)); got != 2 {
|
|
t.Fatalf("clears after one run = %d, want 2 so the client draws 3%%", got)
|
|
}
|
|
// 满槽仍然封顶在 100%,不会因为翻倍而画过头。
|
|
if got := currentGuildDungeonClears(stage, 999999); got != stage.GaugeLimit/stage.GaugeOnClear {
|
|
t.Fatalf("over-credited gauge rendered %d clears, want the full-bar clamp", got)
|
|
}
|
|
}
|
|
|
|
func TestCurrentGuildDungeonGaugeOnClearHandlesDegenerateStages(t *testing.T) {
|
|
if got := currentGuildDungeonGaugeOnClear(dnfguild.DungeonStage{}); got != 0 {
|
|
t.Fatalf("a stage with no gauge credited %d, want 0", got)
|
|
}
|
|
// 翻倍不能溢出成一个更小的数。
|
|
huge := dnfguild.DungeonStage{GaugeOnClear: ^uint32(0) - 1}
|
|
if got := currentGuildDungeonGaugeOnClear(huge); got != huge.GaugeOnClear {
|
|
t.Fatalf("overflowing stage credited %d, want the un-doubled %d", got, huge.GaugeOnClear)
|
|
}
|
|
}
|