You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
7.5 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package model
import (
"context"
"encoding/json"
"log/slog"
"sync/atomic"
"time"
)
// BuffJSON is DB/admin JSON for one buff type (durations in ms).
type BuffJSON struct {
Name string `json:"name"`
DurationMs int64 `json:"durationMs"`
Magnitude float64 `json:"magnitude"`
CooldownMs int64 `json:"cooldownMs"`
}
// DebuffJSON is DB/admin JSON for one debuff type (duration in ms).
type DebuffJSON struct {
Name string `json:"name"`
DurationMs int64 `json:"durationMs"`
Magnitude float64 `json:"magnitude"`
}
type buffDebuffPayload struct {
Buffs map[string]BuffJSON `json:"buffs"`
Debuffs map[string]DebuffJSON `json:"debuffs"`
}
type buffDebuffCatalogData struct {
buffs map[BuffType]Buff
debuffs map[DebuffType]Debuff
}
var buffDebuffCatalog atomic.Value
func init() {
buffDebuffCatalog.Store(&buffDebuffCatalogData{
buffs: seedBuffMap(),
debuffs: seedDebuffMap(),
})
}
func seedBuffMap() map[BuffType]Buff {
// Magnitudes follow docs/specification.md §7.1, then weakened by ⅓ (×2/3) vs the prior canon.
// Shield applies only in combat.CalculateIncomingDamage (not defense stats).
return map[BuffType]Buff{
BuffRush: {
Type: BuffRush, Name: "Rush",
Duration: 5 * time.Minute, Magnitude: 1.0 / 3.0, // was +50% move → ~+33%
CooldownDuration: 15 * time.Minute,
},
BuffRage: {
Type: BuffRage, Name: "Rage",
Duration: 3 * time.Minute, Magnitude: 2.0 / 3.0, // ~+67% damage
CooldownDuration: 10 * time.Minute,
},
BuffShield: {
Type: BuffShield, Name: "Shield",
Duration: 5 * time.Minute, Magnitude: 1.0 / 3.0, // ~33% incoming
CooldownDuration: 12 * time.Minute,
},
BuffLuck: {
Type: BuffLuck, Name: "Luck",
Duration: 30 * time.Minute, Magnitude: 1.0,
CooldownDuration: 2 * time.Hour,
},
BuffResurrection: {
Type: BuffResurrection, Name: "Resurrection",
Duration: 10 * time.Minute, Magnitude: 1.0 / 3.0, // ~33% max HP
CooldownDuration: 30 * time.Minute,
},
BuffHeal: {
Type: BuffHeal, Name: "Heal",
Duration: 1 * time.Second, Magnitude: 1.0 / 3.0, // ~+33% max HP
CooldownDuration: 5 * time.Minute,
},
BuffPowerPotion: {
Type: BuffPowerPotion, Name: "Power Potion",
Duration: 5 * time.Minute, Magnitude: 1.0, // was +150% → +100% after ⅔ scaling
CooldownDuration: 20 * time.Minute,
},
BuffWarCry: {
Type: BuffWarCry, Name: "War Cry",
Duration: 3 * time.Minute, Magnitude: 2.0 / 3.0, // ~+67% attack speed
CooldownDuration: 10 * time.Minute,
},
}
}
func seedDebuffMap() map[DebuffType]Debuff {
return map[DebuffType]Debuff{
DebuffPoison: {
Type: DebuffPoison, Name: "Poison",
Duration: 50 * time.Second, Magnitude: 0.012,
},
DebuffFreeze: {
Type: DebuffFreeze, Name: "Freeze",
Duration: 30 * time.Second, Magnitude: 0.50,
},
DebuffBurn: {
Type: DebuffBurn, Name: "Burn",
Duration: 40 * time.Second, Magnitude: 0.011,
},
DebuffStun: {
Type: DebuffStun, Name: "Stun",
Duration: 5 * time.Second, Magnitude: 1.0,
},
DebuffSlow: {
Type: DebuffSlow, Name: "Slow",
Duration: 40 * time.Second, Magnitude: 0.40,
},
DebuffWeaken: {
Type: DebuffWeaken, Name: "Weaken",
Duration: 50 * time.Second, Magnitude: 0.30,
},
DebuffIceSlow: {
Type: DebuffIceSlow, Name: "Ice Slow",
Duration: 40 * time.Second, Magnitude: 0.20,
},
}
}
func buffFromStrictJSON(bt BuffType, j BuffJSON) Buff {
return Buff{
Type: bt,
Name: j.Name,
Duration: time.Duration(j.DurationMs) * time.Millisecond,
Magnitude: j.Magnitude,
CooldownDuration: time.Duration(j.CooldownMs) * time.Millisecond,
}
}
func debuffFromStrictJSON(dt DebuffType, j DebuffJSON) Debuff {
return Debuff{
Type: dt,
Name: j.Name,
Duration: time.Duration(j.DurationMs) * time.Millisecond,
Magnitude: j.Magnitude,
}
}
func cloneBuffMap(src map[BuffType]Buff) map[BuffType]Buff {
out := make(map[BuffType]Buff, len(src))
for k, v := range src {
out[k] = v
}
return out
}
func cloneDebuffMap(src map[DebuffType]Debuff) map[DebuffType]Debuff {
out := make(map[DebuffType]Debuff, len(src))
for k, v := range src {
out[k] = v
}
return out
}
// BuffDebuffPayloadLoader loads raw JSON from persistence.
type BuffDebuffPayloadLoader interface {
LoadBuffDebuffConfigPayload(ctx context.Context) ([]byte, error)
}
// ReloadBuffDebuffCatalog merges DB payload into built-in seeds (same pattern as tuning).
func ReloadBuffDebuffCatalog(ctx context.Context, logger *slog.Logger, loader BuffDebuffPayloadLoader) error {
payload, err := loader.LoadBuffDebuffConfigPayload(ctx)
if err != nil {
if logger != nil {
logger.Warn("buff/debuff config load failed", "error", err)
}
return err
}
buffs := cloneBuffMap(seedBuffMap())
debuffs := cloneDebuffMap(seedDebuffMap())
if len(payload) > 0 {
var raw buffDebuffPayload
if err := json.Unmarshal(payload, &raw); err != nil {
if logger != nil {
logger.Warn("buff/debuff config parse failed", "error", err)
}
return err
}
// Per-key full replace: payload must include all fields for edited types (admin UI sends full effective maps).
for key, j := range raw.Buffs {
bt := BuffType(key)
if _, ok := buffs[bt]; ok {
buffs[bt] = buffFromStrictJSON(bt, j)
}
}
for key, j := range raw.Debuffs {
dt := DebuffType(key)
if _, ok := debuffs[dt]; ok {
debuffs[dt] = debuffFromStrictJSON(dt, j)
}
}
}
buffDebuffCatalog.Store(&buffDebuffCatalogData{buffs: buffs, debuffs: debuffs})
return nil
}
func catalogData() *buffDebuffCatalogData {
return buffDebuffCatalog.Load().(*buffDebuffCatalogData)
}
// BuffDefinition returns the active buff template (DB + defaults).
func BuffDefinition(bt BuffType) (Buff, bool) {
b, ok := catalogData().buffs[bt]
return b, ok
}
// DebuffDefinition returns the active debuff template (DB + defaults).
func DebuffDefinition(dt DebuffType) (Debuff, bool) {
d, ok := catalogData().debuffs[dt]
return d, ok
}
// BuffCatalogSnapshot returns copies for admin/API.
func BuffCatalogSnapshot() map[BuffType]Buff {
src := catalogData().buffs
out := make(map[BuffType]Buff, len(src))
for k, v := range src {
out[k] = v
}
return out
}
// AttachDebuffCatalogForClient fills h.DebuffCatalog from the active catalog (for JSON responses only).
func AttachDebuffCatalogForClient(h *Hero) {
if h == nil {
return
}
_, deb := BuffCatalogEffectiveJSON()
h.DebuffCatalog = deb
}
// DebuffCatalogSnapshot returns copies for admin/API.
func DebuffCatalogSnapshot() map[DebuffType]Debuff {
src := catalogData().debuffs
out := make(map[DebuffType]Debuff, len(src))
for k, v := range src {
out[k] = v
}
return out
}
// BuffToJSON converts Buff to BuffJSON (ms).
func BuffToJSON(b Buff) BuffJSON {
return BuffJSON{
Name: b.Name,
DurationMs: b.Duration.Milliseconds(),
Magnitude: b.Magnitude,
CooldownMs: b.CooldownDuration.Milliseconds(),
}
}
// DebuffToJSON converts Debuff to DebuffJSON (ms).
func DebuffToJSON(d Debuff) DebuffJSON {
return DebuffJSON{
Name: d.Name,
DurationMs: d.Duration.Milliseconds(),
Magnitude: d.Magnitude,
}
}
// BuffCatalogEffectiveJSON builds string-keyed maps for admin/API.
func BuffCatalogEffectiveJSON() (map[string]BuffJSON, map[string]DebuffJSON) {
buffs := BuffCatalogSnapshot()
outB := make(map[string]BuffJSON, len(buffs))
for t, b := range buffs {
outB[string(t)] = BuffToJSON(b)
}
debuffs := DebuffCatalogSnapshot()
outD := make(map[string]DebuffJSON, len(debuffs))
for t, d := range debuffs {
outD[string(t)] = DebuffToJSON(d)
}
return outB, outD
}