服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
141 lines
4.7 KiB
Go
141 lines
4.7 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
"longheng.io/server/internal/modules/dnf/dungeoncmd"
|
|
"longheng.io/server/internal/modules/dnf/worldmap"
|
|
)
|
|
|
|
const (
|
|
currentFruzStationDungeonID = int64(102)
|
|
currentFruzStationNarkeQuestID = int64(3733)
|
|
currentFruzStationNarkeActorID = int64(11526)
|
|
currentFruzStationObstacleSmall = int64(16109)
|
|
currentFruzStationObstacleLarge = int64(16110)
|
|
)
|
|
|
|
func currentFruzStationNarkeObstacleMap(mapID int64) bool {
|
|
switch mapID {
|
|
case 76503, 76504, 76506:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func currentFruzStationNarkeActorInRoom(room *runtimeDungeonRoom, objectKey uint32) bool {
|
|
if room == nil || objectKey == 0 {
|
|
return false
|
|
}
|
|
for _, actor := range room.Snapshot().ExtendedActors {
|
|
if actor.ObjectKey != objectKey || actor.Kind != runtimeDungeonActorAICharacter ||
|
|
actor.State != runtimeDungeonMonsterAnnounced || actor.HostileReference != nil ||
|
|
actor.Packet.Code != uint32(currentFruzStationNarkeActorID) || actor.AICharacter == nil {
|
|
continue
|
|
}
|
|
return actor.AICharacter.Code == currentFruzStationNarkeActorID &&
|
|
normalizeDungeonPVFSymbol(actor.AICharacter.Faction) == "character" &&
|
|
normalizeDungeonPVFSymbol(actor.AICharacter.AIType) == "normal"
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Narke follows the player from the first room while keeping the actor key
|
|
// announced there. Check both the live room and cached visits, but accept only
|
|
// the exact friendly APC definition owned by quest 3733.
|
|
func currentFruzStationRuntimeContainsNarke(runtime *runtimeDungeonState, objectKey uint32) bool {
|
|
if runtime == nil || objectKey == 0 {
|
|
return false
|
|
}
|
|
if currentFruzStationNarkeActorInRoom(runtime.Room, objectKey) {
|
|
return true
|
|
}
|
|
for _, visit := range runtime.Rooms {
|
|
if visit == nil || visit.Room == nil || visit.Room == runtime.Room {
|
|
continue
|
|
}
|
|
if currentFruzStationNarkeActorInRoom(visit.Room, objectKey) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// handleCurrentFruzStationNarkeObstacleDeathLocked owns only the native
|
|
// variable/count-zero op39 emitted after Narke's grenade hits quest rubble.
|
|
// The obstacles are client-side passive objects and are not ordinary runtime
|
|
// actors, so they need an op38 receipt without monster, reward, quest or room
|
|
// clear mutation. Missing this receipt leaves Narke waiting forever after the
|
|
// room's monsters are dead.
|
|
func (s *Service) handleCurrentFruzStationNarkeObstacleDeathLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
scene worldmap.DungeonRoomScene,
|
|
request dungeoncmd.DieMonsterRequest,
|
|
) (bool, error) {
|
|
if s == nil || session == nil || runtime == nil || runtime.Session == nil || runtime.Room == nil ||
|
|
session.dungeon.runtime != runtime || !dungeonRuntimeOwnsCharacter(runtime, session.selectedCharacterID) ||
|
|
runtime.Dungeon.ID != currentFruzStationDungeonID ||
|
|
currentDungeonStoryQuestID(runtime) != currentFruzStationNarkeQuestID ||
|
|
!currentFruzStationNarkeObstacleMap(scene.Map.Map.ID) ||
|
|
request.Layout != dungeoncmd.DieMonsterRequestLayoutVariableCombat ||
|
|
request.CombatEntryCount != 0 {
|
|
return false, nil
|
|
}
|
|
|
|
objectID := int64(request.RuntimeObjectKey)
|
|
if objectID != currentFruzStationObstacleSmall && objectID != currentFruzStationObstacleLarge {
|
|
return false, nil
|
|
}
|
|
available := currentDungeonScenePassiveObjectCount(scene, objectID)
|
|
if available == 0 {
|
|
return false, nil
|
|
}
|
|
ownerIsNarke := request.OwnerObjectKey != ^uint16(0) &&
|
|
currentFruzStationRuntimeContainsNarke(runtime, uint32(request.OwnerObjectKey))
|
|
if request.OwnerObjectKey != ^uint16(0) && !ownerIsNarke {
|
|
return false, nil
|
|
}
|
|
|
|
visit, err := runtime.currentDungeonRoomVisit(scene)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
responseBody, err := buildCurrentDungeonDeathNotificationBody(
|
|
request.RuntimeObjectKey,
|
|
currentDungeonDeathResponseAICharacter,
|
|
)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err := s.sendGameUpperRawClass(
|
|
session,
|
|
uint16(dnfenum.CmdPacketNotifyDieMonster),
|
|
responseBody,
|
|
0,
|
|
); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
retired := visit.QuestPassiveObjectDeaths[objectID]
|
|
if retired < available {
|
|
if visit.QuestPassiveObjectDeaths == nil {
|
|
visit.QuestPassiveObjectDeaths = make(map[int64]uint32)
|
|
}
|
|
retired++
|
|
visit.QuestPassiveObjectDeaths[objectID] = retired
|
|
}
|
|
s.logGameEvent(session, "game-dungeon-fruz-narke-obstacle-death-accepted",
|
|
"dungeon_id", runtime.Dungeon.ID,
|
|
"quest_id", currentFruzStationNarkeQuestID,
|
|
"maze_index", runtime.MazeIndex,
|
|
"map_id", scene.Map.Map.ID,
|
|
"object_key", request.RuntimeObjectKey,
|
|
"related_actor_object_key", request.OwnerObjectKey,
|
|
"owner_is_narke", ownerIsNarke,
|
|
"retired", retired,
|
|
"available", available,
|
|
"response_opcode", uint16(dnfenum.CmdPacketNotifyDieMonster))
|
|
return true, nil
|
|
}
|