DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,122 @@
package inventory
import (
"math"
"strings"
"longheng.io/server/internal/modules/dnf/alignedcmd"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// enchantRandomWeightBase is the er_enchant weight base: a candidate weight of
// 100000 is a certain outcome.
const enchantRandomWeightBase = 100000
// enchantRandomRestrictionAllows applies the [enchant random] er_slot /
// er_grade / er_seal target gates shared by the op50 reinforcement ticket
// scene and the op438 weapon-forging ticket family: er_slot 0 allows every
// part, 1 weapons, 2 armor, 3 accessories; an empty er_grade list allows every
// rarity; only er_seal == 1 demands a sealed target (the common er_seal 0/2
// carries no check).
func enchantRandomRestrictionAllows(ticket alignedcmd.EnchantRandomTicket, equipmentType string, rarity int64, target dnfrepo.ItemStack) bool {
switch ticket.SlotRestriction {
case 0:
case 1, 2, 3:
if enchantRandomSlotClass(equipmentType) != ticket.SlotRestriction {
return false
}
default:
return false
}
if len(ticket.Rarities) > 0 {
allowed := false
for _, candidate := range ticket.Rarities {
if candidate == rarity {
allowed = true
break
}
}
if !allowed {
return false
}
}
if ticket.SealRestriction == 1 && firstExtraInt(target.Extra, 0, "seal_flag", "seal") == 0 {
return false
}
return true
}
// enchantRandomSlotClass folds a PVF [equipment type] token into the er_slot
// classes: 1 weapon, 2 armor, 3 accessory, 0 anything else.
func enchantRandomSlotClass(equipmentType string) int64 {
switch strings.ToLower(strings.Trim(equipmentType, "` ")) {
case "[weapon]":
return 1
case "[coat]", "[shoulder]", "[pants]", "[shoes]", "[waist]":
return 2
case "[amulet]", "[wrist]", "[ring]":
return 3
default:
return 0
}
}
// selectEnchantRandomEntry rolls one er_enchant candidate. A single candidate
// keeps its own weight, so a one-entry ticket can still fail and retain the
// current level like a plain ticket; a weighted pick across several candidates
// always succeeds at the picked level.
func (o *Owner) selectEnchantRandomEntry(entries []alignedcmd.UpgradeTicketRandomEntry) (alignedcmd.UpgradeTicketRandomEntry, bool) {
if len(entries) == 0 {
return alignedcmd.UpgradeTicketRandomEntry{}, false
}
if len(entries) == 1 {
return entries[0], true
}
total := int64(0)
for _, entry := range entries {
if entry.Weight <= 0 {
continue
}
if total > math.MaxInt64-entry.Weight {
return alignedcmd.UpgradeTicketRandomEntry{}, false
}
total += entry.Weight
}
if total <= 0 {
return alignedcmd.UpgradeTicketRandomEntry{}, false
}
roll := o.enchantRandomIntn(total)
cursor := int64(0)
picked := alignedcmd.UpgradeTicketRandomEntry{}
for _, entry := range entries {
if entry.Weight <= 0 {
continue
}
picked = entry
cursor += entry.Weight
if roll < cursor {
break
}
}
return alignedcmd.UpgradeTicketRandomEntry{TargetLevel: picked.TargetLevel, Weight: enchantRandomWeightBase}, true
}
// enchantRandomIntn draws from the injected random source so ticket rolls stay
// deterministic under test, mirroring chronicleRandomIntn.
func (o *Owner) enchantRandomIntn(limit int64) int64 {
if limit <= 1 || o == nil || o.enchantRandom == nil {
return 0
}
bounded := limit
if bounded > math.MaxInt32 {
bounded = math.MaxInt32
}
value := int64(o.enchantRandom(int(bounded)))
if value < 0 {
return 0
}
if value >= limit {
return value % limit
}
return value
}