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.

273 lines
7.2 KiB
Go

package model
import (
"fmt"
"time"
"github.com/denisovdennis/autohero/internal/tuning"
)
// BuffFreeChargesPerType defines per-buff charge limits per BuffChargePeriod (default 24h).
var BuffFreeChargesPerType = map[BuffType]int{
BuffRush: 2,
BuffRage: 1,
BuffShield: 1,
BuffLuck: 1,
BuffResurrection: 1,
BuffHeal: 2,
BuffPowerPotion: 1,
BuffWarCry: 1,
}
// BuffSubscriberChargesPerType defines higher per-buff caps for active subscribers (not a flat 2x).
var BuffSubscriberChargesPerType = map[BuffType]int{
BuffRush: 3,
BuffRage: 2,
BuffShield: 2,
BuffLuck: 2,
BuffResurrection: 2,
BuffHeal: 3,
BuffPowerPotion: 1,
BuffWarCry: 2,
}
func SubscriptionWeeklyPrice() int64 {
return tuning.Get().SubscriptionWeeklyPriceRUB
}
func SubscriptionDurationRuntime() time.Duration {
return time.Duration(tuning.Get().SubscriptionDurationMs) * time.Millisecond
}
func SubscriptionDurationLabel() string {
d := SubscriptionDurationRuntime()
if d <= 0 {
return "0 hours"
}
if d%(24*time.Hour) == 0 {
days := int64(d / (24 * time.Hour))
if days == 1 {
return "1 day"
}
return fmt.Sprintf("%d days", days)
}
hours := int64(d / time.Hour)
if hours == 1 {
return "1 hour"
}
return fmt.Sprintf("%d hours", hours)
}
func BuffChargePeriod() time.Duration {
return time.Duration(tuning.Get().BuffChargePeriodMs) * time.Millisecond
}
func FreeBuffActivationsPerPeriodRuntime() int {
return int(tuning.Get().FreeBuffActivationsPerPeriod)
}
// RefreshSubscription checks if the subscription has expired and updates SubscriptionActive.
// Returns true if the hero state was changed (caller should persist).
func (h *Hero) RefreshSubscription(now time.Time) bool {
if !h.SubscriptionActive {
return false
}
if h.SubscriptionExpiresAt != nil && now.After(*h.SubscriptionExpiresAt) {
h.SubscriptionActive = false
h.SubscriptionExpiresAt = nil
return true
}
return false
}
// ActivateSubscription sets the hero as a subscriber for SubscriptionDuration.
// If already subscribed, extends from current expiry.
func (h *Hero) ActivateSubscription(now time.Time) {
if h.SubscriptionActive && h.SubscriptionExpiresAt != nil && h.SubscriptionExpiresAt.After(now) {
// Extend from current expiry.
extended := h.SubscriptionExpiresAt.Add(SubscriptionDurationRuntime())
h.SubscriptionExpiresAt = &extended
} else {
expires := now.Add(SubscriptionDurationRuntime())
h.SubscriptionExpiresAt = &expires
}
h.SubscriptionActive = true
}
// MaxBuffCharges returns the max charges for a buff type, considering subscription status.
func (h *Hero) MaxBuffCharges(bt BuffType) int {
if h.SubscriptionActive {
if v, ok := BuffSubscriberChargesPerType[bt]; ok {
return v
}
}
if v, ok := BuffFreeChargesPerType[bt]; ok {
return v
}
return FreeBuffActivationsPerPeriodRuntime()
}
// MaxRevives returns the max free revives per period (1 free, 2 for subscribers).
func (h *Hero) MaxRevives() int {
if h.SubscriptionActive {
return int(tuning.Get().MaxRevivesSubscriber)
}
return int(tuning.Get().MaxRevivesFree)
}
// ApplyBuffQuotaRollover refills free buff charges when the 24h window has passed.
// Returns true if the hero was mutated (caller may persist).
// Deprecated: kept for backward compat with the shared counter. New code should
// use GetBuffCharges / ConsumeBuffCharge which handle rollover inline.
func (h *Hero) ApplyBuffQuotaRollover(now time.Time) bool {
if h.SubscriptionActive {
return false
}
if h.BuffQuotaPeriodEnd == nil {
return false
}
changed := false
for now.After(*h.BuffQuotaPeriodEnd) {
h.BuffFreeChargesRemaining = FreeBuffActivationsPerPeriodRuntime()
next := h.BuffQuotaPeriodEnd.Add(BuffChargePeriod())
h.BuffQuotaPeriodEnd = &next
changed = true
}
return changed
}
// GetBuffCharges returns the current charge state for a specific buff type,
// rolling over the 24h window if expired.
func (h *Hero) GetBuffCharges(bt BuffType, now time.Time) BuffChargeState {
if h.BuffCharges == nil {
h.BuffCharges = make(map[string]BuffChargeState)
}
maxCharges := h.MaxBuffCharges(bt)
state, exists := h.BuffCharges[string(bt)]
if !exists {
// First access for this buff type — initialize with full charges.
pe := now.Add(BuffChargePeriod())
state = BuffChargeState{
Remaining: maxCharges,
PeriodEnd: &pe,
}
h.BuffCharges[string(bt)] = state
return state
}
// Roll over if the period has expired.
if state.PeriodEnd != nil && now.After(*state.PeriodEnd) {
for state.PeriodEnd != nil && now.After(*state.PeriodEnd) {
next := state.PeriodEnd.Add(BuffChargePeriod())
state.PeriodEnd = &next
}
state.Remaining = maxCharges
h.BuffCharges[string(bt)] = state
}
return state
}
// ConsumeBuffCharge decrements one free charge for the specific buff type.
// Returns an error if no charges remain.
func (h *Hero) ConsumeBuffCharge(bt BuffType, now time.Time) error {
state := h.GetBuffCharges(bt, now)
if state.Remaining <= 0 {
periodStr := "unknown"
if state.PeriodEnd != nil {
periodStr = state.PeriodEnd.Format(time.RFC3339)
}
return fmt.Errorf(
"no free %s charges left; next refresh at %s",
string(bt), periodStr,
)
}
state.Remaining--
h.BuffCharges[string(bt)] = state
// Keep legacy counter roughly in sync.
h.BuffFreeChargesRemaining--
if h.BuffFreeChargesRemaining < 0 {
h.BuffFreeChargesRemaining = 0
}
return nil
}
// RefundBuffCharge increments one charge back for the specific buff type.
func (h *Hero) RefundBuffCharge(bt BuffType) {
if h.BuffCharges == nil {
return
}
state, exists := h.BuffCharges[string(bt)]
if !exists {
return
}
maxCap := h.MaxBuffCharges(bt)
state.Remaining++
if state.Remaining > maxCap {
state.Remaining = maxCap
}
h.BuffCharges[string(bt)] = state
// Keep legacy counter roughly in sync.
h.BuffFreeChargesRemaining++
}
// ResetBuffCharges resets charges to max. If bt is nil, resets ALL buff types.
// If bt is non-nil, resets only that buff type.
func (h *Hero) ResetBuffCharges(bt *BuffType, now time.Time) {
if h.BuffCharges == nil {
h.BuffCharges = make(map[string]BuffChargeState)
}
pe := now.Add(BuffChargePeriod())
if bt != nil {
maxCharges := h.MaxBuffCharges(*bt)
h.BuffCharges[string(*bt)] = BuffChargeState{
Remaining: maxCharges,
PeriodEnd: &pe,
}
return
}
// Reset ALL buff types.
for buffType := range BuffFreeChargesPerType {
h.BuffCharges[string(buffType)] = BuffChargeState{
Remaining: h.MaxBuffCharges(buffType),
PeriodEnd: &pe,
}
}
// Also reset legacy counter.
h.BuffFreeChargesRemaining = FreeBuffActivationsPerPeriodRuntime()
h.BuffQuotaPeriodEnd = &pe
}
// EnsureBuffChargesPopulated initializes BuffCharges for all buff types if the map
// is empty. Returns true if the map was freshly populated (caller should persist).
func (h *Hero) EnsureBuffChargesPopulated(now time.Time) bool {
if h.BuffCharges == nil {
h.BuffCharges = make(map[string]BuffChargeState)
}
if len(h.BuffCharges) == 0 {
pe := now.Add(BuffChargePeriod())
if h.BuffQuotaPeriodEnd != nil {
pe = *h.BuffQuotaPeriodEnd
}
for bt := range BuffFreeChargesPerType {
h.BuffCharges[string(bt)] = BuffChargeState{
Remaining: h.MaxBuffCharges(bt),
PeriodEnd: &pe,
}
}
return true
}
return false
}