You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
563 lines
16 KiB
Go
563 lines
16 KiB
Go
package game
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"math/rand"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/denisovdennis/autohero/internal/model"
|
|
"github.com/denisovdennis/autohero/internal/tuning"
|
|
)
|
|
|
|
// TownTourOfflineAtNPC resolves a town NPC visit without UI (offline catch-up).
|
|
type TownTourOfflineAtNPC func(heroID int64, hm *HeroMovement, graph *RoadGraph, npc TownNPC, now time.Time, adventureLog AdventureLogWriter)
|
|
|
|
func scheduleTownTourWanderRetarget(hm *HeroMovement, now time.Time) {
|
|
cfg := tuning.Get()
|
|
minMs := cfg.TownTourWanderRetargetMinMs
|
|
maxMs := cfg.TownTourWanderRetargetMaxMs
|
|
if minMs <= 0 {
|
|
minMs = tuning.DefaultValues().TownTourWanderRetargetMinMs
|
|
}
|
|
if maxMs <= 0 {
|
|
maxMs = tuning.DefaultValues().TownTourWanderRetargetMaxMs
|
|
}
|
|
hm.Excursion.WanderNextAt = now.Add(randomDurationBetweenMs(minMs, maxMs))
|
|
}
|
|
|
|
// beginTownTourExcursion starts attractor-based wandering in the current town (StateInTown).
|
|
func beginTownTourExcursion(hm *HeroMovement, now time.Time, graph *RoadGraph) {
|
|
if hm == nil || graph == nil {
|
|
return
|
|
}
|
|
clearLegacyTownNPCState(hm)
|
|
dur := randomRestDuration()
|
|
hm.Excursion = model.ExcursionSession{
|
|
Kind: model.ExcursionKindTown,
|
|
Phase: model.ExcursionWild,
|
|
StartedAt: now,
|
|
TownTourPhase: string(model.TownTourPhaseWander),
|
|
TownTourEndsAt: now.Add(dur),
|
|
}
|
|
scheduleTownTourWanderRetarget(hm, now)
|
|
pickTownTourWanderAttractor(hm, graph, now)
|
|
}
|
|
|
|
func clearLegacyTownNPCState(hm *HeroMovement) {
|
|
if hm == nil {
|
|
return
|
|
}
|
|
hm.TownNPCQueue = nil
|
|
hm.NextTownNPCRollAt = time.Time{}
|
|
hm.TownLastNPCLingerUntil = time.Time{}
|
|
hm.TownNPCWalkTargetID = 0
|
|
hm.TownNPCWalkToX = 0
|
|
hm.TownNPCWalkToY = 0
|
|
hm.TownCenterWalkActive = false
|
|
hm.TownCenterWalkToX = 0
|
|
hm.TownCenterWalkToY = 0
|
|
hm.TownPlazaHealActive = false
|
|
hm.TownLeaveAt = time.Time{}
|
|
hm.TownVisitNPCName = ""
|
|
hm.TownVisitNPCKey = ""
|
|
hm.TownVisitNPCType = ""
|
|
hm.TownVisitStartedAt = time.Time{}
|
|
hm.TownVisitLogsEmitted = 0
|
|
hm.TownNPCUILock = false
|
|
}
|
|
|
|
func clearTownVisitLogFields(hm *HeroMovement) {
|
|
if hm == nil {
|
|
return
|
|
}
|
|
hm.TownVisitNPCName = ""
|
|
hm.TownVisitNPCKey = ""
|
|
hm.TownVisitNPCType = ""
|
|
hm.TownVisitStartedAt = time.Time{}
|
|
hm.TownVisitLogsEmitted = 0
|
|
}
|
|
|
|
func transitionTownTourToWander(hm *HeroMovement, graph *RoadGraph, now time.Time) {
|
|
ex := &hm.Excursion
|
|
ex.TownTourPhase = string(model.TownTourPhaseWander)
|
|
ex.TownTourNpcID = 0
|
|
ex.TownTourStandX = 0
|
|
ex.TownTourStandY = 0
|
|
ex.TownWelcomeUntil = time.Time{}
|
|
ex.TownServiceUntil = time.Time{}
|
|
ex.TownTourDialogOpen = false
|
|
ex.TownTourInteractionOpen = false
|
|
clearTownVisitLogFields(hm)
|
|
scheduleTownTourWanderRetarget(hm, now)
|
|
pickTownTourWanderAttractor(hm, graph, now)
|
|
}
|
|
|
|
// pickTownTourWanderAttractor chooses the next wander target: random point in town or stand near an NPC.
|
|
func pickTownTourWanderAttractor(hm *HeroMovement, graph *RoadGraph, now time.Time) {
|
|
if hm == nil || graph == nil {
|
|
return
|
|
}
|
|
ex := &hm.Excursion
|
|
if ex.TownExitPending {
|
|
return
|
|
}
|
|
town := graph.Towns[hm.CurrentTownID]
|
|
if town == nil {
|
|
return
|
|
}
|
|
cfg := tuning.Get()
|
|
npcs := graph.TownNPCs[hm.CurrentTownID]
|
|
pNpc := cfg.TownTourNpcAttractorChance
|
|
if pNpc <= 0 {
|
|
pNpc = tuning.DefaultValues().TownTourNpcAttractorChance
|
|
}
|
|
if pNpc > 1 {
|
|
pNpc = 1
|
|
}
|
|
|
|
if len(npcs) > 0 && rand.Float64() < pNpc {
|
|
npc := npcs[rand.Intn(len(npcs))]
|
|
npcWX, npcWY, posOk := graph.NPCWorldPos(npc.ID, hm.CurrentTownID)
|
|
if !posOk {
|
|
npcWX = town.WorldX + npc.OffsetX
|
|
npcWY = town.WorldY + npc.OffsetY
|
|
}
|
|
standoff := cfg.TownNPCStandoffWorld
|
|
if standoff <= 0 {
|
|
standoff = tuning.DefaultValues().TownNPCStandoffWorld
|
|
}
|
|
toX, toY := townNPCStandPoint(npcWX, npcWY, hm.CurrentX, hm.CurrentY, standoff)
|
|
ex.TownTourPhase = string(model.TownTourPhaseNpcApproach)
|
|
ex.TownTourNpcID = npc.ID
|
|
ex.TownTourStandX = toX
|
|
ex.TownTourStandY = toY
|
|
ex.AttractorSet = false
|
|
return
|
|
}
|
|
|
|
// Random point inside town circle (keep margin from edge).
|
|
cx, cy := town.WorldX, town.WorldY
|
|
radius := town.Radius
|
|
if radius < 1 {
|
|
radius = 8
|
|
}
|
|
margin := radius * 0.12
|
|
maxR := radius - margin
|
|
if maxR < margin {
|
|
maxR = radius * 0.5
|
|
}
|
|
for attempt := 0; attempt < 24; attempt++ {
|
|
theta := rand.Float64() * 2 * math.Pi
|
|
rd := margin + rand.Float64()*math.Max(0.01, maxR-margin)
|
|
px := cx + math.Cos(theta)*rd
|
|
py := cy + math.Sin(theta)*rd
|
|
if graph.HeroInTownAt(px, py) {
|
|
ex.AttractorX = px
|
|
ex.AttractorY = py
|
|
ex.AttractorSet = true
|
|
ex.TownTourPhase = string(model.TownTourPhaseWander)
|
|
ex.TownTourNpcID = 0
|
|
return
|
|
}
|
|
}
|
|
ex.AttractorX = cx
|
|
ex.AttractorY = cy
|
|
ex.AttractorSet = true
|
|
ex.TownTourPhase = string(model.TownTourPhaseWander)
|
|
ex.TownTourNpcID = 0
|
|
}
|
|
|
|
// AdminTownTourApproachNPC forces npc_approach toward npcID in the hero's current town (admin only).
|
|
func (hm *HeroMovement) AdminTownTourApproachNPC(graph *RoadGraph, npcID int64, now time.Time) error {
|
|
if hm == nil || graph == nil {
|
|
return errors.New("nil movement or graph")
|
|
}
|
|
if hm.Excursion.Kind != model.ExcursionKindTown {
|
|
return errors.New("hero is not on town tour excursion")
|
|
}
|
|
if hm.State != model.StateInTown {
|
|
return errors.New("hero must be in town")
|
|
}
|
|
npc, ok := graph.NPCByID[npcID]
|
|
if !ok {
|
|
return errors.New("npc not found in world graph")
|
|
}
|
|
found := false
|
|
for _, n := range graph.TownNPCs[hm.CurrentTownID] {
|
|
if n.ID == npcID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return errors.New("npc is not in hero's current town")
|
|
}
|
|
town := graph.Towns[hm.CurrentTownID]
|
|
if town == nil {
|
|
return errors.New("town not found")
|
|
}
|
|
cfg := tuning.Get()
|
|
npcWX, npcWY, posOk := graph.NPCWorldPos(npc.ID, hm.CurrentTownID)
|
|
if !posOk {
|
|
npcWX = town.WorldX + npc.OffsetX
|
|
npcWY = town.WorldY + npc.OffsetY
|
|
}
|
|
standoff := cfg.TownNPCStandoffWorld
|
|
if standoff <= 0 {
|
|
standoff = tuning.DefaultValues().TownNPCStandoffWorld
|
|
}
|
|
ex := &hm.Excursion
|
|
toX, toY := townNPCStandPoint(npcWX, npcWY, hm.CurrentX, hm.CurrentY, standoff)
|
|
ex.TownTourPhase = string(model.TownTourPhaseNpcApproach)
|
|
ex.TownTourNpcID = npc.ID
|
|
ex.TownTourStandX = toX
|
|
ex.TownTourStandY = toY
|
|
ex.AttractorSet = false
|
|
ex.TownWelcomeUntil = time.Time{}
|
|
ex.TownServiceUntil = time.Time{}
|
|
ex.TownTourDialogOpen = false
|
|
ex.TownTourInteractionOpen = false
|
|
hm.TownNPCUILock = false
|
|
hm.sentTownTourWireSig = ""
|
|
return nil
|
|
}
|
|
|
|
// NotifyTownTourClients pushes town_tour_phase, hero_state, and hero_move after an out-of-tick town tour change.
|
|
func NotifyTownTourClients(sender MessageSender, heroID int64, hm *HeroMovement, graph *RoadGraph, now time.Time) {
|
|
if sender == nil || hm == nil || graph == nil || hm.Excursion.Kind != model.ExcursionKindTown {
|
|
return
|
|
}
|
|
hm.sentTownTourWireSig = ""
|
|
sendTownTourUpdate(sender, heroID, hm, graph)
|
|
h := hm.Hero
|
|
if h != nil {
|
|
h.EnsureGearMap()
|
|
h.RefreshDerivedCombatStats(now)
|
|
sender.SendToHero(heroID, "hero_state", h)
|
|
}
|
|
sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
|
|
}
|
|
|
|
func townTourWireSig(hm *HeroMovement) string {
|
|
if hm == nil {
|
|
return ""
|
|
}
|
|
ex := hm.Excursion
|
|
return ex.TownTourPhase + ":" + strconv.FormatInt(ex.TownTourNpcID, 10) + ":" + strconv.FormatBool(ex.TownExitPending)
|
|
}
|
|
|
|
func sendTownTourUpdate(sender MessageSender, heroID int64, hm *HeroMovement, graph *RoadGraph) {
|
|
if sender == nil || hm == nil || graph == nil {
|
|
return
|
|
}
|
|
ex := hm.Excursion
|
|
town := graph.Towns[hm.CurrentTownID]
|
|
var townKey string
|
|
if town != nil {
|
|
townKey = town.NameKey
|
|
}
|
|
var npcID int64
|
|
var name, nameKey, npcType string
|
|
var wx, wy float64
|
|
if ex.TownTourNpcID != 0 {
|
|
if npc, ok := graph.NPCByID[ex.TownTourNpcID]; ok {
|
|
npcID = npc.ID
|
|
name = npc.Name
|
|
nameKey = npc.NameKey
|
|
npcType = npc.Type
|
|
if x, y, ok2 := graph.NPCWorldPos(npc.ID, hm.CurrentTownID); ok2 {
|
|
wx, wy = x, y
|
|
} else if town != nil {
|
|
wx = town.WorldX + npc.OffsetX
|
|
wy = town.WorldY + npc.OffsetY
|
|
}
|
|
}
|
|
}
|
|
payload := model.TownTourPhasePayload{
|
|
Phase: ex.TownTourPhase,
|
|
TownID: hm.CurrentTownID,
|
|
TownNameKey: townKey,
|
|
NpcID: npcID,
|
|
NpcName: name,
|
|
NpcNameKey: nameKey,
|
|
NpcType: npcType,
|
|
WorldX: wx,
|
|
WorldY: wy,
|
|
ExitPending: ex.TownExitPending,
|
|
}
|
|
sender.SendToHero(heroID, "town_tour_phase", payload)
|
|
}
|
|
|
|
func processTownTourMovement(
|
|
heroID int64,
|
|
hm *HeroMovement,
|
|
graph *RoadGraph,
|
|
now time.Time,
|
|
sender MessageSender,
|
|
adventureLog AdventureLogWriter,
|
|
townTourOffline TownTourOfflineAtNPC,
|
|
) {
|
|
if hm == nil || graph == nil {
|
|
return
|
|
}
|
|
ex := &hm.Excursion
|
|
cfg := tuning.Get()
|
|
dt := now.Sub(hm.LastMoveTick).Seconds()
|
|
if dt <= 0 {
|
|
dt = movementTickRate().Seconds()
|
|
}
|
|
hm.LastMoveTick = now
|
|
hm.refreshSpeed(now)
|
|
|
|
if !now.Before(ex.TownTourEndsAt) {
|
|
ex.TownExitPending = true
|
|
}
|
|
|
|
uiOpen := ex.TownTourDialogOpen || ex.TownTourInteractionOpen
|
|
if uiOpen && dt > 0 {
|
|
shift := time.Duration(dt * float64(time.Second))
|
|
switch model.TownTourPhase(ex.TownTourPhase) {
|
|
case model.TownTourPhaseNpcWelcome:
|
|
if !ex.TownWelcomeUntil.IsZero() {
|
|
ex.TownWelcomeUntil = ex.TownWelcomeUntil.Add(shift)
|
|
}
|
|
case model.TownTourPhaseNpcService:
|
|
if !ex.TownServiceUntil.IsZero() {
|
|
ex.TownServiceUntil = ex.TownServiceUntil.Add(shift)
|
|
}
|
|
case model.TownTourPhaseRest:
|
|
if !ex.TownRestUntil.IsZero() {
|
|
ex.TownRestUntil = ex.TownRestUntil.Add(shift)
|
|
}
|
|
ex.TownTourEndsAt = ex.TownTourEndsAt.Add(shift)
|
|
}
|
|
}
|
|
hm.TownNPCUILock = uiOpen
|
|
|
|
walkSpeed := cfg.TownNPCWalkSpeed
|
|
if walkSpeed <= 0 {
|
|
walkSpeed = tuning.DefaultValues().TownNPCWalkSpeed
|
|
}
|
|
|
|
switch model.TownTourPhase(ex.TownTourPhase) {
|
|
case model.TownTourPhaseWander:
|
|
if !ex.AttractorSet {
|
|
// Defensive: pick a wander target.
|
|
pickTownTourWanderAttractor(hm, graph, now)
|
|
}
|
|
arrived := hm.stepTowardAttractor(now, dt)
|
|
if !arrived {
|
|
break
|
|
}
|
|
// At wander attractor.
|
|
if ex.TownExitPending {
|
|
hm.LeaveTown(graph, now)
|
|
hm.Excursion = model.ExcursionSession{}
|
|
hm.sentTownTourWireSig = ""
|
|
if sender != nil {
|
|
sender.SendToHero(heroID, "town_exit", model.TownExitPayload{})
|
|
if route := hm.RoutePayload(); route != nil {
|
|
sender.SendToHero(heroID, "route_assigned", route)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if now.Before(ex.WanderNextAt) {
|
|
break
|
|
}
|
|
hpFrac := 1.0
|
|
if hm.Hero != nil && hm.Hero.MaxHP > 0 {
|
|
hpFrac = float64(hm.Hero.HP) / float64(hm.Hero.MaxHP)
|
|
}
|
|
th := cfg.TownRestHpThreshold
|
|
if th <= 0 {
|
|
th = tuning.DefaultValues().TownRestHpThreshold
|
|
}
|
|
rch := cfg.TownRestChance
|
|
if rch <= 0 {
|
|
rch = tuning.DefaultValues().TownRestChance
|
|
}
|
|
if rch > 1 {
|
|
rch = 1
|
|
}
|
|
if hpFrac < th && rand.Float64() < rch && !ex.TownExitPending {
|
|
minR := cfg.TownTourRestMinMs
|
|
maxR := cfg.TownTourRestMaxMs
|
|
if minR <= 0 {
|
|
minR = tuning.DefaultValues().TownTourRestMinMs
|
|
}
|
|
if maxR <= 0 {
|
|
maxR = tuning.DefaultValues().TownTourRestMaxMs
|
|
}
|
|
ex.TownTourPhase = string(model.TownTourPhaseRest)
|
|
ex.TownRestUntil = now.Add(randomDurationBetweenMs(minR, maxR))
|
|
ex.AttractorSet = false
|
|
break
|
|
}
|
|
scheduleTownTourWanderRetarget(hm, now)
|
|
pickTownTourWanderAttractor(hm, graph, now)
|
|
|
|
case model.TownTourPhaseNpcApproach:
|
|
arrived := hm.stepTowardWorldPoint(dt, ex.TownTourStandX, ex.TownTourStandY, walkSpeed)
|
|
if !arrived {
|
|
break
|
|
}
|
|
npc, ok := graph.NPCByID[ex.TownTourNpcID]
|
|
if !ok {
|
|
transitionTownTourToWander(hm, graph, now)
|
|
break
|
|
}
|
|
if sender != nil {
|
|
// Online: welcome + dialog.
|
|
ex.TownTourPhase = string(model.TownTourPhaseNpcWelcome)
|
|
welcomeMs := cfg.TownWelcomeDurationMs
|
|
if welcomeMs <= 0 {
|
|
welcomeMs = tuning.DefaultValues().TownWelcomeDurationMs
|
|
}
|
|
ex.TownWelcomeUntil = now.Add(time.Duration(welcomeMs) * time.Millisecond)
|
|
hm.TownVisitNPCName = npc.Name
|
|
hm.TownVisitNPCKey = npc.NameKey
|
|
hm.TownVisitNPCType = npc.Type
|
|
hm.TownVisitStartedAt = now
|
|
hm.TownVisitLogsEmitted = 0
|
|
townNameKey := ""
|
|
if tt := graph.Towns[hm.CurrentTownID]; tt != nil {
|
|
townNameKey = tt.NameKey
|
|
}
|
|
sender.SendToHero(heroID, "town_npc_visit", model.TownNPCVisitPayload{
|
|
NPCID: npc.ID, Name: npc.Name, NameKey: npc.NameKey, Type: npc.Type, TownID: hm.CurrentTownID,
|
|
TownNameKey: townNameKey,
|
|
WorldX: ex.TownTourStandX, WorldY: ex.TownTourStandY,
|
|
})
|
|
legacyMerchantSell := model.IsGearVendorType(npc.Type)
|
|
if legacyMerchantSell {
|
|
share := cfg.MerchantTownAutoSellShare
|
|
if share <= 0 || share > 1 {
|
|
share = tuning.DefaultValues().MerchantTownAutoSellShare
|
|
}
|
|
soldItems, soldGold := AutoSellRandomInventoryShare(hm.Hero, share, nil)
|
|
if soldItems > 0 && adventureLog != nil {
|
|
adventureLog(heroID, model.AdventureLogLine{
|
|
Event: &model.AdventureLogEvent{
|
|
Code: model.LogPhraseSoldItemsMerchant,
|
|
Args: map[string]any{"count": soldItems, "npcKey": npc.NameKey, "gold": soldGold},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
emitTownNPCVisitLogs(heroID, hm, now, adventureLog)
|
|
} else {
|
|
if townTourOffline != nil {
|
|
townTourOffline(heroID, hm, graph, npc, now, adventureLog)
|
|
}
|
|
transitionTownTourToWander(hm, graph, now)
|
|
}
|
|
|
|
case model.TownTourPhaseNpcWelcome:
|
|
emitTownNPCVisitLogs(heroID, hm, now, adventureLog)
|
|
if !ex.TownWelcomeUntil.IsZero() && !now.Before(ex.TownWelcomeUntil) && !ex.TownTourDialogOpen {
|
|
transitionTownTourToWander(hm, graph, now)
|
|
break
|
|
}
|
|
|
|
case model.TownTourPhaseNpcService:
|
|
emitTownNPCVisitLogs(heroID, hm, now, adventureLog)
|
|
svcMs := cfg.TownServiceMaxMs
|
|
if svcMs <= 0 {
|
|
svcMs = tuning.DefaultValues().TownServiceMaxMs
|
|
}
|
|
if !ex.TownServiceUntil.IsZero() && !now.Before(ex.TownServiceUntil) {
|
|
if sender != nil {
|
|
sender.SendToHero(heroID, "town_tour_service_end", model.TownTourServiceEndPayload{Reason: "timeout"})
|
|
}
|
|
transitionTownTourToWander(hm, graph, now)
|
|
break
|
|
}
|
|
|
|
case model.TownTourPhaseRest:
|
|
hm.applyTownRestHeal(dt)
|
|
if !ex.TownRestUntil.IsZero() && now.After(ex.TownRestUntil) {
|
|
ex.TownTourPhase = string(model.TownTourPhaseWander)
|
|
ex.TownRestUntil = time.Time{}
|
|
scheduleTownTourWanderRetarget(hm, now)
|
|
pickTownTourWanderAttractor(hm, graph, now)
|
|
}
|
|
}
|
|
|
|
sig := townTourWireSig(hm)
|
|
if sender != nil && hm.Excursion.Kind == model.ExcursionKindTown {
|
|
if sig != hm.sentTownTourWireSig {
|
|
hm.sentTownTourWireSig = sig
|
|
sendTownTourUpdate(sender, heroID, hm, graph)
|
|
}
|
|
sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
|
|
}
|
|
hm.SyncToHero()
|
|
}
|
|
|
|
// Town tour client command handlers (engine calls under lock).
|
|
func (hm *HeroMovement) townTourNPCDialogClosed(now time.Time, graph *RoadGraph) {
|
|
if hm == nil || graph == nil {
|
|
return
|
|
}
|
|
if hm.Excursion.Kind != model.ExcursionKindTown {
|
|
return
|
|
}
|
|
ex := &hm.Excursion
|
|
ex.TownTourDialogOpen = false
|
|
hm.TownNPCUILock = ex.TownTourDialogOpen || ex.TownTourInteractionOpen
|
|
switch model.TownTourPhase(ex.TownTourPhase) {
|
|
case model.TownTourPhaseNpcWelcome:
|
|
transitionTownTourToWander(hm, graph, now)
|
|
case model.TownTourPhaseNpcService:
|
|
if !ex.TownTourInteractionOpen {
|
|
transitionTownTourToWander(hm, graph, now)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (hm *HeroMovement) townTourNPCInteractionOpened(now time.Time, graph *RoadGraph) {
|
|
if hm == nil || graph == nil {
|
|
return
|
|
}
|
|
if hm.Excursion.Kind != model.ExcursionKindTown {
|
|
return
|
|
}
|
|
ex := &hm.Excursion
|
|
ex.TownTourInteractionOpen = true
|
|
hm.TownNPCUILock = ex.TownTourDialogOpen || ex.TownTourInteractionOpen
|
|
if model.TownTourPhase(ex.TownTourPhase) == model.TownTourPhaseNpcWelcome {
|
|
ex.TownTourPhase = string(model.TownTourPhaseNpcService)
|
|
svcMs := tuning.Get().TownServiceMaxMs
|
|
if svcMs <= 0 {
|
|
svcMs = tuning.DefaultValues().TownServiceMaxMs
|
|
}
|
|
ex.TownServiceUntil = now.Add(time.Duration(svcMs) * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func (hm *HeroMovement) townTourNPCInteractionClosed(now time.Time, graph *RoadGraph) {
|
|
if hm == nil || graph == nil {
|
|
return
|
|
}
|
|
if hm.Excursion.Kind != model.ExcursionKindTown {
|
|
return
|
|
}
|
|
ex := &hm.Excursion
|
|
ex.TownTourInteractionOpen = false
|
|
hm.TownNPCUILock = ex.TownTourDialogOpen || ex.TownTourInteractionOpen
|
|
if model.TownTourPhase(ex.TownTourPhase) == model.TownTourPhaseNpcService {
|
|
transitionTownTourToWander(hm, graph, now)
|
|
}
|
|
}
|
|
|
|
func (hm *HeroMovement) townTourSetDialogOpen(open bool) {
|
|
if hm == nil || hm.Excursion.Kind != model.ExcursionKindTown {
|
|
return
|
|
}
|
|
hm.Excursion.TownTourDialogOpen = open
|
|
hm.TownNPCUILock = hm.Excursion.TownTourDialogOpen || hm.Excursion.TownTourInteractionOpen
|
|
}
|