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

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

136 lines
5.5 KiB
Go

package dnfbridge
import (
"bytes"
"testing"
"longheng.io/server/internal/modules/dnf/worldmap"
)
func TestCurrentDungeonBossEntranceConditionCountsDefeatedTargetsAcrossRoomsOnce(t *testing.T) {
roomA := bossEntranceTestRoom(69201, runtimeDungeonMonsterDefeated)
roomB := bossEntranceTestRoom(69201, runtimeDungeonMonsterDefeated)
roomC := bossEntranceTestRoom(69210, runtimeDungeonMonsterDefeated)
roomD := bossEntranceTestRoom(69210, runtimeDungeonMonsterAnnounced)
runtime := &runtimeDungeonState{
Dungeon: worldmap.Dungeon{Mazes: []worldmap.Maze{{
BossRoomEntranceCondition: &worldmap.BossRoomEntranceCondition{
Action: "clear action",
TimeCondition: []int64{1, 0},
HuntMonsters: []worldmap.BossRoomEntranceHuntMonster{
{TargetID: 69201, Count: 2},
{TargetID: 69210, Count: 2},
},
},
}}},
MazeIndex: 0,
Room: roomD,
Rooms: map[runtimeDungeonRoomKey]*runtimeDungeonRoomVisit{
{X: 1, Y: 1, MapID: 17061}: {Room: roomA},
{X: 3, Y: 1, MapID: 17063}: {Room: roomB},
{X: 1, Y: 3, MapID: 17067}: {Room: roomC},
{X: 3, Y: 3, MapID: 17065}: {Room: roomD},
// A second cache key for the same room must not double-count it.
{X: 9, Y: 9, MapID: 17065}: {Room: roomD},
},
}
progress, found := currentDungeonBossEntranceConditionProgress(runtime)
if !found || progress.Ready || len(progress.Targets) != 2 ||
progress.Targets[0] != (currentDungeonBossEntranceHuntProgress{TargetID: 69201, Required: 2, Defeated: 2}) ||
progress.Targets[1] != (currentDungeonBossEntranceHuntProgress{TargetID: 69210, Required: 2, Defeated: 1}) {
t.Fatalf("before final tower progress=%+v found=%t", progress, found)
}
roomD.mu.Lock()
roomD.monsters[0].State = runtimeDungeonMonsterDefeated
roomD.mu.Unlock()
progress, found = currentDungeonBossEntranceConditionProgress(runtime)
if !found || !progress.Ready || progress.Targets[1].Defeated != 2 {
t.Fatalf("after final tower progress=%+v found=%t", progress, found)
}
}
func TestCurrentDungeonCompleteConditionPassGateMatchesCurrentEXEAndPublishesOnce(t *testing.T) {
wantBody := []byte{0, 0, 0, 0, 0}
if body := buildCurrentDungeonCompleteConditionPassGateBody(); !bytes.Equal(body, wantBody) {
t.Fatalf("body=%x want=%x", body, wantBody)
}
roomA := bossEntranceTestRoom(69201, runtimeDungeonMonsterDefeated)
roomB := bossEntranceTestRoom(69201, runtimeDungeonMonsterDefeated)
roomC := bossEntranceTestRoom(69210, runtimeDungeonMonsterDefeated)
roomD := bossEntranceTestRoom(69210, runtimeDungeonMonsterAnnounced)
runtime := &runtimeDungeonState{
Dungeon: worldmap.Dungeon{ID: 2005, Mazes: []worldmap.Maze{{
BossRoomEntranceCondition: &worldmap.BossRoomEntranceCondition{
Action: "clear action",
HuntMonsters: []worldmap.BossRoomEntranceHuntMonster{
{TargetID: 69201, Count: 2},
{TargetID: 69210, Count: 2},
},
},
}}},
Rooms: map[runtimeDungeonRoomKey]*runtimeDungeonRoomVisit{
{X: 1, Y: 1, MapID: 17061}: {Room: roomA},
{X: 3, Y: 1, MapID: 17063}: {Room: roomB},
{X: 1, Y: 3, MapID: 17067}: {Room: roomC},
{X: 3, Y: 3, MapID: 17065}: {Room: roomD},
},
}
connection := &bufferConn{}
session := &gameSession{conn: connection}
service := &Service{}
if err := service.publishCurrentDungeonBossEntranceGateAfterDeathLocked(session, runtime); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 || runtime.bossEntranceConditionComplete || runtime.bossEntranceGateNotificationSent {
t.Fatalf("premature publish bytes=%x complete=%t sent=%t", connection.write.Bytes(), runtime.bossEntranceConditionComplete, runtime.bossEntranceGateNotificationSent)
}
roomD.mu.Lock()
roomD.monsters[0].State = runtimeDungeonMonsterDefeated
roomD.mu.Unlock()
if err := service.publishCurrentDungeonBossEntranceGateAfterDeathLocked(session, runtime); err != nil {
t.Fatal(err)
}
packet, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(rest) != 0 || packet.Header.Classification != 0 ||
packet.Header.MsgID != currentDungeonCompleteConditionPassGateMsgID ||
!bytes.Equal(packet.Body, wantBody) {
t.Fatalf("packet class=%d msg=%d body=%x rest=%x", packet.Header.Classification, packet.Header.MsgID, packet.Body, rest)
}
if !runtime.bossEntranceConditionComplete || !runtime.bossEntranceGateNotificationSent {
t.Fatalf("complete=%t sent=%t", runtime.bossEntranceConditionComplete, runtime.bossEntranceGateNotificationSent)
}
written := connection.write.Len()
if err := service.publishCurrentDungeonBossEntranceGateAfterDeathLocked(session, runtime); err != nil {
t.Fatal(err)
}
if connection.write.Len() != written {
t.Fatalf("duplicate publish wrote=%x", connection.write.Bytes()[written:])
}
}
func TestCurrentDungeonBossEntranceConditionRejectsMissingOrUnsupportedGate(t *testing.T) {
for _, runtime := range []*runtimeDungeonState{
nil,
{},
{Dungeon: worldmap.Dungeon{Mazes: []worldmap.Maze{{}}}},
{Dungeon: worldmap.Dungeon{Mazes: []worldmap.Maze{{BossRoomEntranceCondition: &worldmap.BossRoomEntranceCondition{
Action: "future action", HuntMonsters: []worldmap.BossRoomEntranceHuntMonster{{TargetID: 1, Count: 1}},
}}}}},
} {
if progress, found := currentDungeonBossEntranceConditionProgress(runtime); found {
t.Fatalf("unsupported runtime=%+v progress=%+v", runtime, progress)
}
}
}
func bossEntranceTestRoom(monsterID int64, state runtimeDungeonMonsterState) *runtimeDungeonRoom {
return &runtimeDungeonRoom{monsters: []runtimeDungeonMonster{{
Spawn: worldmap.MonsterSpawn{MonsterID: monsterID},
State: state,
}}}
}