合并本地领先源码:旧UI、GM疲劳燃烧、白金徽章孔、宠物修复、支援兵普攻开关

This commit is contained in:
hanfuit committed 2026-11-04 19:20:49 +08:00
1 parent f2debaf9bf
commit 70b3c5a6dc
57 files changed
+4256 -30

No files matched your search

@@ -0,0 +1,306 @@
package dnfbridge
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
dnfcharstat "longheng.io/server/internal/modules/dnf/charstat"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// Companion panel snapshot (Option B): the client captures the source
// character's final character-info panel values and stores them in a settings
// record; the support-soldier projection prefers this snapshot over the
// PVF-derived base vector so the companion can mirror the source panel.
const (
companionPanelSnapshotKeyPrefix = "character:"
companionPanelSnapshotKeySuffix = ":companion-panel-v1"
)
var errCompanionPanelCharacterID = errors.New("companion panel: character_id is required")
type companionPanelSnapshot struct {
HPMax int64
MPMax int64
Strength int64
Intelligence int64
Vitality int64
Spirit int64
PhysicalAttack int64
MagicalAttack int64
IndependentAttack int64
PhysicalDefense int64
MagicalDefense int64
FireResistance int64
WaterResistance int64
DarkResistance int64
LightResistance int64
MoveSpeed int64
AttackSpeed int64
CastSpeed int64
HitRecovery int64
JumpPower int64
Weight int64
InventoryLimit int64
HPRegenSpeed int64
MPRegenSpeed int64
SourceClient string
// The following values are shown on the character-info panel but are not
// part of the 92-byte companion mode1 blob; they are stored so the GM panel
// can mirror the full panel and future projections can consume them.
// Percentages are stored scaled by 10 (70.0% -> 700); the three speed fields
// above use the blob's scale of 100 (60.50% -> 6050).
PhysicalCritical int64
MagicalCritical int64
MagicResistance int64
HitRate int64
DodgeRate int64
Stiffness int64
FireDamage int64
WaterDamage int64
LightDamage int64
DarkDamage int64
}
func companionPanelSnapshotScope(characterID string) string {
return companionPanelSnapshotKeyPrefix + strings.TrimSpace(characterID) +
companionPanelSnapshotKeySuffix
}
func companionPanelSnapshotInt(values map[string]string, names ...string) int64 {
for _, name := range names {
raw, ok := values[name]
if !ok {
continue
}
parsed, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err == nil {
return parsed
}
}
return 0
}
func loadCompanionPanelSnapshot(
ctx context.Context,
repos dnfrepo.Group,
characterID string,
) (companionPanelSnapshot, bool) {
if repos.Settings == nil || strings.TrimSpace(characterID) == "" {
return companionPanelSnapshot{}, false
}
record, found, err := repos.Settings.Load(
ctx, companionPanelSnapshotScope(characterID))
if err != nil || !found {
return companionPanelSnapshot{}, false
}
values := record.Values
snapshot := companionPanelSnapshot{
HPMax: companionPanelSnapshotInt(values, "hp_max"),
MPMax: companionPanelSnapshotInt(values, "mp_max"),
Strength: companionPanelSnapshotInt(values, "strength"),
Intelligence: companionPanelSnapshotInt(values, "intelligence"),
Vitality: companionPanelSnapshotInt(values, "vitality"),
Spirit: companionPanelSnapshotInt(values, "spirit"),
PhysicalAttack: companionPanelSnapshotInt(values, "physical_attack"),
MagicalAttack: companionPanelSnapshotInt(values, "magical_attack"),
IndependentAttack: companionPanelSnapshotInt(values, "independent_attack"),
PhysicalDefense: companionPanelSnapshotInt(values, "physical_defense"),
MagicalDefense: companionPanelSnapshotInt(values, "magical_defense"),
FireResistance: companionPanelSnapshotInt(values, "fire_resistance"),
WaterResistance: companionPanelSnapshotInt(values, "water_resistance"),
DarkResistance: companionPanelSnapshotInt(values, "dark_resistance"),
LightResistance: companionPanelSnapshotInt(values, "light_resistance"),
MoveSpeed: companionPanelSnapshotInt(values, "move_speed"),
AttackSpeed: companionPanelSnapshotInt(values, "attack_speed"),
CastSpeed: companionPanelSnapshotInt(values, "cast_speed"),
HitRecovery: companionPanelSnapshotInt(values, "hit_recovery"),
JumpPower: companionPanelSnapshotInt(values, "jump_power"),
Weight: companionPanelSnapshotInt(values, "weight"),
InventoryLimit: companionPanelSnapshotInt(values, "inventory_limit"),
HPRegenSpeed: companionPanelSnapshotInt(values, "hp_regen"),
MPRegenSpeed: companionPanelSnapshotInt(values, "mp_regen"),
SourceClient: strings.TrimSpace(values["source_client"]),
PhysicalCritical: companionPanelSnapshotInt(values, "physical_critical"),
MagicalCritical: companionPanelSnapshotInt(values, "magical_critical"),
MagicResistance: companionPanelSnapshotInt(values, "magic_resistance"),
HitRate: companionPanelSnapshotInt(values, "hit_rate"),
DodgeRate: companionPanelSnapshotInt(values, "dodge_rate"),
Stiffness: companionPanelSnapshotInt(values, "stiffness"),
FireDamage: companionPanelSnapshotInt(values, "fire_damage"),
WaterDamage: companionPanelSnapshotInt(values, "water_damage"),
LightDamage: companionPanelSnapshotInt(values, "light_damage"),
DarkDamage: companionPanelSnapshotInt(values, "dark_damage"),
}
if snapshot.HPMax <= 0 || snapshot.MPMax <= 0 || snapshot.Strength <= 0 {
return companionPanelSnapshot{}, false
}
return snapshot, true
}
func (s companionPanelSnapshot) Vector() dnfcharstat.Vector {
return dnfcharstat.Vector{
HPMax: s.HPMax,
MPMax: s.MPMax,
Strength: s.Strength,
Intelligence: s.Intelligence,
Vitality: s.Vitality,
Spirit: s.Spirit,
PhysicalAttack: s.PhysicalAttack,
PhysicalDefense: s.PhysicalDefense,
MagicalAttack: s.MagicalAttack,
MagicalDefense: s.MagicalDefense,
IndependentAttack: s.IndependentAttack,
FireResistance: s.FireResistance,
WaterResistance: s.WaterResistance,
DarkResistance: s.DarkResistance,
LightResistance: s.LightResistance,
InventoryLimit: s.InventoryLimit,
HPRegenSpeed: s.HPRegenSpeed,
MPRegenSpeed: s.MPRegenSpeed,
MoveSpeed: s.MoveSpeed,
AttackSpeed: s.AttackSpeed,
CastSpeed: s.CastSpeed,
HitRecovery: s.HitRecovery,
JumpPower: s.JumpPower,
Weight: s.Weight,
}
}
// CompanionPanelValues returns the raw stored panel snapshot values for the GM
// editor. found is false when no snapshot has been saved yet.
func (s *Service) CompanionPanelValues(ctx context.Context, characterID string) (map[string]string, bool, error) {
repos, ok := s.repositoryGroup()
if !ok {
return nil, false, ErrGMRepositoryUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return nil, false, errCompanionPanelCharacterID
}
record, found, err := repos.Settings.Load(ctx, companionPanelSnapshotScope(characterID))
if err != nil {
return nil, false, err
}
if !found {
return map[string]string{}, false, nil
}
values := make(map[string]string, len(record.Values))
nonEmpty := false
for k, v := range record.Values {
values[k] = v
if strings.TrimSpace(v) != "" {
nonEmpty = true
}
}
// A record whose values are all empty is a leftover of a blank GM save and
// carries no usable panel; report it as not configured so the editor does
// not show a misleading "已读取".
if !nonEmpty {
return map[string]string{}, false, nil
}
return values, true, nil
}
// SaveCompanionPanelValues replaces the stored panel snapshot values.
func (s *Service) SaveCompanionPanelValues(ctx context.Context, characterID string, values map[string]string) error {
repos, ok := s.repositoryGroup()
if !ok {
return ErrGMRepositoryUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return errCompanionPanelCharacterID
}
cleaned := make(map[string]string, len(values))
for k, v := range values {
key := strings.TrimSpace(k)
if key == "" {
continue
}
cleaned[key] = strings.TrimSpace(v)
}
return repos.Settings.Save(ctx, dnfrepo.SettingsRecord{
Scope: companionPanelSnapshotScope(characterID),
Values: cleaned,
})
}
// GenerateCompanionPanelSnapshot builds a best-effort panel snapshot from the
// server's PVF character growth plus the character's worn equipment PVF stats
// (and weapon forging). It deliberately does not model enchant cards,
// amplification, reinforcement, quality, set effects, titles, auras or pets:
// the saved values are a BASE ESTIMATE that the GM page can correct.
func (s *Service) GenerateCompanionPanelSnapshot(ctx context.Context, characterID string) (map[string]string, error) {
repos, ok := s.repositoryGroup()
if !ok {
return nil, ErrGMRepositoryUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" {
return nil, errCompanionPanelCharacterID
}
character, found, err := repos.Character.Load(ctx, characterID)
if err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("character %s not found", characterID)
}
base, ok := s.characterPVFStatsForUserInfo(ctx, nil, character, true)
if !ok {
return nil, fmt.Errorf("character %s PVF stats unavailable", characterID)
}
stats := s.addEquipmentPVFStatsForUserInfo(ctx, nil, character, base)
values := companionPanelValuesFromVector(stats, character)
return values, nil
}
func companionPanelValuesFromVector(stats dnfcharstat.Vector, character dnfrepo.CharacterRecord) map[string]string {
values := make(map[string]string, 24)
put := func(key string, value int64) {
values[key] = strconv.FormatInt(value, 10)
}
put("hp_max", stats.HPMax)
put("mp_max", stats.MPMax)
put("strength", stats.Strength)
put("intelligence", stats.Intelligence)
put("vitality", stats.Vitality)
put("spirit", stats.Spirit)
put("physical_attack", stats.PhysicalAttack)
put("magical_attack", stats.MagicalAttack)
put("independent_attack", stats.IndependentAttack)
put("physical_defense", stats.PhysicalDefense)
put("magical_defense", stats.MagicalDefense)
put("fire_resistance", stats.FireResistance)
put("water_resistance", stats.WaterResistance)
put("dark_resistance", stats.DarkResistance)
put("light_resistance", stats.LightResistance)
put("move_speed", stats.MoveSpeed)
put("attack_speed", stats.AttackSpeed)
put("cast_speed", stats.CastSpeed)
put("hit_recovery", stats.HitRecovery)
jump := stats.JumpPower
if jump == 0 {
jump = character.Stats["stat_jump_power"]
}
put("jump_power", jump)
weight := stats.Weight
if weight == 0 {
weight = character.Stats["stat_weight"]
}
put("weight", weight)
inventory := stats.InventoryLimit
if inventory == 0 {
inventory = character.Stats["stat_inventory_limit"]
}
put("inventory_limit", inventory)
put("hp_regen", stats.HPRegenSpeed)
put("mp_regen", stats.MPRegenSpeed)
values["source_client"] = "server-estimate"
return values
}