Files
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

162 lines
7.2 KiB
Go

package expertjob
import (
"errors"
"testing"
)
func TestPlanCompoundUsesLearnedRecipeRateAndSuccessfulExperienceOnly(t *testing.T) {
config := &Config{
JobType: AlchemistType,
ExperienceThresholds: []int64{10, 20, 30},
AutoLearnRecipes: map[int]int64{1: 1001},
CompoundRates: []CompoundRate{{
MaximumLevelDifference: 3,
SuccessRatePercent: 50,
MinimumExperienceGain: 2,
MaximumExperienceGain: 4,
}},
Recipes: map[int64]Recipe{
1002: {
RecipeItemID: 1002,
ProductItemID: 2002,
RequiredLevel: 2,
Materials: []RecipeEntry{{ItemID: 3001, Count: 2}},
Output: RecipeEntry{ItemID: 2002, Count: 3},
},
},
}
catalog := &Catalog{configs: map[byte]*Config{AlchemistType: config}}
rolls := []int{0, 2, 99}
position := 0
plan, err := catalog.PlanCompoundWithLearned(AlchemistType, 0, 1002, 2, true, func(int) int {
value := rolls[position]
position++
return value
})
if err != nil {
t.Fatal(err)
}
if plan.SuccessCount != 1 || plan.FailureCount != 1 || plan.ExperienceGain != 4 || plan.FinalExperience != 4 ||
len(plan.Materials) != 1 || plan.Materials[0].Count != 4 ||
len(plan.AttemptedOutputs) != 1 || plan.AttemptedOutputs[0].Count != 6 ||
len(plan.Rewards) != 1 || plan.Rewards[0].Count != 3 {
t.Fatalf("compound plan = %+v", plan)
}
if _, err := catalog.PlanCompound(AlchemistType, 0, 1002, 1, nil); err != ErrRecipeUnavailable {
t.Fatalf("unlearned recipe error = %v", err)
}
}
func TestPlanExtractionUsesEquipmentStateAndWeightedAdditionalResult(t *testing.T) {
config := &Config{
JobType: DollControllerType,
ExperienceThresholds: []int64{10, 20},
Extractors: map[int64]Extractor{
4001: {ItemID: 4001, RequiredLevel: 1, MinimumExperienceGain: 1, MaximumExperienceGain: 1},
},
ExtractionRules: map[extractionRuleKey]ExtractionRule{
{extractorID: 4001, rarity: 3, state: 1}: {ResultItemID: 5001, Multiplier: 0.1, AdditionalTable: 2},
},
AdditionalResults: map[int][]SelectionRule{
2: {{MinimumLevel: 1, MaximumLevel: 99, ItemID: 5002, Weight: 1, QuantityMultiplier: 0.2}},
},
}
catalog := &Catalog{configs: map[byte]*Config{DollControllerType: config}}
plan, err := catalog.PlanExtraction(DollControllerType, 0, 4001, EquipmentMetadata{Rarity: 3, Level: 50, State: 1}, nil)
if err != nil {
t.Fatal(err)
}
if len(plan.Materials) != 2 || plan.Materials[0] != (RecipeEntry{ItemID: 5001, Count: 5}) ||
plan.Materials[1] != (RecipeEntry{ItemID: 5002, Count: 10}) || plan.ExperienceGain != 1 {
t.Fatalf("extraction plan = %+v", plan)
}
}
func TestPlanEnchanterBeadAndStoreUseCardQualificationAndEndurance(t *testing.T) {
recipes := &Config{JobType: EnchanterType, ExperienceThresholds: []int64{10, 20}, Recipes: map[int64]Recipe{}}
enchanter := &EnchanterConfig{
Recipes: recipes, InitialEndurance: 300, EnduranceReduction: 3, EnduranceMinimumLevel: 1,
CardRecipes: map[int64]EnchanterCardRecipe{100: {Qualification: 1, RecipeItemID: 100, RequiredLevel: 1, Materials: []RecipeEntry{{ItemID: 300, Count: 2}}}},
Cards: map[int64]EnchanterCard{200: {Qualification: 1}}, BeadByCard: map[int64]int64{200: 400},
CardExperienceByLevel: map[int]EnchanterCardExperienceRule{1: {Level: 1, SuccessRates: [5]int{100, 70, 60, 50, 0}, MinimumExperienceGain: 3, MaximumExperienceGain: 6}},
}
catalog := &Catalog{configs: map[byte]*Config{EnchanterType: recipes}, enchanter: enchanter}
bead, err := catalog.PlanEnchanterBead(0, 100, 200, func(limit int) int {
if limit == 100 {
return 69
}
return limit - 1
})
if err != nil || !bead.Success || bead.BeadItemID != 400 || bead.ExperienceGain != 6 || bead.FinalExperience != 6 {
t.Fatalf("bead plan=%+v error=%v", bead, err)
}
store, err := catalog.PlanEnchanterStore(0, 100, 200, 3, func(int) int { return 99 })
if err != nil || store.Success || store.EnduranceReduction != 3 || store.ExperienceGain != 0 || store.FinalExperience != 0 {
t.Fatalf("store plan=%+v error=%v", store, err)
}
if _, err := catalog.PlanEnchanterStore(0, 100, 200, 2, nil); err != ErrMachineEndurance {
t.Fatalf("low-endurance error=%v", err)
}
}
func TestPlanEnchanterHighPresentationQualificationUsesFinalAuthoredRate(t *testing.T) {
recipes := &Config{JobType: EnchanterType, ExperienceThresholds: []int64{10}, Recipes: map[int64]Recipe{}}
enchanter := &EnchanterConfig{
Recipes: recipes, InitialEndurance: 300, EnduranceReduction: 3, EnduranceMinimumLevel: 1,
CardRecipes: map[int64]EnchanterCardRecipe{600: {Qualification: 6, RecipeItemID: 600, RequiredLevel: 1}},
Cards: map[int64]EnchanterCard{700: {Qualification: 6}}, BeadByCard: map[int64]int64{700: 800},
CardExperienceByLevel: map[int]EnchanterCardExperienceRule{1: {
Level: 1, SuccessRates: [5]int{10, 20, 30, 40, 100}, MinimumExperienceGain: 1, MaximumExperienceGain: 1,
}},
}
catalog := &Catalog{configs: map[byte]*Config{EnchanterType: recipes}, enchanter: enchanter}
bead, err := catalog.PlanEnchanterBead(0, 600, 700, func(int) int { return 99 })
if err != nil || !bead.Success || bead.BeadItemID != 800 {
t.Fatalf("bead=%+v err=%v", bead, err)
}
store, err := catalog.PlanEnchanterStore(0, 600, 700, 300, func(int) int { return 99 })
if err != nil || !store.Success {
t.Fatalf("store=%+v err=%v", store, err)
}
}
func TestPlanDisjointerRepairAndUpgradeUseMachineRules(t *testing.T) {
config := &DisjointerConfig{
InitialEndurance: 300, BaseConst: 150, EnduranceReduceMin: 1, EnduranceReduceMax: 1,
ExperienceGainMin: 1, ExperienceGainMax: 1,
ExperienceThresholds: []int64{10, 20},
Results: map[disjointerResultKey]DisjointerResultRule{{MachineGrade: 0, Rarity: 2, State: 0}: {ItemID: 500, Multiplier: 1}},
RepairRules: []RepairRule{{FullRepairCost: 300, MaxEndurance: 300}, {FullRepairCost: 600, MaxEndurance: 400}},
UpgradeCosts: map[int]int64{2: 1000}, CharacterLevelLimits: map[int]int{2: 20},
}
catalog := &Catalog{disjointer: config}
plan, err := catalog.PlanDisjointer(0, 1, EquipmentMetadata{Rarity: 2, Grade: 20, SellGold: 150}, false, nil)
if err != nil || len(plan.Materials) != 1 || plan.Materials[0] != (RecipeEntry{ItemID: 500, Count: 1}) || plan.EnduranceReduction != 1 || plan.ExperienceGain != 1 {
t.Fatalf("disjointer plan=%+v error=%v", plan, err)
}
repair, err := PlanMachineRepair(50, 200, config.RepairRules[0])
if err != nil || repair.Cost != 50 || repair.Endurance != 250 || repair.Gold != 0 {
t.Fatalf("repair plan=%+v error=%v", repair, err)
}
upgrade, err := config.PlanUpgrade(1500, 10, 300, 1, 20)
if err != nil || upgrade.Grade != 2 || upgrade.Gold != 500 || upgrade.Endurance != 400 {
t.Fatalf("upgrade plan=%+v error=%v", upgrade, err)
}
}
func TestPlanGiveUpUsesPVFCostTierAndCapsAtLastTier(t *testing.T) {
costs := []int64{1000, 10_000, 100_000, 1_000_000}
first, err := PlanGiveUp(60_000, 0, costs)
if err != nil || first.FinalGold != 59_000 || first.Cost != 1000 || first.FinalState != 1 {
t.Fatalf("first give-up plan=%+v error=%v", first, err)
}
last, err := PlanGiveUp(2_000_000, 99, costs)
if err != nil || last.FinalGold != 1_000_000 || last.Cost != 1_000_000 || last.FinalState != 3 {
t.Fatalf("last give-up plan=%+v error=%v", last, err)
}
if _, err := PlanGiveUp(999, 0, costs); !errors.Is(err, ErrInsufficientGold) {
t.Fatalf("insufficient-gold error=%v", err)
}
}