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,147 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnfdrops "longheng.io/server/internal/modules/dnf/drop"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestCurrentDungeonRandomClearCardRewardNeverFallsBackToStackable(t *testing.T) {
|
||||
const (
|
||||
dungeonLevel = 84
|
||||
equipmentID = uint32(1001)
|
||||
projectileID = uint32(2001)
|
||||
)
|
||||
rule := dnfdrops.ClearRewardRule{
|
||||
EquipmentRate: dnfdrops.ClearRewardItemRateDenominator,
|
||||
GradeUp: 1,
|
||||
RarityThresholds: [7]uint32{1, 1, 1, 1, 1, 1, 1},
|
||||
RarityScale: 1,
|
||||
}
|
||||
stackableOnly := &pvfDungeonDropCatalog{
|
||||
source: dungeonDropCatalogTestSource{},
|
||||
itemCache: map[uint32]dungeonDropItemDefinition{},
|
||||
genericLoaded: true,
|
||||
genericStackables: map[dungeonDropGradeRarityKey][]uint32{
|
||||
{Grade: dungeonLevel, Rarity: 0}: {projectileID},
|
||||
},
|
||||
genericEquipment: map[dungeonDropGradeRarityKey][]dungeonDropWeightedItem{},
|
||||
}
|
||||
bundle, err := currentDungeonRandomCardRewardBundle(
|
||||
newCurrentDungeonDropLCG(1),
|
||||
currentDungeonGoldReference{Level: dungeonLevel, Base: 1_000},
|
||||
currentDungeonCardGoldTerms{PartyMembers: 1, PartyRate: 1, WeightedKills: 40},
|
||||
stackableOnly,
|
||||
dungeonLevel,
|
||||
rule,
|
||||
time.Now().UTC(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(bundle.Items) != 0 || bundle.Gold <= 0 {
|
||||
t.Fatalf("stackable fallback leaked into ordinary card: %+v", bundle)
|
||||
}
|
||||
|
||||
withEquipment := &pvfDungeonDropCatalog{
|
||||
source: dungeonDropCatalogTestSource{},
|
||||
itemCache: map[uint32]dungeonDropItemDefinition{
|
||||
equipmentID: {
|
||||
ItemID: equipmentID, Kind: dungeonDropItemEquipment,
|
||||
PVFPath: "equipment/test.equ", Grade: dungeonLevel,
|
||||
},
|
||||
},
|
||||
genericLoaded: true,
|
||||
genericStackables: map[dungeonDropGradeRarityKey][]uint32{
|
||||
{Grade: dungeonLevel, Rarity: 0}: {projectileID},
|
||||
},
|
||||
genericEquipment: map[dungeonDropGradeRarityKey][]dungeonDropWeightedItem{
|
||||
{Grade: dungeonLevel, Rarity: 0}: {{ItemID: equipmentID, Weight: 1}},
|
||||
},
|
||||
}
|
||||
bundle, err = currentDungeonRandomCardRewardBundle(
|
||||
newCurrentDungeonDropLCG(1),
|
||||
currentDungeonGoldReference{Level: dungeonLevel, Base: 1_000},
|
||||
currentDungeonCardGoldTerms{PartyMembers: 1, PartyRate: 1, WeightedKills: 40},
|
||||
withEquipment,
|
||||
dungeonLevel,
|
||||
rule,
|
||||
time.Now().UTC(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(bundle.Items) != 1 || bundle.Items[0].ItemID != int64(equipmentID) || bundle.Items[0].Stackable {
|
||||
t.Fatalf("equipment clear-card reward=%+v", bundle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentDungeonClearCardEquipmentRarityUsesPVFThresholds(t *testing.T) {
|
||||
thresholds := [7]uint32{50, 80, 99, 100, 101, 102, 103}
|
||||
tests := []struct {
|
||||
roll uint32
|
||||
want int
|
||||
}{{0, 0}, {49, 0}, {50, 1}, {79, 1}, {80, 2}, {102, 6}, {103, 6}}
|
||||
for _, test := range tests {
|
||||
if got := currentDungeonClearCardEquipmentRarity(thresholds, test.roll); got != test.want {
|
||||
t.Fatalf("roll=%d rarity=%d want=%d", test.roll, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFDungeonCardEquipmentPoolAudit(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to audit real Script.pvf clear-card pools")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules, err := dnfdrops.LoadClearRewardRules(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rule, err := rules.Resolve(84, 0, 1, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// [gold card cost table] is the only source of the commission op35 prints
|
||||
// under 黄金卡牌 N (the 86JP reference's GetPaidCardCost reads the same
|
||||
// rows). Resolve already rejects a missing row, so a zero here would mean
|
||||
// the table collapsed to a free card and every gold card would render 0 金币.
|
||||
if rule.PaidCardCost == 0 {
|
||||
t.Fatalf("real level-84 gold card commission resolved to zero: rule=%+v", rule)
|
||||
}
|
||||
catalog, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := catalog.ensureGenericPools(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if candidates := catalog.genericEquipmentCandidates(84, rule.GradeDown, rule.GradeUp, 0); len(candidates) == 0 {
|
||||
t.Fatal("real level-84 clear-card equipment pool is empty")
|
||||
}
|
||||
for seed := uint32(1); seed <= 256; seed++ {
|
||||
item, found, rewardErr := currentDungeonRandomClearCardEquipment(
|
||||
newCurrentDungeonDropLCG(seed), catalog, 84, rule, time.Now().UTC(),
|
||||
)
|
||||
if rewardErr != nil {
|
||||
t.Fatal(rewardErr)
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
definition, resolveErr := catalog.ResolveItem(uint32(item.ItemID))
|
||||
if resolveErr != nil {
|
||||
t.Fatal(resolveErr)
|
||||
}
|
||||
if definition.Kind != dungeonDropItemEquipment || item.Stackable {
|
||||
t.Fatalf("seed=%d ordinary card selected non-equipment: item=%+v definition=%+v", seed, item, definition)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user