服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
586 lines
19 KiB
Go
586 lines
19 KiB
Go
package honor
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
platformpvf "longheng.io/server/internal/platform/pvf"
|
|
)
|
|
|
|
func TestTablesParseRepeatedOrdinaryGradesAndResolveExpertProgress(t *testing.T) {
|
|
tables, err := LoadTables(honorMemorySource{TablePath: honorTestDocument()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantSnapshot := Snapshot{
|
|
OrdinaryGrades: 2,
|
|
OrdinaryLevels: 3,
|
|
MaxOrdinaryLevel: 3,
|
|
MaxLevelExperience: 29,
|
|
MaxTotalExperience: 59,
|
|
ExpertGrades: 2,
|
|
ExpertExperienceRows: 3,
|
|
ExpertCalculationReady: true,
|
|
}
|
|
if snapshot := tables.Snapshot(); snapshot != wantSnapshot {
|
|
t.Fatalf("snapshot=%+v want=%+v", snapshot, wantSnapshot)
|
|
}
|
|
if tables.MaxOrdinaryLevel() != 3 || tables.MaxLevelExperience() != 29 || tables.MaxTotalExperience() != 59 {
|
|
t.Fatalf("ordinary maxima level=%d level_exp=%d total_exp=%d", tables.MaxOrdinaryLevel(), tables.MaxLevelExperience(), tables.MaxTotalExperience())
|
|
}
|
|
|
|
grades := tables.OrdinaryGrades()
|
|
if len(grades) != 2 || grades[0].Grade != 1 || grades[1].Grade != 2 ||
|
|
grades[0].EffectPath != "effect/one.ani" ||
|
|
grades[0].Medal != (ImageReference{Path: "medal.img", Index: 0}) ||
|
|
grades[1].Icon != (ImageReference{Path: "icon.img", Index: 1}) ||
|
|
!reflect.DeepEqual(grades[0].Levels, []LevelRequirement{
|
|
{Level: 1, RequiredExperience: 0},
|
|
{Level: 2, RequiredExperience: 10},
|
|
}) ||
|
|
!reflect.DeepEqual(grades[1].Levels, []LevelRequirement{{Level: 3, RequiredExperience: 20}}) {
|
|
t.Fatalf("ordinary grades=%+v", grades)
|
|
}
|
|
grades[0].Levels[0].RequiredExperience = 999
|
|
if fresh := tables.OrdinaryGrades(); fresh[0].Levels[0].RequiredExperience != 0 {
|
|
t.Fatal("ordinary grade accessor leaked mutable level slice")
|
|
}
|
|
|
|
expertGrades := tables.ExpertGrades()
|
|
if !reflect.DeepEqual(expertGrades, []ExpertGradeInfo{
|
|
{Grade: 0, Name: "challenger", MinLevel: 0, MaxLevel: 0},
|
|
{
|
|
Grade: 1, Name: "veteran",
|
|
Medal: ImageReference{Path: "expert-medal.img", Index: 2}, HasMedal: true,
|
|
Icon: ImageReference{Path: "expert-icon.img", Index: 3}, HasIcon: true,
|
|
MinLevel: 1, MaxLevel: -1,
|
|
},
|
|
}) {
|
|
t.Fatalf("expert grades=%+v", expertGrades)
|
|
}
|
|
wantExpertRows := []ExpertExperienceRow{
|
|
{Level: 1, Experience: 100},
|
|
{Level: 2, Experience: 204},
|
|
{Level: 3, Experience: 9999999999},
|
|
}
|
|
if rows := tables.ExpertExperienceRows(); !reflect.DeepEqual(rows, wantExpertRows) {
|
|
t.Fatalf("expert rows=%+v", rows)
|
|
}
|
|
if !tables.Snapshot().ExpertCalculationReady {
|
|
t.Fatal("expert progression calculation is unavailable")
|
|
}
|
|
|
|
for level, wantRequired := range map[int]uint64{1: 0, 2: 10, 3: 20} {
|
|
if got, found := tables.RequiredExperienceToEnter(level); !found || got != wantRequired {
|
|
t.Fatalf("required level=%d got=%d,%t want=%d", level, got, found, wantRequired)
|
|
}
|
|
}
|
|
if _, found := tables.RequiredExperienceToEnter(4); found {
|
|
t.Fatal("out-of-table ordinary requirement fabricated")
|
|
}
|
|
for level, wantGrade := range map[int]int{1: 1, 2: 1, 3: 2} {
|
|
if got, found := tables.GradeForLevel(level); !found || got != wantGrade {
|
|
t.Fatalf("grade level=%d got=%d,%t want=%d", level, got, found, wantGrade)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTablesAdvanceExpertUsesPerLevelThresholds(t *testing.T) {
|
|
tables, err := LoadTables(honorMemorySource{TablePath: honorTestDocument()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
state ExpertProgress
|
|
gain uint64
|
|
want ExpertProgress
|
|
}{
|
|
{name: "zero gain", state: ExpertProgress{}, gain: 0, want: ExpertProgress{}},
|
|
{name: "partial level zero", state: ExpertProgress{}, gain: 99, want: ExpertProgress{CurrentLevelExperience: 99}},
|
|
{name: "first level", state: ExpertProgress{CurrentLevelExperience: 99}, gain: 1, want: ExpertProgress{Level: 1}},
|
|
{name: "cross multiple levels", state: ExpertProgress{}, gain: 304, want: ExpertProgress{Level: 2}},
|
|
{name: "final level caps excess", state: ExpertProgress{Level: 2}, gain: 9999999999 + 17, want: ExpertProgress{Level: 3}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := tables.AdvanceExpert(test.state, test.gain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != test.want {
|
|
t.Fatalf("AdvanceExpert(%+v, %d) = %+v, want %+v", test.state, test.gain, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, state := range []ExpertProgress{
|
|
{Level: 4},
|
|
{CurrentLevelExperience: 100},
|
|
{Level: 3, CurrentLevelExperience: 1},
|
|
} {
|
|
if _, err := tables.ResolveExpert(state); !errors.Is(err, ErrExpertStateInvalid) {
|
|
t.Fatalf("ResolveExpert(%+v) error = %v", state, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTablesAdvanceExpertRepairsAFullBarInsteadOfRefusing pins the 2026-10-03
|
|
// operator fix.
|
|
//
|
|
// ResolveExpert refuses a "full bar" -- progress at or above the current level's
|
|
// requirement, or any progress at the final level. AdvanceExpert used to hand
|
|
// that refusal straight back, and both of its callers treat the error as fatal
|
|
// to their whole transaction, so a record in that shape made a character unable
|
|
// to finish ANY dungeon: the boss died, the settlement rolled back, and the
|
|
// client stayed in the instance with no result window until it was force-closed.
|
|
//
|
|
// The stored bar is now folded into the level-ups it already earned, so the
|
|
// result is exactly what the missing award would have produced, and the repaired
|
|
// state is always one ResolveExpert accepts.
|
|
func TestTablesAdvanceExpertRepairsAFullBarInsteadOfRefusing(t *testing.T) {
|
|
tables, err := LoadTables(honorMemorySource{TablePath: honorTestDocument()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Levels 0/1/2 cost 100 / 204 / 9999999999 to leave; level 3 is the last row.
|
|
tests := []struct {
|
|
name string
|
|
state ExpertProgress
|
|
gain uint64
|
|
want ExpertProgress
|
|
}{
|
|
{
|
|
name: "full bar at level zero is the level-up it earned",
|
|
state: ExpertProgress{CurrentLevelExperience: 100},
|
|
want: ExpertProgress{Level: 1},
|
|
},
|
|
{
|
|
name: "full bar carries its excess into the next level",
|
|
state: ExpertProgress{CurrentLevelExperience: 150},
|
|
want: ExpertProgress{Level: 1, CurrentLevelExperience: 50},
|
|
},
|
|
{
|
|
name: "a bar holding several levels is folded in one step",
|
|
state: ExpertProgress{CurrentLevelExperience: 304},
|
|
want: ExpertProgress{Level: 2},
|
|
},
|
|
{
|
|
name: "the final level discards progress it cannot show",
|
|
state: ExpertProgress{Level: 3, CurrentLevelExperience: 1},
|
|
want: ExpertProgress{Level: 3},
|
|
},
|
|
{
|
|
name: "a level beyond the table is clamped to the final level",
|
|
state: ExpertProgress{Level: 4},
|
|
want: ExpertProgress{Level: 3},
|
|
},
|
|
{
|
|
name: "clamping also drops the unshowable progress",
|
|
state: ExpertProgress{Level: 7, CurrentLevelExperience: 123},
|
|
want: ExpertProgress{Level: 3},
|
|
},
|
|
{
|
|
name: "a repaired bar still accepts the new award",
|
|
state: ExpertProgress{CurrentLevelExperience: 100},
|
|
gain: 100,
|
|
want: ExpertProgress{Level: 1, CurrentLevelExperience: 100},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := tables.AdvanceExpert(test.state, test.gain)
|
|
if err != nil {
|
|
t.Fatalf("AdvanceExpert(%+v, %d) refused a record it must repair: %v", test.state, test.gain, err)
|
|
}
|
|
if got != test.want {
|
|
t.Fatalf("AdvanceExpert(%+v, %d) = %+v, want %+v", test.state, test.gain, got, test.want)
|
|
}
|
|
if _, err := tables.ResolveExpert(got); err != nil {
|
|
t.Fatalf("repair produced a state the validator still refuses: %+v: %v", got, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTablesAdvanceExpertNeverRefusesAnyStoredRecord is the property the
|
|
// operator fix actually has to hold: no durable (level, progress) pair may be
|
|
// able to fail an award, because failing an award fails a whole dungeon clear.
|
|
//
|
|
// The grid deliberately includes every boundary the old code refused -- the
|
|
// exact requirement, one past it, several levels' worth, the final level with
|
|
// progress, and levels past the table -- plus a huge unaligned progress value.
|
|
func TestTablesAdvanceExpertNeverRefusesAnyStoredRecord(t *testing.T) {
|
|
tables, err := LoadTables(honorMemorySource{TablePath: honorTestDocument()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows := tables.ExpertExperienceRows()
|
|
finalLevel := uint32(len(rows))
|
|
|
|
levels := []uint32{0, 1, 2, 3, 4, 255, math.MaxUint32}
|
|
progresses := []uint64{
|
|
0, 1, 99, 100, 101, 204, 205, 304, 9999999999, 9999999999 + 1,
|
|
math.MaxUint64,
|
|
}
|
|
checked := 0
|
|
for _, level := range levels {
|
|
for _, progress := range progresses {
|
|
for _, gain := range []uint64{0, 1, 100, 304, 9999999999 + 17, math.MaxUint64} {
|
|
state := ExpertProgress{Level: level, CurrentLevelExperience: progress}
|
|
got, err := tables.AdvanceExpert(state, gain)
|
|
if err != nil {
|
|
t.Fatalf("AdvanceExpert(%+v, %d) refused: %v", state, gain, err)
|
|
}
|
|
if got.Level > finalLevel {
|
|
t.Fatalf("AdvanceExpert(%+v, %d) = %+v exceeds the final level %d", state, gain, got, finalLevel)
|
|
}
|
|
if _, err := tables.ResolveExpert(got); err != nil {
|
|
t.Fatalf("AdvanceExpert(%+v, %d) = %+v is still invalid: %v", state, gain, got, err)
|
|
}
|
|
checked++
|
|
}
|
|
}
|
|
}
|
|
if checked != len(levels)*len(progresses)*6 {
|
|
t.Fatalf("checked=%d, want %d", checked, len(levels)*len(progresses)*6)
|
|
}
|
|
}
|
|
|
|
func TestTablesResolveOrdinaryCumulativeExperience(t *testing.T) {
|
|
tables, err := LoadTables(honorMemorySource{TablePath: honorTestDocument()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tests := []struct {
|
|
total uint64
|
|
want Progress
|
|
}{
|
|
{total: 0, want: Progress{TotalExperience: 0, Level: 1, CurrentLevelExperience: 0, Grade: 1}},
|
|
{total: 9, want: Progress{TotalExperience: 9, Level: 1, CurrentLevelExperience: 9, Grade: 1}},
|
|
{total: 10, want: Progress{TotalExperience: 10, Level: 2, CurrentLevelExperience: 0, Grade: 1}},
|
|
{total: 29, want: Progress{TotalExperience: 29, Level: 2, CurrentLevelExperience: 19, Grade: 1}},
|
|
{total: 30, want: Progress{TotalExperience: 30, Level: 3, CurrentLevelExperience: 0, Grade: 2}},
|
|
{total: 59, want: Progress{TotalExperience: 59, Level: 3, CurrentLevelExperience: 29, Grade: 2}},
|
|
{total: math.MaxUint64, want: Progress{TotalExperience: 59, Level: 3, CurrentLevelExperience: 29, Grade: 2}},
|
|
}
|
|
for _, test := range tests {
|
|
got, err := tables.Resolve(test.total)
|
|
if err != nil {
|
|
t.Fatalf("total=%d: %v", test.total, err)
|
|
}
|
|
if got != test.want {
|
|
t.Fatalf("total=%d got=%+v want=%+v", test.total, got, test.want)
|
|
}
|
|
}
|
|
var nilTables *Tables
|
|
if _, err := nilTables.Resolve(0); !errors.Is(err, ErrTablesRequired) {
|
|
t.Fatalf("nil resolve error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCalculateHonorExperienceGainUsesCallerCharacterCap(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
level int
|
|
previous uint32
|
|
gained uint32
|
|
cap CharacterExperienceCap
|
|
want uint32
|
|
}{
|
|
{
|
|
name: "already caller max level",
|
|
level: 70, previous: 123, gained: 1000,
|
|
cap: CharacterExperienceCap{MaxLevel: 70, MaxLevelEntryExperience: 9000}, want: 1000,
|
|
},
|
|
{
|
|
name: "below caller threshold",
|
|
level: 69, previous: 800, gained: 100,
|
|
cap: CharacterExperienceCap{MaxLevel: 70, MaxLevelEntryExperience: 1000}, want: 0,
|
|
},
|
|
{
|
|
name: "exactly reaches caller threshold",
|
|
level: 69, previous: 900, gained: 100,
|
|
cap: CharacterExperienceCap{MaxLevel: 70, MaxLevelEntryExperience: 1000}, want: 0,
|
|
},
|
|
{
|
|
name: "only crossing overflow becomes honor",
|
|
level: 69, previous: 900, gained: 250,
|
|
cap: CharacterExperienceCap{MaxLevel: 70, MaxLevelEntryExperience: 1000}, want: 150,
|
|
},
|
|
{
|
|
name: "inconsistent pre-cap level above threshold keeps only new delta",
|
|
level: 69, previous: 1100, gained: 25,
|
|
cap: CharacterExperienceCap{MaxLevel: 70, MaxLevelEntryExperience: 1000}, want: 25,
|
|
},
|
|
{
|
|
name: "uint32 cumulative experience saturates",
|
|
level: 69, previous: math.MaxUint32 - 10, gained: 100,
|
|
cap: CharacterExperienceCap{MaxLevel: 70, MaxLevelEntryExperience: math.MaxUint32 - 20}, want: 10,
|
|
},
|
|
{
|
|
name: "zero gain",
|
|
level: 89, previous: 900, gained: 0,
|
|
cap: CharacterExperienceCap{MaxLevel: 90, MaxLevelEntryExperience: 1000}, want: 0,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := CalculateHonorExperienceGain(test.level, test.previous, test.gained, test.cap)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != test.want {
|
|
t.Fatalf("got=%d want=%d", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
for _, request := range []struct {
|
|
level int
|
|
cap CharacterExperienceCap
|
|
}{
|
|
{level: 0, cap: CharacterExperienceCap{MaxLevel: 90, MaxLevelEntryExperience: 1000}},
|
|
{level: 89, cap: CharacterExperienceCap{MaxLevel: 0, MaxLevelEntryExperience: 1000}},
|
|
{level: 89, cap: CharacterExperienceCap{MaxLevel: 90, MaxLevelEntryExperience: 0}},
|
|
} {
|
|
if _, err := CalculateHonorExperienceGain(request.level, 0, 1, request.cap); !errors.Is(err, ErrCharacterCapInvalid) {
|
|
t.Fatalf("request=%+v error=%v", request, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTablesRejectMalformedOrPartialPVF(t *testing.T) {
|
|
if _, err := LoadTables(nil); !errors.Is(err, ErrSourceRequired) {
|
|
t.Fatalf("nil source error=%v", err)
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(string) string
|
|
}{
|
|
{
|
|
name: "missing ordinary grade",
|
|
mutate: func(text string) string {
|
|
start := strings.Index(text, "[grade]")
|
|
end := strings.LastIndex(text, "[/grade]") + len("[/grade]")
|
|
return text[:start] + text[end:]
|
|
},
|
|
},
|
|
{
|
|
name: "duplicate ordinary grade id",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[grade]\n2 `effect/two.ani`", "[grade]\n1 `effect/two.ani`", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "ordinary level gap",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "3 20", "4 20", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "ordinary level one nonzero",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "1 0 2 10", "1 1 2 10", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "ordinary positive level zero requirement",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "2 10", "2 0", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "ordinary grade missing close",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[/grade]\n[grade]", "[grade]", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "duplicate maxexp section",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[expert info]", "[maxexp on maxlevel]\n29\n[expert info]", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "zero maxexp",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[maxexp on maxlevel]\n29", "[maxexp on maxlevel]\n0", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "missing expert info close",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[/expert info]", "", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "duplicate expert grade id",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[grade info]\n1 `veteran`", "[grade info]\n0 `veteran`", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "expert grade missing icon",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[icon img]\n`expert-icon.img` 3\n", "", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "expert grade invalid range",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[max lv]\n-1", "[max lv]\n0", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "expert exp level gap",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "2 `204`", "3 `204`", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "expert exp nonnumeric",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "2 `204`", "2 `unknown`", 1)
|
|
},
|
|
},
|
|
{
|
|
name: "expert exp missing close",
|
|
mutate: func(text string) string {
|
|
return strings.Replace(text, "[/honor expert exp table]", "", 1)
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := LoadTables(honorMemorySource{TablePath: test.mutate(honorTestDocument())})
|
|
if !errors.Is(err, ErrSectionShape) && !errors.Is(err, ErrSectionMissing) {
|
|
t.Fatalf("error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTablesRejectOrdinaryTotalOverflow(t *testing.T) {
|
|
text := honorTestDocument()
|
|
text = strings.Replace(text, "1 0 2 10", fmt.Sprintf("1 0 2 %d", int64(math.MaxInt64)), 1)
|
|
text = strings.Replace(text, "3 20", fmt.Sprintf("3 %d", int64(math.MaxInt64)), 1)
|
|
text = strings.Replace(text, "[maxexp on maxlevel]\n29", fmt.Sprintf("[maxexp on maxlevel]\n%d", int64(math.MaxInt64)), 1)
|
|
if _, err := LoadTables(honorMemorySource{TablePath: text}); !errors.Is(err, ErrSectionShape) {
|
|
t.Fatalf("overflow error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestRealScriptPVFHonorTablesPreserveOrdinaryAndExpertSources(t *testing.T) {
|
|
pvfPath := os.Getenv("DNF_HONOR_REAL_PVF_SMOKE")
|
|
if pvfPath == "" {
|
|
pvfPath = os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
|
}
|
|
if pvfPath == "" {
|
|
t.Skip("set DNF_HONOR_REAL_PVF_SMOKE to verify real honor PVF tables")
|
|
}
|
|
archive, err := platformpvf.Open(pvfPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tables, err := LoadTables(archive)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantSnapshot := Snapshot{
|
|
OrdinaryGrades: 6,
|
|
OrdinaryLevels: 59,
|
|
MaxOrdinaryLevel: 59,
|
|
MaxLevelExperience: 589999999,
|
|
MaxTotalExperience: 17699999999,
|
|
ExpertGrades: 7,
|
|
ExpertExperienceRows: 289,
|
|
ExpertCalculationReady: true,
|
|
}
|
|
if snapshot := tables.Snapshot(); snapshot != wantSnapshot {
|
|
t.Fatalf("real snapshot=%+v want=%+v", snapshot, wantSnapshot)
|
|
}
|
|
progress, err := tables.Resolve(35000000)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if progress != (Progress{TotalExperience: 35000000, Level: 3, CurrentLevelExperience: 5000000, Grade: 1}) {
|
|
t.Fatalf("real progress=%+v", progress)
|
|
}
|
|
capped, err := tables.Resolve(math.MaxUint64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if capped != (Progress{TotalExperience: 17699999999, Level: 59, CurrentLevelExperience: 589999999, Grade: 6}) {
|
|
t.Fatalf("real capped progress=%+v", capped)
|
|
}
|
|
rows := tables.ExpertExperienceRows()
|
|
for level, want := range map[int]uint64{
|
|
1: 1000000,
|
|
2: 2040000,
|
|
100: 48562450,
|
|
200: 2452644029,
|
|
289: 80464018485,
|
|
} {
|
|
if got := rows[level-1]; got != (ExpertExperienceRow{Level: level, Experience: want}) {
|
|
t.Fatalf("real expert row level=%d got=%+v want=%d", level, got, want)
|
|
}
|
|
}
|
|
expertGrades := tables.ExpertGrades()
|
|
if expertGrades[0].HasMedal || expertGrades[0].HasIcon || expertGrades[0].MinLevel != 0 || expertGrades[0].MaxLevel != 0 {
|
|
t.Fatalf("real expert grade zero=%+v", expertGrades[0])
|
|
}
|
|
if !expertGrades[6].HasMedal || !expertGrades[6].HasIcon || expertGrades[6].MinLevel != 200 || expertGrades[6].MaxLevel != -1 {
|
|
t.Fatalf("real expert grade six=%+v", expertGrades[6])
|
|
}
|
|
}
|
|
|
|
type honorMemorySource map[string]string
|
|
|
|
func (source honorMemorySource) ReadText(path string) (string, error) {
|
|
text, found := source[path]
|
|
if !found {
|
|
return "", os.ErrNotExist
|
|
}
|
|
return text, nil
|
|
}
|
|
|
|
func honorTestDocument() string {
|
|
return `[grade]
|
|
1 ` + "`effect/one.ani` `medal.img` 0 `icon.img` 0" + ` 1 0 2 10
|
|
[/grade]
|
|
[grade]
|
|
2 ` + "`effect/two.ani` `medal.img` 2 `icon.img` 1" + ` 3 20
|
|
[/grade]
|
|
[maxexp on maxlevel]
|
|
29
|
|
[expert info]
|
|
[grade info]
|
|
0 ` + "`challenger`" + `
|
|
[min lv]
|
|
0
|
|
[max lv]
|
|
0
|
|
[/grade info]
|
|
[grade info]
|
|
1 ` + "`veteran`" + `
|
|
[medal img]
|
|
` + "`expert-medal.img`" + ` 2
|
|
[icon img]
|
|
` + "`expert-icon.img`" + ` 3
|
|
[min lv]
|
|
1
|
|
[max lv]
|
|
-1
|
|
[/grade info]
|
|
[/expert info]
|
|
[honor expert exp table]
|
|
1 ` + "`100`" + ` 2 ` + "`204`" + ` 3 ` + "`9999999999`" + `
|
|
[/honor expert exp table]
|
|
`
|
|
}
|