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,186 @@
|
||||
package adventuregroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
var ErrCreditAccountNotFound = errors.New("adventure-group credit account is missing")
|
||||
|
||||
// ExperienceCredit 描述"一次已经发给角色的经验"该怎么进账号级冒险团账本。
|
||||
//
|
||||
// 口径(2026-09-24 服主确认,此前只有满级经验才记账):
|
||||
//
|
||||
// - 骁勇结晶:**任意等级**获得的经验都按 PVF `[exp to get shop point]` 累计
|
||||
// (真值 1500 万 = 1 个,封顶 `[max shop point]` = 250)。角色自己的升级
|
||||
// 经验完全不受影响 —— 这里只是把同一笔经验**镜像**一份进账号账本,
|
||||
// "获得多少经验就累计多少"。
|
||||
// - 成长胶囊槽 / 名誉经验:**满级专属**。客户端自己的说明写着
|
||||
// "Lv90以上的角色获得经验值时,经验值会累积在成长胶囊槽中",
|
||||
// 名誉等级同样是满级之后的系统,所以只有越过等级上限的那部分进这两个账本。
|
||||
//
|
||||
// 三个账本共用一个数值来源,是为了让同一笔结算永远算出互相一致的账号状态;
|
||||
// 现在两个数值来源必须一起给出,免得调用方只填一个而悄悄改了别人的口径。
|
||||
type ExperienceCredit struct {
|
||||
// AwardedExperience 是这次**真正发给角色**的经验(含成长契约 / 装备 /
|
||||
// 疲劳蓄电池 / 任务连续性补足等加成),进骁勇结晶。
|
||||
AwardedExperience uint64
|
||||
// MaximumLevelExperience 是其中**越过等级上限**的那部分,进成长胶囊槽与
|
||||
// 名誉经验。它永远不该大于 AwardedExperience,Normalize 会兜住。
|
||||
MaximumLevelExperience uint64
|
||||
}
|
||||
|
||||
// Normalize 收敛两个数值的关系:满级部分不可能多于总经验。
|
||||
func (c ExperienceCredit) Normalize() ExperienceCredit {
|
||||
if c.MaximumLevelExperience > c.AwardedExperience {
|
||||
c.MaximumLevelExperience = c.AwardedExperience
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Empty 表示这笔经验什么都不用记(零经验或重放)。
|
||||
func (c ExperienceCredit) Empty() bool {
|
||||
return c.AwardedExperience == 0 && c.MaximumLevelExperience == 0
|
||||
}
|
||||
|
||||
// ExperienceCreditResult 汇报记入之后的账号级数值,便于日志与测试核对。
|
||||
type ExperienceCreditResult struct {
|
||||
AccountID string
|
||||
AwardedExperience uint64
|
||||
MaximumLevelExperience uint64
|
||||
BravePoints uint32
|
||||
BraveExperience uint64
|
||||
CapsuleExperience uint64
|
||||
HonorExperience uint64
|
||||
// GrowthLevel/GrowthExperience are the adventure-group level ledger after
|
||||
// this credit, for logging and tests. A credit that carries no creditable
|
||||
// level experience (a max-level character, or a zero-exp replay) is a no-op
|
||||
// and reports zero here without touching the stored ledger.
|
||||
GrowthLevel int
|
||||
GrowthExperience uint64
|
||||
}
|
||||
|
||||
// ApplyExperienceCredit 在**调用方已经加载、并即将在自己的事务里保存**的账号
|
||||
// 记录上记入一笔经验:
|
||||
//
|
||||
// - 骁勇结晶:任意等级的经验都算,按 PVF `[exp to get shop point]` 换算,
|
||||
// 封顶 `[max shop point]`;
|
||||
// - 成长胶囊槽:只算满级溢出部分,封顶 `[capsule max exp] × [capsule max count]`;
|
||||
// - 账号名誉经验 `accounts.honor_exp`:只算满级溢出部分(名誉等级是满级专属)。
|
||||
//
|
||||
// 调用方负责保存;本函数不碰仓库,因此不会和调用方已做的账号改动互相覆盖。
|
||||
func ApplyExperienceCredit(
|
||||
account *dnfrepo.AccountRecord,
|
||||
config RuntimeConfig,
|
||||
credit ExperienceCredit,
|
||||
now time.Time,
|
||||
) (ExperienceCreditResult, error) {
|
||||
if account == nil {
|
||||
return ExperienceCreditResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
accountID := strings.TrimSpace(account.AccountID)
|
||||
if accountID == "" {
|
||||
return ExperienceCreditResult{}, ErrAccountRequired
|
||||
}
|
||||
credit = credit.Normalize()
|
||||
if credit.Empty() {
|
||||
return ExperienceCreditResult{AccountID: accountID}, nil
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
state, err := ParseRuntimeState(*account, config, now)
|
||||
if err != nil {
|
||||
return ExperienceCreditResult{}, err
|
||||
}
|
||||
state.AddExperience(config, credit.AwardedExperience, credit.MaximumLevelExperience)
|
||||
// 测试口径:满级角色获得的经验也计入冒险团等级账本,验证冒险团经验
|
||||
// 是否正常增长;原始规则原本只计算 AwardedExperience - MaximumLevelExperience。
|
||||
levelGain := credit.AwardedExperience
|
||||
growth, err := ApplyGrowthExperience(account, config.Growth, Summary{}, uint32(min(levelGain, math.MaxUint32)), now)
|
||||
if err != nil {
|
||||
return ExperienceCreditResult{}, err
|
||||
}
|
||||
encoded, err := state.Marshal()
|
||||
if err != nil {
|
||||
return ExperienceCreditResult{}, err
|
||||
}
|
||||
if account.Metadata == nil {
|
||||
account.Metadata = make(map[string]string)
|
||||
}
|
||||
account.Metadata[RuntimeStateMetadataKey] = encoded
|
||||
// 名誉经验只收满级溢出:未满级经验不进名誉面板,这是服主确认的口径。
|
||||
if credit.MaximumLevelExperience > 0 {
|
||||
if account.HonorExp > math.MaxUint64-credit.MaximumLevelExperience {
|
||||
account.HonorExp = math.MaxUint64
|
||||
} else {
|
||||
account.HonorExp += credit.MaximumLevelExperience
|
||||
}
|
||||
}
|
||||
account.UpdatedAt = now
|
||||
return ExperienceCreditResult{
|
||||
AccountID: accountID,
|
||||
AwardedExperience: credit.AwardedExperience,
|
||||
MaximumLevelExperience: credit.MaximumLevelExperience,
|
||||
BravePoints: state.ShopPoints[ShopPointBrave],
|
||||
BraveExperience: state.BraveExperience,
|
||||
CapsuleExperience: state.GrowthExperience,
|
||||
HonorExperience: account.HonorExp,
|
||||
GrowthLevel: growth.Level,
|
||||
GrowthExperience: growth.Experience,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreditExperience 是 ApplyExperienceCredit 的读-改-写版本:在**调用方已经
|
||||
// 打开的事务**里加载账号、记入经验并保存。
|
||||
//
|
||||
// 怪物击杀、任务完成这些路径平时只碰角色/技能两张表,没有账号记录在手,
|
||||
// 用这个版本就能把账号级冒险团状态并进同一个事务提交,而不是事后再补一次
|
||||
// 可能失败的写(补写失败会让"经验涨了结晶没涨"这种状态静默留存)。
|
||||
func CreditExperience(
|
||||
ctx context.Context,
|
||||
accounts dnfrepo.AccountRepository,
|
||||
config RuntimeConfig,
|
||||
accountID string,
|
||||
credit ExperienceCredit,
|
||||
now time.Time,
|
||||
) (ExperienceCreditResult, error) {
|
||||
if accounts == nil {
|
||||
return ExperienceCreditResult{}, ErrOwnerUnavailable
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return ExperienceCreditResult{}, ErrAccountRequired
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return ExperienceCreditResult{}, err
|
||||
}
|
||||
if credit.Normalize().Empty() {
|
||||
return ExperienceCreditResult{AccountID: accountID}, nil
|
||||
}
|
||||
account, found, err := accounts.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return ExperienceCreditResult{}, fmt.Errorf("load adventure-group credit account: %w", err)
|
||||
}
|
||||
if !found || strings.TrimSpace(account.AccountID) != accountID {
|
||||
return ExperienceCreditResult{}, fmt.Errorf("%w: account=%s", ErrCreditAccountNotFound, accountID)
|
||||
}
|
||||
account = dnfrepo.CloneAccount(account)
|
||||
result, err := ApplyExperienceCredit(&account, config, credit, now)
|
||||
if err != nil {
|
||||
return ExperienceCreditResult{}, err
|
||||
}
|
||||
if err := accounts.Save(ctx, account); err != nil {
|
||||
return ExperienceCreditResult{}, fmt.Errorf("save adventure-group credit account: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user