Files
90CNServer/server/internal/modules/dnf/pet/growth_test.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

254 lines
8.6 KiB
Go

package pet
import (
"errors"
"math"
"os"
"testing"
"time"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestPetGrowthDungeonClearLevelsAndAutoEvolvesFromPVF(t *testing.T) {
catalog := newPetGrowthTestCatalog(t, false)
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
update, err := engine.ApplyDungeonClear(PetGrowthState{
ItemID: 10, Experience: 1, Level: 1, Satiety: 100,
}, 1)
if err != nil {
t.Fatal(err)
}
if update.After.Experience != 2 || update.After.Level != 2 || update.After.ItemID != 11 || update.ExperienceGained != 1 {
t.Fatalf("update=%+v", update)
}
if !update.Evolution.Changed || update.Evolution.QuestEligible ||
update.Evolution.Definition.CurrentCreatureID != 1 || update.Evolution.Definition.TargetCreatureID != 2 {
t.Fatalf("evolution=%+v", update.Evolution)
}
}
func TestPetGrowthDungeonClearExposesQuestEvolutionWithoutAutoMutation(t *testing.T) {
catalog := newPetGrowthTestCatalog(t, true)
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
update, err := engine.ApplyDungeonClear(PetGrowthState{
ItemID: 10, Experience: 1, Level: 1, Satiety: 100,
}, 1)
if err != nil {
t.Fatal(err)
}
if update.After.ItemID != 10 || update.Evolution.Changed || !update.Evolution.QuestEligible ||
update.Evolution.Definition.QuestReference != "quest/faras.qst" {
t.Fatalf("update=%+v", update)
}
}
func TestPetGrowthDungeonClearHonorsSatietyAndExperienceBounds(t *testing.T) {
catalog := newPetGrowthTestCatalog(t, false)
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
dead := PetGrowthState{ItemID: 10, Experience: 1, Level: 1, Satiety: 0}
update, err := engine.ApplyDungeonClear(dead, 50)
if err != nil || update.After != dead || update.ExperienceGained != 0 {
t.Fatalf("dead update=%+v err=%v", update, err)
}
maxed := PetGrowthState{ItemID: 11, Experience: MaxCreatureExperience, Level: MaxCreatureLevel, Satiety: 100}
update, err = engine.ApplyDungeonClear(maxed, math.MaxInt32)
if err != nil || update.After != maxed || update.ExperienceGained != 0 {
t.Fatalf("max update=%+v err=%v", update, err)
}
}
func TestPetGrowthEvolutionMissingPVFFailsBeforeReturningMutation(t *testing.T) {
source := petGrowthTestSource(false)
delete(source, petCreatureListPath)
catalog, err := NewPVFCatalog(source)
if err != nil {
t.Fatal(err)
}
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
state := PetGrowthState{ItemID: 10, Experience: 1, Level: 1, Satiety: 100}
if _, err := engine.ApplyDungeonClear(state, 1); err == nil {
t.Fatal("missing creature evolution PVF did not fail closed")
}
}
func TestPetGrowthSatietyUsesTypedArtifactRatesAndTownRecovery(t *testing.T) {
catalog := newPetGrowthTestCatalog(t, false)
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
modifiers, err := catalog.ResolveSatietyModifiers([]int64{20, 20})
if err != nil {
t.Fatal(err)
}
if modifiers.FoodConsumeRatePercent() != 50 || modifiers.FoodConsumeMultiplier() != 1.5 {
t.Fatalf("modifiers=%+v multiplier=%v", modifiers, modifiers.FoodConsumeMultiplier())
}
state := PetGrowthState{ItemID: 10, Experience: 0, Level: 1, Satiety: 10}
dungeon, err := engine.ApplyDungeonElapsed(state, time.Minute, modifiers)
if err != nil {
t.Fatal(err)
}
if dungeon.After.Satiety != 8 || dungeon.SatietyDelta != -2 || !dungeon.Changed {
t.Fatalf("dungeon=%+v", dungeon)
}
town, err := engine.ApplyTownElapsed(dungeon.After, 360*time.Second)
if err != nil {
t.Fatal(err)
}
if town.After.Satiety != 9 || town.SatietyDelta != 1 || !town.Changed {
t.Fatalf("town=%+v", town)
}
reduction, err := catalog.ResolveSatietyModifiers([]int64{21})
if err != nil {
t.Fatal(err)
}
if reduction.FoodConsumeMultiplier() != 0.01 {
t.Fatalf("clamped multiplier=%v", reduction.FoodConsumeMultiplier())
}
}
func TestPetGrowthSatietyPreservesFractionalGaugeAcrossSettlements(t *testing.T) {
engine, err := NewPetGrowthEngine(newPetGrowthTestCatalog(t, false))
if err != nil {
t.Fatal(err)
}
state := PetGrowthState{ItemID: 10, Level: 1, Satiety: 100}
first, err := engine.ApplyDungeonElapsed(state, 30*time.Second, PetSatietyModifiers{})
if err != nil {
t.Fatal(err)
}
if first.After.Satiety != 99 || first.After.SatietyMicros != 99_500_000 {
t.Fatalf("first=%+v", first)
}
second, err := engine.ApplyDungeonElapsed(first.After, 30*time.Second, PetSatietyModifiers{})
if err != nil {
t.Fatal(err)
}
if second.After.Satiety != 99 || second.After.SatietyMicros != 99_000_000 {
t.Fatalf("second=%+v", second)
}
empty := PetGrowthState{ItemID: 10, Level: 1, Satiety: 0}
recovery1, err := engine.ApplyTownElapsed(empty, 180*time.Second)
if err != nil {
t.Fatal(err)
}
if recovery1.After.Satiety != 0 || recovery1.After.SatietyMicros != 500_000 {
t.Fatalf("recovery1=%+v", recovery1)
}
recovery2, err := engine.ApplyTownElapsed(recovery1.After, 180*time.Second)
if err != nil {
t.Fatal(err)
}
if recovery2.After.Satiety != 1 || recovery2.After.SatietyMicros != 1_000_000 {
t.Fatalf("recovery2=%+v", recovery2)
}
}
func TestPetGrowthRejectsInvalidStateElapsedAndArtifact(t *testing.T) {
catalog := newPetGrowthTestCatalog(t, false)
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
invalid := PetGrowthState{ItemID: 10, Level: 1, Satiety: 101}
if _, err := engine.ApplyTownElapsed(invalid, time.Second); !errors.Is(err, ErrPetGrowthStateInvalid) {
t.Fatalf("state error=%v", err)
}
valid := PetGrowthState{ItemID: 10, Level: 1, Satiety: 10}
if _, err := engine.ApplyTownElapsed(valid, -time.Second); !errors.Is(err, ErrPetGrowthElapsedInvalid) {
t.Fatalf("elapsed error=%v", err)
}
if _, err := catalog.ResolveSatietyModifiers([]int64{10}); !errors.Is(err, ErrPetPVFArtifactInvalid) {
t.Fatalf("artifact error=%v", err)
}
}
func TestRealScriptPVFPetGrowthEvolutionAndArtifactRate(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify runtime pet growth PVF")
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
if err != nil {
t.Fatal(err)
}
catalog, err := NewPVFCatalog(archive)
if err != nil {
t.Fatal(err)
}
evolution, found, err := catalog.ResolveEvolution(63000)
if err != nil {
t.Fatal(err)
}
if !found || evolution.CurrentCreatureID != 1 || evolution.TargetCreatureID != 2 ||
evolution.TargetItemID != 63001 || evolution.RequiredLevel != 20 || evolution.RequiresQuest {
t.Fatalf("real evolution=%+v found=%t", evolution, found)
}
modifiers, err := catalog.ResolveSatietyModifiers([]int64{2747170})
if err != nil {
t.Fatal(err)
}
if modifiers.FoodConsumeRatePercent() != 50 || modifiers.FoodConsumeMultiplier() != 1.5 {
t.Fatalf("real artifact modifiers=%+v", modifiers)
}
engine, err := NewPetGrowthEngine(catalog)
if err != nil {
t.Fatal(err)
}
update, err := engine.ApplyDungeonClear(PetGrowthState{
ItemID: 63000, Experience: 314, Level: 19, Satiety: 100,
}, 1)
if err != nil {
t.Fatal(err)
}
if update.After.Level != 20 || update.After.ItemID != 63001 || !update.Evolution.Changed {
t.Fatalf("real growth update=%+v", update)
}
}
func newPetGrowthTestCatalog(t *testing.T, quest bool) *PVFCatalog {
t.Helper()
catalog, err := NewPVFCatalog(petGrowthTestSource(quest))
if err != nil {
t.Fatal(err)
}
return catalog
}
func petGrowthTestSource(quest bool) petCatalogTestSource {
questValue := "0"
if quest {
questValue = "`quest/faras.qst`"
}
return petCatalogTestSource{
petEquipmentListPath: "10 `creature/faras.equ`\n11 `creature/pentabas.equ`\n" +
"20 `creature/artifact_red/test.equ`\n21 `creature/artifact_blue/reduction.equ`\n" +
"30 `weapon/support/test.equ`\n",
"equipment/creature/faras.equ": "[equipment type] `[creature]`\n[output index] 11\n",
"equipment/creature/pentabas.equ": "[equipment type] `[creature]`\n",
"equipment/creature/artifact_red/test.equ": "[equipment type] `[artifact red]`\n[creature food consume rate] 50\n",
"equipment/creature/artifact_blue/reduction.equ": "[equipment type] `[artifact blue]`\n[creature food consume rate] -200\n",
"equipment/weapon/support/test.equ": "[equipment type] `[support weapon]`\n",
petCreatureExperiencePath: petCatalogTestExperienceText(),
petCreatureListPath: "1 `faras/faras.cre`\n2 `pentabas/pentabas.cre`\n",
"creature/faras/faras.cre": "[evolution quest] " + questValue + "\n" +
"[evolution creature id] 2\n[evolution level] 2\n",
"creature/pentabas/pentabas.cre": "[evolution quest] 0\n[evolution creature id] 0\n[evolution level] 0\n",
}
}