From 94d8a0cda852881c8a440297d00cafe1e72fabe3 Mon Sep 17 00:00:00 2001 From: Denis Ranneft Date: Tue, 31 Mar 2026 01:33:03 +0300 Subject: [PATCH] fix regen --- backend/internal/game/combat.go | 17 ++++++++++++----- backend/internal/game/engine.go | 2 +- backend/internal/game/offline.go | 3 ++- backend/internal/handler/admin.go | 6 +++++- backend/internal/model/combat.go | 2 ++ backend/internal/router/router.go | 4 +++- backend/internal/tuning/runtime.go | 2 +- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/backend/internal/game/combat.go b/backend/internal/game/combat.go index edca49f..72a7562 100644 --- a/backend/internal/game/combat.go +++ b/backend/internal/game/combat.go @@ -350,8 +350,8 @@ func ProcessDebuffDamage(hero *model.Hero, tickDuration time.Duration, now time. } // ProcessEnemyRegen handles HP regeneration for enemies with the regen ability. -// Should be called each combat tick. -func ProcessEnemyRegen(enemy *model.Enemy, tickDuration time.Duration) int { +// Should be called each combat tick. Uses remainder to avoid per-tick rounding spikes. +func ProcessEnemyRegen(enemy *model.Enemy, tickDuration time.Duration, remainder *float64) int { if !enemy.HasAbility(model.AbilityRegen) { return 0 } @@ -368,9 +368,16 @@ func ProcessEnemyRegen(enemy *model.Enemy, tickDuration time.Duration) int { regenRate = cfg.EnemyRegenBattleLizard } - healed := int(float64(enemy.MaxHP) * regenRate * tickDuration.Seconds()) - if healed < 1 { - healed = 1 + healFloat := float64(enemy.MaxHP) * regenRate * tickDuration.Seconds() + if remainder != nil { + healFloat += *remainder + } + healed := int(healFloat) + if remainder != nil { + *remainder = healFloat - float64(healed) + } + if healed <= 0 { + return 0 } before := enemy.HP enemy.HP += healed diff --git a/backend/internal/game/engine.go b/backend/internal/game/engine.go index cde2319..37bbd5f 100644 --- a/backend/internal/game/engine.go +++ b/backend/internal/game/engine.go @@ -1170,7 +1170,7 @@ func (e *Engine) processCombatTick(now time.Time) { } ProcessDebuffDamage(cs.Hero, tickDur, now) - regenHealed := ProcessEnemyRegen(&cs.Enemy, tickDur) + regenHealed := ProcessEnemyRegen(&cs.Enemy, tickDur, &cs.EnemyRegenRemainder) ProcessSummonDamage(cs.Hero, &cs.Enemy, cs.StartedAt, cs.LastTickAt, now) cs.LastTickAt = now if regenHealed > 0 && e.sender != nil { diff --git a/backend/internal/game/offline.go b/backend/internal/game/offline.go index fd658f7..3e0de3a 100644 --- a/backend/internal/game/offline.go +++ b/backend/internal/game/offline.go @@ -313,6 +313,7 @@ func SimulateOneFight(hero *model.Hero, now time.Time, encounterEnemy *model.Ene combatStart := now lastTick := now + var regenRemainder float64 heroNext := now.Add(attackInterval(hero.EffectiveSpeedAt(now))) enemyNext := now.Add(attackInterval(enemy.Speed)) const maxCombatSteps = 100000 @@ -330,7 +331,7 @@ func SimulateOneFight(hero *model.Hero, now time.Time, encounterEnemy *model.Ene tickDur := nextTime.Sub(lastTick) if tickDur > 0 { ProcessDebuffDamage(hero, tickDur, nextTime) - ProcessEnemyRegen(&enemy, tickDur) + ProcessEnemyRegen(&enemy, tickDur, ®enRemainder) ProcessSummonDamage(hero, &enemy, combatStart, lastTick, nextTime) } lastTick = nextTime diff --git a/backend/internal/handler/admin.go b/backend/internal/handler/admin.go index e07d7f8..5c0c05b 100644 --- a/backend/internal/handler/admin.go +++ b/backend/internal/handler/admin.go @@ -33,10 +33,12 @@ type AdminHandler struct { hub *Hub pool *pgxpool.Pool logger *slog.Logger + adminUser string + adminPass string } // NewAdminHandler creates a new AdminHandler with all required dependencies. -func NewAdminHandler(store *storage.HeroStore, gearStore *storage.GearStore, questStore *storage.QuestStore, engine *game.Engine, hub *Hub, pool *pgxpool.Pool, logger *slog.Logger) *AdminHandler { +func NewAdminHandler(store *storage.HeroStore, gearStore *storage.GearStore, questStore *storage.QuestStore, engine *game.Engine, hub *Hub, pool *pgxpool.Pool, logger *slog.Logger, adminUser, adminPass string) *AdminHandler { return &AdminHandler{ store: store, gearStore: gearStore, @@ -45,6 +47,8 @@ func NewAdminHandler(store *storage.HeroStore, gearStore *storage.GearStore, que hub: hub, pool: pool, logger: logger, + adminUser: adminUser, + adminPass: adminPass, } } diff --git a/backend/internal/model/combat.go b/backend/internal/model/combat.go index f0d88c5..fd9cae2 100644 --- a/backend/internal/model/combat.go +++ b/backend/internal/model/combat.go @@ -22,6 +22,8 @@ type CombatState struct { EnemyNextAttack time.Time `json:"enemyNextAttack"` StartedAt time.Time `json:"startedAt"` LastTickAt time.Time `json:"-"` // tracks previous tick for periodic effects + // Fractional regen carry between ticks to avoid rounding to 1 HP each tick. + EnemyRegenRemainder float64 `json:"-"` } // AttackEvent is a min-heap entry for scheduling attacks by next_attack_at. diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 268b2ec..103710b 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -68,7 +68,7 @@ func New(deps Deps) *chi.Mux { r.Post("/api/v1/payments/telegram-webhook", paymentsH.TelegramWebhook) // Admin routes protected with HTTP Basic authentication. - adminH := handler.NewAdminHandler(heroStore, gearStore, questStore, deps.Engine, deps.Hub, deps.PgPool, deps.Logger) + adminH := handler.NewAdminHandler(heroStore, gearStore, questStore, deps.Engine, deps.Hub, deps.PgPool, deps.Logger, deps.AdminBasicAuthUsername, deps.AdminBasicAuthPassword) r.Route("/admin", func(r chi.Router) { r.Use(handler.BasicAuthMiddleware(handler.BasicAuthConfig{ Username: deps.AdminBasicAuthUsername, @@ -128,6 +128,8 @@ func New(deps Deps) *chi.Mux { r.Get("/payments/{paymentId}", adminH.GetPayment) r.Post("/payments/set-webhook", paymentsH.SetWebhook) }) + // Admin WebSocket snapshot (auth via query params in handler). + r.Get("/admin-ws/hero/{heroId}", adminH.AdminHeroSnapshotWS) // API v1 (authenticated routes). gameH := handler.NewGameHandler(deps.Engine, heroStore, logStore, worldSvc, deps.Logger, deps.ServerStartedAt, questStore, gearStore, achievementStore, taskStore, deps.Hub) diff --git a/backend/internal/tuning/runtime.go b/backend/internal/tuning/runtime.go index 995df6e..2c5fb06 100644 --- a/backend/internal/tuning/runtime.go +++ b/backend/internal/tuning/runtime.go @@ -298,7 +298,7 @@ func DefaultValues() Values { EnemyRegenDefault: 0.02, EnemyRegenSkeletonKing: 0.10, EnemyRegenForestWarden: 0.05, - EnemyRegenBattleLizard: 0.02, + EnemyRegenBattleLizard: 0.01, SummonCycleSeconds: 15, SummonDamageDivisor: 4, LuckBuffMultiplier: 1.75,