服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
// Package profession owns the PVF-backed character advancement state machine.
|
|
// It contains no network opcodes and performs no repository I/O.
|
|
package profession
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
ErrRewardUnsupported = errors.New("dnf profession reward type is unsupported")
|
|
ErrRewardMalformed = errors.New("dnf profession reward data is malformed")
|
|
ErrTransitionInvalid = errors.New("dnf profession transition is invalid")
|
|
ErrTransitionOutOfStep = errors.New("dnf profession transition is out of sequence")
|
|
)
|
|
|
|
type Kind uint8
|
|
|
|
const (
|
|
KindNone Kind = iota
|
|
KindClassChange
|
|
KindAwakening
|
|
)
|
|
|
|
// Transition is the detached result of applying one final profession quest
|
|
// reward to the packed grow_type byte. Low four bits select the class branch;
|
|
// high four bits select the awakening stage.
|
|
type Transition struct {
|
|
Kind Kind
|
|
ChainType byte
|
|
GrowNumber byte
|
|
PreviousGrowType byte
|
|
NewGrowType byte
|
|
FirstGrowType byte
|
|
AwakeningStage byte
|
|
}
|
|
|
|
// Request is the detached profession reward encoded by one quest definition.
|
|
// It contains no character state; the repository owner must resolve it through
|
|
// the job's PVF profile inside the same transaction as the quest completion.
|
|
type Request struct {
|
|
Kind Kind
|
|
ChainType byte
|
|
GrowNumber byte
|
|
JobChangeQuest int
|
|
}
|
|
|
|
func Decode(growType byte) (first byte, awakening byte) {
|
|
return growType & 0x0f, (growType >> 4) & 0x0f
|
|
}
|
|
|
|
func Encode(first byte, awakening byte) (byte, error) {
|
|
if first > 0x0f || awakening > 0x0f {
|
|
return 0, fmt.Errorf("%w: first=%d awakening=%d", ErrTransitionInvalid, first, awakening)
|
|
}
|
|
return awakening<<4 | first, nil
|
|
}
|
|
|
|
// ParseReward accepts only the two profession reward tags used by the runtime
|
|
// PVF. It deliberately does not inspect or change grow_type.
|
|
func ParseReward(jobChangeQuest int, rewardType string, rewardData []int64) (Request, error) {
|
|
tag := normalizeTag(rewardType)
|
|
if tag != "grow type" && tag != "awakening type" {
|
|
return Request{}, fmt.Errorf("%w: type=%q", ErrRewardUnsupported, rewardType)
|
|
}
|
|
if len(rewardData) == 0 || rewardData[0] <= 0 || rewardData[0] > math.MaxUint8 {
|
|
return Request{}, fmt.Errorf("%w: type=%q values=%v", ErrRewardMalformed, rewardType, rewardData)
|
|
}
|
|
|
|
value := byte(rewardData[0])
|
|
switch tag {
|
|
case "grow type":
|
|
if value > 0x0f {
|
|
return Request{}, fmt.Errorf("%w: class=%d", ErrTransitionInvalid, value)
|
|
}
|
|
if jobChangeQuest != 0 && jobChangeQuest != 1 {
|
|
return Request{}, fmt.Errorf("%w: class change quest marker=%d", ErrTransitionInvalid, jobChangeQuest)
|
|
}
|
|
return Request{
|
|
Kind: KindClassChange, ChainType: 1, GrowNumber: value, JobChangeQuest: jobChangeQuest,
|
|
}, nil
|
|
case "awakening type":
|
|
if value > 2 {
|
|
return Request{}, fmt.Errorf("%w: awakening target=%d", ErrTransitionInvalid, value)
|
|
}
|
|
if jobChangeQuest != 0 && jobChangeQuest != int(value)+1 {
|
|
return Request{}, fmt.Errorf("%w: awakening target=%d quest marker=%d", ErrTransitionInvalid, value, jobChangeQuest)
|
|
}
|
|
return Request{
|
|
Kind: KindAwakening, ChainType: 2, GrowNumber: value, JobChangeQuest: jobChangeQuest,
|
|
}, nil
|
|
default:
|
|
return Request{}, fmt.Errorf("%w: type=%q", ErrRewardUnsupported, rewardType)
|
|
}
|
|
}
|
|
|
|
func normalizeTag(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if len(value) >= 2 && value[0] == '[' && value[len(value)-1] == ']' {
|
|
value = strings.TrimSpace(value[1 : len(value)-1])
|
|
}
|
|
return value
|
|
}
|