|
|
|
@ -14,8 +14,7 @@ const (
|
|
|
|
// townNPCVisitLogLines is how many log lines to emit per NPC visit.
|
|
|
|
// townNPCVisitLogLines is how many log lines to emit per NPC visit.
|
|
|
|
townNPCVisitLogLines = 6
|
|
|
|
townNPCVisitLogLines = 6
|
|
|
|
|
|
|
|
|
|
|
|
restKindTown = "town"
|
|
|
|
restKindTown = "town"
|
|
|
|
restKindRoadside = "roadside"
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func movementTickRate() time.Duration {
|
|
|
|
func movementTickRate() time.Duration {
|
|
|
|
@ -76,24 +75,6 @@ type HeroMovement struct {
|
|
|
|
// TownLeaveAt: after all town NPCs are visited (queue empty), leave only once now >= TownLeaveAt (TownNPCVisitTownPause).
|
|
|
|
// TownLeaveAt: after all town NPCs are visited (queue empty), leave only once now >= TownLeaveAt (TownNPCVisitTownPause).
|
|
|
|
TownLeaveAt time.Time
|
|
|
|
TownLeaveAt time.Time
|
|
|
|
|
|
|
|
|
|
|
|
// Off-road excursion ("looking for trouble"): timers not persisted; cleared on town enter and when it ends.
|
|
|
|
|
|
|
|
AdventureStartAt time.Time
|
|
|
|
|
|
|
|
AdventureEndAt time.Time
|
|
|
|
|
|
|
|
AdventureSide int // +1 or -1 perpendicular direction while adventuring; 0 = not adventuring
|
|
|
|
|
|
|
|
// AdventureWanderX/Y: small display-only random drift while adventuring (reset when adventure ends).
|
|
|
|
|
|
|
|
AdventureWanderX float64
|
|
|
|
|
|
|
|
AdventureWanderY float64
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Roadside rest (low HP): unified under StateResting with a roadside flag; persisted in heroes.town_pause.
|
|
|
|
|
|
|
|
// RoadsideRestActive indicates "resting on roadside" flavor inside the unified resting state.
|
|
|
|
|
|
|
|
RoadsideRestActive bool
|
|
|
|
|
|
|
|
RoadsideRestEndAt time.Time
|
|
|
|
|
|
|
|
RoadsideRestStartedAt time.Time // wall time when this roadside session began (approach / return animation)
|
|
|
|
|
|
|
|
RoadsideRestSide int // +1 / -1 perpendicular; 0 = not resting
|
|
|
|
|
|
|
|
RoadsideRestNextLog time.Time
|
|
|
|
|
|
|
|
// Accumulates fractional roadside regen between ticks.
|
|
|
|
|
|
|
|
RoadsideRestHealRemainder float64
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Accumulates fractional town-rest regen between ticks.
|
|
|
|
// Accumulates fractional town-rest regen between ticks.
|
|
|
|
TownRestHealRemainder float64
|
|
|
|
TownRestHealRemainder float64
|
|
|
|
|
|
|
|
|
|
|
|
@ -427,13 +408,6 @@ func (hm *HeroMovement) ShiftGameDeadlines(d time.Duration, now time.Time) {
|
|
|
|
hm.NextTownNPCRollAt = shift(hm.NextTownNPCRollAt)
|
|
|
|
hm.NextTownNPCRollAt = shift(hm.NextTownNPCRollAt)
|
|
|
|
hm.TownVisitStartedAt = shift(hm.TownVisitStartedAt)
|
|
|
|
hm.TownVisitStartedAt = shift(hm.TownVisitStartedAt)
|
|
|
|
hm.TownLeaveAt = shift(hm.TownLeaveAt)
|
|
|
|
hm.TownLeaveAt = shift(hm.TownLeaveAt)
|
|
|
|
hm.AdventureStartAt = shift(hm.AdventureStartAt)
|
|
|
|
|
|
|
|
hm.AdventureEndAt = shift(hm.AdventureEndAt)
|
|
|
|
|
|
|
|
if hm.RoadsideRestActive {
|
|
|
|
|
|
|
|
hm.RoadsideRestEndAt = shift(hm.RoadsideRestEndAt)
|
|
|
|
|
|
|
|
hm.RoadsideRestStartedAt = shift(hm.RoadsideRestStartedAt)
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = shift(hm.RoadsideRestNextLog)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.WanderingMerchantDeadline = shift(hm.WanderingMerchantDeadline)
|
|
|
|
hm.WanderingMerchantDeadline = shift(hm.WanderingMerchantDeadline)
|
|
|
|
hm.LastMoveTick = now
|
|
|
|
hm.LastMoveTick = now
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -461,21 +435,6 @@ func (hm *HeroMovement) AdvanceTick(now time.Time, graph *RoadGraph) (reachedTow
|
|
|
|
hm.refreshSpeed(now)
|
|
|
|
hm.refreshSpeed(now)
|
|
|
|
distThisTick := hm.Speed * dt
|
|
|
|
distThisTick := hm.Speed * dt
|
|
|
|
|
|
|
|
|
|
|
|
var wAdv float64
|
|
|
|
|
|
|
|
if hm.adventureActive(now) {
|
|
|
|
|
|
|
|
wAdv = hm.wildernessFactor(now)
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
frac := cfg.AdventureForwardSpeedWildFraction
|
|
|
|
|
|
|
|
if frac < 0 {
|
|
|
|
|
|
|
|
frac = 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if frac > 1 {
|
|
|
|
|
|
|
|
frac = 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// w=0: full road speed; w=1: frac of road speed (exploring, not rushing to town).
|
|
|
|
|
|
|
|
distThisTick *= (1-wAdv) + wAdv*frac
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for distThisTick > 0 && hm.WaypointIndex < len(hm.Road.Waypoints)-1 {
|
|
|
|
for distThisTick > 0 && hm.WaypointIndex < len(hm.Road.Waypoints)-1 {
|
|
|
|
from := hm.Road.Waypoints[hm.WaypointIndex]
|
|
|
|
from := hm.Road.Waypoints[hm.WaypointIndex]
|
|
|
|
to := hm.Road.Waypoints[hm.WaypointIndex+1]
|
|
|
|
to := hm.Road.Waypoints[hm.WaypointIndex+1]
|
|
|
|
@ -545,90 +504,6 @@ func (hm *HeroMovement) TargetPoint() (float64, float64) {
|
|
|
|
return wp.X, wp.Y
|
|
|
|
return wp.X, wp.Y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) adventureActive(now time.Time) bool {
|
|
|
|
|
|
|
|
return !hm.AdventureStartAt.IsZero() && now.Before(hm.AdventureEndAt)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) expireAdventureIfNeeded(now time.Time) {
|
|
|
|
|
|
|
|
if hm.AdventureEndAt.IsZero() {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if now.Before(hm.AdventureEndAt) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.AdventureStartAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureEndAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureSide = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderX = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderY = 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) roadsideRestInProgress() bool {
|
|
|
|
|
|
|
|
return hm.State == model.StateResting && hm.RoadsideRestActive
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) endRoadsideRest() {
|
|
|
|
|
|
|
|
wasActive := hm.RoadsideRestActive
|
|
|
|
|
|
|
|
hm.RoadsideRestActive = false
|
|
|
|
|
|
|
|
hm.RoadsideRestEndAt = time.Time{}
|
|
|
|
|
|
|
|
hm.RoadsideRestStartedAt = time.Time{}
|
|
|
|
|
|
|
|
hm.RoadsideRestSide = 0
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = time.Time{}
|
|
|
|
|
|
|
|
hm.RoadsideRestHealRemainder = 0
|
|
|
|
|
|
|
|
if wasActive && hm.State == model.StateResting {
|
|
|
|
|
|
|
|
hm.State = model.StateWalking
|
|
|
|
|
|
|
|
if hm.Hero != nil {
|
|
|
|
|
|
|
|
hm.Hero.State = model.StateWalking
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if wasActive {
|
|
|
|
|
|
|
|
hm.RestUntil = time.Time{}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// EndRoadsideRest ends pull-over roadside rest (no-op if not active).
|
|
|
|
|
|
|
|
func (hm *HeroMovement) EndRoadsideRest() {
|
|
|
|
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// beginRoadsideRestSession starts a roadside session until endAt. Does not clear an active adventure timer
|
|
|
|
|
|
|
|
// so low-HP pull-over during a mini-adventure resumes the same excursion after rest.
|
|
|
|
|
|
|
|
func (hm *HeroMovement) beginRoadsideRestSession(now, endAt time.Time) {
|
|
|
|
|
|
|
|
hm.RoadsideRestActive = true
|
|
|
|
|
|
|
|
hm.RoadsideRestEndAt = endAt
|
|
|
|
|
|
|
|
hm.RoadsideRestStartedAt = now
|
|
|
|
|
|
|
|
hm.RestUntil = endAt
|
|
|
|
|
|
|
|
hm.State = model.StateResting
|
|
|
|
|
|
|
|
if hm.Hero != nil {
|
|
|
|
|
|
|
|
hm.Hero.State = model.StateResting
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.RoadsideRestHealRemainder = 0
|
|
|
|
|
|
|
|
hm.TownRestHealRemainder = 0
|
|
|
|
|
|
|
|
if rand.Float64() < 0.5 {
|
|
|
|
|
|
|
|
hm.RoadsideRestSide = 1
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.RoadsideRestSide = -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = now.Add(randomRoadsideRestThoughtDelay())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) applyRoadsideRestHeal(dt float64) {
|
|
|
|
|
|
|
|
if dt <= 0 || hm.Hero == nil || hm.Hero.MaxHP <= 0 {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
rawGain := float64(hm.Hero.MaxHP)*cfg.RoadsideRestHPPerS*dt + hm.RoadsideRestHealRemainder
|
|
|
|
|
|
|
|
gain := int(math.Floor(rawGain))
|
|
|
|
|
|
|
|
hm.RoadsideRestHealRemainder = rawGain - float64(gain)
|
|
|
|
|
|
|
|
if gain <= 0 {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.Hero.HP += gain
|
|
|
|
|
|
|
|
if hm.Hero.HP > hm.Hero.MaxHP {
|
|
|
|
|
|
|
|
hm.Hero.HP = hm.Hero.MaxHP
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) applyTownRestHeal(dt float64) {
|
|
|
|
func (hm *HeroMovement) applyTownRestHeal(dt float64) {
|
|
|
|
if dt <= 0 || hm.Hero == nil || hm.Hero.MaxHP <= 0 {
|
|
|
|
if dt <= 0 || hm.Hero == nil || hm.Hero.MaxHP <= 0 {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
@ -646,128 +521,6 @@ func (hm *HeroMovement) applyTownRestHeal(dt float64) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// tryStartRoadsideRest pulls the hero off the road when HP is low; an active adventure timer keeps running.
|
|
|
|
|
|
|
|
func (hm *HeroMovement) tryStartRoadsideRest(now time.Time) {
|
|
|
|
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
if hm.Hero == nil || hm.Hero.MaxHP <= 0 {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if float64(hm.Hero.HP)/float64(hm.Hero.MaxHP) > cfg.LowHPThreshold {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
restMin := time.Duration(cfg.RoadsideRestMinMs) * time.Millisecond
|
|
|
|
|
|
|
|
restMax := time.Duration(cfg.RoadsideRestMaxMs) * time.Millisecond
|
|
|
|
|
|
|
|
spanNs := (restMax - restMin).Nanoseconds()
|
|
|
|
|
|
|
|
if spanNs < 1 {
|
|
|
|
|
|
|
|
spanNs = 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
endAt := now.Add(restMin + time.Duration(rand.Int63n(spanNs+1)))
|
|
|
|
|
|
|
|
hm.beginRoadsideRestSession(now, endAt)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func randomRoadsideRestThoughtDelay() time.Duration {
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
minDelay := time.Duration(cfg.RoadsideThoughtMinMs) * time.Millisecond
|
|
|
|
|
|
|
|
maxDelay := time.Duration(cfg.RoadsideThoughtMaxMs) * time.Millisecond
|
|
|
|
|
|
|
|
span := maxDelay - minDelay
|
|
|
|
|
|
|
|
if span < 0 {
|
|
|
|
|
|
|
|
span = 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return minDelay + time.Duration(rand.Int63n(int64(span)+1))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// emitRoadsideRestThoughts appends occasional journal lines while the hero rests off the road.
|
|
|
|
|
|
|
|
func emitRoadsideRestThoughts(heroID int64, hm *HeroMovement, now time.Time, log AdventureLogWriter) {
|
|
|
|
|
|
|
|
if log == nil || !hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.RoadsideRestNextLog.IsZero() {
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = now.Add(randomRoadsideRestThoughtDelay())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if now.Before(hm.RoadsideRestNextLog) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
log(heroID, randomRoadsideRestThought())
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = now.Add(randomRoadsideRestThoughtDelay())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// tryStartAdventure begins a timed off-road excursion with small probability.
|
|
|
|
|
|
|
|
func (hm *HeroMovement) tryStartAdventure(now time.Time) {
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
if hm.adventureActive(now) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if rand.Float64() >= cfg.StartAdventurePerTick {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.AdventureStartAt = now
|
|
|
|
|
|
|
|
minDur := time.Duration(cfg.AdventureDurationMinMs) * time.Millisecond
|
|
|
|
|
|
|
|
maxDur := time.Duration(cfg.AdventureDurationMaxMs) * time.Millisecond
|
|
|
|
|
|
|
|
spanNs := (maxDur - minDur).Nanoseconds()
|
|
|
|
|
|
|
|
if spanNs < 1 {
|
|
|
|
|
|
|
|
spanNs = 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.AdventureEndAt = now.Add(minDur + time.Duration(rand.Int63n(spanNs+1)))
|
|
|
|
|
|
|
|
hm.AdventureWanderX = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderY = 0
|
|
|
|
|
|
|
|
if rand.Float64() < 0.5 {
|
|
|
|
|
|
|
|
hm.AdventureSide = 1
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.AdventureSide = -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// StartAdventureForced starts an off-road adventure immediately (admin).
|
|
|
|
|
|
|
|
func (hm *HeroMovement) StartAdventureForced(now time.Time) bool {
|
|
|
|
|
|
|
|
if hm.Hero == nil || hm.Hero.State == model.StateDead || hm.Hero.HP <= 0 {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.State != model.StateWalking {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.adventureActive(now) {
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
minDur := time.Duration(cfg.AdventureDurationMinMs) * time.Millisecond
|
|
|
|
|
|
|
|
maxDur := time.Duration(cfg.AdventureDurationMaxMs) * time.Millisecond
|
|
|
|
|
|
|
|
spanNs := (maxDur - minDur).Nanoseconds()
|
|
|
|
|
|
|
|
if spanNs < 1 {
|
|
|
|
|
|
|
|
spanNs = 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.AdventureStartAt = now
|
|
|
|
|
|
|
|
hm.AdventureEndAt = now.Add(minDur + time.Duration(rand.Int63n(spanNs+1)))
|
|
|
|
|
|
|
|
hm.AdventureWanderX = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderY = 0
|
|
|
|
|
|
|
|
if rand.Float64() < 0.5 {
|
|
|
|
|
|
|
|
hm.AdventureSide = 1
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.AdventureSide = -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ForceAdventureReturnToRoad snaps the adventure to the outward walk-back leg (same return duration as roadside rest).
|
|
|
|
|
|
|
|
func (hm *HeroMovement) ForceAdventureReturnToRoad(now time.Time) bool {
|
|
|
|
|
|
|
|
if !hm.adventureActive(now) {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
dtIn := time.Duration(cfg.RoadsideRestGoInMs) * time.Millisecond
|
|
|
|
|
|
|
|
dtOut := time.Duration(cfg.RoadsideRestReturnMs) * time.Millisecond
|
|
|
|
|
|
|
|
total := dtIn + dtOut
|
|
|
|
|
|
|
|
dtIn2, dtOut2 := roadsideRestPhaseDurations(total)
|
|
|
|
|
|
|
|
hm.AdventureEndAt = now.Add(dtOut2)
|
|
|
|
|
|
|
|
hm.AdventureStartAt = now.Add(-dtIn2)
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// AdminPlaceInTown moves the hero to a town center and applies EnterTown logic (NPC tour or rest).
|
|
|
|
// AdminPlaceInTown moves the hero to a town center and applies EnterTown logic (NPC tour or rest).
|
|
|
|
func (hm *HeroMovement) AdminPlaceInTown(graph *RoadGraph, townID int64, now time.Time) error {
|
|
|
|
func (hm *HeroMovement) AdminPlaceInTown(graph *RoadGraph, townID int64, now time.Time) error {
|
|
|
|
if graph == nil || townID == 0 {
|
|
|
|
if graph == nil || townID == 0 {
|
|
|
|
@ -781,12 +534,6 @@ func (hm *HeroMovement) AdminPlaceInTown(graph *RoadGraph, townID int64, now tim
|
|
|
|
hm.WaypointFraction = 0
|
|
|
|
hm.WaypointFraction = 0
|
|
|
|
hm.DestinationTownID = townID
|
|
|
|
hm.DestinationTownID = townID
|
|
|
|
hm.spawnAtRoadStart = false
|
|
|
|
hm.spawnAtRoadStart = false
|
|
|
|
hm.AdventureStartAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureEndAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureSide = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderX = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderY = 0
|
|
|
|
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
hm.TownVisitNPCName = ""
|
|
|
|
hm.TownVisitNPCName = ""
|
|
|
|
hm.TownVisitNPCType = ""
|
|
|
|
hm.TownVisitNPCType = ""
|
|
|
|
@ -808,12 +555,6 @@ func (hm *HeroMovement) AdminStartRest(now time.Time, graph *RoadGraph) bool {
|
|
|
|
if hm.State == model.StateFighting {
|
|
|
|
if hm.State == model.StateFighting {
|
|
|
|
return false
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
hm.AdventureStartAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureEndAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureSide = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderX = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderY = 0
|
|
|
|
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
hm.TownNPCQueue = nil
|
|
|
|
hm.TownNPCQueue = nil
|
|
|
|
hm.NextTownNPCRollAt = time.Time{}
|
|
|
|
hm.NextTownNPCRollAt = time.Time{}
|
|
|
|
@ -831,219 +572,6 @@ func (hm *HeroMovement) AdminStartRest(now time.Time, graph *RoadGraph) bool {
|
|
|
|
return true
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminStartRoadsideRest forces roadside rest while walking (ignores HP). Extends duration if already resting.
|
|
|
|
|
|
|
|
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.StateWalking {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
restMin := time.Duration(cfg.RoadsideRestMinMs) * time.Millisecond
|
|
|
|
|
|
|
|
restMax := time.Duration(cfg.RoadsideRestMaxMs) * time.Millisecond
|
|
|
|
|
|
|
|
spanNs := (restMax - restMin).Nanoseconds()
|
|
|
|
|
|
|
|
if spanNs < 1 {
|
|
|
|
|
|
|
|
spanNs = 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
endAt := now.Add(restMin + time.Duration(rand.Int63n(spanNs+1)))
|
|
|
|
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
hm.RoadsideRestEndAt = endAt
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.beginRoadsideRestSession(now, endAt)
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// adventureDepthFactor is 0 on the road, then smoothsteps in (RoadsideRestGoInMs), holds, then out before AdventureEndAt.
|
|
|
|
|
|
|
|
// Same timing and depth scale as roadside rest so "looking for trouble" pulls off the road as visibly as pull-over rest.
|
|
|
|
|
|
|
|
func (hm *HeroMovement) adventureDepthFactor(now time.Time) float64 {
|
|
|
|
|
|
|
|
if !hm.adventureActive(now) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
t0 := hm.AdventureStartAt
|
|
|
|
|
|
|
|
tEnd := hm.AdventureEndAt
|
|
|
|
|
|
|
|
if tEnd.IsZero() {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if !now.Before(tEnd) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if t0.IsZero() {
|
|
|
|
|
|
|
|
t0 = tEnd.Add(-365 * 24 * time.Hour)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
total := tEnd.Sub(t0)
|
|
|
|
|
|
|
|
if total <= 0 {
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
dtIn, dtOut := roadsideRestPhaseDurations(total)
|
|
|
|
|
|
|
|
if now.Before(t0) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtIn > 0 && now.Before(t0.Add(dtIn)) {
|
|
|
|
|
|
|
|
e := float64(now.Sub(t0)) / float64(dtIn)
|
|
|
|
|
|
|
|
return smoothstep01(e)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtOut > 0 && !now.Before(tEnd.Add(-dtOut)) {
|
|
|
|
|
|
|
|
e := float64(tEnd.Sub(now)) / float64(dtOut)
|
|
|
|
|
|
|
|
return smoothstep01(e)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// wildernessFactor matches adventureDepthFactor while an adventure is active (encounters, forward speed).
|
|
|
|
|
|
|
|
func (hm *HeroMovement) wildernessFactor(now time.Time) float64 {
|
|
|
|
|
|
|
|
return hm.adventureDepthFactor(now)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// stepAdventureWander applies a small bounded random drift in world space while off-road (display feel).
|
|
|
|
|
|
|
|
func (hm *HeroMovement) stepAdventureWander(now time.Time, dt float64) {
|
|
|
|
|
|
|
|
if !hm.adventureActive(now) || dt <= 0 || hm.State != model.StateWalking {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
w := hm.wildernessFactor(now)
|
|
|
|
|
|
|
|
if w <= 0 {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
twitch := cfg.AdventureWanderSpeedRatio
|
|
|
|
|
|
|
|
if twitch <= 0 {
|
|
|
|
|
|
|
|
twitch = tuning.DefaultValues().AdventureWanderSpeedRatio
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
step := hm.Speed * twitch * w * dt
|
|
|
|
|
|
|
|
hm.AdventureWanderX += (rand.Float64()*2 - 1) * step
|
|
|
|
|
|
|
|
hm.AdventureWanderY += (rand.Float64()*2 - 1) * step
|
|
|
|
|
|
|
|
maxR := cfg.AdventureWanderMaxRadius
|
|
|
|
|
|
|
|
if maxR <= 0 {
|
|
|
|
|
|
|
|
maxR = tuning.DefaultValues().AdventureWanderMaxRadius
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
r := math.Hypot(hm.AdventureWanderX, hm.AdventureWanderY)
|
|
|
|
|
|
|
|
if r > maxR && r > 1e-9 {
|
|
|
|
|
|
|
|
s := maxR / r
|
|
|
|
|
|
|
|
hm.AdventureWanderX *= s
|
|
|
|
|
|
|
|
hm.AdventureWanderY *= s
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func smoothstep01(t float64) float64 {
|
|
|
|
|
|
|
|
if t <= 0 {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if t >= 1 {
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return t * t * (3 - 2*t)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func roadsideRestPhaseDurations(total time.Duration) (time.Duration, time.Duration) {
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
dtIn := time.Duration(cfg.RoadsideRestGoInMs) * time.Millisecond
|
|
|
|
|
|
|
|
dtOut := time.Duration(cfg.RoadsideRestReturnMs) * time.Millisecond
|
|
|
|
|
|
|
|
if dtIn+dtOut > total {
|
|
|
|
|
|
|
|
r := float64(total) / float64(dtIn+dtOut)
|
|
|
|
|
|
|
|
dtIn = time.Duration(float64(dtIn) * r)
|
|
|
|
|
|
|
|
dtOut = time.Duration(float64(dtOut) * r)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtIn < 0 {
|
|
|
|
|
|
|
|
dtIn = 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtOut < 0 {
|
|
|
|
|
|
|
|
dtOut = 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtIn+dtOut > total {
|
|
|
|
|
|
|
|
dtIn = total / 2
|
|
|
|
|
|
|
|
dtOut = total - dtIn
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return dtIn, dtOut
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// roadsideRestDepthFactor is 0..1: 0 on road, 1 at the forest camp; animates in at session start and out before RestUntil.
|
|
|
|
|
|
|
|
func (hm *HeroMovement) roadsideRestDepthFactor(now time.Time) float64 {
|
|
|
|
|
|
|
|
if !hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
t0 := hm.RoadsideRestStartedAt
|
|
|
|
|
|
|
|
tEnd := hm.RoadsideRestEndAt
|
|
|
|
|
|
|
|
if tEnd.IsZero() {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if !now.Before(tEnd) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if t0.IsZero() {
|
|
|
|
|
|
|
|
// Legacy blob without start time: assume already deep in the woods until the final return window.
|
|
|
|
|
|
|
|
t0 = tEnd.Add(-365 * 24 * time.Hour)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
total := tEnd.Sub(t0)
|
|
|
|
|
|
|
|
if total <= 0 {
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
dtIn, dtOut := roadsideRestPhaseDurations(total)
|
|
|
|
|
|
|
|
if now.Before(t0) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtIn > 0 && now.Before(t0.Add(dtIn)) {
|
|
|
|
|
|
|
|
e := float64(now.Sub(t0)) / float64(dtIn)
|
|
|
|
|
|
|
|
return smoothstep01(e)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtOut > 0 && !now.Before(tEnd.Add(-dtOut)) {
|
|
|
|
|
|
|
|
e := float64(tEnd.Sub(now)) / float64(dtOut)
|
|
|
|
|
|
|
|
return smoothstep01(e)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// roadsideRestAtCamp returns true only during the "actual rest" plateau (after go-in, before return).
|
|
|
|
|
|
|
|
func (hm *HeroMovement) roadsideRestAtCamp(now time.Time) bool {
|
|
|
|
|
|
|
|
if !hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
tEnd := hm.RoadsideRestEndAt
|
|
|
|
|
|
|
|
if tEnd.IsZero() || !now.Before(tEnd) {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Legacy blob without start time: assume already at camp, but still reserve the final return window.
|
|
|
|
|
|
|
|
if hm.RoadsideRestStartedAt.IsZero() {
|
|
|
|
|
|
|
|
dtOut := time.Duration(tuning.Get().RoadsideRestReturnMs) * time.Millisecond
|
|
|
|
|
|
|
|
return dtOut <= 0 || now.Before(tEnd.Add(-dtOut))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
total := tEnd.Sub(hm.RoadsideRestStartedAt)
|
|
|
|
|
|
|
|
if total <= 0 {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
dtIn, dtOut := roadsideRestPhaseDurations(total)
|
|
|
|
|
|
|
|
if now.Before(hm.RoadsideRestStartedAt.Add(dtIn)) {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if dtOut > 0 && !now.Before(tEnd.Add(-dtOut)) {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func roadsideRestDepthWorldUnits() float64 {
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
if cfg.RoadsideRestDepthMax > 0 {
|
|
|
|
|
|
|
|
return cfg.RoadsideRestDepthMax
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return cfg.RoadsideRestLateral
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// adventureWildDepthWorldUnits is max perpendicular reach at full adventure depth: same base as roadside camp,
|
|
|
|
|
|
|
|
// scaled further into the wild (AdventureWildDepthScale).
|
|
|
|
|
|
|
|
func adventureWildDepthWorldUnits() float64 {
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
scale := cfg.AdventureWildDepthScale
|
|
|
|
|
|
|
|
if scale <= 0 {
|
|
|
|
|
|
|
|
scale = tuning.DefaultValues().AdventureWildDepthScale
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return roadsideRestDepthWorldUnits() * scale
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) roadPerpendicularUnit() (float64, float64) {
|
|
|
|
func (hm *HeroMovement) roadPerpendicularUnit() (float64, float64) {
|
|
|
|
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
|
|
|
|
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
|
|
|
|
return 0, 1
|
|
|
|
return 0, 1
|
|
|
|
@ -1090,28 +618,7 @@ func (hm *HeroMovement) roadForwardUnit() (float64, float64) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (hm *HeroMovement) displayOffset(now time.Time) (float64, float64) {
|
|
|
|
func (hm *HeroMovement) displayOffset(now time.Time) (float64, float64) {
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
_ = now
|
|
|
|
if hm.RoadsideRestSide == 0 {
|
|
|
|
|
|
|
|
return 0, 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
px, py := hm.roadPerpendicularUnit()
|
|
|
|
|
|
|
|
f := hm.roadsideRestDepthFactor(now)
|
|
|
|
|
|
|
|
mag := float64(hm.RoadsideRestSide) * roadsideRestDepthWorldUnits() * f
|
|
|
|
|
|
|
|
return px * mag, py * mag
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.adventureActive(now) && hm.AdventureSide != 0 && hm.Road != nil && len(hm.Road.Waypoints) >= 2 {
|
|
|
|
|
|
|
|
f := hm.adventureDepthFactor(now)
|
|
|
|
|
|
|
|
depth := adventureWildDepthWorldUnits()
|
|
|
|
|
|
|
|
cfg := tuning.Get()
|
|
|
|
|
|
|
|
if cfg.AdventureWildLateralMax > 0 {
|
|
|
|
|
|
|
|
if alt := cfg.AdventureWildLateralMax; alt > depth {
|
|
|
|
|
|
|
|
depth = alt
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
px, py := hm.roadPerpendicularUnit()
|
|
|
|
|
|
|
|
mag := float64(hm.AdventureSide) * depth * f
|
|
|
|
|
|
|
|
return px*mag + hm.AdventureWanderX, py*mag + hm.AdventureWanderY
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0, 0
|
|
|
|
return 0, 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1130,15 +637,11 @@ func (hm *HeroMovement) rollRoadEncounter(now time.Time) (monster bool, enemy mo
|
|
|
|
if now.Sub(hm.LastEncounterAt) < time.Duration(cfg.EncounterCooldownBaseMs)*time.Millisecond {
|
|
|
|
if now.Sub(hm.LastEncounterAt) < time.Duration(cfg.EncounterCooldownBaseMs)*time.Millisecond {
|
|
|
|
return false, model.Enemy{}, false
|
|
|
|
return false, model.Enemy{}, false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w := hm.wildernessFactor(now)
|
|
|
|
if rand.Float64() >= cfg.EncounterActivityBase {
|
|
|
|
// More encounter checks on the road; still ramps up further from the road.
|
|
|
|
|
|
|
|
activity := cfg.EncounterActivityBase * (0.62 + 0.38*w)
|
|
|
|
|
|
|
|
if rand.Float64() >= activity {
|
|
|
|
|
|
|
|
return false, model.Enemy{}, false
|
|
|
|
return false, model.Enemy{}, false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// On the road (w=0): mostly monsters, merchants occasional. Deep off-road: almost only monsters.
|
|
|
|
monsterW := cfg.MonsterEncounterWeightBase
|
|
|
|
monsterW := cfg.MonsterEncounterWeightBase + cfg.MonsterEncounterWeightWildBonus*w*w
|
|
|
|
merchantW := cfg.MerchantEncounterWeightBase + cfg.MerchantEncounterWeightRoadBonus
|
|
|
|
merchantW := cfg.MerchantEncounterWeightBase + cfg.MerchantEncounterWeightRoadBonus*(1-w)*(1-w)
|
|
|
|
|
|
|
|
total := monsterW + merchantW
|
|
|
|
total := monsterW + merchantW
|
|
|
|
r := rand.Float64() * total
|
|
|
|
r := rand.Float64() * total
|
|
|
|
if r < monsterW {
|
|
|
|
if r < monsterW {
|
|
|
|
@ -1163,19 +666,12 @@ func (hm *HeroMovement) EnterTown(now time.Time, graph *RoadGraph) {
|
|
|
|
hm.TownVisitLogsEmitted = 0
|
|
|
|
hm.TownVisitLogsEmitted = 0
|
|
|
|
hm.TownLeaveAt = time.Time{}
|
|
|
|
hm.TownLeaveAt = time.Time{}
|
|
|
|
hm.TownRestHealRemainder = 0
|
|
|
|
hm.TownRestHealRemainder = 0
|
|
|
|
hm.AdventureStartAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureEndAt = time.Time{}
|
|
|
|
|
|
|
|
hm.AdventureSide = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderX = 0
|
|
|
|
|
|
|
|
hm.AdventureWanderY = 0
|
|
|
|
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ids := graph.TownNPCIDs(destID)
|
|
|
|
ids := graph.TownNPCIDs(destID)
|
|
|
|
if len(ids) == 0 {
|
|
|
|
if len(ids) == 0 {
|
|
|
|
hm.State = model.StateResting
|
|
|
|
hm.State = model.StateResting
|
|
|
|
hm.Hero.State = model.StateResting
|
|
|
|
hm.Hero.State = model.StateResting
|
|
|
|
hm.RestUntil = now.Add(randomRestDuration())
|
|
|
|
hm.RestUntil = now.Add(randomRestDuration())
|
|
|
|
hm.RoadsideRestActive = false
|
|
|
|
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1199,7 +695,6 @@ func (hm *HeroMovement) LeaveTown(graph *RoadGraph, now time.Time) {
|
|
|
|
hm.TownLeaveAt = time.Time{}
|
|
|
|
hm.TownLeaveAt = time.Time{}
|
|
|
|
hm.TownRestHealRemainder = 0
|
|
|
|
hm.TownRestHealRemainder = 0
|
|
|
|
hm.RestUntil = time.Time{}
|
|
|
|
hm.RestUntil = time.Time{}
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
hm.State = model.StateWalking
|
|
|
|
hm.State = model.StateWalking
|
|
|
|
hm.Hero.State = model.StateWalking
|
|
|
|
hm.Hero.State = model.StateWalking
|
|
|
|
// Prevent a huge movement step on the first tick after town: AdvanceTick uses now - LastMoveTick.
|
|
|
|
// Prevent a huge movement step on the first tick after town: AdvanceTick uses now - LastMoveTick.
|
|
|
|
@ -1223,7 +718,6 @@ func randomTownNPCDelay() time.Duration {
|
|
|
|
// StartFighting pauses movement for combat.
|
|
|
|
// StartFighting pauses movement for combat.
|
|
|
|
func (hm *HeroMovement) StartFighting() {
|
|
|
|
func (hm *HeroMovement) StartFighting() {
|
|
|
|
hm.State = model.StateFighting
|
|
|
|
hm.State = model.StateFighting
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
hm.WanderingMerchantDeadline = time.Time{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -1237,7 +731,6 @@ func (hm *HeroMovement) ResumeWalking(now time.Time) {
|
|
|
|
// Die sets the movement state to dead.
|
|
|
|
// Die sets the movement state to dead.
|
|
|
|
func (hm *HeroMovement) Die() {
|
|
|
|
func (hm *HeroMovement) Die() {
|
|
|
|
hm.State = model.StateDead
|
|
|
|
hm.State = model.StateDead
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SyncToHero writes movement state back to the hero model for persistence.
|
|
|
|
// SyncToHero writes movement state back to the hero model for persistence.
|
|
|
|
@ -1263,11 +756,7 @@ func (hm *HeroMovement) SyncToHero() {
|
|
|
|
hm.Hero.MoveState = string(hm.State)
|
|
|
|
hm.Hero.MoveState = string(hm.State)
|
|
|
|
hm.Hero.RestKind = ""
|
|
|
|
hm.Hero.RestKind = ""
|
|
|
|
if hm.State == model.StateResting {
|
|
|
|
if hm.State == model.StateResting {
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
hm.Hero.RestKind = restKindTown
|
|
|
|
hm.Hero.RestKind = restKindRoadside
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.Hero.RestKind = restKindTown
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hm.Hero.TownPause = hm.townPauseBlob()
|
|
|
|
hm.Hero.TownPause = hm.townPauseBlob()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -1280,26 +769,9 @@ func (hm *HeroMovement) townPauseBlob() *model.TownPausePersisted {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
t := hm.RestUntil
|
|
|
|
t := hm.RestUntil
|
|
|
|
p := &model.TownPausePersisted{
|
|
|
|
p := &model.TownPausePersisted{
|
|
|
|
RestUntil: &t,
|
|
|
|
RestUntil: &t,
|
|
|
|
TownRestHealRemainder: hm.TownRestHealRemainder,
|
|
|
|
RestKind: restKindTown,
|
|
|
|
RoadsideRestHealRemainder: hm.RoadsideRestHealRemainder,
|
|
|
|
TownRestHealRemainder: hm.TownRestHealRemainder,
|
|
|
|
}
|
|
|
|
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
p.RestKind = restKindRoadside
|
|
|
|
|
|
|
|
p.RoadsideRestActive = true
|
|
|
|
|
|
|
|
end := hm.RoadsideRestEndAt
|
|
|
|
|
|
|
|
p.RoadsideRestEndAt = &end
|
|
|
|
|
|
|
|
p.RoadsideRestSide = hm.RoadsideRestSide
|
|
|
|
|
|
|
|
if !hm.RoadsideRestStartedAt.IsZero() {
|
|
|
|
|
|
|
|
ts := hm.RoadsideRestStartedAt
|
|
|
|
|
|
|
|
p.RoadsideRestStartedAt = &ts
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if !hm.RoadsideRestNextLog.IsZero() {
|
|
|
|
|
|
|
|
tNext := hm.RoadsideRestNextLog
|
|
|
|
|
|
|
|
p.RoadsideRestNextLog = &tNext
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
p.RestKind = restKindTown
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return p
|
|
|
|
return p
|
|
|
|
case model.StateInTown:
|
|
|
|
case model.StateInTown:
|
|
|
|
@ -1336,37 +808,6 @@ func (hm *HeroMovement) applyTownPauseFromHero(hero *model.Hero, now time.Time)
|
|
|
|
if blob != nil && blob.RestUntil != nil && !blob.RestUntil.IsZero() {
|
|
|
|
if blob != nil && blob.RestUntil != nil && !blob.RestUntil.IsZero() {
|
|
|
|
hm.RestUntil = *blob.RestUntil
|
|
|
|
hm.RestUntil = *blob.RestUntil
|
|
|
|
hm.TownRestHealRemainder = blob.TownRestHealRemainder
|
|
|
|
hm.TownRestHealRemainder = blob.TownRestHealRemainder
|
|
|
|
hm.RoadsideRestHealRemainder = blob.RoadsideRestHealRemainder
|
|
|
|
|
|
|
|
restKind := blob.RestKind
|
|
|
|
|
|
|
|
if restKind == "" && (blob.RoadsideRestActive || (blob.RoadsideRestEndAt != nil && !blob.RoadsideRestEndAt.IsZero())) {
|
|
|
|
|
|
|
|
restKind = restKindRoadside
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if restKind == restKindRoadside {
|
|
|
|
|
|
|
|
hm.RoadsideRestActive = true
|
|
|
|
|
|
|
|
if blob.RoadsideRestEndAt != nil && !blob.RoadsideRestEndAt.IsZero() {
|
|
|
|
|
|
|
|
hm.RoadsideRestEndAt = *blob.RoadsideRestEndAt
|
|
|
|
|
|
|
|
hm.RestUntil = hm.RoadsideRestEndAt
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.RoadsideRestEndAt = hm.RestUntil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if blob.RoadsideRestSide == 0 {
|
|
|
|
|
|
|
|
if rand.Float64() < 0.5 {
|
|
|
|
|
|
|
|
hm.RoadsideRestSide = 1
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.RoadsideRestSide = -1
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.RoadsideRestSide = blob.RoadsideRestSide
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if blob.RoadsideRestNextLog != nil && !blob.RoadsideRestNextLog.IsZero() {
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = *blob.RoadsideRestNextLog
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.RoadsideRestNextLog = now.Add(randomRoadsideRestThoughtDelay())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if blob.RoadsideRestStartedAt != nil && !blob.RoadsideRestStartedAt.IsZero() {
|
|
|
|
|
|
|
|
hm.RoadsideRestStartedAt = *blob.RoadsideRestStartedAt
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Legacy row without town_pause: treat rest as already elapsed so offline/ reconnect unblocks.
|
|
|
|
// Legacy row without town_pause: treat rest as already elapsed so offline/ reconnect unblocks.
|
|
|
|
@ -1551,8 +992,7 @@ func townNPCVisitLogMessage(npcType, npcName string, lineIndex int) string {
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// sender may be nil to suppress all WebSocket payloads (offline ticks).
|
|
|
|
// sender may be nil to suppress all WebSocket payloads (offline ticks).
|
|
|
|
// onEncounter is required for walking encounter rolls; if nil, encounters are not triggered.
|
|
|
|
// onEncounter is required for walking encounter rolls; if nil, encounters are not triggered.
|
|
|
|
// adventureLog may be nil; when set, town NPC visits append timed lines (per NPC narration block),
|
|
|
|
// adventureLog may be nil; when set, town NPC visits append timed lines (per NPC narration block).
|
|
|
|
// and roadside rest emits occasional thoughts.
|
|
|
|
|
|
|
|
// persistAfterTownEnter, if non-nil, is invoked after SyncToHero when the hero has just reached a town.
|
|
|
|
// persistAfterTownEnter, if non-nil, is invoked after SyncToHero when the hero has just reached a town.
|
|
|
|
func ProcessSingleHeroMovementTick(
|
|
|
|
func ProcessSingleHeroMovementTick(
|
|
|
|
heroID int64,
|
|
|
|
heroID int64,
|
|
|
|
@ -1574,38 +1014,19 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
case model.StateResting:
|
|
|
|
case model.StateResting:
|
|
|
|
hm.expireAdventureIfNeeded(now)
|
|
|
|
|
|
|
|
// Advance logical movement time while idle so leaving town does not apply a huge dt (teleport).
|
|
|
|
// Advance logical movement time while idle so leaving town does not apply a huge dt (teleport).
|
|
|
|
dt := now.Sub(hm.LastMoveTick).Seconds()
|
|
|
|
dt := now.Sub(hm.LastMoveTick).Seconds()
|
|
|
|
if dt <= 0 {
|
|
|
|
if dt <= 0 {
|
|
|
|
dt = movementTickRate().Seconds()
|
|
|
|
dt = movementTickRate().Seconds()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
hm.LastMoveTick = now
|
|
|
|
hm.LastMoveTick = now
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
hm.applyTownRestHeal(dt)
|
|
|
|
if hm.roadsideRestAtCamp(now) {
|
|
|
|
|
|
|
|
hm.applyRoadsideRestHeal(dt)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
emitRoadsideRestThoughts(heroID, hm, now, adventureLog)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
hm.applyTownRestHeal(dt)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keep Hero.TownPause (restUntil) aligned with hm for any code reading hero between ticks.
|
|
|
|
|
|
|
|
hm.SyncToHero()
|
|
|
|
hm.SyncToHero()
|
|
|
|
if sender != nil && hm.Hero != nil {
|
|
|
|
if sender != nil && hm.Hero != nil {
|
|
|
|
sender.SendToHero(heroID, "hero_state", hm.Hero)
|
|
|
|
sender.SendToHero(heroID, "hero_state", hm.Hero)
|
|
|
|
sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
|
|
|
|
sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if now.After(hm.RestUntil) {
|
|
|
|
if now.After(hm.RestUntil) {
|
|
|
|
if hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
hm.endRoadsideRest()
|
|
|
|
|
|
|
|
hm.LastMoveTick = now
|
|
|
|
|
|
|
|
hm.SyncToHero()
|
|
|
|
|
|
|
|
if sender != nil && hm.Hero != nil {
|
|
|
|
|
|
|
|
sender.SendToHero(heroID, "hero_state", hm.Hero)
|
|
|
|
|
|
|
|
sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.LeaveTown(graph, now)
|
|
|
|
hm.LeaveTown(graph, now)
|
|
|
|
hm.SyncToHero()
|
|
|
|
hm.SyncToHero()
|
|
|
|
if sender != nil {
|
|
|
|
if sender != nil {
|
|
|
|
@ -1685,7 +1106,6 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
|
|
|
|
|
|
|
|
case model.StateWalking:
|
|
|
|
case model.StateWalking:
|
|
|
|
cfg := tuning.Get()
|
|
|
|
cfg := tuning.Get()
|
|
|
|
hm.expireAdventureIfNeeded(now)
|
|
|
|
|
|
|
|
hadNoRoad := hm.Road == nil || len(hm.Road.Waypoints) < 2
|
|
|
|
hadNoRoad := hm.Road == nil || len(hm.Road.Waypoints) < 2
|
|
|
|
if hadNoRoad {
|
|
|
|
if hadNoRoad {
|
|
|
|
hm.Road = nil
|
|
|
|
hm.Road = nil
|
|
|
|
@ -1717,27 +1137,7 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hm.tryStartRoadsideRest(now)
|
|
|
|
|
|
|
|
if hm.State == model.StateResting && hm.roadsideRestInProgress() {
|
|
|
|
|
|
|
|
hm.LastMoveTick = now
|
|
|
|
|
|
|
|
emitRoadsideRestThoughts(heroID, hm, now, adventureLog)
|
|
|
|
|
|
|
|
if sender != nil {
|
|
|
|
|
|
|
|
sender.SendToHero(heroID, "hero_state", hm.Hero)
|
|
|
|
|
|
|
|
sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hm.SyncToHero()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hm.tryStartAdventure(now)
|
|
|
|
|
|
|
|
dtMove := now.Sub(hm.LastMoveTick).Seconds()
|
|
|
|
|
|
|
|
if dtMove <= 0 {
|
|
|
|
|
|
|
|
dtMove = movementTickRate().Seconds()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
reachedTown := hm.AdvanceTick(now, graph)
|
|
|
|
reachedTown := hm.AdvanceTick(now, graph)
|
|
|
|
if !reachedTown {
|
|
|
|
|
|
|
|
hm.stepAdventureWander(now, dtMove)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if reachedTown {
|
|
|
|
if reachedTown {
|
|
|
|
hm.EnterTown(now, graph)
|
|
|
|
hm.EnterTown(now, graph)
|
|
|
|
|