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,239 @@
|
||||
package pet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MaxCreatureExperience int64 = math.MaxInt32
|
||||
|
||||
const petSatietyScale int64 = 1_000_000
|
||||
|
||||
var (
|
||||
ErrPetGrowthCatalogRequired = errors.New("pet growth catalog is required")
|
||||
ErrPetGrowthStateInvalid = errors.New("pet growth state is invalid")
|
||||
ErrPetGrowthElapsedInvalid = errors.New("pet growth elapsed duration is invalid")
|
||||
)
|
||||
|
||||
// PetGrowthState contains the typed fields changed by the pure growth domain.
|
||||
type PetGrowthState struct {
|
||||
ItemID int64
|
||||
Experience int64
|
||||
Level int
|
||||
Satiety int
|
||||
// SatietyMicros is the authoritative continuous gauge. A zero value with a
|
||||
// positive Satiety is treated as a legacy integer-only state.
|
||||
SatietyMicros int64
|
||||
}
|
||||
|
||||
// PetEvolutionUpdate distinguishes an automatic item transition from an
|
||||
// eligible quest-gated transition.
|
||||
type PetEvolutionUpdate struct {
|
||||
Definition PetEvolutionDefinition
|
||||
Changed bool
|
||||
QuestEligible bool
|
||||
}
|
||||
|
||||
// PetGrowthUpdate is a pure before/after result; persistence is owned by the
|
||||
// caller's aggregate transaction.
|
||||
type PetGrowthUpdate struct {
|
||||
Before PetGrowthState
|
||||
After PetGrowthState
|
||||
ExperienceGained int64
|
||||
Evolution PetEvolutionUpdate
|
||||
}
|
||||
|
||||
// PetSatietyUpdate is the pure result of one elapsed-time calculation.
|
||||
type PetSatietyUpdate struct {
|
||||
Before PetGrowthState
|
||||
After PetGrowthState
|
||||
Elapsed time.Duration
|
||||
SatietyDelta int
|
||||
Changed bool
|
||||
FoodMultiplier float64
|
||||
}
|
||||
|
||||
// PetGrowthEngine applies 86JP's domain rules using only typed PVF catalog
|
||||
// inputs. It has no repository, clock, timer, or protocol responsibility.
|
||||
type PetGrowthEngine struct {
|
||||
experience CreatureExperienceTable
|
||||
evolution PetEvolutionResolver
|
||||
creatures PetCreatureResolver
|
||||
}
|
||||
|
||||
func NewPetGrowthEngine(catalog *PVFCatalog) (*PetGrowthEngine, error) {
|
||||
if catalog == nil || catalog.source == nil {
|
||||
return nil, ErrPetGrowthCatalogRequired
|
||||
}
|
||||
return &PetGrowthEngine{experience: catalog.ExperienceTable(), evolution: catalog, creatures: catalog}, nil
|
||||
}
|
||||
|
||||
func (e *PetGrowthEngine) resolveCreature(itemID int64) (PetCreatureDefinition, error) {
|
||||
if e == nil || e.creatures == nil {
|
||||
return PetCreatureDefinition{}, ErrPetGrowthCatalogRequired
|
||||
}
|
||||
definition, err := e.creatures.ResolveCreature(itemID)
|
||||
if err != nil {
|
||||
return PetCreatureDefinition{}, err
|
||||
}
|
||||
if definition.ItemID != itemID || definition.PVFPath == "" {
|
||||
return PetCreatureDefinition{}, fmt.Errorf("%w: item_id=%d definition=%+v", ErrPetGrowthCatalogRequired, itemID, definition)
|
||||
}
|
||||
return definition, nil
|
||||
}
|
||||
|
||||
// ApplyDungeonClear grants cumulative creature experience equal to consumed
|
||||
// fatigue only while satiety is positive. A single clear can trigger at most
|
||||
// one automatic evolution, matching the 86JP domain owner.
|
||||
func (e *PetGrowthEngine) ApplyDungeonClear(state PetGrowthState, consumedFatigue int) (PetGrowthUpdate, error) {
|
||||
return e.applyDungeonClear(state, consumedFatigue, true)
|
||||
}
|
||||
|
||||
// applyDungeonClear keeps current online progression independent from the
|
||||
// still opt-in evolution route. Runtime callers pass resolveEvolution=false
|
||||
// until the current-client evolution wire is closed; that path must not lose
|
||||
// earned experience merely because a valid creature equipment document has no
|
||||
// creature.lst identity.
|
||||
func (e *PetGrowthEngine) applyDungeonClear(state PetGrowthState, consumedFatigue int, resolveEvolution bool) (PetGrowthUpdate, error) {
|
||||
if e == nil || (resolveEvolution && e.evolution == nil) {
|
||||
return PetGrowthUpdate{}, ErrPetGrowthCatalogRequired
|
||||
}
|
||||
if err := validatePetGrowthState(state); err != nil {
|
||||
return PetGrowthUpdate{}, err
|
||||
}
|
||||
update := PetGrowthUpdate{Before: state, After: state}
|
||||
if consumedFatigue <= 0 || petSatietyMicros(state) <= 0 || state.Level >= MaxCreatureLevel {
|
||||
return update, nil
|
||||
}
|
||||
gained := int64(consumedFatigue)
|
||||
afterExperience := state.Experience
|
||||
if gained >= MaxCreatureExperience-afterExperience {
|
||||
afterExperience = MaxCreatureExperience
|
||||
} else {
|
||||
afterExperience += gained
|
||||
}
|
||||
afterLevel := e.experience.LevelForExperience(afterExperience)
|
||||
if afterLevel < state.Level {
|
||||
afterLevel = state.Level
|
||||
}
|
||||
if afterLevel > MaxCreatureLevel {
|
||||
afterLevel = MaxCreatureLevel
|
||||
}
|
||||
update.After.Experience = afterExperience
|
||||
update.After.Level = afterLevel
|
||||
update.ExperienceGained = afterExperience - state.Experience
|
||||
if !resolveEvolution || afterLevel <= state.Level {
|
||||
return update, nil
|
||||
}
|
||||
definition, found, err := e.evolution.ResolveEvolution(state.ItemID)
|
||||
if err != nil {
|
||||
return PetGrowthUpdate{}, fmt.Errorf("resolve pet evolution: %w", err)
|
||||
}
|
||||
if !found || afterLevel < definition.RequiredLevel {
|
||||
return update, nil
|
||||
}
|
||||
update.Evolution.Definition = definition
|
||||
if definition.RequiresQuest {
|
||||
update.Evolution.QuestEligible = true
|
||||
return update, nil
|
||||
}
|
||||
update.After.ItemID = definition.TargetItemID
|
||||
update.Evolution.Changed = true
|
||||
return update, nil
|
||||
}
|
||||
|
||||
// ApplyDungeonElapsed consumes one satiety point per elapsed minute multiplied
|
||||
// by the typed artifact modifier. Positive fractional remainder is truncated;
|
||||
// a still-positive creature remains visible with at least one satiety.
|
||||
func (e *PetGrowthEngine) ApplyDungeonElapsed(state PetGrowthState, elapsed time.Duration, modifiers PetSatietyModifiers) (PetSatietyUpdate, error) {
|
||||
if e == nil {
|
||||
return PetSatietyUpdate{}, ErrPetGrowthCatalogRequired
|
||||
}
|
||||
if err := validatePetGrowthState(state); err != nil {
|
||||
return PetSatietyUpdate{}, err
|
||||
}
|
||||
if elapsed < 0 {
|
||||
return PetSatietyUpdate{}, ErrPetGrowthElapsedInvalid
|
||||
}
|
||||
multiplier := modifiers.FoodConsumeMultiplier()
|
||||
update := PetSatietyUpdate{Before: state, After: state, Elapsed: elapsed, FoodMultiplier: multiplier}
|
||||
beforeMicros := petSatietyMicros(state)
|
||||
if elapsed == 0 || beforeMicros <= 0 {
|
||||
return update, nil
|
||||
}
|
||||
consumedMicros := int64(math.Round(elapsed.Seconds() / 60 * multiplier * float64(petSatietyScale)))
|
||||
afterMicros := beforeMicros - consumedMicros
|
||||
if afterMicros < 0 {
|
||||
afterMicros = 0
|
||||
}
|
||||
after := visiblePetSatietyFromMicros(afterMicros, true)
|
||||
update.After.Satiety = after
|
||||
update.After.SatietyMicros = afterMicros
|
||||
update.SatietyDelta = after - state.Satiety
|
||||
update.Changed = afterMicros != beforeMicros
|
||||
return update, nil
|
||||
}
|
||||
|
||||
// ApplyTownElapsed restores one satiety point per 360 elapsed seconds, capped
|
||||
// at 100. Artifact modifiers do not affect recovery.
|
||||
func (e *PetGrowthEngine) ApplyTownElapsed(state PetGrowthState, elapsed time.Duration) (PetSatietyUpdate, error) {
|
||||
if e == nil {
|
||||
return PetSatietyUpdate{}, ErrPetGrowthCatalogRequired
|
||||
}
|
||||
if err := validatePetGrowthState(state); err != nil {
|
||||
return PetSatietyUpdate{}, err
|
||||
}
|
||||
if elapsed < 0 {
|
||||
return PetSatietyUpdate{}, ErrPetGrowthElapsedInvalid
|
||||
}
|
||||
update := PetSatietyUpdate{Before: state, After: state, Elapsed: elapsed, FoodMultiplier: 1}
|
||||
beforeMicros := petSatietyMicros(state)
|
||||
if elapsed == 0 || beforeMicros >= 100*petSatietyScale {
|
||||
return update, nil
|
||||
}
|
||||
recoveredMicros := int64(math.Round(elapsed.Seconds() / 360 * float64(petSatietyScale)))
|
||||
afterMicros := beforeMicros + recoveredMicros
|
||||
if afterMicros > 100*petSatietyScale {
|
||||
afterMicros = 100 * petSatietyScale
|
||||
}
|
||||
after := visiblePetSatietyFromMicros(afterMicros, false)
|
||||
update.After.Satiety = after
|
||||
update.After.SatietyMicros = afterMicros
|
||||
update.SatietyDelta = after - state.Satiety
|
||||
update.Changed = afterMicros != beforeMicros
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func petSatietyMicros(state PetGrowthState) int64 {
|
||||
if state.SatietyMicros > 0 || state.Satiety == 0 {
|
||||
return state.SatietyMicros
|
||||
}
|
||||
return int64(state.Satiety) * petSatietyScale
|
||||
}
|
||||
|
||||
func visiblePetSatietyFromMicros(value int64, clampAliveMinimum bool) int {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
if value >= 100*petSatietyScale {
|
||||
return 100
|
||||
}
|
||||
visible := int(value / petSatietyScale)
|
||||
if visible == 0 && clampAliveMinimum {
|
||||
return 1
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
func validatePetGrowthState(state PetGrowthState) error {
|
||||
if state.ItemID <= 0 || state.ItemID > math.MaxUint32 ||
|
||||
state.Experience < 0 || state.Experience > MaxCreatureExperience ||
|
||||
state.Level < 1 || state.Level > MaxCreatureLevel ||
|
||||
state.Satiety < 0 || state.Satiety > 100 ||
|
||||
state.SatietyMicros < 0 || state.SatietyMicros > 100*petSatietyScale {
|
||||
return fmt.Errorf("%w: %+v", ErrPetGrowthStateInvalid, state)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user