Files
90CNServer/server/internal/modules/dnf/adventuregroup/credit.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

187 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}