DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,636 @@
package main
import (
"context"
"database/sql"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"net"
"os"
"sort"
"strconv"
"strings"
"time"
mysql "github.com/go-sql-driver/mysql"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
batchID = "20260803-level90-strength-loadout"
mainListType = byte(0)
medalListType = byte(38)
extraSlotMask = byte(0x13)
topQualitySeed = uint32(999999998)
amplifyStamina = byte(1)
amplifyStrength = byte(3)
amplifyBaseValue = uint16(7)
itemRowSize = 0x77
)
type instanceConfig struct {
Database struct {
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
Name string `json:"name"`
} `json:"database"`
}
type itemSpec struct {
ListType byte
Slot int16
ItemID int64
Name string
PVFPath string
Kind string
PVFType string
Rarity int64
Grade int64
Durability uint16
Count int64
UpgradeLevel byte
Amplify bool
}
type character struct {
ID string
AccountID string
Name string
Level int
SlotState byte
}
type amplifyRepairTarget struct {
Equipped bool
CollectionName string
EntryKey string
ItemID int64
RawEntry []byte
}
func main() {
configPath := flag.String("config", "../runtime/config/instance.json", "runtime instance config")
pvfPath := flag.String("pvf", "../runtime/data/dnf/Script.pvf", "runtime Script.pvf")
apply := flag.Bool("apply", false, "commit the validated grant")
repairStrength := flag.Bool("repair-strength", false, "repair this batch from dimensional stamina to dimensional strength")
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
specs := requestedItems()
if err := validateManifest(*pvfPath, specs); err != nil {
fatal(err)
}
cfg, err := loadConfig(*configPath)
if err != nil {
fatal(err)
}
db, err := openDatabase(cfg)
if err != nil {
fatal(err)
}
defer db.Close()
if err := db.PingContext(ctx); err != nil {
fatal(fmt.Errorf("ping database: %w", err))
}
if *repairStrength {
result, repaired, repairErr := repairBatchStrength(ctx, db, specs, *apply)
if repairErr != nil {
fatal(repairErr)
}
mode := "DRY-RUN"
if *apply {
mode = "APPLIED"
}
fmt.Printf("%s strength-repair character=%s id=%s level=%d rows=%d old_type=%d new_type=%d batch=%s\n",
mode, result.Name, result.ID, result.Level, repaired, amplifyStamina, amplifyStrength, batchID)
return
}
result, err := grant(ctx, db, specs, *apply)
if err != nil {
fatal(err)
}
mode := "DRY-RUN"
if *apply {
mode = "APPLIED"
}
fmt.Printf("%s character=%s id=%s level=%d old_slot_mask=%#x new_slot_mask=%#x rows=%d main=%d medals=%d guardian_gem_stacks=%d guardian_gem_units=%d batch=%s\n",
mode, result.Name, result.ID, result.Level, result.SlotState, result.SlotState|extraSlotMask,
len(specs), countList(specs, mainListType), countMedals(specs), countGuardianGems(specs), guardianGemUnits(specs), batchID)
}
func loadConfig(path string) (instanceConfig, error) {
var cfg instanceConfig
raw, err := os.ReadFile(path)
if err != nil {
return cfg, fmt.Errorf("read instance config: %w", err)
}
if err := json.Unmarshal(raw, &cfg); err != nil {
return cfg, fmt.Errorf("parse instance config: %w", err)
}
if strings.TrimSpace(cfg.Database.Host) == "" || cfg.Database.Port <= 0 || strings.TrimSpace(cfg.Database.User) == "" || strings.TrimSpace(cfg.Database.Name) == "" {
return cfg, errors.New("instance config has incomplete database settings")
}
return cfg, nil
}
func openDatabase(cfg instanceConfig) (*sql.DB, error) {
mysqlConfig := mysql.Config{
User: cfg.Database.User,
Passwd: cfg.Database.Password,
Net: "tcp",
Addr: net.JoinHostPort(cfg.Database.Host, strconv.Itoa(cfg.Database.Port)),
DBName: cfg.Database.Name,
ParseTime: true,
Loc: time.UTC,
Params: map[string]string{"charset": "utf8mb4"},
}
dsn := mysqlConfig.FormatDSN()
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
return db, nil
}
func grant(ctx context.Context, db *sql.DB, specs []itemSpec, apply bool) (character, error) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return character{}, fmt.Errorf("begin grant transaction: %w", err)
}
defer tx.Rollback()
rows, err := tx.QueryContext(ctx, `SELECT character_id, account_id, name, level, ex_equip_slot_stat FROM dnf_characters WHERE level = 90 AND delete_flag = 0 FOR UPDATE`)
if err != nil {
return character{}, fmt.Errorf("lock level-90 character: %w", err)
}
var characters []character
for rows.Next() {
var row character
if err := rows.Scan(&row.ID, &row.AccountID, &row.Name, &row.Level, &row.SlotState); err != nil {
rows.Close()
return character{}, fmt.Errorf("scan level-90 character: %w", err)
}
characters = append(characters, row)
}
if err := rows.Close(); err != nil {
return character{}, fmt.Errorf("close character rows: %w", err)
}
if len(characters) != 1 {
return character{}, fmt.Errorf("expected exactly one active level-90 character, found %d", len(characters))
}
selected := characters[0]
occupiedRows, err := tx.QueryContext(ctx, `SELECT entry_key FROM dnf_inventory_items WHERE character_id = ? AND collection_name = 'slots' FOR UPDATE`, selected.ID)
if err != nil {
return character{}, fmt.Errorf("lock inventory: %w", err)
}
occupied := make(map[string]struct{})
for occupiedRows.Next() {
var key string
if err := occupiedRows.Scan(&key); err != nil {
occupiedRows.Close()
return character{}, fmt.Errorf("scan occupied inventory slot: %w", err)
}
occupied[key] = struct{}{}
}
if err := occupiedRows.Close(); err != nil {
return character{}, fmt.Errorf("close inventory rows: %w", err)
}
for _, spec := range specs {
key := itemKey(spec)
if _, exists := occupied[key]; exists {
return character{}, fmt.Errorf("required destination slot %s is occupied", key)
}
}
if !apply {
return selected, nil
}
if _, err := tx.ExecContext(ctx, `UPDATE dnf_characters SET ex_equip_slot_stat = ex_equip_slot_stat | ?, updated_at = UTC_TIMESTAMP(6) WHERE character_id = ?`, extraSlotMask, selected.ID); err != nil {
return character{}, fmt.Errorf("unlock extra equipment slots: %w", err)
}
if _, err := tx.ExecContext(ctx, `UPDATE dnf_inventories SET updated_at = UTC_TIMESTAMP(6) WHERE character_id = ?`, selected.ID); err != nil {
return character{}, fmt.Errorf("touch inventory: %w", err)
}
for _, spec := range specs {
if err := insertItem(ctx, tx, selected.ID, spec); err != nil {
return character{}, err
}
}
if err := tx.Commit(); err != nil {
return character{}, fmt.Errorf("commit loadout grant: %w", err)
}
return selected, nil
}
func repairBatchStrength(ctx context.Context, db *sql.DB, specs []itemSpec, apply bool) (character, int, error) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return character{}, 0, fmt.Errorf("begin amplification repair transaction: %w", err)
}
defer tx.Rollback()
rows, err := tx.QueryContext(ctx, `SELECT character_id, account_id, name, level, ex_equip_slot_stat FROM dnf_characters WHERE level = 90 AND delete_flag = 0 FOR UPDATE`)
if err != nil {
return character{}, 0, fmt.Errorf("lock level-90 character: %w", err)
}
var characters []character
for rows.Next() {
var row character
if err := rows.Scan(&row.ID, &row.AccountID, &row.Name, &row.Level, &row.SlotState); err != nil {
rows.Close()
return character{}, 0, fmt.Errorf("scan level-90 character: %w", err)
}
characters = append(characters, row)
}
if err := rows.Close(); err != nil {
return character{}, 0, fmt.Errorf("close character rows: %w", err)
}
if len(characters) != 1 {
return character{}, 0, fmt.Errorf("expected exactly one active level-90 character, found %d", len(characters))
}
selected := characters[0]
targets, err := loadAmplifyRepairTargets(ctx, tx, selected.ID)
if err != nil {
return character{}, 0, err
}
expected := make(map[int64]struct{}, 16)
for _, spec := range specs {
if spec.Amplify {
expected[spec.ItemID] = struct{}{}
}
}
if len(targets) != len(expected) {
return character{}, 0, fmt.Errorf("expected %d stamina-amplified batch rows, found %d", len(expected), len(targets))
}
seen := make(map[int64]struct{}, len(targets))
for _, target := range targets {
if _, ok := expected[target.ItemID]; !ok {
return character{}, 0, fmt.Errorf("unexpected batch item %d at %s", target.ItemID, target.EntryKey)
}
if _, duplicate := seen[target.ItemID]; duplicate {
return character{}, 0, fmt.Errorf("duplicate batch item %d", target.ItemID)
}
seen[target.ItemID] = struct{}{}
if len(target.RawEntry) != itemRowSize || target.RawEntry[0x0A]&0x1F != 13 || target.RawEntry[0x13] != amplifyStamina || binary.LittleEndian.Uint16(target.RawEntry[0x14:0x16]) != amplifyBaseValue {
return character{}, 0, fmt.Errorf("item %d at %s has unexpected raw amplification state", target.ItemID, target.EntryKey)
}
}
if !apply {
return selected, len(targets), nil
}
for _, target := range targets {
raw := append([]byte(nil), target.RawEntry...)
raw[0x13] = amplifyStrength
if target.Equipped {
result, err := tx.ExecContext(ctx, `UPDATE dnf_equipment_entries SET raw_entry = ? WHERE character_id = ? AND entry_key = ? AND item_id = ?`, raw, selected.ID, target.EntryKey, target.ItemID)
if err != nil {
return character{}, 0, fmt.Errorf("update equipped raw item %d: %w", target.ItemID, err)
}
if err := requireAffected(result, 1, "equipped raw", target.ItemID); err != nil {
return character{}, 0, err
}
result, err = tx.ExecContext(ctx, `UPDATE dnf_equipment_entry_extra SET extra_value = ? WHERE character_id = ? AND entry_key = ? AND extra_key IN ('amplify_type','amplification_type','byte_13','value_13','value_c') AND extra_value = ?`,
strconv.Itoa(int(amplifyStrength)), selected.ID, target.EntryKey, strconv.Itoa(int(amplifyStamina)))
if err != nil {
return character{}, 0, fmt.Errorf("update equipped amplification metadata item %d: %w", target.ItemID, err)
}
if err := requireAffected(result, 5, "equipped amplification metadata", target.ItemID); err != nil {
return character{}, 0, err
}
continue
}
result, err := tx.ExecContext(ctx, `UPDATE dnf_inventory_items SET raw_entry = ? WHERE character_id = ? AND collection_name = ? AND entry_key = ? AND item_id = ?`,
raw, selected.ID, target.CollectionName, target.EntryKey, target.ItemID)
if err != nil {
return character{}, 0, fmt.Errorf("update inventory raw item %d: %w", target.ItemID, err)
}
if err := requireAffected(result, 1, "inventory raw", target.ItemID); err != nil {
return character{}, 0, err
}
result, err = tx.ExecContext(ctx, `UPDATE dnf_inventory_item_extra SET extra_value = ? WHERE character_id = ? AND collection_name = ? AND entry_key = ? AND extra_key IN ('amplify_type','amplification_type','byte_13','value_13','value_c') AND extra_value = ?`,
strconv.Itoa(int(amplifyStrength)), selected.ID, target.CollectionName, target.EntryKey, strconv.Itoa(int(amplifyStamina)))
if err != nil {
return character{}, 0, fmt.Errorf("update inventory amplification metadata item %d: %w", target.ItemID, err)
}
if err := requireAffected(result, 5, "inventory amplification metadata", target.ItemID); err != nil {
return character{}, 0, err
}
}
if _, err := tx.ExecContext(ctx, `UPDATE dnf_inventories SET updated_at = UTC_TIMESTAMP(6) WHERE character_id = ?`, selected.ID); err != nil {
return character{}, 0, fmt.Errorf("touch inventory after amplification repair: %w", err)
}
if err := tx.Commit(); err != nil {
return character{}, 0, fmt.Errorf("commit amplification repair: %w", err)
}
return selected, len(targets), nil
}
func loadAmplifyRepairTargets(ctx context.Context, tx *sql.Tx, characterID string) ([]amplifyRepairTarget, error) {
var targets []amplifyRepairTarget
rows, err := tx.QueryContext(ctx, `SELECT i.collection_name, i.entry_key, i.item_id, i.raw_entry FROM dnf_inventory_items i JOIN dnf_inventory_item_extra b ON b.character_id=i.character_id AND b.collection_name=i.collection_name AND b.entry_key=i.entry_key AND b.extra_key='grant_batch' AND b.extra_value=? JOIN dnf_inventory_item_extra a ON a.character_id=i.character_id AND a.collection_name=i.collection_name AND a.entry_key=i.entry_key AND a.extra_key='amplify_type' AND a.extra_value=? WHERE i.character_id=? FOR UPDATE`,
batchID, strconv.Itoa(int(amplifyStamina)), characterID)
if err != nil {
return nil, fmt.Errorf("lock inventory amplification rows: %w", err)
}
for rows.Next() {
var target amplifyRepairTarget
if err := rows.Scan(&target.CollectionName, &target.EntryKey, &target.ItemID, &target.RawEntry); err != nil {
rows.Close()
return nil, fmt.Errorf("scan inventory amplification row: %w", err)
}
targets = append(targets, target)
}
if err := rows.Close(); err != nil {
return nil, fmt.Errorf("close inventory amplification rows: %w", err)
}
rows, err = tx.QueryContext(ctx, `SELECT e.entry_key, e.item_id, e.raw_entry FROM dnf_equipment_entries e JOIN dnf_equipment_entry_extra b ON b.character_id=e.character_id AND b.entry_key=e.entry_key AND b.extra_key='grant_batch' AND b.extra_value=? JOIN dnf_equipment_entry_extra a ON a.character_id=e.character_id AND a.entry_key=e.entry_key AND a.extra_key='amplify_type' AND a.extra_value=? WHERE e.character_id=? FOR UPDATE`,
batchID, strconv.Itoa(int(amplifyStamina)), characterID)
if err != nil {
return nil, fmt.Errorf("lock equipped amplification rows: %w", err)
}
for rows.Next() {
target := amplifyRepairTarget{Equipped: true}
if err := rows.Scan(&target.EntryKey, &target.ItemID, &target.RawEntry); err != nil {
rows.Close()
return nil, fmt.Errorf("scan equipped amplification row: %w", err)
}
targets = append(targets, target)
}
if err := rows.Close(); err != nil {
return nil, fmt.Errorf("close equipped amplification rows: %w", err)
}
return targets, nil
}
func requireAffected(result sql.Result, want int64, field string, itemID int64) error {
got, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("read affected rows for %s item %d: %w", field, itemID, err)
}
if got != want {
return fmt.Errorf("%s item %d affected %d rows, want %d", field, itemID, got, want)
}
return nil
}
func insertItem(ctx context.Context, tx *sql.Tx, characterID string, spec itemSpec) error {
key := itemKey(spec)
raw := buildRaw(spec)
if _, err := tx.ExecContext(ctx, `INSERT INTO dnf_inventory_items (character_id, collection_name, entry_key, item_id, item_count, bind_flag, expire_at, raw_entry) VALUES (?, 'slots', ?, ?, ?, 0, NULL, ?)`,
characterID, key, spec.ItemID, spec.Count, raw); err != nil {
return fmt.Errorf("insert %s (%d) at %s: %w", spec.Name, spec.ItemID, key, err)
}
extra := itemExtra(spec)
keys := make([]string, 0, len(extra))
for extraKey := range extra {
keys = append(keys, extraKey)
}
sort.Strings(keys)
for _, extraKey := range keys {
if _, err := tx.ExecContext(ctx, `INSERT INTO dnf_inventory_item_extra (character_id, collection_name, entry_key, extra_key, extra_value) VALUES (?, 'slots', ?, ?, ?)`,
characterID, key, extraKey, extra[extraKey]); err != nil {
return fmt.Errorf("insert metadata %s for %s at %s: %w", extraKey, spec.Name, key, err)
}
}
return nil
}
func buildRaw(spec itemSpec) []byte {
raw := make([]byte, itemRowSize)
binary.LittleEndian.PutUint16(raw[0x00:0x02], uint16(spec.Slot))
binary.LittleEndian.PutUint32(raw[0x02:0x06], uint32(spec.ItemID))
amount := uint32(spec.Count)
if spec.ListType == mainListType && spec.Kind == "equipment" {
amount = topQualitySeed
}
binary.LittleEndian.PutUint32(raw[0x06:0x0A], amount)
raw[0x0A] = spec.UpgradeLevel & 0x1F
binary.LittleEndian.PutUint16(raw[0x0B:0x0D], spec.Durability)
if spec.Kind == "stackable" {
binary.LittleEndian.PutUint32(raw[0x0E:0x12], uint32(spec.ItemID))
}
if spec.Amplify {
raw[0x13] = amplifyStrength
binary.LittleEndian.PutUint16(raw[0x14:0x16], amplifyBaseValue)
}
return raw
}
func itemExtra(spec itemSpec) map[string]string {
extra := map[string]string{
"source": "operator_direct_database_grant",
"grant_batch": batchID,
"item_name": spec.Name,
"item_kind": spec.Kind,
"pvf_path": spec.PVFPath,
}
if spec.PVFType != "" {
if spec.Kind == "equipment" {
extra["equipment_type"] = spec.PVFType
} else {
extra["stackable_type"] = spec.PVFType
}
}
if spec.Kind == "equipment" {
extra["durability"] = strconv.Itoa(int(spec.Durability))
extra["max_durability"] = strconv.Itoa(int(spec.Durability))
if spec.ListType == mainListType {
extra["quality_seed"] = strconv.FormatUint(uint64(topQualitySeed), 10)
}
if spec.UpgradeLevel != 0 {
level := strconv.Itoa(int(spec.UpgradeLevel))
extra["ext_data0"] = level
extra["packed_flag_byte"] = level
extra["reinforce"] = level
extra["upgrade_level"] = level
}
} else {
count := strconv.FormatInt(spec.Count, 10)
extra["amount"] = count
extra["amount_or_count"] = count
extra["count"] = count
extra["value_a"] = strconv.FormatInt(spec.ItemID, 10)
}
if spec.Amplify {
typeValue := strconv.Itoa(int(amplifyStrength))
baseValue := strconv.Itoa(int(amplifyBaseValue))
for _, key := range []string{"amplify_type", "amplification_type", "byte_13", "value_13", "value_c"} {
extra[key] = typeValue
}
for _, key := range []string{"amplify_value", "amplification_value", "marker_16", "marker16", "value_d"} {
extra[key] = baseValue
}
}
return extra
}
func itemKey(spec itemSpec) string {
return fmt.Sprintf("%d:%d", spec.ListType, spec.Slot)
}
func validateManifest(pvfPath string, specs []itemSpec) error {
archive, err := platformpvf.Open(pvfPath)
if err != nil {
return fmt.Errorf("open runtime PVF: %w", err)
}
seen := make(map[string]struct{}, len(specs))
for _, spec := range specs {
key := itemKey(spec)
if _, exists := seen[key]; exists {
return fmt.Errorf("duplicate destination slot %s", key)
}
seen[key] = struct{}{}
text, err := archive.ReadText(spec.PVFPath)
if err != nil {
return fmt.Errorf("read PVF item %d at %s: %w", spec.ItemID, spec.PVFPath, err)
}
doc, err := dnfpvf.Parse(spec.PVFPath, text)
if err != nil {
return fmt.Errorf("parse PVF item %d at %s: %w", spec.ItemID, spec.PVFPath, err)
}
name, _ := doc.Text("name")
if name != spec.Name {
return fmt.Errorf("PVF name mismatch item %d: got %q want %q", spec.ItemID, name, spec.Name)
}
section := "equipment type"
if spec.Kind == "stackable" {
section = "stackable type"
}
pvfType, _ := doc.Text(section)
if !strings.EqualFold(strings.TrimSpace(pvfType), spec.PVFType) {
return fmt.Errorf("PVF type mismatch item %d: got %q want %q", spec.ItemID, pvfType, spec.PVFType)
}
rarity, _ := doc.Int("rarity")
if rarity != spec.Rarity {
return fmt.Errorf("PVF rarity mismatch item %d: got %d want %d", spec.ItemID, rarity, spec.Rarity)
}
if spec.Grade != 0 {
grade, _ := doc.Int("grade")
if grade != spec.Grade {
return fmt.Errorf("PVF grade mismatch item %d: got %d want %d", spec.ItemID, grade, spec.Grade)
}
}
}
return nil
}
func requestedItems() []itemSpec {
main := []itemSpec{
{ItemID: 100070551, Name: "超大陆 - 瓦巴拉的大地", PVFPath: "equipment/character/common/jacket/larmor/100070551.equ", PVFType: "[coat]", Rarity: 4, Durability: 38},
{ItemID: 100120544, Name: "超大陆 - 盘古大陆的地震", PVFPath: "equipment/character/common/pants/larmor/100120544.equ", PVFType: "[pants]", Rarity: 4, Durability: 32},
{ItemID: 100170519, Name: "超大陆 - 潘诺西亚的火山", PVFPath: "equipment/character/common/shoulder/larmor/100170519.equ", PVFType: "[shoulder]", Rarity: 4, Durability: 30},
{ItemID: 100220519, Name: "超大陆 - 罗迪尼亚的熔岩", PVFPath: "equipment/character/common/belt/larmor/100220519.equ", PVFType: "[waist]", Rarity: 4, Durability: 25},
{ItemID: 100270515, Name: "超大陆 - 凯诺兰的地壳", PVFPath: "equipment/character/common/shoes/larmor/100270515.equ", PVFType: "[shoes]", Rarity: 4, Durability: 25},
{ItemID: 100300733, Name: "氤氲之息", PVFPath: "equipment/character/common/amulet/100300733.equ", PVFType: "[amulet]", Rarity: 4},
{ItemID: 100312425, Name: "启明星的指引", PVFPath: "equipment/character/common/wrist/100312425.equ", PVFType: "[wrist]", Rarity: 4},
{ItemID: 100322294, Name: "清泉流响", PVFPath: "equipment/character/common/ring/100322294.equ", PVFType: "[ring]", Rarity: 4},
{ItemID: 100390011, Name: "黑暗祭礼", PVFPath: "equipment/character/common/earring/100390011.equ", PVFType: "[earring]", Rarity: 4},
{ItemID: 100344511, Name: "波利斯的黄金杯", PVFPath: "equipment/character/common/support/100344511.equ", PVFType: "[support]", Rarity: 4},
{ItemID: 100352822, Name: "罗塞塔石碑", PVFPath: "equipment/character/common/magicstone/100352822.equ", PVFType: "[magic stone]", Rarity: 4},
{ItemID: 100390003, Name: "英雄王的象征", PVFPath: "equipment/character/common/earring/100390003.equ", PVFType: "[earring]", Rarity: 4},
{ItemID: 100344527, Name: "王冠非冠", PVFPath: "equipment/character/common/support/100344527.equ", PVFType: "[support]", Rarity: 4},
{ItemID: 100352839, Name: "王座本源", PVFPath: "equipment/character/common/magicstone/100352839.equ", PVFType: "[magic stone]", Rarity: 4},
{ItemID: 101010864, Name: "圣耀救赎太刀", PVFPath: "equipment/character/swordman/weapon/katana/101010864.equ", PVFType: "[weapon]", Rarity: 4, Durability: 45},
{ItemID: 101030741, Name: "圣耀救赎巨剑", PVFPath: "equipment/character/swordman/weapon/hsword/101030741.equ", PVFType: "[weapon]", Rarity: 4, Durability: 45},
{ItemID: 400330106, Name: "天选之人", PVFPath: "equipment/character/common/title/chn_400330106.equ", PVFType: "[title name]", Rarity: 6},
}
for index := range main {
main[index].ListType = mainListType
main[index].Slot = int16(9 + index)
main[index].Kind = "equipment"
main[index].Count = 1
if index < len(main)-1 {
main[index].UpgradeLevel = 13
main[index].Amplify = true
}
}
medalNames := []string{
"完美的野兽图腾勋章", "完美的神速图腾勋章", "完美的斗志图腾勋章", "完美的集中图腾勋章", "完美的减速图腾勋章", "完美的协作图腾勋章", "完美的再生图腾勋章",
"完美的守护图腾勋章", "完美的祝福图腾勋章", "完美的惩罚图腾勋章", "完美的元气图腾勋章", "完美的觉醒图腾勋章", "完美的坚韧图腾勋章",
}
medals := make([]itemSpec, 0, len(medalNames))
for index, name := range medalNames {
itemID := int64(100380051 + index)
medals = append(medals, itemSpec{
ListType: medalListType, Slot: int16(index), ItemID: itemID, Name: name,
PVFPath: fmt.Sprintf("equipment/character/common/flag/%d.equ", itemID), Kind: "equipment", PVFType: "[flag]",
Rarity: 6, Grade: 55, Count: 1, UpgradeLevel: 16,
})
}
gemIDs := []int64{90003, 90013, 90023, 90033, 90043, 90053, 90063, 90073, 90083, 90093, 90103, 90113, 90123}
gemNames := []string{
"玲珑之光守护珠 (物理防御力)", "玲珑之光守护珠 (魔法防御力)", "玲珑之光守护珠 (攻击速度)", "玲珑之光守护珠 (移动速度)", "玲珑之光守护珠 (施放速度)",
"玲珑之光守护珠 (物理暴击)", "玲珑之光守护珠 (魔法暴击)", "玲珑之光守护珠 (火属性强化)", "玲珑之光守护珠 (冰属性强化)", "玲珑之光守护珠 (暗属性强化)",
"玲珑之光守护珠 (光属性强化)", "玲珑之光守护珠 (命中率)", "玲珑之光守护珠 (负重上限)",
}
gems := make([]itemSpec, 0, len(gemIDs))
for index, itemID := range gemIDs {
gems = append(gems, itemSpec{
ListType: medalListType, Slot: int16(49 + index), ItemID: itemID, Name: gemNames[index],
PVFPath: fmt.Sprintf("stackable/flaggem/%d.stk", itemID), Kind: "stackable", PVFType: "[flag gem]",
Rarity: 3, Grade: 3, Count: 10,
})
}
return append(append(main, medals...), gems...)
}
func countList(specs []itemSpec, listType byte) int {
count := 0
for _, spec := range specs {
if spec.ListType == listType {
count++
}
}
return count
}
func countMedals(specs []itemSpec) int {
count := 0
for _, spec := range specs {
if spec.PVFType == "[flag]" {
count++
}
}
return count
}
func countGuardianGems(specs []itemSpec) int {
count := 0
for _, spec := range specs {
if spec.PVFType == "[flag gem]" {
count++
}
}
return count
}
func guardianGemUnits(specs []itemSpec) int64 {
var count int64
for _, spec := range specs {
if spec.PVFType == "[flag gem]" {
count += spec.Count
}
}
return count
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "grantlevel90loadout:", err)
os.Exit(1)
}