package model import "time" // ExcursionPhase tracks where the hero is within a mini-adventure session. // The lifecycle is: Out → Wild → Return → (back to road, phase cleared). // For KindTown, Phase is usually ExcursionWild while using attractor movement during wander/rest. type ExcursionPhase string const ( ExcursionNone ExcursionPhase = "" ExcursionOut ExcursionPhase = "out" // moving off-road into the forest ExcursionWild ExcursionPhase = "wild" // in the wilderness (encounters happen here) ExcursionReturn ExcursionPhase = "return" // returning to the road (encounters still possible) ) // ExcursionKind distinguishes roadside rest vs walking adventure vs in-town tour. type ExcursionKind string const ( ExcursionKindNone ExcursionKind = "" ExcursionKindRoadside ExcursionKind = "roadside" ExcursionKindAdventure ExcursionKind = "adventure" ExcursionKindTown ExcursionKind = "town" ) // TownTourPhase is the sub-state machine while ExcursionKind == town (StateInTown). type TownTourPhase string const ( TownTourPhaseWander TownTourPhase = "wander" TownTourPhaseNpcApproach TownTourPhase = "npc_approach" TownTourPhaseNpcWelcome TownTourPhase = "npc_welcome" TownTourPhaseNpcService TownTourPhase = "npc_service" TownTourPhaseRest TownTourPhase = "rest" ) // ExcursionSession holds the live state of an active mini-adventure (off-road excursion) or town tour. // When Phase == ExcursionNone the session is inactive and all other fields are zero-valued (except Kind for town cleared on leave). type ExcursionSession struct { Kind ExcursionKind Phase ExcursionPhase StartedAt time.Time // OutUntil / WildUntil / ReturnUntil: legacy time-based FSM (ignored when Kind is set). OutUntil time.Time WildUntil time.Time ReturnUntil time.Time // DepthWorldUnits is used to place forest attractors (perpendicular distance from road spine). DepthWorldUnits float64 // RoadFreezeWaypoint / RoadFreezeFraction capture road progress at the moment the hero // left the road, so it can be restored exactly when the excursion ends. RoadFreezeWaypoint int RoadFreezeFraction float64 // Attractor-based movement (Kind != ""): hero walks in world space toward AttractorX/Y. StartX, StartY float64 AttractorX, AttractorY float64 AttractorSet bool // Adventure-only: wall-time when wandering should end (then return to road). AdventureEndsAt time.Time // Adventure / town wander: next time to pick a new wander attractor (wild phase). WanderNextAt time.Time // PendingReturnAfterCombat: adventure timer elapsed; wait for combat end then enter return phase. PendingReturnAfterCombat bool // --- Town tour (Kind == ExcursionKindTown) --- TownTourPhase string // TownTourEndsAt: wall-time when the hero should leave the town (may defer until idle). TownTourEndsAt time.Time TownTourNpcID int64 // Stand point near NPC during approach / welcome / service. TownTourStandX float64 TownTourStandY float64 // TownWelcomeUntil: npc_welcome phase deadline (30s, shifted while dialog open). TownWelcomeUntil time.Time // TownServiceUntil: npc_service phase max wall time (4 min, shifted while UI open). TownServiceUntil time.Time // TownRestUntil: in-town rest phase end. TownRestUntil time.Time TownExitPending bool // Client has NPCDialog (welcome or service) open — shifts welcome/service deadlines. TownTourDialogOpen bool // Client has NPCInteraction panel open — shifts service deadline; with dialog shifts welcome too. TownTourInteractionOpen bool } // Active reports whether an excursion session is in progress. func (s *ExcursionSession) Active() bool { if s == nil { return false } if s.Kind == ExcursionKindTown { return s.TownTourPhase != "" } return s.Phase != ExcursionNone } // ExcursionPersisted is the JSON-serialisable subset of ExcursionSession stored in the // heroes.town_pause JSONB column so that reconnect / offline catch-up can resume mid-adventure. type ExcursionPersisted struct { Kind string `json:"kind,omitempty"` Phase string `json:"phase,omitempty"` StartedAt *time.Time `json:"startedAt,omitempty"` OutUntil *time.Time `json:"outUntil,omitempty"` WildUntil *time.Time `json:"wildUntil,omitempty"` ReturnUntil *time.Time `json:"returnUntil,omitempty"` DepthWorldUnits float64 `json:"depthWorldUnits,omitempty"` RoadFreezeWaypoint int `json:"roadFreezeWaypoint,omitempty"` RoadFreezeFraction float64 `json:"roadFreezeFraction,omitempty"` StartX float64 `json:"startX,omitempty"` StartY float64 `json:"startY,omitempty"` AttractorX float64 `json:"attractorX,omitempty"` AttractorY float64 `json:"attractorY,omitempty"` AttractorSet bool `json:"attractorSet,omitempty"` AdventureEndsAt *time.Time `json:"adventureEndsAt,omitempty"` WanderNextAt *time.Time `json:"wanderNextAt,omitempty"` PendingReturnAfterCombat bool `json:"pendingReturnAfterCombat,omitempty"` TownTourPhase string `json:"townTourPhase,omitempty"` TownTourEndsAt *time.Time `json:"townTourEndsAt,omitempty"` TownTourNpcID int64 `json:"townTourNpcId,omitempty"` TownTourStandX float64 `json:"townTourStandX,omitempty"` TownTourStandY float64 `json:"townTourStandY,omitempty"` TownWelcomeUntil *time.Time `json:"townWelcomeUntil,omitempty"` TownServiceUntil *time.Time `json:"townServiceUntil,omitempty"` TownRestUntil *time.Time `json:"townRestUntil,omitempty"` TownExitPending bool `json:"townExitPending,omitempty"` TownTourDialogOpen bool `json:"townTourDialogOpen,omitempty"` TownTourInteractionOpen bool `json:"townTourInteractionOpen,omitempty"` }