From aab12c1567e3499a2abd1cac035daef938a72304 Mon Sep 17 00:00:00 2001 From: Denis Ranneft Date: Tue, 31 Mar 2026 11:38:52 +0300 Subject: [PATCH] admin debugg --- admin-web/index.html | 50 +++++++++++ backend/internal/game/combat.go | 6 +- backend/internal/handler/admin.go | 143 ++++++++++++++++++++++++++++++ backend/internal/router/router.go | 2 + 4 files changed, 198 insertions(+), 3 deletions(-) diff --git a/admin-web/index.html b/admin-web/index.html index 20c39f3..cd75983 100644 --- a/admin-web/index.html +++ b/admin-web/index.html @@ -112,6 +112,10 @@ }; state._confirmAction = null; + /** Matches model.AllBuffTypes / AllDebuffTypes (admin manual apply). */ + const ADMIN_BUFF_TYPES = ["rush", "rage", "shield", "luck", "resurrection", "heal", "power_potion", "war_cry"]; + const ADMIN_DEBUFF_TYPES = ["poison", "freeze", "burn", "stun", "slow", "weaken", "ice_slow"]; + function e(v) { return String(v ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); } function authHeader() { return `Basic ${btoa(`${state.auth.username}:${state.auth.password}`)}`; } function setMessage(text) { state.message = text; render(); } @@ -1185,6 +1189,25 @@ async function claimQuest(questId) { await api(`heroes/${state.selectedHeroId}/quests/${questId}/claim`, { method: "POST", body: "{}" }); await loadHero(state.selectedHeroId); } async function abandonQuest(questId) { await api(`heroes/${state.selectedHeroId}/quests/${questId}`, { method: "DELETE" }); await loadHero(state.selectedHeroId); } + async function applyHeroBuffAdmin() { + if (!state.selectedHeroId) { setMessage("Сначала выберите героя"); return; } + const buffType = document.getElementById("hero-admin-buff-type")?.value; + if (!buffType) { setMessage("Выберите тип баффа"); return; } + await withRowAction("hero-admin-buff", async () => { + await api(`heroes/${state.selectedHeroId}/apply-buff`, { method: "POST", body: JSON.stringify({ buffType }) }); + await loadHero(state.selectedHeroId); + }, "Бафф применён"); + } + async function applyHeroDebuffAdmin() { + if (!state.selectedHeroId) { setMessage("Сначала выберите героя"); return; } + const debuffType = document.getElementById("hero-admin-debuff-type")?.value; + if (!debuffType) { setMessage("Выберите тип дебаффа"); return; } + await withRowAction("hero-admin-debuff", async () => { + await api(`heroes/${state.selectedHeroId}/apply-debuff`, { method: "POST", body: JSON.stringify({ debuffType }) }); + await loadHero(state.selectedHeroId); + }, "Дебафф применён"); + } + function login() { state.auth.username = document.getElementById("login-user").value.trim(); state.auth.password = document.getElementById("login-pass").value.trim(); @@ -1412,6 +1435,33 @@

Towns come from the loaded road graph (GET /admin/towns). Hero must be alive and not in combat.

+
+

Баффы / дебаффы (вручную)

+

Эффект из серверного каталога, без списания бесплатных зарядов. Только вне боя (как и прочие правки героя).

+
+
+ + +
+
+ + +
+
+
+
+ + ${e(state.rowStatus["hero-admin-buff"]?.message || "")} + + ${e(state.rowStatus["hero-admin-debuff"]?.message || "")} +
+
` : `
Select hero from list
`} diff --git a/backend/internal/game/combat.go b/backend/internal/game/combat.go index 72a7562..513d2ad 100644 --- a/backend/internal/game/combat.go +++ b/backend/internal/game/combat.go @@ -272,15 +272,15 @@ func tryApplyDebuff(hero *model.Hero, enemy *model.Enemy, now time.Time) string continue } - applyDebuff(hero, rule.debuff, now) + ApplyDebuff(hero, rule.debuff, now) return string(rule.debuff) } return "" } -// applyDebuff adds a debuff to the hero. If the same debuff type is already active, it refreshes. -func applyDebuff(hero *model.Hero, debuffType model.DebuffType, now time.Time) { +// ApplyDebuff adds a debuff to the hero. If the same debuff type is already active, it refreshes. +func ApplyDebuff(hero *model.Hero, debuffType model.DebuffType, now time.Time) { def, ok := model.DebuffDefinition(debuffType) if !ok { return diff --git a/backend/internal/handler/admin.go b/backend/internal/handler/admin.go index 4ecf806..f77dd84 100644 --- a/backend/internal/handler/admin.go +++ b/backend/internal/handler/admin.go @@ -1312,6 +1312,149 @@ func (h *AdminHandler) ResetBuffCharges(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, hero) } +type applyBuffAdminRequest struct { + BuffType string `json:"buffType"` +} + +// ApplyHeroBuff applies a buff from the catalog without consuming free-charge quota (admin/testing). +// POST /admin/heroes/{heroId}/apply-buff +func (h *AdminHandler) ApplyHeroBuff(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 + } + if h.isHeroInCombat(w, heroID) { + return + } + + var req applyBuffAdminRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "invalid request body: " + err.Error(), + }) + return + } + bt, ok := model.ValidBuffType(req.BuffType) + if !ok { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "invalid buffType: " + req.BuffType, + }) + return + } + + hero, err := h.store.GetByID(r.Context(), heroID) + if err != nil { + h.logger.Error("admin: get hero for apply-buff", "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() + if game.ApplyBuff(hero, bt, now) == nil { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "buff could not be applied (unknown catalog entry)", + }) + return + } + + if err := h.store.Save(r.Context(), hero); err != nil { + h.logger.Error("admin: save hero after apply-buff", "hero_id", heroID, "error", err) + writeJSON(w, http.StatusInternalServerError, map[string]string{ + "error": "failed to save hero", + }) + return + } + + h.logger.Info("admin: buff applied", "hero_id", heroID, "buff_type", bt) + hero.EnsureGearMap() + hero.RefreshDerivedCombatStats(now) + h.engine.ApplyAdminHeroSnapshot(hero) + writeJSON(w, http.StatusOK, hero) +} + +type applyDebuffAdminRequest struct { + DebuffType string `json:"debuffType"` +} + +// ApplyHeroDebuff applies a debuff from the catalog (admin/testing). +// POST /admin/heroes/{heroId}/apply-debuff +func (h *AdminHandler) ApplyHeroDebuff(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 + } + if h.isHeroInCombat(w, heroID) { + return + } + + var req applyDebuffAdminRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "invalid request body: " + err.Error(), + }) + return + } + dt, ok := model.ValidDebuffType(req.DebuffType) + if !ok { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "invalid debuffType: " + req.DebuffType, + }) + return + } + + hero, err := h.store.GetByID(r.Context(), heroID) + if err != nil { + h.logger.Error("admin: get hero for apply-debuff", "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() + if _, defOk := model.DebuffDefinition(dt); !defOk { + writeJSON(w, http.StatusBadRequest, map[string]string{ + "error": "debuff not in catalog: " + req.DebuffType, + }) + return + } + game.ApplyDebuff(hero, dt, now) + + if err := h.store.Save(r.Context(), hero); err != nil { + h.logger.Error("admin: save hero after apply-debuff", "hero_id", heroID, "error", err) + writeJSON(w, http.StatusInternalServerError, map[string]string{ + "error": "failed to save hero", + }) + return + } + + h.logger.Info("admin: debuff applied", "hero_id", heroID, "debuff_type", dt) + hero.EnsureGearMap() + hero.RefreshDerivedCombatStats(now) + h.engine.ApplyAdminHeroSnapshot(hero) + 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) { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 103710b..1997dc0 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -84,6 +84,8 @@ func New(deps Deps) *chi.Mux { r.Post("/heroes/{heroId}/revive", adminH.ReviveHero) r.Post("/heroes/{heroId}/reset", adminH.ResetHero) r.Post("/heroes/{heroId}/reset-buff-charges", adminH.ResetBuffCharges) + r.Post("/heroes/{heroId}/apply-buff", adminH.ApplyHeroBuff) + r.Post("/heroes/{heroId}/apply-debuff", adminH.ApplyHeroDebuff) r.Post("/heroes/{heroId}/teleport-town", adminH.TeleportHeroTown) r.Post("/heroes/{heroId}/start-rest", adminH.StartHeroRest) r.Post("/heroes/{heroId}/start-roadside-rest", adminH.StartHeroRoadsideRest)