roadside rest

master
Denis Ranneft 1 month ago
parent 97d29f7c2a
commit ca9aff89f3

@ -749,6 +749,55 @@ func (e *Engine) ApplyAdminStartRest(heroID int64) (*model.Hero, bool) {
return h, true
}
// ApplyAdminStartRoadsideRest puts an online hero into roadside rest at the current road position.
func (e *Engine) ApplyAdminStartRoadsideRest(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.AdminStartRoadsideRest(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, "hero_move", hm.MovePayload(now))
}
return h, true
}
// ApplyAdminStopRest exits a hero from non-town rest (roadside / adventure-inline) back to walking.
func (e *Engine) ApplyAdminStopRest(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.AdminStopRest(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, "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.
func (e *Engine) ListActiveCombats() []CombatInfo {
e.mu.RLock()

@ -153,6 +153,23 @@ func NewHeroMovement(hero *model.Hero, graph *RoadGraph, now time.Time) *HeroMov
if hero.State == model.StateResting || hero.State == model.StateInTown {
hm.State = hero.State
hm.applyTownPauseFromHero(hero, now)
// For roadside / adventure-inline rest the hero needs a road for display offset calculations.
if hm.ActiveRestKind == model.RestKindRoadside || hm.ActiveRestKind == model.RestKindAdventureInline {
if hm.DestinationTownID == 0 {
hm.pickDestination(graph)
}
hm.assignRoad(graph)
if hm.Excursion.Active() && hm.Road != nil && hm.Excursion.RoadFreezeWaypoint < len(hm.Road.Waypoints)-1 {
hm.WaypointIndex = hm.Excursion.RoadFreezeWaypoint
hm.WaypointFraction = hm.Excursion.RoadFreezeFraction
from := hm.Road.Waypoints[hm.WaypointIndex]
to := hm.Road.Waypoints[hm.WaypointIndex+1]
hm.CurrentX = from.X + (to.X-from.X)*hm.WaypointFraction
hm.CurrentY = from.Y + (to.Y-from.Y)*hm.WaypointFraction
}
}
return hm
}
@ -583,6 +600,39 @@ func (hm *HeroMovement) AdminPlaceInTown(graph *RoadGraph, townID int64, now tim
return nil
}
// AdminStartRoadsideRest forces the hero into roadside rest on the current road segment.
func (hm *HeroMovement) AdminStartRoadsideRest(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
}
hm.WanderingMerchantDeadline = time.Time{}
hm.beginRoadsideRest(now)
return true
}
// AdminStopRest exits any non-town rest (roadside or adventure-inline) back to walking.
func (hm *HeroMovement) AdminStopRest(now time.Time) bool {
if hm.State != model.StateResting {
return false
}
if hm.ActiveRestKind != model.RestKindRoadside && hm.ActiveRestKind != model.RestKindAdventureInline {
return false
}
if hm.ActiveRestKind == model.RestKindAdventureInline && hm.Excursion.Active() {
hm.endExcursion(now)
}
hm.ActiveRestKind = model.RestKindNone
hm.RestUntil = time.Time{}
hm.RestHealRemainder = 0
hm.State = model.StateWalking
hm.Hero.State = model.StateWalking
hm.refreshSpeed(now)
return true
}
// AdminStartRest forces a resting period (same duration model as town rest).
func (hm *HeroMovement) AdminStartRest(now time.Time, graph *RoadGraph) bool {
if hm.Hero == nil || hm.Hero.HP <= 0 || hm.Hero.State == model.StateDead {
@ -846,20 +896,22 @@ func (hm *HeroMovement) townPauseBlob() *model.TownPausePersisted {
switch hm.State {
case model.StateResting:
if hm.RestUntil.IsZero() {
break
}
t := hm.RestUntil
rk := model.RestKindTown
if hm.ActiveRestKind != model.RestKindNone {
rk = hm.ActiveRestKind
}
if rk == model.RestKindTown && hm.RestUntil.IsZero() {
break
}
p = &model.TownPausePersisted{
RestUntil: &t,
RestKind: rk,
TownRestHealRemainder: hm.TownRestHealRemainder,
RestHealRemainder: hm.RestHealRemainder,
}
if !hm.RestUntil.IsZero() {
t := hm.RestUntil
p.RestUntil = &t
}
case model.StateInTown:
p = &model.TownPausePersisted{
TownVisitNPCName: hm.TownVisitNPCName,
@ -923,8 +975,10 @@ func (hm *HeroMovement) applyTownPauseFromHero(hero *model.Hero, now time.Time)
blob := hero.TownPause
switch hero.State {
case model.StateResting:
if blob != nil && blob.RestUntil != nil && !blob.RestUntil.IsZero() {
if blob != nil && (blob.RestKind != model.RestKindNone || (blob.RestUntil != nil && !blob.RestUntil.IsZero())) {
if blob.RestUntil != nil && !blob.RestUntil.IsZero() {
hm.RestUntil = *blob.RestUntil
}
hm.ActiveRestKind = blob.RestKind
hm.TownRestHealRemainder = blob.TownRestHealRemainder
hm.RestHealRemainder = blob.RestHealRemainder

@ -1556,6 +1556,117 @@ func (h *AdminHandler) ForceLeaveTown(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, hero2)
}
// StartHeroRoadsideRest forces a hero into roadside rest at the current road position.
// POST /admin/heroes/{heroId}/start-roadside-rest
func (h *AdminHandler) StartHeroRoadsideRest(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-roadside-rest", "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 hm := h.engine.GetMovements(heroID); hm != nil {
out, ok := h.engine.ApplyAdminStartRoadsideRest(heroID)
if !ok || out == nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot start roadside rest"})
return
}
if err := h.store.Save(r.Context(), out); err != nil {
h.logger.Error("admin: save after start-roadside-rest", "hero_id", heroID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save hero"})
return
}
h.logger.Info("admin: start roadside rest", "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.AdminStartRoadsideRest(now) {
return fmt.Errorf("cannot start roadside rest")
}
return nil
})
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
h.logger.Info("admin: start roadside rest (offline)", "hero_id", heroID)
h.writeAdminHeroDetail(w, hero2)
}
// StopHeroRest exits a hero from non-town rest (roadside or adventure-inline) back to walking.
// POST /admin/heroes/{heroId}/stop-rest
func (h *AdminHandler) StopHeroRest(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 stop-rest", "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.ApplyAdminStopRest(heroID)
if !ok || out == nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "hero is not in roadside/adventure rest"})
return
}
if err := h.store.Save(r.Context(), out); err != nil {
h.logger.Error("admin: save after stop-rest", "hero_id", heroID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save hero"})
return
}
h.logger.Info("admin: stop rest", "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.AdminStopRest(now) {
return fmt.Errorf("hero is not in roadside/adventure rest")
}
return nil
})
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
h.logger.Info("admin: stop rest (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) {

@ -86,6 +86,8 @@ func New(deps Deps) *chi.Mux {
r.Post("/heroes/{heroId}/reset-buff-charges", adminH.ResetBuffCharges)
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)
r.Post("/heroes/{heroId}/stop-rest", adminH.StopHeroRest)
r.Post("/heroes/{heroId}/leave-town", adminH.ForceLeaveTown)
r.Get("/heroes/{heroId}/gear", adminH.GetHeroGear)
r.Post("/heroes/{heroId}/gear/grant", adminH.GrantHeroGear)

Loading…
Cancel
Save