Files
90CNServer/server/internal/services/dnfbridge/raid_fiendwar_reveal_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

173 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"testing"
"longheng.io/server/internal/modules/dnf/raid"
)
// 百科(地图构造):"雷达:该副本通关后增加所有玩家10%技能伤害,并显示地图中怪物
// 头像。(反之怪物头像显示问号)" —— 符号 81 RADAR STATE 就是那个开关。
func fiendWarRevealBattle() *raid.FiendWarBattle {
return &raid.FiendWarBattle{
Config: &raid.FiendWarBattleConfig{
Symbols: map[string]uint32{"radar state": 81},
Areas: map[int]raid.FiendWarArea{
14: {Index: 14, Dungeon: 4962}, // 普通作战区域
15: {Index: 15, Dungeon: 4963, Kind: "radar"}, // 单图设施
19: {Index: 19, Dungeon: 4967, Kind: "top outpost"}, // 三图设施(预抽 maze 2)
},
},
Symbols: map[uint32]int{81: 0},
Areas: map[int]raid.FiendWarAreaState{},
FixtureMazes: map[int]int{19: 2},
}
}
func TestFiendWarRadarGatesBoardMonster(t *testing.T) {
b := fiendWarRevealBattle()
// 雷达未通关:**ID 照发、只换脸**。
// ID —— 红框只由 unitID 决定,清零就整格空白(既没框也没问号),
// 2026-10-01 的 packet log 与现场截图双向印证过;
// 脸 —— type 换成 fiendWarFaceUnknown(= 0)。type 14/15/16 实测是"框在、里面空的",
// 只有 0 从来没发出去过:服务端过去拿 Type==0 当"这格没单位"的哨兵,
// 带 0 的行在发出去之前就被自己的守卫吃掉了 —— 所以现在用第二个返回值
// (present)来表示"有没有单位",与 Type 彻底分开。
if got, present := runtimeFiendWarRevealEnemy(b, 19, raid.FiendWarUnit{ID: 4967, Type: 7}); !present || got.ID != 110708 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的前哨格 = %+v present=%v,want ID=110708(框要画)Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
if got, present := runtimeFiendWarRevealEnemy(b, 15, raid.FiendWarUnit{ID: 4963, Type: 5}); !present || got.ID != 110973 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的雷达格 = %+v present=%v,want ID=110973 Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
if got, present := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{ID: 3, Type: 2}); !present || got.ID != 110977 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的军团格 = %+v present=%v,want ID=110977 Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
if got, present := runtimeFiendWarRevealMonster(b, 14, raid.FiendWarUnit{ID: 5, Type: 1}); !present || got.ID != 110714 || got.Type != fiendWarFaceUnknown {
t.Fatalf("雷达未通关的中立怪格 = %+v present=%v,want ID=110714 Type=%d(问号)", got, present, fiendWarFaceUnknown)
}
// 问号那个 type 必须能真的发出去:它不再是"这格没单位"的哨兵值。
if fiendWarFaceUnknown != 0 {
t.Logf("问号 type = %d(若 0 被证伪,这里就是换上的新值)", fiendWarFaceUnknown)
}
// 真的没有单位的格子依然一行都不发。
if _, present := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{}); present {
t.Fatal("空区域不该发号")
}
b.Symbols[81] = 1 // 雷达通关
// 设施格:露出这一格**固定**的那张图的领主。fixture 给 19 号格钉的是 maze 2,
// 所以露出的是 110708(4967 maze 2 的领主);脸仍然是这一格的位置 kind(7)——
// 客户端那张 kind→脸 表按位置排,而实际打到的怪也钉在同一位置,两者必然一致
// (见 TestFiendWarFixtureBoardAndEntryAreFixedTogether)。
if got, present := runtimeFiendWarRevealEnemy(b, 19, raid.FiendWarUnit{ID: 4967, Type: 7}); !present || got.ID != 110708 || got.Type != 7 {
t.Fatalf("雷达通关后的前哨格 = %+v,want ID=110708 Type=7", got)
}
// 单图设施:没有预抽记录时 maze=0,露出恒定的那只(雷达基地 = 磁力恶魔:马格内特)。
if got, present := runtimeFiendWarRevealEnemy(b, 15, raid.FiendWarUnit{ID: 4963, Type: 5}); !present || got.ID != 110973 || got.Type != 5 {
t.Fatalf("雷达通关后的雷达格 = %+v,want ID=110973 Type=5", got)
}
// 军团格:兵种 type 决定它走进哪张 maze,头像用那张图的领主。
for enemyType, want := range map[byte]uint32{1: 110979, 2: 110977, 3: 110987, 4: 110982} {
if got, _ := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{ID: uint32(enemyType), Type: enemyType}); got.ID != want {
t.Fatalf("兵种 %d 的头像 = %d,want %d", enemyType, got.ID, want)
}
}
// 中立怪:monster type 1..2 → maze 4/5 的领主。
for monsterType, want := range map[byte]uint32{1: 110714, 2: 110975} {
if got, _ := runtimeFiendWarRevealMonster(b, 14, raid.FiendWarUnit{ID: 7, Type: monsterType}); got.ID != want {
t.Fatalf("原生怪 %d 的头像 = %d,want %d", monsterType, got.ID, want)
}
}
// 空格子(没有单位)永远不发:雷达通关也不会凭空长出一只怪。
if got, present := runtimeFiendWarRevealEnemy(b, 14, raid.FiendWarUnit{}); present || got != (raid.FiendWarUnit{}) {
t.Fatalf("空区域的揭示 = %+v present=%v,want 零值且不发", got, present)
}
}
// 进图用的 maze 必须与雷达通关后战况表显示的那只是同一个:
// selectRuntimeFiendWarMaze 读的是同一张 FixtureMazes。
func TestFiendWarFixtureMazeAndBoardAgree(t *testing.T) {
b := fiendWarRevealBattle()
b.Symbols[81] = 1
maze, ok := b.MazeForArea(19)
if !ok {
t.Fatal("前哨没有预抽记录")
}
board, present := runtimeFiendWarRevealEnemy(b, 19, raid.FiendWarUnit{ID: 4967, Type: 7})
if !present {
t.Fatal("前哨格应该发号")
}
fromEntry, ok := raid.FiendWarFixtureBossID(4967, maze)
if !ok {
t.Fatal("进图侧查不到这只领主")
}
if board.ID != fromEntry {
t.Fatalf("表上 %d != 进图 %d", board.ID, fromEntry)
}
}
// 战况表上画的必须就是进图会打到的那一只 —— 而且这是**固定**的,不靠抽签对齐。
//
// 操作员口径(2026-10-01 复核历史截图):
//
// "我所说的这些格子里的怪物是固定的……雷达打完以后客户端就是这么画的,服务端也
// 理应按照客户端画的去让怪物真正百分百地出现 —— 就不再抽签了。"
//
// 客户端那张 kind→脸 的表按格子位置排(上哨=宾柏 / 中哨=薛丁恶 / 下哨=克鲁瑟,
// 兵站本营同理),而战况表发的 type 正是这个位置 kind。所以这里钉死:
// 表格子画谁,它固定用的那张 maze 的领主就必须是谁。
func TestFiendWarFixtureBoardAndEntryAreFixedTogether(t *testing.T) {
cases := []struct {
area int
kind string
maze int
face byte
boss uint32
dungeon int
}{
{19, "top outpost", 0, 7, 110981, 4967}, // 上哨 · 宾柏
{21, "mid outpost", 1, 8, 110984, 4969}, // 中哨 · 薛丁恶
{23, "bot outpost", 2, 9, 110708, 4971}, // 下哨 · 克鲁瑟
{27, "top command center", 0, 10, 110709, 4975}, // 上兵营 · 光之恶魔
{28, "mid command center", 1, 11, 110978, 4976}, // 中兵营 · 暗之恶魔
{29, "bot command center", 2, 12, 110992, 4977}, // 下兵营 · 重力恶魔
}
areas := map[int]raid.FiendWarArea{}
for _, tc := range cases {
areas[tc.area] = raid.FiendWarArea{Index: tc.area, Dungeon: tc.dungeon, Kind: tc.kind}
}
b, err := raid.NewFiendWarBattle(&raid.FiendWarBattleConfig{
Raid: &raid.AntonConfig{PhaseMax: 1, MemberMax: 8},
Rules: []raid.FiendWarRule{{}},
Areas: areas,
Symbols: map[string]uint32{"radar state": 81},
})
if err != nil {
t.Fatal(err)
}
b.Symbols[81] = 1 // 雷达通关:这一格该露脸了
for _, tc := range cases {
maze, ok := b.MazeForArea(tc.area)
if !ok {
t.Fatalf("设施 %d 没有固定图", tc.area)
}
if maze != tc.maze {
t.Fatalf("设施 %d 的固定图 = %d,want %d", tc.area, maze, tc.maze)
}
got, present := runtimeFiendWarRevealEnemy(b, tc.area, raid.FiendWarUnit{ID: uint32(tc.dungeon), Type: tc.face})
if !present {
t.Fatalf("设施 %d 应该发号", tc.area)
}
if got.ID != tc.boss || got.Type != tc.face {
t.Fatalf("设施 %d 表上画的 = %+v,want ID=%d Type=%d", tc.area, got, tc.boss, tc.face)
}
// 进图侧读的是同一张图(selectRuntimeFiendWarMaze → MazeForArea),
// 所以这里查出来的领主就是那一格真正会打到的领主。
fromEntry, ok := raid.FiendWarFixtureBossID(tc.dungeon, maze)
if !ok || fromEntry != tc.boss {
t.Fatalf("设施 %d 进图侧查到的领主 = %d(ok=%t),want %d", tc.area, fromEntry, ok, tc.boss)
}
}
}