服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
112 lines
4.5 KiB
Go
112 lines
4.5 KiB
Go
package dnfbridge
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
// 现场 2026-10-03(服主报「塔罗牌用不了」,角色 1 礼白666):连点 6 次,6 次的拒绝
|
||
// 原因**逐字相同**,都是奖池的第一项:
|
||
//
|
||
// reason=random reward item requires a complete runtime-PVF outcome table:
|
||
// item=108040415 kind="equipment" target_list=0 slots=9..64
|
||
//
|
||
// 抽 6 次全是同一项,概率上不可能是运气(1/4 的表,6 次全中同一项的几率是 1/4096)。
|
||
// 追下去发现 stackable_lottery_pvf.go 里:
|
||
//
|
||
// roll := int64(0)
|
||
// if s.dungeonChoice != nil { ... } // 只在测试里被赋值
|
||
//
|
||
// —— `dungeonChoice` 在全仓**非测试代码里没有任何一处赋值**,线上它恒为 nil,于是
|
||
// roll 永远停在 0,`dungeonDropLotteryPick(entries, 0)` 永远返回第一项。这张卡从来
|
||
// 没有真正抽过。
|
||
//
|
||
// 本用例就是这条的回归锁:**不注入随机源**(线上就是这样)时也必须真的掷骰子。
|
||
func TestTarotLotteryRollsWithoutAnInjectedRandomSource(t *testing.T) {
|
||
catalog := mustCurrentTarotLotteryTestCatalog(t)
|
||
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(definition.LotteryEntries) != 2 {
|
||
t.Fatalf("夹具奖池=%d 项 want 2(这条用例的前提)", len(definition.LotteryEntries))
|
||
}
|
||
// 刻意**不**设 service.dungeonChoice:这就是线上的形状。
|
||
service := &Service{pvfItemCatalog: catalog}
|
||
|
||
const draws = 200
|
||
first, second := definition.LotteryEntries[0].ItemID, definition.LotteryEntries[1].ItemID
|
||
seen := map[int64]int{}
|
||
sawRollOutsideRange := false
|
||
for i := 0; i < draws; i++ {
|
||
resolution, resolveErr := service.resolveCurrentTarotLotteryCard(definition)
|
||
if resolveErr != nil {
|
||
t.Fatalf("第 %d 次抽取失败: %v", i, resolveErr)
|
||
}
|
||
if resolution == nil || resolution.Drawn == nil {
|
||
t.Fatalf("第 %d 次抽取没有结果(missed=%t)", i, resolution != nil && resolution.Missed)
|
||
}
|
||
if resolution.Roll < 0 || resolution.Roll >= resolution.TotalWeight {
|
||
sawRollOutsideRange = true
|
||
}
|
||
seen[resolution.Drawn.Reward.ItemID]++
|
||
}
|
||
if sawRollOutsideRange {
|
||
t.Fatalf("%d 次抽取里有 roll 落在 [0,total) 之外", draws)
|
||
}
|
||
// 权重 10000:10000,200 次里两项都该出现。钉死"不止一项"就够 ——
|
||
// 旧代码(roll 恒 0)在这里必红:它只可能产出第一项。
|
||
if seen[second] == 0 {
|
||
t.Fatalf("抽了 %d 次,第二项 %d 一次都没出现(命中分布=%v)——"+
|
||
"roll 又被钉死了", draws, second, seen)
|
||
}
|
||
if seen[first] == 0 {
|
||
t.Fatalf("抽了 %d 次,第一项 %d 一次都没出现(命中分布=%v)", draws, first, seen)
|
||
}
|
||
t.Logf("dungeonChoice=nil(线上形状)下 %d 次抽取分布=%v", draws, seen)
|
||
}
|
||
|
||
// 注入源在时必须仍然优先,这样既有测试与"可复现抽样"的能力不被这次兜底破坏。
|
||
func TestTarotLotteryPrefersTheInjectedRandomSource(t *testing.T) {
|
||
catalog := mustCurrentTarotLotteryTestCatalog(t)
|
||
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
service := &Service{pvfItemCatalog: catalog}
|
||
second := definition.LotteryEntries[1]
|
||
boundary := definition.LotteryEntries[0].Weight // 第二项的起始 roll
|
||
service.dungeonChoice = func(int) (int, error) { return int(boundary), nil }
|
||
|
||
resolution, err := service.resolveCurrentTarotLotteryCard(definition)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if resolution == nil || resolution.Drawn == nil {
|
||
t.Fatal("注入源下没有抽到结果")
|
||
}
|
||
if got := resolution.Drawn.Reward.ItemID; got != second.ItemID {
|
||
t.Fatalf("注入 roll=%d 抽到 %d want %d(第二项)", boundary, got, second.ItemID)
|
||
}
|
||
if resolution.Roll != boundary {
|
||
t.Fatalf("resolution.Roll=%d want %d", resolution.Roll, boundary)
|
||
}
|
||
}
|
||
|
||
// 注入源越界必须拒绝,而不是被静默夹到边界上:宁可这次不生效,也不能给出错误的奖。
|
||
func TestTarotLotteryRejectsAnOutOfRangeInjectedRoll(t *testing.T) {
|
||
catalog := mustCurrentTarotLotteryTestCatalog(t)
|
||
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
service := &Service{pvfItemCatalog: catalog}
|
||
total := dungeonDropLotteryTotalWeight(definition.LotteryEntries)
|
||
if total <= 0 {
|
||
t.Fatal("夹具奖池总权重必须为正")
|
||
}
|
||
service.dungeonChoice = func(int) (int, error) { return int(total), nil } // total 已在区间外
|
||
if _, err := service.resolveCurrentTarotLotteryCard(definition); err == nil {
|
||
t.Fatal("越界的注入 roll 必须报错,不能被当成合法抽取")
|
||
}
|
||
}
|