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,492 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dungeoncmd"
|
||||
dnfhellparty "longheng.io/server/internal/modules/dnf/hellparty"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
const (
|
||||
currentHellPartyMonsterInfoWireOpcode uint16 = 679
|
||||
currentHellPartyModeA byte = 1
|
||||
)
|
||||
|
||||
var (
|
||||
errCurrentHellPartyUnavailable = errors.New("dnf hell party is unavailable")
|
||||
errCurrentHellPartyActor = errors.New("dnf hell party actor is invalid")
|
||||
errCurrentHellPartyMoveBlocked = errors.New("dnf hell party encounter is incomplete")
|
||||
errCurrentHellPartyLocked = errors.New("dnf hell party is not unlocked for this region")
|
||||
)
|
||||
|
||||
type currentDungeonHellPartyRewardPlan struct {
|
||||
ActorObjectKey uint32
|
||||
RollCount uint32
|
||||
Suppressed bool
|
||||
Selected []currentDungeonHellPartySelectedDrop
|
||||
}
|
||||
|
||||
func (s *Service) authorizeCurrentDungeonHellParty(ctx context.Context, session *gameSession, table *worldmap.Table, dungeonID int64) error {
|
||||
if s == nil || session == nil || session.selectedCharacterID == 0 || table == nil {
|
||||
return errCurrentHellPartyLocked
|
||||
}
|
||||
area, found := table.FindAreaByDungeon(dungeonID)
|
||||
if !found || !area.HellDungeon.Set || area.HellDungeon.Value != 1 || len(area.HellQuestIDs) == 0 {
|
||||
return errCurrentHellPartyLocked
|
||||
}
|
||||
repositories, available := s.repositoryGroup()
|
||||
if !available || repositories.HellPartyPermission == nil {
|
||||
return errCurrentHellPartyLocked
|
||||
}
|
||||
// The same predicate backs the selector page's hell-party button
|
||||
// (currentHellPartyButtonBlockedSlots), so the button projected to the
|
||||
// client can never disagree with this gate.
|
||||
granted, err := s.currentHellPartyAreaGranted(ctx, repositories, session.selectedCharacterID, area)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if granted {
|
||||
return nil
|
||||
}
|
||||
characterID := fmt.Sprintf("%d", session.selectedCharacterID)
|
||||
return fmt.Errorf("%w: area=%d character=%s quests=%v", errCurrentHellPartyLocked, area.ID, characterID, area.HellQuestIDs)
|
||||
}
|
||||
|
||||
func currentHellQuestRecordUnlocksArea(record dnfrepo.QuestRecord, area worldmap.Area) bool {
|
||||
_, cleared := dungeonQuestStateSets(record)
|
||||
for _, questID := range area.HellQuestIDs {
|
||||
if questID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := cleared[questID]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type currentDungeonHellPartyActor struct {
|
||||
Identity dnfhellparty.ActorIdentity
|
||||
Code uint32
|
||||
EntityType uint8
|
||||
Level byte
|
||||
}
|
||||
|
||||
type currentDungeonHellPartyOwner struct {
|
||||
Room dnfhellparty.Room
|
||||
Runtime *dnfhellparty.Runtime
|
||||
Mode byte
|
||||
Actors map[uint32]currentDungeonHellPartyActor
|
||||
MonsterInfoRows map[uint32]uint32
|
||||
MonsterInfoSent bool
|
||||
RewardPlans []currentDungeonHellPartyRewardPlan
|
||||
DeathResponses map[uint32][]byte
|
||||
PillarObjectKey uint32
|
||||
}
|
||||
|
||||
func (s *Service) currentHellPartyCatalog() (*dnfhellparty.Catalog, error) {
|
||||
if s == nil {
|
||||
return nil, errCurrentHellPartyUnavailable
|
||||
}
|
||||
s.hellPartyMu.Lock()
|
||||
defer s.hellPartyMu.Unlock()
|
||||
if s.hellPartyCatalog != nil || s.hellPartyCatalogLoadErr != nil {
|
||||
return s.hellPartyCatalog, s.hellPartyCatalogLoadErr
|
||||
}
|
||||
s.initialEquipmentMu.Lock()
|
||||
archive, err := s.initialEquipmentArchiveLocked()
|
||||
s.initialEquipmentMu.Unlock()
|
||||
if err == nil {
|
||||
s.hellPartyCatalog, err = dnfhellparty.Load(archive)
|
||||
}
|
||||
s.hellPartyCatalogLoadErr = err
|
||||
return s.hellPartyCatalog, s.hellPartyCatalogLoadErr
|
||||
}
|
||||
|
||||
func (s *Service) preloadHellPartyCatalog(ctx context.Context) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
started := time.Now()
|
||||
catalog, err := s.currentHellPartyCatalog()
|
||||
if err != nil {
|
||||
return fmt.Errorf("preload current hell party catalog: %w", err)
|
||||
}
|
||||
rules, groups, actors := catalog.Snapshot()
|
||||
s.logPacketEvent("dnf-hell-party-catalog-loaded",
|
||||
"difficulty_rules", rules,
|
||||
"groups", groups,
|
||||
"actors", actors,
|
||||
"elapsed_ms", time.Since(started).Milliseconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentDungeonHellPartyRequest accepts only the two fixed 21-byte request
|
||||
// shapes documented by the current select-dungeon writer. Other nonzero mode
|
||||
// combinations remain fail-closed.
|
||||
func currentDungeonHellPartyRequest(request dungeoncmd.SelectDungeonRequest) bool {
|
||||
if request.EntryOption != 0 || request.RuntimeState != 0 || request.Reserved != 0 ||
|
||||
request.PartyState != 0 || len(request.OpaqueTail) != 0 {
|
||||
return false
|
||||
}
|
||||
dedicated := request.SelectionMode == 1 && request.RuntimeToken == 0xffff && request.SpecialMode == 0
|
||||
manual := request.SelectionMode == 0 && request.RuntimeToken == 0xffff && request.SpecialMode == 1
|
||||
return dedicated || manual
|
||||
}
|
||||
|
||||
// currentDungeonSelectedMazeEscapeHell identifies the third PVF-owned hell
|
||||
// route: a [hell dungeon] whose selected [maze info] carries [escape hell].
|
||||
// The marker is intentionally evaluated only for the selected maze. Scanning
|
||||
// every maze would misclassify an ordinary route merely because a sibling
|
||||
// route owns the escape-hell state machine.
|
||||
func currentDungeonSelectedMazeEscapeHell(
|
||||
request dungeoncmd.SelectDungeonRequest,
|
||||
dungeon worldmap.Dungeon,
|
||||
mazeIndex int,
|
||||
) bool {
|
||||
if !currentDungeonHellPartyRequest(request) || mazeIndex < 0 || mazeIndex >= len(dungeon.Mazes) {
|
||||
return false
|
||||
}
|
||||
hellDungeon, found := dungeon.Metadata.IntegerValues["hell dungeon"]
|
||||
if !found || hellDungeon != 1 {
|
||||
return false
|
||||
}
|
||||
escapeHell := dungeon.Mazes[mazeIndex].EscapeHell
|
||||
return escapeHell.Set && escapeHell.Value == 1
|
||||
}
|
||||
|
||||
// currentDungeonExplicitHellPartyRequest separates the shared selector wire
|
||||
// shape from the dungeon's PVF-owned runtime. Inferno, "particular hell", and
|
||||
// selected escape-hell mazes use selection_mode=1/token=UINT16_MAX as their
|
||||
// standalone entry request; they do not own a seal-door room and must not be
|
||||
// rewritten into the optional in-dungeon hell-party encounter.
|
||||
func currentDungeonExplicitHellPartyRequest(
|
||||
request dungeoncmd.SelectDungeonRequest,
|
||||
dungeon worldmap.Dungeon,
|
||||
mazeIndex int,
|
||||
) bool {
|
||||
return currentDungeonHellPartyRequest(request) &&
|
||||
!currentDungeonStandaloneRiftForSelection(request, dungeon, mazeIndex)
|
||||
}
|
||||
|
||||
// currentDungeonStandaloneRift identifies PVF-owned hell routes whose boss is
|
||||
// the reward owner. They share parts of the selector and drop tables with an
|
||||
// optional in-dungeon hell party, but do not contain a seal-door encounter.
|
||||
func currentDungeonStandaloneRift(dungeon worldmap.Dungeon) bool {
|
||||
return dungeon.Metadata.InfernoDungeon ||
|
||||
normalizeDungeonPVFSymbol(dungeon.Metadata.DungeonType) == "particular hell"
|
||||
}
|
||||
|
||||
func currentDungeonStandaloneRiftForSelection(
|
||||
request dungeoncmd.SelectDungeonRequest,
|
||||
dungeon worldmap.Dungeon,
|
||||
mazeIndex int,
|
||||
) bool {
|
||||
return currentDungeonStandaloneRift(dungeon) ||
|
||||
currentDungeonSelectedMazeEscapeHell(request, dungeon, mazeIndex)
|
||||
}
|
||||
|
||||
func currentDungeonStandaloneRiftRuntime(runtime *runtimeDungeonState) bool {
|
||||
return runtime != nil && currentDungeonStandaloneRiftForSelection(
|
||||
runtime.Request,
|
||||
runtime.Dungeon,
|
||||
runtime.MazeIndex,
|
||||
)
|
||||
}
|
||||
|
||||
// currentDungeonTimeRiftRuntime is narrower than the boss-reward predicate:
|
||||
// it includes Time Rift and selected escape-hell mazes, but deliberately
|
||||
// excludes Magic Rift's [particular hell] route. Direction-specific features
|
||||
// such as epic-wish snapshots must not leak onto dungeon 316 merely because
|
||||
// all three routes share the standalone boss reward owner.
|
||||
func currentDungeonTimeRiftRuntime(runtime *runtimeDungeonState) bool {
|
||||
if runtime == nil || normalizeDungeonPVFSymbol(runtime.Dungeon.Metadata.DungeonType) == "particular hell" {
|
||||
return false
|
||||
}
|
||||
return runtime.Dungeon.Metadata.InfernoDungeon || currentDungeonSelectedMazeEscapeHell(
|
||||
runtime.Request,
|
||||
runtime.Dungeon,
|
||||
runtime.MazeIndex,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) currentDungeonStandaloneRiftRewardRollCount() (uint32, error) {
|
||||
catalog, err := s.currentHellPartyCatalog()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rule, found := catalog.Difficulty("A")
|
||||
if !found || rule.RewardRollCount == 0 {
|
||||
return 0, fmt.Errorf("%w: standalone rift difficulty A is unavailable", errCurrentHellPartyUnavailable)
|
||||
}
|
||||
return rule.RewardRollCount, nil
|
||||
}
|
||||
|
||||
func (s *Service) prepareCurrentDungeonHellParty(
|
||||
table *worldmap.Table,
|
||||
topology *worldmap.DungeonTopology,
|
||||
dungeon worldmap.Dungeon,
|
||||
mazeIndex int,
|
||||
seed uint32,
|
||||
) (*currentDungeonHellPartyOwner, error) {
|
||||
catalog, err := s.currentHellPartyCatalog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
room, err := dnfhellparty.ResolveRoom(table, topology, dungeon.ID, mazeIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plan, err := dnfhellparty.BuildPlan(catalog, dnfhellparty.PlanOptions{
|
||||
GroupKey: "A",
|
||||
Seed: uint64(seed),
|
||||
Entries: room.Entries,
|
||||
SuppressGroupReward: func(code uint32, entityType uint8) bool {
|
||||
return s.currentDungeonHellMonsterScript(code, entityType)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime, err := dnfhellparty.NewRuntime(plan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := dnfhellparty.ApplyRoom(topology, room); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¤tDungeonHellPartyOwner{
|
||||
Room: room,
|
||||
Runtime: runtime,
|
||||
Mode: currentHellPartyModeA,
|
||||
Actors: make(map[uint32]currentDungeonHellPartyActor),
|
||||
MonsterInfoRows: make(map[uint32]uint32),
|
||||
DeathResponses: make(map[uint32][]byte),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) currentDungeonHellMonsterScript(code uint32, entityType uint8) bool {
|
||||
if code == 0 {
|
||||
return false
|
||||
}
|
||||
if entityType == 0 {
|
||||
catalog, err := s.dungeonMonsterCatalog()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
value, err := catalog.HellMonster(int64(code))
|
||||
return err == nil && value
|
||||
}
|
||||
catalog, err := s.dungeonAICharacterCatalog()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
definition, found, err := catalog.Find(int64(code))
|
||||
if err != nil || !found {
|
||||
return false
|
||||
}
|
||||
for _, section := range definition.Sections {
|
||||
if normalizeDungeonPVFSymbol(section.Name) != "hell monster" {
|
||||
continue
|
||||
}
|
||||
for _, token := range section.Tokens {
|
||||
if token.Int == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) appendCurrentDungeonHellPartyActors(
|
||||
plan runtimeDungeonExtendedActorPlan,
|
||||
owner *currentDungeonHellPartyOwner,
|
||||
scene worldmap.DungeonRoomScene,
|
||||
basis worldmap.OptionalInt,
|
||||
monsterCatalog *pvfDungeonMonsterCatalog,
|
||||
aiCatalog *pvfDungeonAICharacterCatalog,
|
||||
) (runtimeDungeonExtendedActorPlan, error) {
|
||||
if owner == nil || owner.Runtime == nil || scene.Coordinate != owner.Room.Coordinate || scene.Map.Map.ID != owner.Room.HellMap.ID {
|
||||
return plan, nil
|
||||
}
|
||||
packetIndex := len(scene.Monsters) + len(plan.Actors)
|
||||
for _, wave := range owner.Runtime.Plan().Waves {
|
||||
if wave.Order == 0 || wave.Order > uint32(^uint16(0)) {
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: wave order=%d", errCurrentHellPartyActor, wave.Order)
|
||||
}
|
||||
for _, planned := range wave.Actors {
|
||||
level := byte(0)
|
||||
actorType := byte(0)
|
||||
switch planned.EntityType {
|
||||
case 0:
|
||||
if monsterCatalog == nil {
|
||||
return runtimeDungeonExtendedActorPlan{}, errDungeonMonsterCatalogUnavailable
|
||||
}
|
||||
if _, found, err := monsterCatalog.Find(int64(planned.Code)); err != nil || !found {
|
||||
if err != nil {
|
||||
return runtimeDungeonExtendedActorPlan{}, err
|
||||
}
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: monster=%d", errDungeonMonsterDefinitionMiss, planned.Code)
|
||||
}
|
||||
if !basis.Set || basis.Value <= 0 || basis.Value > 0xff {
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: basis_level=%d set=%t", errCurrentHellPartyActor, basis.Value, basis.Set)
|
||||
}
|
||||
level = byte(basis.Value)
|
||||
case 1:
|
||||
if aiCatalog == nil {
|
||||
var err error
|
||||
aiCatalog, err = s.dungeonAICharacterCatalog()
|
||||
if err != nil {
|
||||
return runtimeDungeonExtendedActorPlan{}, err
|
||||
}
|
||||
}
|
||||
definition, found, err := aiCatalog.Find(int64(planned.Code))
|
||||
if err != nil {
|
||||
return runtimeDungeonExtendedActorPlan{}, err
|
||||
}
|
||||
if !found {
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: AI=%d", errDungeonAICharacterDefinitionMiss, planned.Code)
|
||||
}
|
||||
level = definition.Level
|
||||
actorType = 5
|
||||
default:
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: entity_type=%d", errCurrentHellPartyActor, planned.EntityType)
|
||||
}
|
||||
actor, err := newRuntimeDungeonExtendedActor(
|
||||
runtimeDungeonActorHellParty,
|
||||
plan.NextObjectKey,
|
||||
int64(planned.Code),
|
||||
currentDungeonStartMapActor{
|
||||
TemplateOrder: uint16(wave.Order),
|
||||
PacketIndex: uint32(packetIndex),
|
||||
Level: level,
|
||||
Type: actorType,
|
||||
Flag0: 1,
|
||||
Flag1: 0xff,
|
||||
Blocking: 1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return runtimeDungeonExtendedActorPlan{}, err
|
||||
}
|
||||
if _, exists := owner.Actors[actor.ObjectKey]; exists {
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: duplicate object_key=%d", errCurrentHellPartyActor, actor.ObjectKey)
|
||||
}
|
||||
owner.Actors[actor.ObjectKey] = currentDungeonHellPartyActor{
|
||||
Identity: planned.Identity,
|
||||
Code: planned.Code,
|
||||
EntityType: planned.EntityType,
|
||||
Level: level,
|
||||
}
|
||||
if _, exists := owner.MonsterInfoRows[planned.Code]; !exists {
|
||||
owner.MonsterInfoRows[planned.Code] = uint32(level)
|
||||
}
|
||||
plan.Actors = append(plan.Actors, actor)
|
||||
plan.NextObjectKey++
|
||||
packetIndex++
|
||||
}
|
||||
}
|
||||
for _, actor := range plan.Actors {
|
||||
if actor.Kind == runtimeDungeonActorSpecialObject && uint32(actor.Packet.Code) == uint32(owner.Room.Pillar.ObjectID) {
|
||||
owner.PillarObjectKey = actor.ObjectKey
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(scene.Monsters)+len(plan.Actors) > 0xff {
|
||||
return runtimeDungeonExtendedActorPlan{}, fmt.Errorf("%w: normal=%d extended=%d", errDungeonStartMapActorCount, len(scene.Monsters), len(plan.Actors))
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
func buildCurrentHellPartyMonsterInfoBody(rows map[uint32]uint32) []byte {
|
||||
codes := make([]uint32, 0, len(rows))
|
||||
for code := range rows {
|
||||
if code != 0 {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
}
|
||||
sort.Slice(codes, func(i, j int) bool { return codes[i] < codes[j] })
|
||||
var writer packetWriter
|
||||
writer.writeUint32(uint32(len(codes)))
|
||||
for _, code := range codes {
|
||||
level := rows[code]
|
||||
if level == 0 {
|
||||
level = 1
|
||||
}
|
||||
writer.writeUint32(code)
|
||||
writer.writeUint32(level)
|
||||
}
|
||||
return writer.bytes()
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentHellPartyMonsterInfo(session *gameSession, runtime *runtimeDungeonState, scene worldmap.DungeonRoomScene, source string) error {
|
||||
if runtime == nil || runtime.HellParty == nil || runtime.HellParty.MonsterInfoSent ||
|
||||
scene.Coordinate != runtime.HellParty.Room.Coordinate || scene.Map.Map.ID != runtime.HellParty.Room.HellMap.ID {
|
||||
return nil
|
||||
}
|
||||
body := buildCurrentHellPartyMonsterInfoBody(runtime.HellParty.MonsterInfoRows)
|
||||
if len(body) <= 4 {
|
||||
return nil
|
||||
}
|
||||
if err := s.sendGameUpperRawClass(session, currentHellPartyMonsterInfoWireOpcode, body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.HellParty.MonsterInfoSent = true
|
||||
s.logGameEvent(session, "game-dungeon-hell-party-monster-info-sent",
|
||||
"source", source,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"msg_id", currentHellPartyMonsterInfoWireOpcode,
|
||||
"classification", 0,
|
||||
"body_len", len(body),
|
||||
"row_count", len(runtime.HellParty.MonsterInfoRows))
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendCurrentHellPartyMonsterInfoAfterFinishLoading publishes the typed Hell
|
||||
// Party monster table only after the encounter room's op37/op30 boundary.
|
||||
// Current-EXE sub_1D39E60 proves op679 is only a count followed by code/level
|
||||
// rows; a zero-row body has no reset semantics and must not be invented for
|
||||
// ordinary, story, altar, or non-encounter rooms.
|
||||
func (s *Service) sendCurrentHellPartyMonsterInfoAfterFinishLoading(session *gameSession, source string) error {
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
session.dungeon.mu.Lock()
|
||||
defer session.dungeon.mu.Unlock()
|
||||
runtime := session.dungeon.runtime
|
||||
if runtime == nil || runtime.Session == nil {
|
||||
return nil
|
||||
}
|
||||
scene, ok := runtime.Session.Scene()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if runtime.HellParty != nil {
|
||||
if scene.Coordinate == runtime.HellParty.Room.Coordinate &&
|
||||
scene.Map.Map.ID == runtime.HellParty.Room.HellMap.ID {
|
||||
return s.sendCurrentHellPartyMonsterInfo(session, runtime, scene, source)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentDungeonHellPartyMoveBlocked(runtime *runtimeDungeonState, source worldmap.DungeonRoomScene) bool {
|
||||
return runtime != nil && runtime.HellParty != nil && runtime.HellParty.Runtime != nil &&
|
||||
source.Coordinate == runtime.HellParty.Room.Coordinate &&
|
||||
source.Map.Map.ID == runtime.HellParty.Room.HellMap.ID &&
|
||||
runtime.HellParty.Runtime.Phase() == dnfhellparty.PhaseStarted
|
||||
}
|
||||
Reference in new issue
Block a user