new admin rest methods

master
Denis Ranneft 1 month ago
parent ca9aff89f3
commit d2d7cc88ab

@ -798,6 +798,59 @@ func (e *Engine) ApplyAdminStopRest(heroID int64) (*model.Hero, bool) {
return h, true return h, true
} }
// ApplyAdminStartExcursion forces an online hero into a mini-adventure session on the current road.
func (e *Engine) ApplyAdminStartExcursion(heroID int64) (*model.Hero, bool) {
e.mu.Lock()
defer e.mu.Unlock()
hm, ok := e.movements[heroID]
if !ok {
return nil, false
}
now := time.Now()
if !hm.AdminStartExcursion(now) {
return nil, false
}
hm.SyncToHero()
h := hm.Hero
if e.sender != nil {
h.EnsureGearMap()
h.RefreshDerivedCombatStats(now)
e.sender.SendToHero(heroID, "hero_state", h)
e.sender.SendToHero(heroID, "excursion_start", model.ExcursionStartPayload{
DepthWorldUnits: hm.Excursion.DepthWorldUnits,
})
e.sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
}
return h, true
}
// ApplyAdminStopExcursion ends an online hero's excursion immediately.
func (e *Engine) ApplyAdminStopExcursion(heroID int64) (*model.Hero, bool) {
e.mu.Lock()
defer e.mu.Unlock()
hm, ok := e.movements[heroID]
if !ok {
return nil, false
}
now := time.Now()
if !hm.AdminStopExcursion(now) {
return nil, false
}
hm.SyncToHero()
h := hm.Hero
if e.sender != nil {
h.EnsureGearMap()
h.RefreshDerivedCombatStats(now)
e.sender.SendToHero(heroID, "excursion_end", model.ExcursionEndPayload{})
e.sender.SendToHero(heroID, "hero_state", h)
e.sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
if route := hm.RoutePayload(); route != nil {
e.sender.SendToHero(heroID, "route_assigned", route)
}
}
return h, true
}
// ListActiveCombats returns a snapshot of all active combat sessions. // ListActiveCombats returns a snapshot of all active combat sessions.
func (e *Engine) ListActiveCombats() []CombatInfo { func (e *Engine) ListActiveCombats() []CombatInfo {
e.mu.RLock() e.mu.RLock()

@ -613,6 +613,50 @@ func (hm *HeroMovement) AdminStartRoadsideRest(now time.Time) bool {
return true return true
} }
// AdminStartExcursion forces a mini-adventure (excursion) session while the hero is walking on a road.
// Cooldown and random roll are bypassed; the hero must not already be in an excursion.
func (hm *HeroMovement) AdminStartExcursion(now time.Time) bool {
if hm.Hero == nil || hm.Hero.HP <= 0 || hm.Hero.State == model.StateDead {
return false
}
if hm.State == model.StateFighting {
return false
}
if hm.State != model.StateWalking {
return false
}
if hm.Excursion.Active() {
return false
}
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
return false
}
hm.WanderingMerchantDeadline = time.Time{}
hm.beginExcursion(now)
return true
}
// AdminStopExcursion ends an active excursion immediately (hero back on the road spine).
// Works during walking phases or adventure-inline rest; rejects combat.
func (hm *HeroMovement) AdminStopExcursion(now time.Time) bool {
if !hm.Excursion.Active() {
return false
}
if hm.State == model.StateFighting {
return false
}
if hm.State == model.StateResting && hm.ActiveRestKind == model.RestKindAdventureInline {
hm.ActiveRestKind = model.RestKindNone
hm.RestUntil = time.Time{}
hm.RestHealRemainder = 0
hm.State = model.StateWalking
hm.Hero.State = model.StateWalking
}
hm.endExcursion(now)
hm.refreshSpeed(now)
return true
}
// AdminStopRest exits any non-town rest (roadside or adventure-inline) back to walking. // AdminStopRest exits any non-town rest (roadside or adventure-inline) back to walking.
func (hm *HeroMovement) AdminStopRest(now time.Time) bool { func (hm *HeroMovement) AdminStopRest(now time.Time) bool {
if hm.State != model.StateResting { if hm.State != model.StateResting {

@ -72,6 +72,9 @@ type adminLiveMovementJSON struct {
CurrentTownID int64 `json:"currentTownId,omitempty"` CurrentTownID int64 `json:"currentTownId,omitempty"`
DestinationTownID int64 `json:"destinationTownId,omitempty"` DestinationTownID int64 `json:"destinationTownId,omitempty"`
WanderingMerchantDeadline *time.Time `json:"wanderingMerchantDeadline,omitempty"` WanderingMerchantDeadline *time.Time `json:"wanderingMerchantDeadline,omitempty"`
ExcursionPhase string `json:"excursionPhase,omitempty"`
ExcursionWildUntil *time.Time `json:"excursionWildUntil,omitempty"`
ExcursionReturnUntil *time.Time `json:"excursionReturnUntil,omitempty"`
} }
// adminHeroDetailResponse is the full admin JSON for one hero: base hero + persisted town_pause + live movement snapshot. // adminHeroDetailResponse is the full admin JSON for one hero: base hero + persisted town_pause + live movement snapshot.
@ -111,6 +114,17 @@ func buildAdminLiveMovementSnap(hm *game.HeroMovement, now time.Time) *adminLive
t := hm.WanderingMerchantDeadline t := hm.WanderingMerchantDeadline
s.WanderingMerchantDeadline = &t s.WanderingMerchantDeadline = &t
} }
if hm.Excursion.Active() {
s.ExcursionPhase = string(hm.Excursion.Phase)
if !hm.Excursion.WildUntil.IsZero() {
t := hm.Excursion.WildUntil
s.ExcursionWildUntil = &t
}
if !hm.Excursion.ReturnUntil.IsZero() {
t := hm.Excursion.ReturnUntil
s.ExcursionReturnUntil = &t
}
}
return s return s
} }
@ -1667,6 +1681,124 @@ func (h *AdminHandler) StopHeroRest(w http.ResponseWriter, r *http.Request) {
h.writeAdminHeroDetail(w, hero2) h.writeAdminHeroDetail(w, hero2)
} }
// StartHeroExcursion forces a walking hero on a road into a mini-adventure (excursion) session.
// POST /admin/heroes/{heroId}/start-adventure
func (h *AdminHandler) StartHeroExcursion(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
}
hero, err := h.store.GetByID(r.Context(), heroID)
if err != nil {
h.logger.Error("admin: get hero for start-adventure", "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 hero.State == model.StateFighting || hero.State == model.StateDead || hero.HP <= 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "hero must be alive and not in combat"})
return
}
if hero.State != model.StateWalking {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "hero must be walking on the road to start an excursion"})
return
}
if hm := h.engine.GetMovements(heroID); hm != nil {
out, ok := h.engine.ApplyAdminStartExcursion(heroID)
if !ok || out == nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot start excursion (need active road segment, or excursion already active)"})
return
}
if err := h.store.Save(r.Context(), out); err != nil {
h.logger.Error("admin: save after start-adventure", "hero_id", heroID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save hero"})
return
}
h.logger.Info("admin: start excursion", "hero_id", heroID)
h.writeAdminHeroDetail(w, out)
return
}
hero2, err := h.adminMovementOffline(r.Context(), hero, func(hm *game.HeroMovement, rg *game.RoadGraph, now time.Time) error {
if !hm.AdminStartExcursion(now) {
return fmt.Errorf("cannot start excursion (need active road segment, or excursion already active)")
}
return nil
})
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
h.logger.Info("admin: start excursion (offline)", "hero_id", heroID)
h.writeAdminHeroDetail(w, hero2)
}
// StopHeroExcursion ends the hero's mini-adventure session immediately.
// POST /admin/heroes/{heroId}/stop-adventure
func (h *AdminHandler) StopHeroExcursion(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
}
hero, err := h.store.GetByID(r.Context(), heroID)
if err != nil {
h.logger.Error("admin: get hero for stop-adventure", "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 hm := h.engine.GetMovements(heroID); hm != nil {
out, ok := h.engine.ApplyAdminStopExcursion(heroID)
if !ok || out == nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "hero has no active excursion"})
return
}
if err := h.store.Save(r.Context(), out); err != nil {
h.logger.Error("admin: save after stop-adventure", "hero_id", heroID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save hero"})
return
}
h.logger.Info("admin: stop excursion", "hero_id", heroID)
h.writeAdminHeroDetail(w, out)
return
}
hero2, err := h.adminMovementOffline(r.Context(), hero, func(hm *game.HeroMovement, rg *game.RoadGraph, now time.Time) error {
if !hm.AdminStopExcursion(now) {
return fmt.Errorf("hero has no active excursion")
}
return nil
})
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
h.logger.Info("admin: stop excursion (offline)", "hero_id", heroID)
h.writeAdminHeroDetail(w, hero2)
}
// PauseTime freezes engine ticks, offline simulation, and blocks mutating game API calls. // PauseTime freezes engine ticks, offline simulation, and blocks mutating game API calls.
// POST /admin/time/pause // POST /admin/time/pause
func (h *AdminHandler) PauseTime(w http.ResponseWriter, r *http.Request) { func (h *AdminHandler) PauseTime(w http.ResponseWriter, r *http.Request) {

@ -87,6 +87,8 @@ func New(deps Deps) *chi.Mux {
r.Post("/heroes/{heroId}/teleport-town", adminH.TeleportHeroTown) r.Post("/heroes/{heroId}/teleport-town", adminH.TeleportHeroTown)
r.Post("/heroes/{heroId}/start-rest", adminH.StartHeroRest) r.Post("/heroes/{heroId}/start-rest", adminH.StartHeroRest)
r.Post("/heroes/{heroId}/start-roadside-rest", adminH.StartHeroRoadsideRest) r.Post("/heroes/{heroId}/start-roadside-rest", adminH.StartHeroRoadsideRest)
r.Post("/heroes/{heroId}/start-adventure", adminH.StartHeroExcursion)
r.Post("/heroes/{heroId}/stop-adventure", adminH.StopHeroExcursion)
r.Post("/heroes/{heroId}/stop-rest", adminH.StopHeroRest) r.Post("/heroes/{heroId}/stop-rest", adminH.StopHeroRest)
r.Post("/heroes/{heroId}/leave-town", adminH.ForceLeaveTown) r.Post("/heroes/{heroId}/leave-town", adminH.ForceLeaveTown)
r.Get("/heroes/{heroId}/gear", adminH.GetHeroGear) r.Get("/heroes/{heroId}/gear", adminH.GetHeroGear)

@ -21,6 +21,7 @@ param(
"start-rest", "start-rest",
"leave-town", "leave-town",
"start-roadside-rest", "start-roadside-rest",
"stop-adventure",
"stop-rest", "stop-rest",
"time-pause", "time-pause",
"time-resume" "time-resume"
@ -172,6 +173,10 @@ switch ($Command) {
Require-Value -Name "HeroId" -Value $HeroId Require-Value -Name "HeroId" -Value $HeroId
$result = Invoke-AdminRequest -Method "POST" -Path "/admin/heroes/$HeroId/start-roadside-rest" -Body @{} $result = Invoke-AdminRequest -Method "POST" -Path "/admin/heroes/$HeroId/start-roadside-rest" -Body @{}
} }
"stop-adventure" {
Require-Value -Name "HeroId" -Value $HeroId
$result = Invoke-AdminRequest -Method "POST" -Path "/admin/heroes/$HeroId/stop-adventure" -Body @{}
}
"stop-rest" { "stop-rest" {
Require-Value -Name "HeroId" -Value $HeroId Require-Value -Name "HeroId" -Value $HeroId
$result = Invoke-AdminRequest -Method "POST" -Path "/admin/heroes/$HeroId/stop-rest" -Body @{} $result = Invoke-AdminRequest -Method "POST" -Path "/admin/heroes/$HeroId/stop-rest" -Body @{}

Loading…
Cancel
Save