|
|
|
|
@ -72,6 +72,9 @@ type adminLiveMovementJSON struct {
|
|
|
|
|
CurrentTownID int64 `json:"currentTownId,omitempty"`
|
|
|
|
|
DestinationTownID int64 `json:"destinationTownId,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.
|
|
|
|
|
@ -111,6 +114,17 @@ func buildAdminLiveMovementSnap(hm *game.HeroMovement, now time.Time) *adminLive
|
|
|
|
|
t := hm.WanderingMerchantDeadline
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1667,6 +1681,124 @@ func (h *AdminHandler) StopHeroRest(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
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.
|
|
|
|
|
// POST /admin/time/pause
|
|
|
|
|
func (h *AdminHandler) PauseTime(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
|