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.
689 lines
19 KiB
Go
689 lines
19 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/denisovdennis/autohero/internal/game"
|
|
"github.com/denisovdennis/autohero/internal/model"
|
|
"github.com/denisovdennis/autohero/internal/storage"
|
|
)
|
|
|
|
var serverStartedAt = time.Now()
|
|
|
|
// AdminHandler provides administrative endpoints for hero management,
|
|
// engine inspection, and server diagnostics.
|
|
type AdminHandler struct {
|
|
store *storage.HeroStore
|
|
engine *game.Engine
|
|
hub *Hub
|
|
pool *pgxpool.Pool
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewAdminHandler creates a new AdminHandler with all required dependencies.
|
|
func NewAdminHandler(store *storage.HeroStore, engine *game.Engine, hub *Hub, pool *pgxpool.Pool, logger *slog.Logger) *AdminHandler {
|
|
return &AdminHandler{
|
|
store: store,
|
|
engine: engine,
|
|
hub: hub,
|
|
pool: pool,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// ── Hero Management ─────────────────────────────────────────────────
|
|
|
|
type heroSummary struct {
|
|
ID int64 `json:"id"`
|
|
TelegramID int64 `json:"telegramId"`
|
|
Name string `json:"name"`
|
|
Level int `json:"level"`
|
|
Gold int64 `json:"gold"`
|
|
HP int `json:"hp"`
|
|
MaxHP int `json:"maxHp"`
|
|
State model.GameState `json:"state"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
// ListHeroes returns a paginated list of all heroes.
|
|
// GET /admin/heroes?limit=20&offset=0
|
|
func (h *AdminHandler) ListHeroes(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
|
|
heroes, err := h.store.ListHeroes(r.Context(), limit, offset)
|
|
if err != nil {
|
|
h.logger.Error("admin: list heroes failed", "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to list heroes",
|
|
})
|
|
return
|
|
}
|
|
|
|
summaries := make([]heroSummary, len(heroes))
|
|
for i, hero := range heroes {
|
|
summaries[i] = heroSummary{
|
|
ID: hero.ID,
|
|
TelegramID: hero.TelegramID,
|
|
Name: hero.Name,
|
|
Level: hero.Level,
|
|
Gold: hero.Gold,
|
|
HP: hero.HP,
|
|
MaxHP: hero.MaxHP,
|
|
State: hero.State,
|
|
UpdatedAt: hero.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"heroes": summaries,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
})
|
|
}
|
|
|
|
// GetHero returns full hero detail by database ID.
|
|
// GET /admin/heroes/{heroId}
|
|
func (h *AdminHandler) GetHero(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero failed", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
writeJSON(w, http.StatusOK, h.engine.GetMovements(heroID).Hero)
|
|
}
|
|
|
|
type setLevelRequest struct {
|
|
Level int `json:"level"`
|
|
}
|
|
|
|
// SetHeroLevel sets the hero to a specific level, recalculating stats.
|
|
// POST /admin/heroes/{heroId}/set-level
|
|
func (h *AdminHandler) SetHeroLevel(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var req setLevelRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if req.Level < 1 {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "level must be >= 1",
|
|
})
|
|
return
|
|
}
|
|
const maxAdminLevel = 200
|
|
if req.Level > maxAdminLevel {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "level must be <= 200",
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for set-level", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
if h.isHeroInCombat(w, heroID) {
|
|
return
|
|
}
|
|
|
|
savedGold := hero.Gold
|
|
resetHeroToLevel1(hero)
|
|
hero.Gold = savedGold
|
|
|
|
for hero.Level < req.Level {
|
|
hero.XP = model.XPToNextLevel(hero.Level)
|
|
if !hero.LevelUp() {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after set-level", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("admin: hero level set", "hero_id", heroID, "level", hero.Level)
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
type setGoldRequest struct {
|
|
Gold int64 `json:"gold"`
|
|
}
|
|
|
|
// SetHeroGold sets the hero's gold to an exact value.
|
|
// POST /admin/heroes/{heroId}/set-gold
|
|
func (h *AdminHandler) SetHeroGold(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var req setGoldRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if req.Gold < 0 {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "gold must be >= 0",
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for set-gold", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
if h.isHeroInCombat(w, heroID) {
|
|
return
|
|
}
|
|
|
|
hero.Gold = req.Gold
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after set-gold", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("admin: hero gold set", "hero_id", heroID, "gold", hero.Gold)
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
type addPotionsRequest struct {
|
|
Potions int `json:"potions"`
|
|
}
|
|
|
|
// SetHeroGold sets the hero's gold to an exact value.
|
|
// POST /admin/heroes/{heroId}/add-potions
|
|
func (h *AdminHandler) AddPotions(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var req addPotionsRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if req.Potions < 0 {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "potions must be >= 1",
|
|
})
|
|
return
|
|
}
|
|
|
|
|
|
var hero = h.engine.GetMovements(heroID).Hero
|
|
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
hero.Potions += req.Potions
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after set-gold", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("admin: hero added potions", "hero_id", heroID, "potions", hero.Potions)
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
h.engine.SyncHeroState(hero)
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
type setHPRequest struct {
|
|
HP int `json:"hp"`
|
|
}
|
|
|
|
// SetHeroHP sets the hero's current HP, clamped to [1, maxHp].
|
|
// POST /admin/heroes/{heroId}/set-hp
|
|
func (h *AdminHandler) SetHeroHP(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var req setHPRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for set-hp", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
if h.isHeroInCombat(w, heroID) {
|
|
return
|
|
}
|
|
|
|
hp := req.HP
|
|
if hp < 1 {
|
|
hp = 1
|
|
}
|
|
if hp > hero.MaxHP {
|
|
hp = hero.MaxHP
|
|
}
|
|
hero.HP = hp
|
|
|
|
if hero.State == model.StateDead && hero.HP > 0 {
|
|
hero.State = model.StateWalking
|
|
}
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after set-hp", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("admin: hero HP set", "hero_id", heroID, "hp", hero.HP)
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
// ReviveHero force-revives a hero to full HP regardless of current state.
|
|
// POST /admin/heroes/{heroId}/revive
|
|
func (h *AdminHandler) ReviveHero(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for revive", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
hero.HP = hero.MaxHP
|
|
hero.State = model.StateWalking
|
|
hero.Buffs = nil
|
|
hero.Debuffs = nil
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after revive", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.engine.ApplyAdminHeroRevive(hero)
|
|
|
|
h.logger.Info("admin: hero revived", "hero_id", heroID, "hp", hero.HP)
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
// ResetHero resets a hero to fresh level 1 defaults.
|
|
// POST /admin/heroes/{heroId}/reset
|
|
func (h *AdminHandler) ResetHero(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for reset", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
if h.isHeroInCombat(w, heroID) {
|
|
return
|
|
}
|
|
|
|
resetHeroToLevel1(hero)
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after reset", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("admin: hero reset", "hero_id", heroID)
|
|
hero.RefreshDerivedCombatStats(time.Now())
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
type resetBuffChargesRequest struct {
|
|
BuffType string `json:"buffType"` // optional — if empty, reset ALL
|
|
}
|
|
|
|
// ResetBuffCharges resets per-buff free charges to their maximums.
|
|
// If buffType is provided, only that buff is reset; otherwise all are reset.
|
|
// POST /admin/heroes/{heroId}/reset-buff-charges
|
|
func (h *AdminHandler) ResetBuffCharges(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var req resetBuffChargesRequest
|
|
if r.Body != nil && r.ContentLength > 0 {
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for reset-buff-charges", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
hero.EnsureBuffChargesPopulated(now)
|
|
|
|
if req.BuffType != "" {
|
|
bt, ok := model.ValidBuffType(req.BuffType)
|
|
if !ok {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid buffType: " + req.BuffType,
|
|
})
|
|
return
|
|
}
|
|
hero.ResetBuffCharges(&bt, now)
|
|
h.logger.Info("admin: buff charges reset (single)", "hero_id", heroID, "buff_type", bt)
|
|
} else {
|
|
hero.ResetBuffCharges(nil, now)
|
|
h.logger.Info("admin: buff charges reset (all)", "hero_id", heroID)
|
|
}
|
|
|
|
if err := h.store.Save(r.Context(), hero); err != nil {
|
|
h.logger.Error("admin: save hero after reset-buff-charges", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to save hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
hero.RefreshDerivedCombatStats(now)
|
|
writeJSON(w, http.StatusOK, hero)
|
|
}
|
|
|
|
// DeleteHero permanently removes a hero from the database.
|
|
// DELETE /admin/heroes/{heroId}
|
|
func (h *AdminHandler) DeleteHero(w http.ResponseWriter, r *http.Request) {
|
|
heroID, err := parseHeroID(r)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{
|
|
"error": "invalid heroId: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
hero, err := h.store.GetByID(r.Context(), heroID)
|
|
if err != nil {
|
|
h.logger.Error("admin: get hero for delete", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to load hero",
|
|
})
|
|
return
|
|
}
|
|
if hero == nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{
|
|
"error": "hero not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.engine.StopCombat(heroID)
|
|
|
|
if err := h.store.DeleteByID(r.Context(), heroID); err != nil {
|
|
h.logger.Error("admin: delete hero failed", "hero_id", heroID, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{
|
|
"error": "failed to delete hero",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.logger.Info("admin: hero deleted", "hero_id", heroID)
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"status": "deleted",
|
|
})
|
|
}
|
|
|
|
// ── Game Engine ─────────────────────────────────────────────────────
|
|
|
|
// EngineStatus returns operational status of the game engine.
|
|
// GET /admin/engine/status
|
|
func (h *AdminHandler) EngineStatus(w http.ResponseWriter, r *http.Request) {
|
|
status := h.engine.Status()
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"running": status.Running,
|
|
"tickRateMs": status.TickRate.Milliseconds(),
|
|
"activeCombats": status.ActiveCombats,
|
|
"uptimeMs": status.UptimeMs,
|
|
})
|
|
}
|
|
|
|
// ActiveCombats returns all active combat sessions.
|
|
// GET /admin/engine/combats
|
|
func (h *AdminHandler) ActiveCombats(w http.ResponseWriter, r *http.Request) {
|
|
combats := h.engine.ListActiveCombats()
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"combats": combats,
|
|
"count": len(combats),
|
|
})
|
|
}
|
|
|
|
// ── WebSocket Hub ───────────────────────────────────────────────────
|
|
|
|
// WSConnections returns active WebSocket connection info.
|
|
// GET /admin/ws/connections
|
|
func (h *AdminHandler) WSConnections(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"connectionCount": h.hub.ConnectionCount(),
|
|
"heroIds": h.hub.ConnectedHeroIDs(),
|
|
})
|
|
}
|
|
|
|
// ── Server Info ─────────────────────────────────────────────────────
|
|
|
|
// ServerInfo returns general server diagnostics.
|
|
// GET /admin/info
|
|
func (h *AdminHandler) ServerInfo(w http.ResponseWriter, r *http.Request) {
|
|
poolStat := h.pool.Stat()
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"version": "0.1.0-dev",
|
|
"goVersion": runtime.Version(),
|
|
"uptimeMs": time.Since(serverStartedAt).Milliseconds(),
|
|
"dbPool": map[string]any{
|
|
"totalConns": poolStat.TotalConns(),
|
|
"acquiredConns": poolStat.AcquiredConns(),
|
|
"idleConns": poolStat.IdleConns(),
|
|
"maxConns": poolStat.MaxConns(),
|
|
},
|
|
})
|
|
}
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
|
|
func parseHeroID(r *http.Request) (int64, error) {
|
|
return strconv.ParseInt(chi.URLParam(r, "heroId"), 10, 64)
|
|
}
|
|
|
|
// isHeroInCombat checks if the hero is in active engine combat and writes
|
|
// a 409 Conflict response if so. Returns true when the caller should abort.
|
|
func (h *AdminHandler) isHeroInCombat(w http.ResponseWriter, heroID int64) bool {
|
|
if _, active := h.engine.GetCombat(heroID); active {
|
|
writeJSON(w, http.StatusConflict, map[string]string{
|
|
"error": "hero is in active combat — stop combat first",
|
|
})
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// resetHeroToLevel1 restores a hero to fresh level 1 defaults,
|
|
// preserving identity fields (ID, TelegramID, Name, CreatedAt).
|
|
func resetHeroToLevel1(hero *model.Hero) {
|
|
hero.Level = 1
|
|
hero.XP = 0
|
|
hero.Gold = 0
|
|
hero.HP = 100
|
|
hero.MaxHP = 100
|
|
hero.Attack = 10
|
|
hero.Defense = 5
|
|
hero.Speed = 1.0
|
|
hero.Strength = 1
|
|
hero.Constitution = 1
|
|
hero.Agility = 1
|
|
hero.Luck = 1
|
|
hero.State = model.StateWalking
|
|
hero.Buffs = nil
|
|
hero.Debuffs = nil
|
|
hero.BuffCharges = nil
|
|
hero.BuffFreeChargesRemaining = model.FreeBuffActivationsPerPeriod
|
|
hero.BuffQuotaPeriodEnd = nil
|
|
}
|