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,142 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
)
|
||||
|
||||
// 超时空的小队编制:**8 个编制、每队只能 1 人**,而且把 B 拖进已经有 A 的队时
|
||||
// **A 被挤成单人**(不是整脚拒绝)。
|
||||
//
|
||||
// 操作员 2026-10-02 的定案:
|
||||
//
|
||||
// 「超时空总共有8个编制 跟其他攻坚战不一样的是 每个小队编制仅可放1人
|
||||
// 最有力的证明是 小队编制界面 每个小队后面三格都是锁住的
|
||||
// 就比如说 团长给A放在2队 然后再给B放在2队 A就应该变单人了
|
||||
// 一个小队仅能有1人(所以进图就是我们做的同区域的人一起进图 仅超时空这样)」
|
||||
//
|
||||
// 安东/卢克一个字不改:满队仍然是整脚拒绝(TestRuntimeRaidManagerMoveRejectsFullSubgroup
|
||||
// 钉着那一半)。
|
||||
func TestFiendWarSquadHoldsExactlyOneMemberAndEvictsThePrevious(t *testing.T) {
|
||||
const raidKey = uint32(0x00c80410)
|
||||
channel := channelcatalog.Channel{ID: 184, Type: 42}
|
||||
service := &Service{raids: make(map[uint32]*runtimeRaidState), gameSessions: map[uint16]*gameSession{}}
|
||||
members := []runtimeRaidMemberState{
|
||||
{CharID: 1001, Name: "A", GroupIndex: 2, SlotOrder: 0},
|
||||
{CharID: 1002, Name: "B", GroupIndex: 0, SlotOrder: 0},
|
||||
{CharID: 1003, Name: "C", GroupIndex: 1, SlotOrder: 0},
|
||||
}
|
||||
state := &runtimeRaidState{
|
||||
RaidKey: raidKey,
|
||||
LeaderID: 1001,
|
||||
RaidMode: runtimeRaidOwnerFiendWar,
|
||||
RouteOrRaidType: 2,
|
||||
TailFlag: 0,
|
||||
Members: append([]runtimeRaidMemberState(nil), members...),
|
||||
FrozenMembers: append([]runtimeRaidMemberState(nil), members...),
|
||||
}
|
||||
service.raids[raidKey] = state
|
||||
leader := &gameSession{channel: channel, conn: &bufferConn{}}
|
||||
service.bindGameSessionCharacter(leader, 1001)
|
||||
|
||||
// 尺子:超时空 8 个编制、每队 1 人。
|
||||
if got := runtimeRaidGroupSizeFor(*state); got != 1 {
|
||||
t.Fatalf("超时空每队容量 = %d,want 1", got)
|
||||
}
|
||||
if got := runtimeRaidGroupMaxFor(*state); got != currentFiendWarSquadCount {
|
||||
t.Fatalf("超时空编制数 = %d,want %d", got, currentFiendWarSquadCount)
|
||||
}
|
||||
// 同一个团的安东口径没有被带偏。
|
||||
anton := *state
|
||||
anton.RaidMode = runtimeRaidOwnerAnton
|
||||
if got := runtimeRaidGroupSizeFor(anton); got != runtimeRaidGroupSize {
|
||||
t.Fatalf("安东每队容量被改了:%d,want %d", got, runtimeRaidGroupSize)
|
||||
}
|
||||
|
||||
// [本用例的核心] 把 B 拖进 A 所在的 2 队:B 进 2 队,A 被挤成单人。
|
||||
moved, ok := service.moveRuntimeRaidMember(1001, 1002, 2)
|
||||
if !ok {
|
||||
t.Fatal("把 B 放进已经有人的 2 队被拒了 —— 超时空应当挤人,不是拒绝")
|
||||
}
|
||||
bIndex := runtimeRaidMemberIndex(moved, 1002)
|
||||
aIndex := runtimeRaidMemberIndex(moved, 1001)
|
||||
if bIndex < 0 || moved.Members[bIndex].GroupIndex != 2 {
|
||||
t.Fatalf("B 没有落到 2 队:%+v", moved.Members[bIndex])
|
||||
}
|
||||
if aIndex < 0 || moved.Members[aIndex].GroupIndex != 0 || moved.Members[aIndex].SlotOrder != 0 {
|
||||
t.Fatalf("A 没有被挤成单人(应回到未编队):%+v", moved.Members[aIndex])
|
||||
}
|
||||
// 冻结名册(进图名单/清数记账/战况表都读它)必须一起改。
|
||||
frozenA, okA := runtimeRaidFrozenMember(moved, 1001)
|
||||
if !okA || frozenA.GroupIndex != 0 {
|
||||
t.Fatalf("冻结名册里的 A 还挂在 2 队:%+v", frozenA)
|
||||
}
|
||||
// 一个编制里只能有一个人。
|
||||
if got := runtimeRaidGroupCountExcluding(moved, 2, 0); got != 1 {
|
||||
t.Fatalf("2 队里有 %d 个人,want 1", got)
|
||||
}
|
||||
|
||||
// 9 队不是超时空的编制:越界的拖拽必须被拒(原来 9/10 是给 40 人格子留的)。
|
||||
if _, ok := service.moveRuntimeRaidMember(1001, 1002, currentFiendWarSquadCount+1); ok {
|
||||
t.Fatalf("超时空接受了第 %d 个编制", currentFiendWarSquadCount+1)
|
||||
}
|
||||
// 第 8 个编制仍然可用。
|
||||
if _, ok := service.moveRuntimeRaidMember(1001, 1002, currentFiendWarSquadCount); !ok {
|
||||
t.Fatalf("超时空拒绝了最后一个编制 %d", currentFiendWarSquadCount)
|
||||
}
|
||||
// 拉回未编队照旧。
|
||||
if _, ok := service.moveRuntimeRaidMember(1001, 1002, 0); !ok {
|
||||
t.Fatal("把成员拉回未编队被拒了")
|
||||
}
|
||||
}
|
||||
|
||||
// 超时空里两个人在镇上组了普通队伍:**绝不能**被塞进同一个编制。
|
||||
//
|
||||
// 注意这条路在超时空本来就是关着的(runtimeRaidSquadsFollowOrdinaryParty 对
|
||||
// Fiend War 直接返回 false,理由是操作员那句「组个临时队伍 但是攻坚小队编制不变」)。
|
||||
// 这条用例是保险:万一以后有别的调用点来问"这支队伍该算几队",
|
||||
// 超时空的答案必须是"一队一人、装不下",而不是像安东/卢克那样并成一队。
|
||||
func TestFiendWarOrdinaryPartyTakesOneSquadPerMember(t *testing.T) {
|
||||
state := runtimeRaidState{
|
||||
RaidKey: 0x00c80411,
|
||||
RaidMode: runtimeRaidOwnerFiendWar,
|
||||
Members: []runtimeRaidMemberState{
|
||||
{CharID: 1001, GroupIndex: 1, SlotOrder: 0},
|
||||
{CharID: 1002, GroupIndex: 0, SlotOrder: 0},
|
||||
},
|
||||
}
|
||||
// 两个人一队:超时空每队 1 人,装不下 ⇒ 不给小队号(调用方保持原样)。
|
||||
if group, ok := runtimeRaidSquadForOrdinaryPartyLedBy(state, []uint16{1001, 1002}, 1001); ok {
|
||||
t.Fatalf("两个人的普通队伍在超时空拿到了同一编制 %d(每队只该有 1 人)", group)
|
||||
}
|
||||
// 单人队伍照旧:队长保持自己的号。
|
||||
if group, ok := runtimeRaidSquadForOrdinaryPartyLedBy(state, []uint16{1001}, 1001); !ok || group != 1 {
|
||||
t.Fatalf("单人队伍拿到编制 %d/%t,want 1/true", group, ok)
|
||||
}
|
||||
// 同一个团的安东口径不动:4 人一队仍然装得下。
|
||||
anton := state
|
||||
anton.RaidMode = runtimeRaidOwnerAnton
|
||||
if group, ok := runtimeRaidSquadForOrdinaryPartyLedBy(anton, []uint16{1001, 1002}, 1001); !ok || group != 1 {
|
||||
t.Fatalf("安东的两人队伍被拆了:%d/%t", group, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// 自动落位(第一个空位)也要按团的尺子走:超时空第 1 个人占 1 队 0 号位,
|
||||
// 第 2 个人必须去 2 队,而不是坐进 1 队 1 号位。
|
||||
func TestFiendWarFirstAvailablePositionUsesOneSlotSquads(t *testing.T) {
|
||||
state := runtimeRaidState{
|
||||
RaidMode: runtimeRaidOwnerFiendWar,
|
||||
Members: []runtimeRaidMemberState{{CharID: 1001, GroupIndex: 1, SlotOrder: 0}},
|
||||
}
|
||||
group, slot, ok := runtimeRaidFirstAvailablePosition(state)
|
||||
if !ok || group != 2 || slot != 0 {
|
||||
t.Fatalf("超时空下一个空位 = %d/%d/%t,want 2/0/true", group, slot, ok)
|
||||
}
|
||||
anton := state
|
||||
anton.RaidMode = runtimeRaidOwnerAnton
|
||||
group, slot, ok = runtimeRaidFirstAvailablePosition(anton)
|
||||
if !ok || group != 1 || slot != 1 {
|
||||
t.Fatalf("安东下一个空位 = %d/%d/%t,want 1/1/true", group, slot, ok)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user