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,615 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errDungeonDropObjectKeyRange = errors.New("dnf dungeon drop object key range exhausted")
|
||||
errDungeonDropSceneSlotRange = errors.New("dnf dungeon drop scene slot range exhausted")
|
||||
errDungeonDropObjectConflict = errors.New("dnf dungeon drop object key already exists")
|
||||
errDungeonDropNotFound = errors.New("dnf dungeon drop object was not found")
|
||||
errDungeonDropRoomMismatch = errors.New("dnf dungeon drop belongs to another room")
|
||||
errDungeonDropActorMismatch = errors.New("dnf dungeon drop belongs to another actor")
|
||||
errDungeonDropCoinBranchUnproved = errors.New("dnf dungeon coin/gold pickup branch is not proved")
|
||||
)
|
||||
|
||||
type runtimeDungeonDropStatus byte
|
||||
|
||||
const (
|
||||
runtimeDungeonDropAvailable runtimeDungeonDropStatus = iota + 1
|
||||
runtimeDungeonDropPending
|
||||
runtimeDungeonDropConsumed
|
||||
)
|
||||
|
||||
type runtimeDungeonDrop struct {
|
||||
ObjectKey uint32
|
||||
SceneSlot uint16
|
||||
Item dungeonDropItemDefinition
|
||||
Amount uint32
|
||||
QualitySeed uint32
|
||||
DiscardOrigin *runtimeDungeonDiscardOrigin
|
||||
OwnerActorObjectKey uint16
|
||||
Room runtimeDungeonRoomKey
|
||||
Status runtimeDungeonDropStatus
|
||||
PickupAttempt uint64
|
||||
DestinationSlot uint16
|
||||
GoldAfter int64
|
||||
PickupResponseBody []byte
|
||||
PickupItemUpdateBody []byte
|
||||
PickupEpicBookClearBody []byte
|
||||
PickupEpicBookBody []byte
|
||||
}
|
||||
|
||||
func (drop *runtimeDungeonDrop) isGold() bool {
|
||||
return drop != nil && drop.Item.ItemID == 0
|
||||
}
|
||||
|
||||
type runtimeDungeonDropOwner struct {
|
||||
nextSceneSlot uint16
|
||||
byObjectKey map[uint32]*runtimeDungeonDrop
|
||||
}
|
||||
|
||||
func newRuntimeDungeonDropOwner() *runtimeDungeonDropOwner {
|
||||
return &runtimeDungeonDropOwner{
|
||||
nextSceneSlot: 1,
|
||||
byObjectKey: make(map[uint32]*runtimeDungeonDrop),
|
||||
}
|
||||
}
|
||||
|
||||
func (owner *runtimeDungeonDropOwner) registerBatch(drops []*runtimeDungeonDrop, nextSceneSlot uint16) error {
|
||||
if owner == nil {
|
||||
return errDungeonDropNotFound
|
||||
}
|
||||
if owner.byObjectKey == nil {
|
||||
owner.byObjectKey = make(map[uint32]*runtimeDungeonDrop)
|
||||
}
|
||||
for _, drop := range drops {
|
||||
if drop == nil || drop.ObjectKey == 0 {
|
||||
return errDungeonDropNotFound
|
||||
}
|
||||
if _, exists := owner.byObjectKey[drop.ObjectKey]; exists {
|
||||
return fmt.Errorf("%w: object_key=%d", errDungeonDropObjectConflict, drop.ObjectKey)
|
||||
}
|
||||
}
|
||||
for _, drop := range drops {
|
||||
owner.byObjectKey[drop.ObjectKey] = drop
|
||||
}
|
||||
owner.nextSceneSlot = nextSceneSlot
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextObjectKeyAtOrAbove keeps the room actor/drop namespace monotonic even
|
||||
// when a room transition was planned from a stale actor high-water mark. Drop
|
||||
// receipts intentionally survive room changes for pickup/idempotency checks,
|
||||
// so reusing one of their keys makes the next reward batch fail atomically.
|
||||
func (owner *runtimeDungeonDropOwner) nextObjectKeyAtOrAbove(next uint32) uint32 {
|
||||
if owner == nil {
|
||||
return next
|
||||
}
|
||||
for objectKey := range owner.byObjectKey {
|
||||
if objectKey >= next {
|
||||
if objectKey == math.MaxUint32 {
|
||||
return objectKey
|
||||
}
|
||||
next = objectKey + 1
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func (owner *runtimeDungeonDropOwner) owned(
|
||||
objectKey uint32,
|
||||
room runtimeDungeonRoomKey,
|
||||
actorObjectKey uint16,
|
||||
) (*runtimeDungeonDrop, error) {
|
||||
if owner == nil || objectKey == 0 {
|
||||
return nil, errDungeonDropNotFound
|
||||
}
|
||||
drop := owner.byObjectKey[objectKey]
|
||||
if drop == nil {
|
||||
return nil, fmt.Errorf("%w: object_key=%d", errDungeonDropNotFound, objectKey)
|
||||
}
|
||||
if drop.Room != room {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: object_key=%d owned=%+v current=%+v",
|
||||
errDungeonDropRoomMismatch,
|
||||
objectKey,
|
||||
drop.Room,
|
||||
room,
|
||||
)
|
||||
}
|
||||
if drop.OwnerActorObjectKey != actorObjectKey {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: object_key=%d owned=%d current=%d",
|
||||
errDungeonDropActorMismatch,
|
||||
objectKey,
|
||||
drop.OwnerActorObjectKey,
|
||||
actorObjectKey,
|
||||
)
|
||||
}
|
||||
return drop, nil
|
||||
}
|
||||
|
||||
func (s *Service) planCurrentDungeonMonsterDrops(
|
||||
runtime *runtimeDungeonState,
|
||||
monster runtimeDungeonMonster,
|
||||
ownerActorObjectKey uint16,
|
||||
) ([]currentDungeonDeathDropWire, error) {
|
||||
// A raid gate drops nothing at all -- no monster drops, no regional materials,
|
||||
// no gold. Its reward is the phase settlement, so a farming drop inside the gate
|
||||
// would hand the raid's reward out twice. The 86JP reference reaches the same
|
||||
// conclusion from its own dungeon policy: DungeonRewardPolicy.AntonRaid keeps
|
||||
// monster EXP, quest progress and settlement while switching monster drops and
|
||||
// quest drops off.
|
||||
if currentDungeonRuntimeIsPractice(runtime) || s.currentDungeonRuntimeIsRaidGate(runtime) {
|
||||
return nil, nil
|
||||
}
|
||||
if runtime == nil || runtime.Session == nil || runtime.Room == nil {
|
||||
return nil, errDungeonWorldMapUnavailable
|
||||
}
|
||||
if ownerActorObjectKey == 0 {
|
||||
return nil, errDungeonDropActorMismatch
|
||||
}
|
||||
scene, ok := runtime.Session.Scene()
|
||||
if !ok {
|
||||
return nil, errDungeonWorldMapUnavailable
|
||||
}
|
||||
roomKey := runtimeDungeonRoomKeyFromScene(scene)
|
||||
monsterCatalog, err := s.dungeonMonsterCatalog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rules, err := currentDungeonMonsterDropRules(monsterCatalog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dropCatalog, err := monsterCatalog.DropCatalog()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fallbacksBefore := dropCatalog.GenericFallbackMarks()
|
||||
pool, err := dropCatalog.MonsterPool(monster.Spawn.MonsterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
areaMaterialEntry, areaMaterialMatched := currentDungeonAreaMaterialPoolEntry(runtime.Dungeon.ID)
|
||||
if areaMaterialMatched {
|
||||
// Regional materials are an extension of the monster-template weighted
|
||||
// pool, not a generic creation-rate item and not an independent drop.
|
||||
// Resolve the mapped item up front so a mismatched PVF fails closed.
|
||||
if _, err := dropCatalog.ResolveItem(areaMaterialEntry.ItemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool = append(pool, areaMaterialEntry)
|
||||
}
|
||||
visit, err := runtime.currentDungeonRoomVisit(scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monsterLevel, err := currentDungeonMonsterLevel(monster.Spawn, runtime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monsterType, err := currentDungeonMonsterType(monster.Spawn.Rank)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rng := newCurrentDungeonDropLCG(visit.DropRNG)
|
||||
probability, probabilityFound := rules.Probability(int64(monsterLevel))
|
||||
itemReference, itemReferenceFound := rules.ItemReference(int64(monsterLevel))
|
||||
difficultyRate := currentDungeonDropDifficultyRate(int(runtime.Request.Difficulty))
|
||||
raw := rules.RawTables()
|
||||
|
||||
type plannedDrop struct {
|
||||
itemID uint32
|
||||
amount uint32
|
||||
gold bool
|
||||
}
|
||||
selectedDrops := make([]plannedDrop, 0, 8)
|
||||
|
||||
// 86JP computes gold before the ordinary item-rate rolls, and gold is a
|
||||
// normal scene drop with template id 0. Older Go intentionally consumed the
|
||||
// roll but threw the object away because the coin pickup branch was still
|
||||
// being isolated; that made live dungeons never show金币.
|
||||
if probabilityFound {
|
||||
goldReferences, goldErr := currentDungeonGoldReferences(monsterCatalog)
|
||||
if goldErr != nil {
|
||||
return nil, goldErr
|
||||
}
|
||||
goldReference, goldFound := goldReferences[int64(monsterLevel)]
|
||||
if !goldFound {
|
||||
return nil, fmt.Errorf("%w: monster_level=%d", errCurrentDungeonGoldReferenceInvalid, monsterLevel)
|
||||
}
|
||||
goldAmount := currentDungeonGoldAmount(rng, goldReference, difficultyRate)
|
||||
goldRate := currentDungeonDropRate(probability.Rates[0], raw.MonsterTypeBonus[0][monsterType], difficultyRate)
|
||||
goldRoll := rng.Next(currentDungeonDropDenominator)
|
||||
goldSelected := goldRate > goldRoll
|
||||
s.logGameEvent(nil, "game-dungeon-monster-drop-gold-roll",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"monster_id", monster.Spawn.MonsterID,
|
||||
"monster_object_key", monster.ObjectKey,
|
||||
"monster_level", monsterLevel,
|
||||
"monster_type", monsterType,
|
||||
"base_rate", probability.Rates[0],
|
||||
"monster_type_bonus", raw.MonsterTypeBonus[0][monsterType],
|
||||
"difficulty_bonus", difficultyRate,
|
||||
"gold_rate", goldRate,
|
||||
"gold_roll", goldRoll,
|
||||
"gold_amount", goldAmount,
|
||||
"selected", goldSelected,
|
||||
"formula_source", "86jp_monster_gold_branch_itemdropinfo_common_and_monseter")
|
||||
if goldSelected {
|
||||
selectedDrops = append(selectedDrops, plannedDrop{amount: goldAmount, gold: true})
|
||||
}
|
||||
}
|
||||
|
||||
appendSelected := func(itemID uint32) error {
|
||||
if itemID == 0 {
|
||||
// Real gold is selected only through the gold-rate branch above,
|
||||
// where the amount comes from ItemDropInfo_Common.etc.
|
||||
return nil
|
||||
}
|
||||
if _, err := dropCatalog.ResolveItem(itemID); err != nil {
|
||||
return err
|
||||
}
|
||||
selectedDrops = append(selectedDrops, plannedDrop{itemID: itemID, amount: 1})
|
||||
return nil
|
||||
}
|
||||
randomDropAttempts, err := currentDungeonMapDropAttempts(monster.Spawn.RandomDropCount, "random")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fixedDropAttempts, err := currentDungeonMapDropAttempts(monster.Spawn.FixedDropCount, "specified")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dimensionDrops, err := currentDungeonDimensionDrops(monsterCatalog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dimensionTier, dimensionPool, dimensionMatched, err := currentDungeonDimensionDropPool(
|
||||
dimensionDrops,
|
||||
runtime.Dungeon.ID,
|
||||
runtime.Character.Level,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
characterJob := byte(0)
|
||||
characterGrowType := byte(runtime.Character.Stats["grow_type"]) & 0x0f
|
||||
if dimensionMatched && len(dimensionDrops.ChronicleByCareer) > 0 {
|
||||
jobValue, parseErr := strconv.ParseUint(strings.TrimSpace(runtime.Character.Job), 10, 8)
|
||||
if parseErr != nil {
|
||||
return nil, fmt.Errorf("%w: dimension character job=%q", errCurrentDungeonDimensionDropInvalid, runtime.Character.Job)
|
||||
}
|
||||
characterJob = byte(jobValue)
|
||||
}
|
||||
selectedDimensionChronicleItems := make([]uint32, 0, randomDropAttempts)
|
||||
selectedDimensionChronicleGroups := make([]string, 0, randomDropAttempts)
|
||||
if probabilityFound && itemReferenceFound {
|
||||
for attempt := 0; attempt < randomDropAttempts; attempt++ {
|
||||
if rate := currentDungeonDropRate(probability.Rates[1], raw.MonsterTypeBonus[1][monsterType], difficultyRate); rate > rng.Next(currentDungeonDropDenominator) {
|
||||
rarity := currentDungeonDropRarity(rng, rules)
|
||||
itemID, selected, err := dropCatalog.SelectGenericStackable(rng, int(monsterLevel), itemReference.ValueA, itemReference.ValueB, rarity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if selected {
|
||||
if err := appendSelected(itemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if rate := currentDungeonDropRate(probability.Rates[2], raw.MonsterTypeBonus[2][monsterType], difficultyRate); rate > rng.Next(currentDungeonDropDenominator) {
|
||||
var itemID uint32
|
||||
var selected bool
|
||||
if dimensionMatched && len(dimensionDrops.ChronicleByCareer) > 0 {
|
||||
var group string
|
||||
_, itemID, group, _, selected, err = currentDungeonDimensionChronicleItem(
|
||||
dimensionDrops,
|
||||
runtime.Dungeon.ID,
|
||||
runtime.Character.Level,
|
||||
characterJob,
|
||||
characterGrowType,
|
||||
rng,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if selected {
|
||||
selectedDimensionChronicleItems = append(selectedDimensionChronicleItems, itemID)
|
||||
selectedDimensionChronicleGroups = append(selectedDimensionChronicleGroups, group)
|
||||
}
|
||||
} else {
|
||||
rarity := currentDungeonDropRarity(rng, rules)
|
||||
itemID, selected, err = dropCatalog.SelectGenericEquipment(rng, int(monsterLevel), itemReference.ValueA, itemReference.ValueB, rarity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if selected {
|
||||
if err := appendSelected(itemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if rate := currentDungeonDropRate(probability.Rates[3], raw.MonsterTypeBonus[3][monsterType], difficultyRate); rate > rng.Next(currentDungeonDropDenominator) {
|
||||
rarity := currentDungeonDropRarity(rng, rules)
|
||||
itemID, selected, err := dropCatalog.SelectGenericStackable(rng, int(monsterLevel), itemReference.ValueA, itemReference.ValueB, rarity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if selected {
|
||||
if err := appendSelected(itemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dimensionCandidates := make([]uint32, 0, len(dimensionPool))
|
||||
for _, entry := range dimensionPool {
|
||||
dimensionCandidates = append(dimensionCandidates, entry.ItemID)
|
||||
}
|
||||
selectedSpecialDimensionItems := make([]uint32, 0, fixedDropAttempts*len(dimensionPool))
|
||||
if monsterType == 0 {
|
||||
// Normal monsters expose dimension materials only as ordinary area
|
||||
// candidates. They therefore retain the shared 1000/10000 outer roll
|
||||
// and cannot inherit the fixed special-monster projection.
|
||||
pool = append(pool, dimensionPool...)
|
||||
} else {
|
||||
// Champion, super-champion and boss actors keep the dimension dungeon's
|
||||
// special fixed projection. This was the original fragment path before
|
||||
// normal monsters were separated from it; keep the PVF fixed_drop_count
|
||||
// and every tier/group row authoritative rather than inventing another
|
||||
// rank probability.
|
||||
for attempt := 0; attempt < fixedDropAttempts; attempt++ {
|
||||
for _, entry := range dimensionPool {
|
||||
if err := appendSelected(entry.ItemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedSpecialDimensionItems = append(selectedSpecialDimensionItems, entry.ItemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedPoolItems := make([]uint32, 0, fixedDropAttempts)
|
||||
if len(pool) > 0 {
|
||||
for attempt := 0; attempt < fixedDropAttempts; attempt++ {
|
||||
if currentDungeonExplicitPoolRate <= rng.Next(currentDungeonDropDenominator) {
|
||||
continue
|
||||
}
|
||||
poolEntry, selected := selectDungeonMonsterDrop(pool, uint64(rng.NextUint32()))
|
||||
if selected {
|
||||
if err := appendSelected(poolEntry.ItemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedPoolItems = append(selectedPoolItems, poolEntry.ItemID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if dimensionMatched {
|
||||
eventName := "game-dungeon-monster-dimension-ordinary-pool-roll"
|
||||
dropMode := "ordinary_pool"
|
||||
if monsterType != 0 {
|
||||
eventName = "game-dungeon-monster-dimension-special-rank-projection"
|
||||
dropMode = "special_rank_fixed"
|
||||
}
|
||||
s.logGameEvent(nil, eventName,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"monster_id", monster.Spawn.MonsterID,
|
||||
"monster_object_key", monster.ObjectKey,
|
||||
"monster_rank", monster.Spawn.Rank,
|
||||
"monster_type", monsterType,
|
||||
"character_level", runtime.Character.Level,
|
||||
"dimension_tier", dimensionTier,
|
||||
"drop_mode", dropMode,
|
||||
"ordinary_pool_rate", currentDungeonExplicitPoolRate,
|
||||
"ordinary_pool_denominator", currentDungeonDropDenominator,
|
||||
"ordinary_pool_attempts", fixedDropAttempts,
|
||||
"ordinary_pool_candidate_count", len(pool),
|
||||
"dimension_candidate_items", dimensionCandidates,
|
||||
"selected_pool_items", selectedPoolItems,
|
||||
"selected_special_dimension_items", selectedSpecialDimensionItems,
|
||||
"selected_chronicle_items", selectedDimensionChronicleItems,
|
||||
"selected_chronicle_groups", selectedDimensionChronicleGroups,
|
||||
"chronicle_job", characterJob,
|
||||
"chronicle_grow_type", characterGrowType,
|
||||
"special_probability_shared", false,
|
||||
"source", currentDungeonDimensionGatePath+"+"+currentDungeonDimensionInfoPath+"+"+currentDungeonDimensionDropListPath+"+"+currentDungeonDimensionDropExcludePath+"+rank_split_monster_template_pool")
|
||||
}
|
||||
if areaMaterialMatched {
|
||||
selectedAreaMaterialCount := 0
|
||||
for _, itemID := range selectedPoolItems {
|
||||
if itemID == areaMaterialEntry.ItemID {
|
||||
selectedAreaMaterialCount++
|
||||
}
|
||||
}
|
||||
s.logGameEvent(nil, "game-dungeon-monster-area-material-pool-roll",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"dungeon_path", runtime.Dungeon.Path,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"monster_id", monster.Spawn.MonsterID,
|
||||
"monster_object_key", monster.ObjectKey,
|
||||
"item_id", areaMaterialEntry.ItemID,
|
||||
"item_weight", areaMaterialEntry.Weight,
|
||||
"ordinary_pool_rate", currentDungeonExplicitPoolRate,
|
||||
"ordinary_pool_denominator", currentDungeonDropDenominator,
|
||||
"ordinary_pool_attempts", fixedDropAttempts,
|
||||
"ordinary_pool_candidate_count", len(pool),
|
||||
"selected_pool_items", selectedPoolItems,
|
||||
"selected_area_material_count", selectedAreaMaterialCount,
|
||||
"source", "active_pvf_metro_center_range+86jp_area_material_weighted_pool")
|
||||
}
|
||||
|
||||
specialDrops, err := currentDungeonSpecialDrops(monsterCatalog)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dungeonLevel := int(monsterLevel)
|
||||
if basisLevel := runtime.Dungeon.Metadata.BasisLevel; basisLevel.Set && basisLevel.Value > 0 {
|
||||
dungeonLevel = int(basisLevel.Value)
|
||||
}
|
||||
growthIndependentDropPercent := int64(0)
|
||||
if growthEffect, growthActive := s.currentGrowthContractEffect(
|
||||
context.Background(),
|
||||
runtime.Character.AccountID,
|
||||
); growthActive {
|
||||
// This runtime currently admits one locally owned dungeon actor. The
|
||||
// PVF's first independent-drop value is therefore authoritative.
|
||||
growthIndependentDropPercent = growthEffect.independentDropRatePercent(1)
|
||||
}
|
||||
independentItems := currentDungeonIndependentDropItems(
|
||||
specialDrops,
|
||||
monster.Spawn.MonsterID,
|
||||
int(runtime.Request.Difficulty),
|
||||
dungeonLevel,
|
||||
growthIndependentDropPercent,
|
||||
rng,
|
||||
)
|
||||
for _, itemID := range independentItems {
|
||||
if err := appendSelected(itemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
worldLevel := specialDrops.WorldByLevel[int(monsterLevel)]
|
||||
worldItemID, worldSelected := currentDungeonWorldDropItem(specialDrops, int(monsterLevel), rng)
|
||||
if worldSelected {
|
||||
if err := appendSelected(worldItemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if amount := currentDungeonKurunStrongBreathDropAmount(runtime.Dungeon, monsterType); amount != 0 {
|
||||
if _, err := dropCatalog.ResolveItem(currentDungeonKurunStrongBreathItemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedDrops = append(selectedDrops, plannedDrop{
|
||||
itemID: currentDungeonKurunStrongBreathItemID,
|
||||
amount: amount,
|
||||
})
|
||||
s.logGameEvent(nil, "game-dungeon-monster-kurun-strong-breath-drop",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"dungeon_path", runtime.Dungeon.Path,
|
||||
"monster_id", monster.Spawn.MonsterID,
|
||||
"monster_type", monsterType,
|
||||
"item_id", currentDungeonKurunStrongBreathItemID,
|
||||
"amount", amount,
|
||||
"source", "pvf_dragon_kurun_nateram_rank_rule")
|
||||
}
|
||||
s.logGameEvent(nil, "game-dungeon-monster-special-drop-roll",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"monster_id", monster.Spawn.MonsterID,
|
||||
"monster_level", monsterLevel,
|
||||
"dungeon_basis_level", dungeonLevel,
|
||||
"difficulty", runtime.Request.Difficulty,
|
||||
"independent_entry_count", len(specialDrops.IndependentByMonster[monster.Spawn.MonsterID]),
|
||||
"independent_selected_items", independentItems,
|
||||
"growth_contract_independent_drop_percent", growthIndependentDropPercent,
|
||||
"world_total_weight", worldLevel.TotalWeight,
|
||||
"world_candidate_count", len(worldLevel.Items),
|
||||
"world_denominator", currentDungeonWorldDropDenominator,
|
||||
"world_selected", worldSelected,
|
||||
"world_selected_item", worldItemID,
|
||||
"source", "runtime_pvf_independent_drop_and_world_drop")
|
||||
|
||||
// The LCG is advanced even when no standard scene item survived the real
|
||||
// PVF selection. This mirrors the reference room stream across kills.
|
||||
visit.DropRNG = rng.Seed()
|
||||
s.reportCurrentDungeonGenericDropFallbacks(dropCatalog, fallbacksBefore,
|
||||
"game-dungeon-monster-drop-empty-rarity-pool",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"room", scene.Coordinate.String(),
|
||||
"monster_id", monster.Spawn.MonsterID)
|
||||
if len(selectedDrops) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
owner := runtime.DropOwner
|
||||
if owner == nil {
|
||||
owner = newRuntimeDungeonDropOwner()
|
||||
}
|
||||
nextObjectKey := owner.nextObjectKeyAtOrAbove(runtime.NextObjectKey)
|
||||
count := uint32(len(selectedDrops))
|
||||
if nextObjectKey == 0 || nextObjectKey > math.MaxUint16 ||
|
||||
count > math.MaxUint16-nextObjectKey {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: next=%d count=%d",
|
||||
errDungeonDropObjectKeyRange,
|
||||
nextObjectKey,
|
||||
count,
|
||||
)
|
||||
}
|
||||
if owner.nextSceneSlot == 0 || uint32(owner.nextSceneSlot)+count > math.MaxUint16 {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: next=%d count=%d",
|
||||
errDungeonDropSceneSlotRange,
|
||||
owner.nextSceneSlot,
|
||||
count,
|
||||
)
|
||||
}
|
||||
|
||||
drops := make([]*runtimeDungeonDrop, 0, count)
|
||||
wires := make([]currentDungeonDeathDropWire, 0, count)
|
||||
nextSceneSlot := owner.nextSceneSlot
|
||||
for _, selected := range selectedDrops {
|
||||
definition := dungeonDropItemDefinition{ItemID: 0, Kind: dungeonDropItemStackable}
|
||||
if !selected.gold {
|
||||
var err error
|
||||
definition, err = dropCatalog.ResolveItem(selected.itemID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
itemState, err := currentDungeonDeathDropItemState(nextSceneSlot, definition, selected.amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qualitySeed := uint32(0)
|
||||
if definition.Kind == dungeonDropItemEquipment {
|
||||
qualitySeed = binary.LittleEndian.Uint32(itemState.data[6:10])
|
||||
}
|
||||
drop := &runtimeDungeonDrop{
|
||||
ObjectKey: nextObjectKey,
|
||||
SceneSlot: nextSceneSlot,
|
||||
Item: definition,
|
||||
Amount: selected.amount,
|
||||
QualitySeed: qualitySeed,
|
||||
OwnerActorObjectKey: ownerActorObjectKey,
|
||||
Room: roomKey,
|
||||
Status: runtimeDungeonDropAvailable,
|
||||
}
|
||||
drops = append(drops, drop)
|
||||
wires = append(wires, currentDungeonDeathDropWire{
|
||||
SceneObjectKey: drop.ObjectKey,
|
||||
Item: itemState,
|
||||
UnknownTailSentinel: math.MaxUint16,
|
||||
OwnerActorObjectKey: ownerActorObjectKey,
|
||||
})
|
||||
nextObjectKey++
|
||||
nextSceneSlot++
|
||||
}
|
||||
if err := owner.registerBatch(drops, nextSceneSlot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime.DropOwner = owner
|
||||
runtime.NextObjectKey = nextObjectKey
|
||||
return wires, nil
|
||||
}
|
||||
Reference in new issue
Block a user