package model import ( "math" "time" ) const ( // AgilityCoef follows the project combat specification (agility contribution to APS). AgilityCoef = 0.03 // MaxAttackSpeed enforces the target cap of ~4 attacks/sec. MaxAttackSpeed = 4.0 ) type Hero struct { ID int64 `json:"id"` TelegramID int64 `json:"telegramId"` Name string `json:"name"` HP int `json:"hp"` MaxHP int `json:"maxHp"` Attack int `json:"attack"` Defense int `json:"defense"` Speed float64 `json:"speed"` // attacks per second base rate Strength int `json:"strength"` Constitution int `json:"constitution"` Agility int `json:"agility"` Luck int `json:"luck"` State GameState `json:"state"` WeaponID *int64 `json:"weaponId,omitempty"` // Deprecated: kept for DB backward compat ArmorID *int64 `json:"armorId,omitempty"` // Deprecated: kept for DB backward compat Gear map[EquipmentSlot]*GearItem `json:"gear"` Buffs []ActiveBuff `json:"buffs,omitempty"` Debuffs []ActiveDebuff `json:"debuffs,omitempty"` Gold int64 `json:"gold"` XP int64 `json:"xp"` Level int `json:"level"` XPToNext int64 `json:"xpToNext"` AttackSpeed float64 `json:"attackSpeed,omitempty"` AttackPower int `json:"attackPower,omitempty"` DefensePower int `json:"defensePower,omitempty"` MoveSpeed float64 `json:"moveSpeed,omitempty"` PositionX float64 `json:"positionX"` PositionY float64 `json:"positionY"` Potions int `json:"potions"` ReviveCount int `json:"reviveCount"` SubscriptionActive bool `json:"subscriptionActive"` // Deprecated: BuffFreeChargesRemaining is the legacy shared counter. Use BuffCharges instead. BuffFreeChargesRemaining int `json:"buffFreeChargesRemaining"` // Deprecated: BuffQuotaPeriodEnd is the legacy shared period end. Use BuffCharges instead. BuffQuotaPeriodEnd *time.Time `json:"buffQuotaPeriodEnd,omitempty"` // BuffCharges holds per-buff-type free charge state (remaining count + period window). BuffCharges map[string]BuffChargeState `json:"buffCharges"` // Stat tracking for achievements. TotalKills int `json:"totalKills"` EliteKills int `json:"eliteKills"` TotalDeaths int `json:"totalDeaths"` KillsSinceDeath int `json:"killsSinceDeath"` LegendaryDrops int `json:"legendaryDrops"` // Movement state (persisted to DB for reconnect recovery). CurrentTownID *int64 `json:"currentTownId,omitempty"` DestinationTownID *int64 `json:"destinationTownId,omitempty"` MoveState string `json:"moveState"` LastOnlineAt *time.Time `json:"lastOnlineAt,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // BuffChargeState tracks the remaining free charges and period window for a single buff type. type BuffChargeState struct { Remaining int `json:"remaining"` PeriodEnd *time.Time `json:"periodEnd,omitempty"` } // XPToNextLevel returns the XP delta required to advance from the given level // to level+1. Phase-based curve (spec §9) — v3 scales bases ×10 vs v2 for ~10× // slower leveling when paired with reduced kill XP: // // L 1–9: round(180 * 1.28^(L-1)) // L 10–29: round(1450 * 1.15^(L-10)) // L 30+: round(23000 * 1.10^(L-30)) func XPToNextLevel(level int) int64 { if level < 1 { level = 1 } switch { case level <= 9: return int64(math.Round(180 * math.Pow(1.28, float64(level-1)))) case level <= 29: return int64(math.Round(1450 * math.Pow(1.15, float64(level-10)))) default: return int64(math.Round(23000 * math.Pow(1.10, float64(level-30)))) } } // CanLevelUp returns true if the hero has enough XP to advance to the next level. func (h *Hero) CanLevelUp() bool { return h.XP >= XPToNextLevel(h.Level) } // LevelUp advances the hero to the next level, deducts the required XP, and // applies stat growth. HP is NOT restored on level-up (spec §3.3). // Returns true if the hero leveled up, false if insufficient XP. func (h *Hero) LevelUp() bool { if !h.CanLevelUp() { return false } h.XP -= XPToNextLevel(h.Level) h.Level++ // v3: ~10× rarer than v2 — same formulas, cadences ×10 (spec §3.3). if h.Level%10 == 0 { h.MaxHP += 1 + h.Constitution/6 } if h.Level%30 == 0 { h.Attack++ } if h.Level%30 == 0 { h.Defense++ } if h.Level%40 == 0 { h.Strength++ } if h.Level%50 == 0 { h.Constitution++ } if h.Level%60 == 0 { h.Agility++ } if h.Level%100 == 0 { h.Luck++ } return true } type statBonuses struct { strengthBonus int constitutionBonus int agilityBonus int attackMultiplier float64 speedMultiplier float64 defenseMultiplier float64 critChanceBonus float64 critDamageBonus float64 blockChanceBonus float64 movementMultiplier float64 } func (h *Hero) activeStatBonuses(now time.Time) statBonuses { out := statBonuses{ strengthBonus: 0, constitutionBonus: 0, agilityBonus: 0, attackMultiplier: 1.0, speedMultiplier: 1.0, defenseMultiplier: 1.0, critChanceBonus: 0.0, critDamageBonus: 0.0, blockChanceBonus: 0.0, movementMultiplier: 1.0, } for _, ab := range h.Buffs { if ab.IsExpired(now) { continue } switch ab.Buff.Type { case BuffRush: out.movementMultiplier *= (1 + ab.Buff.Magnitude) case BuffRage: out.attackMultiplier *= (1 + ab.Buff.Magnitude) out.strengthBonus += 10 case BuffPowerPotion: out.attackMultiplier *= (1 + ab.Buff.Magnitude) out.strengthBonus += 12 case BuffWarCry: out.speedMultiplier *= (1 + ab.Buff.Magnitude) out.strengthBonus += 6 out.agilityBonus += 6 case BuffShield: out.constitutionBonus += 10 out.defenseMultiplier *= (1 + ab.Buff.Magnitude) } } return out } // EffectiveSpeed returns the hero's attack speed after weapon, armor, buff, and debuff modifiers. func (h *Hero) EffectiveSpeed() float64 { return h.EffectiveSpeedAt(time.Now()) } func (h *Hero) EffectiveSpeedAt(now time.Time) float64 { bonuses := h.activeStatBonuses(now) effectiveAgility := h.Agility + bonuses.agilityBonus if chest := h.Gear[SlotChest]; chest != nil { effectiveAgility += chest.AgilityBonus } // Base attack speed derives from base speed + agility coefficient. speed := h.Speed + float64(effectiveAgility)*AgilityCoef if speed < 0.1 { speed = 0.1 } if weapon := h.Gear[SlotMainHand]; weapon != nil { speed *= weapon.SpeedModifier } if chest := h.Gear[SlotChest]; chest != nil { speed *= chest.SpeedModifier } speed *= bonuses.speedMultiplier // Apply debuffs that reduce attack speed. // Slow is movement-only per spec §7.2 and does not affect attack speed. for _, ad := range h.Debuffs { if ad.IsExpired(now) { continue } switch ad.Debuff.Type { case DebuffFreeze: speed *= (1 - ad.Debuff.Magnitude) // -50% attack speed case DebuffIceSlow: speed *= (1 - ad.Debuff.Magnitude) // -20% attack speed (Ice Guardian) } } if speed > MaxAttackSpeed { speed = MaxAttackSpeed } if speed < 0.1 { speed = 0.1 } return speed } // EffectiveAttack returns the hero's attack after weapon, buff, and debuff modifiers. func (h *Hero) EffectiveAttack() int { return h.EffectiveAttackAt(time.Now()) } func (h *Hero) EffectiveAttackAt(now time.Time) int { bonuses := h.activeStatBonuses(now) effectiveStrength := h.Strength + bonuses.strengthBonus effectiveAgility := h.Agility + bonuses.agilityBonus effectiveConstitution := h.Constitution + bonuses.constitutionBonus if chest := h.Gear[SlotChest]; chest != nil { effectiveAgility += chest.AgilityBonus } atk := h.Attack + effectiveStrength*2 + effectiveAgility/4 + effectiveConstitution/8 if weapon := h.Gear[SlotMainHand]; weapon != nil { atk += weapon.PrimaryStat } atkF := float64(atk) atkF *= bonuses.attackMultiplier // Apply weaken debuff. for _, ad := range h.Debuffs { if ad.IsExpired(now) { continue } if ad.Debuff.Type == DebuffWeaken { atkF *= (1 - ad.Debuff.Magnitude) // -30% outgoing damage } } if atkF < 1 { atkF = 1 } return int(atkF) } // EffectiveDefense returns the hero's defense after armor and buff modifiers. func (h *Hero) EffectiveDefense() int { return h.EffectiveDefenseAt(time.Now()) } func (h *Hero) EffectiveDefenseAt(now time.Time) int { bonuses := h.activeStatBonuses(now) effectiveConstitution := h.Constitution + bonuses.constitutionBonus effectiveAgility := h.Agility + bonuses.agilityBonus if chest := h.Gear[SlotChest]; chest != nil { effectiveAgility += chest.AgilityBonus } def := h.Defense + effectiveConstitution/4 + effectiveAgility/8 if chest := h.Gear[SlotChest]; chest != nil { def += chest.PrimaryStat } def = int(float64(def) * bonuses.defenseMultiplier) if def < 0 { def = 0 } return def } // MovementSpeedMultiplier returns the hero's movement speed modifier (1.0 = normal). // Rush buff and Slow debuff affect movement, not attack speed, per spec §7. func (h *Hero) MovementSpeedMultiplier(now time.Time) float64 { mult := 1.0 for _, ab := range h.Buffs { if ab.IsExpired(now) { continue } if ab.Buff.Type == BuffRush { mult *= (1 + ab.Buff.Magnitude) // +50% movement } } for _, ad := range h.Debuffs { if ad.IsExpired(now) { continue } if ad.Debuff.Type == DebuffSlow { mult *= (1 - ad.Debuff.Magnitude) // -40% movement } } if mult < 0.1 { mult = 0.1 } return mult } // RefreshDerivedCombatStats updates exported derived combat fields for API/state usage. func (h *Hero) RefreshDerivedCombatStats(now time.Time) { h.XPToNext = XPToNextLevel(h.Level) h.AttackSpeed = h.EffectiveSpeedAt(now) h.AttackPower = h.EffectiveAttackAt(now) h.DefensePower = h.EffectiveDefenseAt(now) h.MoveSpeed = h.MovementSpeedMultiplier(now) } // CombatRatingAt computes a single-number combat effectiveness score used by // the auto-equip system to decide whether new gear is an upgrade. func (h *Hero) CombatRatingAt(now time.Time) float64 { return float64(h.EffectiveAttackAt(now))*h.EffectiveSpeedAt(now) + float64(h.EffectiveDefenseAt(now))*0.35 } // IsAlive returns true if the hero has HP remaining. func (h *Hero) IsAlive() bool { return h.HP > 0 } // IsStunned returns true if the hero currently has an active stun debuff. func (h *Hero) IsStunned(now time.Time) bool { for _, ad := range h.Debuffs { if ad.Debuff.Type == DebuffStun && !ad.IsExpired(now) { return true } } return false }