307 lines
11 KiB
Go
307 lines
11 KiB
Go
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
|
|
}
|