admin ui update: add subscription button added

master
Denis Ranneft 1 month ago
parent 006bee5a5e
commit a93e9a2520

@ -1413,6 +1413,7 @@
<div class="kv"><kbd>Level</kbd><div>${e(h.level)}</div></div> <div class="kv"><kbd>Level</kbd><div>${e(h.level)}</div></div>
<div class="kv"><kbd>HP</kbd><div>${e(h.hp)}/${e(h.maxHp)}</div></div> <div class="kv"><kbd>HP</kbd><div>${e(h.hp)}/${e(h.maxHp)}</div></div>
<div class="kv"><kbd>Gold</kbd><div>${e(h.gold)}</div></div> <div class="kv"><kbd>Gold</kbd><div>${e(h.gold)}</div></div>
<div class="kv"><kbd>Subscription</kbd><div>${h.subscriptionActive ? "active" : "inactive"}${h.subscriptionExpiresAt ? " · until " + e(h.subscriptionExpiresAt) : ""}</div></div>
<div class="row"> <div class="row">
<div><input id="hero-hp" type="number" placeholder="New HP" /></div> <div><input id="hero-hp" type="number" placeholder="New HP" /></div>
<div><input id="hero-gold" type="number" placeholder="New Gold" /></div> <div><input id="hero-gold" type="number" placeholder="New Gold" /></div>
@ -1422,6 +1423,9 @@
<button class="btn" onclick="withAction(() => heroAction('set-gold',{gold:Number(document.getElementById('hero-gold').value)}))">Set Gold</button> <button class="btn" onclick="withAction(() => heroAction('set-gold',{gold:Number(document.getElementById('hero-gold').value)}))">Set Gold</button>
<button class="btn" onclick="withAction(() => heroAction('set-level',{level:Number(document.getElementById('hero-level').value)}))">Set Level</button> <button class="btn" onclick="withAction(() => heroAction('set-level',{level:Number(document.getElementById('hero-level').value)}))">Set Level</button>
<button class="btn" onclick="withAction(() => heroAction('revive',{}))">Revive</button> <button class="btn" onclick="withAction(() => heroAction('revive',{}))">Revive</button>
<span class="muted" style="margin-left:4px">Подписка:</span>
<input id="hero-sub-periods" type="number" min="1" max="52" value="1" style="width:56px" title="Число периодов (как при покупке подписки)" />
<button type="button" class="btn" onclick="withAction(() => heroAction('grant-subscription',{periods:Math.min(52,Math.max(1,parseInt(document.getElementById('hero-sub-periods').value,10)||1))}))" title="Выдать подписку на N периодов (длительность из runtime), без списания RUB">Выдать подписку</button>
<button type="button" class="btn warn" onclick="withAction(() => heroAction('force-death',{}))" title="HP 0, state dead, ends combat; counts as a death if the hero was alive">Режим смерти</button> <button type="button" class="btn warn" onclick="withAction(() => heroAction('force-death',{}))" title="HP 0, state dead, ends combat; counts as a death if the hero was alive">Режим смерти</button>
<button class="btn" onclick="withAction(() => heroAction('start-rest',{}, true))" title="Town rest (same duration as normal town rest)">Start rest (town)</button> <button class="btn" onclick="withAction(() => heroAction('start-rest',{}, true))" title="Town rest (same duration as normal town rest)">Start rest (town)</button>
<button class="btn" onclick="withAction(() => heroAction('start-roadside-rest',{}, true))" title="Roadside rest at current road position (not in excursion)">Start rest (roadside)</button> <button class="btn" onclick="withAction(() => heroAction('start-roadside-rest',{}, true))" title="Roadside rest at current road position (not in excursion)">Start rest (roadside)</button>

Binary file not shown.

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"net/http" "net/http"
"runtime" "runtime"
@ -1197,6 +1198,108 @@ func (h *AdminHandler) ReviveHero(w http.ResponseWriter, r *http.Request) {
writeHeroJSON(w, http.StatusOK, hero) writeHeroJSON(w, http.StatusOK, hero)
} }
type grantSubscriptionRequest struct {
// Periods is how many subscription durations to add (stacking extends from current expiry). Default 1.
Periods int `json:"periods"`
}
// GrantHeroSubscription activates or extends subscription like a purchase, without charging RUB (admin grant).
// POST /admin/heroes/{heroId}/grant-subscription
func (h *AdminHandler) GrantHeroSubscription(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 grantSubscriptionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid request body: " + err.Error(),
})
return
}
periods := req.Periods
if periods < 1 {
periods = 1
}
if periods > 52 {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "periods must be between 1 and 52",
})
return
}
hero, err := h.store.GetByID(r.Context(), heroID)
if err != nil {
h.logger.Error("admin: get hero for grant-subscription", "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()
for i := 0; i < periods; i++ {
hero.ActivateSubscription(now)
}
hero.EnsureBuffChargesPopulated(now)
for bt := range model.BuffFreeChargesPerType {
state := hero.GetBuffCharges(bt, now)
subMax := hero.MaxBuffCharges(bt)
if state.Remaining < subMax {
state.Remaining = subMax
hero.BuffCharges[string(bt)] = state
}
}
payment := &model.Payment{
HeroID: hero.ID,
Type: model.PaymentType("subscription_admin"),
AmountRUB: 0,
Status: model.PaymentCompleted,
CreatedAt: now,
CompletedAt: &now,
}
if err := h.store.CreatePayment(r.Context(), payment); err != nil {
h.logger.Error("admin: grant-subscription payment row", "hero_id", heroID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to record payment",
})
return
}
if err := h.store.Save(r.Context(), hero); err != nil {
h.logger.Error("admin: save hero after grant-subscription", "hero_id", heroID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to save hero",
})
return
}
h.logger.Info("admin: subscription granted",
"hero_id", heroID,
"periods", periods,
"expires_at", hero.SubscriptionExpiresAt,
)
hero.EnsureGearMap()
hero.RefreshDerivedCombatStats(now)
h.engine.ApplyAdminHeroSnapshot(hero)
writeHeroJSON(w, http.StatusOK, hero)
}
// ForceHeroDeath sets the hero to dead (HP 0, state dead), ends active combat, clears buffs/debuffs, // ForceHeroDeath sets the hero to dead (HP 0, state dead), ends active combat, clears buffs/debuffs,
// and increments death stats when transitioning from alive. // and increments death stats when transitioning from alive.
// POST /admin/heroes/{heroId}/force-death // POST /admin/heroes/{heroId}/force-death

@ -82,6 +82,7 @@ func New(deps Deps) *chi.Mux {
r.Post("/heroes/{heroId}/set-hp", adminH.SetHeroHP) r.Post("/heroes/{heroId}/set-hp", adminH.SetHeroHP)
r.Post("/heroes/{heroId}/add-potions", adminH.AddPotions) r.Post("/heroes/{heroId}/add-potions", adminH.AddPotions)
r.Post("/heroes/{heroId}/revive", adminH.ReviveHero) r.Post("/heroes/{heroId}/revive", adminH.ReviveHero)
r.Post("/heroes/{heroId}/grant-subscription", adminH.GrantHeroSubscription)
r.Post("/heroes/{heroId}/force-death", adminH.ForceHeroDeath) r.Post("/heroes/{heroId}/force-death", adminH.ForceHeroDeath)
r.Post("/heroes/{heroId}/reset", adminH.ResetHero) r.Post("/heroes/{heroId}/reset", adminH.ResetHero)
r.Post("/heroes/{heroId}/reset-buff-charges", adminH.ResetBuffCharges) r.Post("/heroes/{heroId}/reset-buff-charges", adminH.ResetBuffCharges)

Loading…
Cancel
Save