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,329 @@
|
||||
package progression
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestPVFTablesDriveExperienceLevelUpsAndSPTP(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := tables.Snapshot(); got.ExperienceThresholds != 4 || got.SkillPointLevels != 5 ||
|
||||
got.TechniquePointLevels != 3 || got.QuestExperienceLevels != 5 || got.QuestGoldLevels != 14 || got.QuestDifficulties != 2 {
|
||||
t.Fatalf("snapshot = %+v", got)
|
||||
}
|
||||
|
||||
experience, err := tables.ApplyExperience(1, 90, 180, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if experience.NewLevel != 3 || experience.NewExperience != 270 || experience.LevelsGained != 2 || experience.Saturated {
|
||||
t.Fatalf("experience result = %+v", experience)
|
||||
}
|
||||
|
||||
points := dnfrepo.SkillPointState{
|
||||
TotalSP: 40, RemainingSP: 15, TotalTP: 3, RemainingTP: 1, SyncedLevel: 2,
|
||||
}
|
||||
advance, err := tables.AdvanceSkillPoints(points, experience.NewLevel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if advance.SPGain != 30 || advance.New.TotalSP != 70 || advance.New.RemainingSP != 45 || advance.New.SyncedLevel != 3 {
|
||||
t.Fatalf("SP advance = %+v", advance)
|
||||
}
|
||||
if advance.TPGain != 2 || advance.New.TotalTP != 5 || advance.New.RemainingTP != 3 {
|
||||
t.Fatalf("TP advance = %+v", advance)
|
||||
}
|
||||
if spentBefore, spentAfter := points.TotalSP-points.RemainingSP, advance.New.TotalSP-advance.New.RemainingSP; spentBefore != spentAfter {
|
||||
t.Fatalf("spent SP changed: before=%d after=%d", spentBefore, spentAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperienceTableReturnsCumulativeLevelEntryThreshold(t *testing.T) {
|
||||
table, err := LoadExperienceTable(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
level int
|
||||
want uint32
|
||||
}{{1, 0}, {2, 100}, {3, 250}, {5, 900}} {
|
||||
got, err := table.EntryExperience(test.level)
|
||||
if err != nil || got != test.want {
|
||||
t.Fatalf("EntryExperience(%d)=%d error=%v, want=%d", test.level, got, err, test.want)
|
||||
}
|
||||
}
|
||||
if _, err := table.EntryExperience(6); !errors.Is(err, ErrLevelOutOfRange) {
|
||||
t.Fatalf("EntryExperience(6) error=%v, want ErrLevelOutOfRange", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanExperienceAndSkillPointsCouplesLevelAndSPTP(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
points := dnfrepo.SkillPointState{TotalSP: 40, RemainingSP: 15, TotalTP: 3, RemainingTP: 1, SyncedLevel: 2}
|
||||
plan, err := tables.PlanExperienceAndSkillPoints(2, 240, 300, 5, points)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plan.Experience.NewLevel != 4 || plan.Experience.NewExperience != 540 {
|
||||
t.Fatalf("experience = %+v", plan.Experience)
|
||||
}
|
||||
if plan.SkillPoints.SPGain != 70 || plan.SkillPoints.New.TotalSP != 110 || plan.SkillPoints.New.RemainingSP != 85 || plan.SkillPoints.New.SyncedLevel != 4 {
|
||||
t.Fatalf("skill points = %+v", plan.SkillPoints)
|
||||
}
|
||||
if plan.SkillPoints.TPGain != 3 || plan.SkillPoints.New.TotalTP != 6 || plan.SkillPoints.New.RemainingTP != 4 {
|
||||
t.Fatalf("technique points = %+v", plan.SkillPoints)
|
||||
}
|
||||
if _, err := tables.PlanExperienceAndSkillPoints(2, 240, 300, 5, dnfrepo.SkillPointState{SyncedLevel: 1}); !errors.Is(err, ErrSkillPointLedger) {
|
||||
t.Fatalf("stale ledger error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyExperienceSaturatesCurrentEXEU32(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := tables.ApplyExperience(4, math.MaxUint32-3, 10, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.NewExperience != 900 || result.NewLevel != 5 || !result.Saturated {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyExperiencePinsLegacyOverflowAtMaximumLevelEntry(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := tables.ApplyExperience(5, 10_000, 20, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.PreviousExperience != 10_000 || result.NewExperience != 900 ||
|
||||
result.NewLevel != 5 || !result.Saturated {
|
||||
t.Fatalf("result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyExperienceRejectsCallerCapAboveDNF90Ceiling(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tables.ApplyExperience(1, 0, 100, MaximumCharacterLevel+1); !errors.Is(err, ErrLevelOutOfRange) {
|
||||
t.Fatalf("ApplyExperience() error = %v, want ErrLevelOutOfRange", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressionRejectsMissingSPLevelInsteadOfUnderpaying(t *testing.T) {
|
||||
source := progressionTestSource()
|
||||
source[SkillPointTablePath] = "[sp table]\n1 10\n3 30\n[/sp table]\n[tp table]\n3 1\n[/tp table]\n"
|
||||
tables, err := Load(context.Background(), source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tables.TotalSkillPoints(3); !errors.Is(err, ErrTableMalformed) {
|
||||
t.Fatalf("TotalSkillPoints error = %v, want ErrTableMalformed", err)
|
||||
}
|
||||
points := dnfrepo.SkillPointState{TotalSP: 10, RemainingSP: 10, SyncedLevel: 1}
|
||||
if _, err := tables.AdvanceSkillPoints(points, 3); !errors.Is(err, ErrTableMalformed) {
|
||||
t.Fatalf("AdvanceSkillPoints error = %v, want ErrTableMalformed", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestExperienceUsesPVFDifficultyAndPenalty(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
playerLevel int
|
||||
questLevel int
|
||||
difficulty rune
|
||||
ignore bool
|
||||
want uint32
|
||||
}{
|
||||
{name: "same level", playerLevel: 1, questLevel: 1, difficulty: 'N', want: 100},
|
||||
{name: "green penalty", playerLevel: 8, questLevel: 1, difficulty: 'N', want: 80},
|
||||
{name: "grey penalty", playerLevel: 13, questLevel: 1, difficulty: 'E', want: 60},
|
||||
{name: "ignore level uses player row", playerLevel: 5, questLevel: 1, difficulty: 'E', ignore: true, want: 1000},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := tables.QuestExperience(test.playerLevel, test.questLevel, test.difficulty, test.ignore)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("QuestExperience = %d, want %d", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if _, err := tables.QuestExperience(1, 1, 'X', false); !errors.Is(err, ErrQuestDifficulty) {
|
||||
t.Fatalf("unknown difficulty error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestGoldUsesPVFBaseMultiplierBeforeIntegerDivision(t *testing.T) {
|
||||
tables, err := Load(context.Background(), progressionTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
playerLevel int
|
||||
questLevel int
|
||||
goldMultiple int
|
||||
ignore bool
|
||||
want uint32
|
||||
}{
|
||||
{name: "default hundred percent keeps low base", playerLevel: 1, questLevel: 1, want: 10},
|
||||
{name: "explicit hundred fifty percent", playerLevel: 1, questLevel: 1, goldMultiple: 150, want: 15},
|
||||
{name: "direct zero-based quest-level row", playerLevel: 3, questLevel: 3, want: 30},
|
||||
{name: "green level gap does not penalize gold", playerLevel: 8, questLevel: 1, want: 10},
|
||||
{name: "grey level gap does not penalize gold", playerLevel: 13, questLevel: 1, want: 10},
|
||||
{name: "ignore EXP level remains on quest gold row", playerLevel: 8, questLevel: 1, goldMultiple: 150, ignore: true, want: 15},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := tables.QuestGold(test.playerLevel, test.questLevel, test.goldMultiple, test.ignore)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("QuestGold = %d, want %d", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFProgressionTables(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify real progression tables")
|
||||
}
|
||||
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tables, err := Load(context.Background(), archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
experienceTable, err := LoadExperienceTable(context.Background(), archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshot := tables.Snapshot()
|
||||
if snapshot.ExperienceThresholds < 90 || snapshot.SkillPointLevels < 90 || snapshot.TechniquePointLevels < 41 ||
|
||||
snapshot.QuestExperienceLevels < 90 || snapshot.QuestGoldLevels != 200 || snapshot.QuestDifficulties == 0 {
|
||||
t.Fatalf("real progression snapshot = %+v", snapshot)
|
||||
}
|
||||
thresholds := []uint32{1000, 2035, 3315}
|
||||
for level, want := range thresholds {
|
||||
got, err := tables.ThresholdToNext(level + 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("real threshold level %d = %d, want %d", level+1, got, want)
|
||||
}
|
||||
}
|
||||
if value, ok := tables.SkillPointsAtLevel(1); !ok || value != 0 {
|
||||
t.Fatalf("real level-1 SP = %d,%t, want 0,true", value, ok)
|
||||
}
|
||||
if value, ok := tables.SkillPointsAtLevel(2); !ok || value != 30 {
|
||||
t.Fatalf("real level-2 SP = %d,%t, want 30,true", value, ok)
|
||||
}
|
||||
advance, err := tables.AdvanceSkillPoints(dnfrepo.SkillPointState{SyncedLevel: 1}, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if advance.SPGain != 30 || advance.New.TotalSP != 30 || advance.New.RemainingSP != 30 {
|
||||
t.Fatalf("real SP advance = %+v", advance)
|
||||
}
|
||||
if totalTP, err := tables.TotalTechniquePoints(90); err != nil || totalTP != 41 {
|
||||
t.Fatalf("real level-90 TP total=%d err=%v, want 41", totalTP, err)
|
||||
}
|
||||
level90Entry, err := experienceTable.EntryExperience(90)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
level89Threshold, err := tables.ThresholdToNext(89)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if level90Entry != level89Threshold || level90Entry == 0 {
|
||||
t.Fatalf("real level-90 entry EXP=%d threshold89=%d", level90Entry, level89Threshold)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
playerLevel int
|
||||
questLevel int
|
||||
goldMultiple int
|
||||
ignore bool
|
||||
want uint32
|
||||
}{
|
||||
{name: "level60 ten percent", playerLevel: 90, questLevel: 60, goldMultiple: 10, want: 442},
|
||||
{name: "level60 ignores player gap", playerLevel: 72, questLevel: 60, goldMultiple: 10, want: 442},
|
||||
{name: "level60 ignores EXP lookup flag", playerLevel: 90, questLevel: 60, goldMultiple: 10, ignore: true, want: 442},
|
||||
{name: "level90 full value", playerLevel: 90, questLevel: 90, goldMultiple: 100, want: 8208},
|
||||
{name: "level118 full value", playerLevel: 90, questLevel: 118, goldMultiple: 100, want: 16272},
|
||||
} {
|
||||
t.Run("quest gold "+test.name, func(t *testing.T) {
|
||||
got, err := tables.QuestGold(test.playerLevel, test.questLevel, test.goldMultiple, test.ignore)
|
||||
if err != nil || got != test.want {
|
||||
t.Fatalf("real QuestGold=%d err=%v want=%d", got, err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Logf("real progression snapshot=%+v level2_threshold=%d level2_sp=%d", snapshot, thresholds[0], advance.SPGain)
|
||||
}
|
||||
|
||||
type progressionMemorySource map[string]string
|
||||
|
||||
func (s progressionMemorySource) ReadText(path string) (string, error) {
|
||||
value, ok := s[path]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %s", platformpvf.ErrFileNotFound, path)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func progressionTestSource() progressionMemorySource {
|
||||
return progressionMemorySource{
|
||||
ExperienceTablePath: "100 250 500 900\n",
|
||||
SkillPointTablePath: "[sp table]\n1 10\n2 30\n3 30\n4 40\n5 50\n[/sp table]\n[tp table]\n3 2\n4 1\n5 1\n[/tp table]\n",
|
||||
QuestParameterPath: `[difficulty]
|
||||
` + "`N` 100\n`E` 200\n" + `[/difficulty]
|
||||
[exp reward table]
|
||||
100 -1
|
||||
200 -1
|
||||
300 -1
|
||||
400 -1
|
||||
500 -1
|
||||
[gold reward table]
|
||||
0 10 20 30 40 50 60 70 80 90 100 110 120 130
|
||||
[green level penalty]
|
||||
80
|
||||
[grey level penalty]
|
||||
30
|
||||
`,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user