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.
251 lines
6.7 KiB
Go
251 lines
6.7 KiB
Go
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// FreeBuffActivationsPerPeriod is the legacy shared limit. Kept for backward compatibility.
|
|
const FreeBuffActivationsPerPeriod = 2
|
|
|
|
// SubscriptionWeeklyPriceRUB is the price for a 7-day subscription in rubles.
|
|
const SubscriptionWeeklyPriceRUB = 299
|
|
|
|
// SubscriptionDuration is how long a subscription lasts.
|
|
const SubscriptionDuration = 7 * 24 * time.Hour
|
|
|
|
// BuffFreeChargesPerType defines the per-buff free charge limits per 24h window.
|
|
var BuffFreeChargesPerType = map[BuffType]int{
|
|
BuffRush: 3,
|
|
BuffRage: 2,
|
|
BuffShield: 2,
|
|
BuffLuck: 1,
|
|
BuffResurrection: 1,
|
|
BuffHeal: 3,
|
|
BuffPowerPotion: 1,
|
|
BuffWarCry: 2,
|
|
}
|
|
|
|
// BuffSubscriberChargesPerType defines the per-buff charge limits for subscribers (x2).
|
|
var BuffSubscriberChargesPerType = map[BuffType]int{
|
|
BuffRush: 6,
|
|
BuffRage: 4,
|
|
BuffShield: 4,
|
|
BuffLuck: 2,
|
|
BuffResurrection: 2,
|
|
BuffHeal: 6,
|
|
BuffPowerPotion: 2,
|
|
BuffWarCry: 4,
|
|
}
|
|
|
|
// 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(SubscriptionDuration)
|
|
h.SubscriptionExpiresAt = &extended
|
|
} else {
|
|
expires := now.Add(SubscriptionDuration)
|
|
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 FreeBuffActivationsPerPeriod
|
|
}
|
|
|
|
// MaxRevives returns the max free revives per period (1 free, 2 for subscribers).
|
|
func (h *Hero) MaxRevives() int {
|
|
if h.SubscriptionActive {
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// 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 = FreeBuffActivationsPerPeriod
|
|
next := h.BuffQuotaPeriodEnd.Add(24 * time.Hour)
|
|
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(24 * time.Hour)
|
|
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(24 * time.Hour)
|
|
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
|
|
}
|
|
maxCharges := BuffFreeChargesPerType[bt]
|
|
if maxCharges == 0 {
|
|
maxCharges = FreeBuffActivationsPerPeriod
|
|
}
|
|
state.Remaining++
|
|
if state.Remaining > maxCharges {
|
|
state.Remaining = maxCharges
|
|
}
|
|
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(24 * time.Hour)
|
|
|
|
if bt != nil {
|
|
maxCharges := BuffFreeChargesPerType[*bt]
|
|
if maxCharges == 0 {
|
|
maxCharges = FreeBuffActivationsPerPeriod
|
|
}
|
|
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 = FreeBuffActivationsPerPeriod
|
|
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(24 * time.Hour)
|
|
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
|
|
}
|