|
|
|
|
@ -912,6 +912,30 @@ func randomTownNPCDelay() time.Duration {
|
|
|
|
|
return minDelay + time.Duration(rand.Int63n(rangeMs+1))*time.Millisecond
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// townNPCStandPoint is a spot near the NPC along the hero's approach (from → npc), not on the NPC tile.
|
|
|
|
|
func townNPCStandPoint(npcX, npcY, fromX, fromY, standoff float64) (sx, sy float64) {
|
|
|
|
|
if standoff <= 0 {
|
|
|
|
|
standoff = tuning.DefaultValues().TownNPCStandoffWorld
|
|
|
|
|
}
|
|
|
|
|
dx := fromX - npcX
|
|
|
|
|
dy := fromY - npcY
|
|
|
|
|
ln := math.Hypot(dx, dy)
|
|
|
|
|
if ln < 1e-4 {
|
|
|
|
|
s := standoff * 0.7071067811865476
|
|
|
|
|
return npcX + s, npcY + s
|
|
|
|
|
}
|
|
|
|
|
ux, uy := dx/ln, dy/ln
|
|
|
|
|
const gap = 0.05
|
|
|
|
|
step := standoff
|
|
|
|
|
if step > ln-gap {
|
|
|
|
|
step = ln - gap
|
|
|
|
|
}
|
|
|
|
|
if step < 0.12 {
|
|
|
|
|
step = math.Min(0.12, math.Max(0.06, ln*0.42))
|
|
|
|
|
}
|
|
|
|
|
return npcX + ux*step, npcY + uy*step
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// clearNPCWalk resets the walk-to-NPC sub-state.
|
|
|
|
|
func (hm *HeroMovement) clearNPCWalk() {
|
|
|
|
|
hm.TownNPCWalkTargetID = 0
|
|
|
|
|
@ -1610,6 +1634,18 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
|
|
|
|
|
|
switch hm.ActiveRestKind {
|
|
|
|
|
case model.RestKindRoadside:
|
|
|
|
|
// For roadside rest, ensure Wild→Return always gets a fresh return
|
|
|
|
|
// deadline so the hero walks back to the road smoothly (prevents
|
|
|
|
|
// advanceExcursionPhases from skipping the return phase on time jumps).
|
|
|
|
|
if hm.Excursion.Phase == model.ExcursionWild && !now.Before(hm.Excursion.WildUntil) {
|
|
|
|
|
hm.Excursion.Phase = model.ExcursionReturn
|
|
|
|
|
speed := hm.Speed
|
|
|
|
|
if speed < 0.1 {
|
|
|
|
|
speed = 0.1
|
|
|
|
|
}
|
|
|
|
|
hm.Excursion.WildUntil = now
|
|
|
|
|
hm.Excursion.ReturnUntil = now.Add(time.Duration(hm.Excursion.DepthWorldUnits / speed * float64(time.Second)))
|
|
|
|
|
}
|
|
|
|
|
excursionEnded := hm.advanceExcursionPhases(now)
|
|
|
|
|
if hm.Excursion.Phase == model.ExcursionWild {
|
|
|
|
|
hm.applyRestHealTick(dt)
|
|
|
|
|
@ -1691,19 +1727,26 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
|
// --- Sub-state: hero is walking toward an NPC inside the town ---
|
|
|
|
|
if hm.TownNPCWalkTargetID != 0 {
|
|
|
|
|
if !now.Before(hm.TownNPCWalkArrive) {
|
|
|
|
|
// Arrived at NPC — snap position and fire the visit event.
|
|
|
|
|
// Arrived at stand point (near NPC) — snap position and fire the visit event.
|
|
|
|
|
hm.CurrentX = hm.TownNPCWalkToX
|
|
|
|
|
hm.CurrentY = hm.TownNPCWalkToY
|
|
|
|
|
npcID := hm.TownNPCWalkTargetID
|
|
|
|
|
npcWX := hm.TownNPCWalkToX
|
|
|
|
|
npcWY := hm.TownNPCWalkToY
|
|
|
|
|
standX := hm.TownNPCWalkToX
|
|
|
|
|
standY := hm.TownNPCWalkToY
|
|
|
|
|
hm.clearNPCWalk()
|
|
|
|
|
|
|
|
|
|
if npc, ok := graph.NPCByID[npcID]; ok {
|
|
|
|
|
if sender != nil {
|
|
|
|
|
sender.SendToHero(heroID, "town_npc_visit", model.TownNPCVisitPayload{
|
|
|
|
|
NPCID: npc.ID, Name: npc.Name, Type: npc.Type, TownID: hm.CurrentTownID,
|
|
|
|
|
WorldX: npcWX, WorldY: npcWY,
|
|
|
|
|
WorldX: standX, WorldY: standY,
|
|
|
|
|
})
|
|
|
|
|
// Snap client interpolation to the NPC tile (visit message alone left the
|
|
|
|
|
// hero short of the last hero_move segment).
|
|
|
|
|
sender.SendToHero(heroID, "hero_move", model.HeroMovePayload{
|
|
|
|
|
X: hm.CurrentX, Y: hm.CurrentY,
|
|
|
|
|
TargetX: hm.CurrentX, TargetY: hm.CurrentY,
|
|
|
|
|
Speed: 0, Heading: 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
hm.TownVisitNPCName = npc.Name
|
|
|
|
|
@ -1797,8 +1840,13 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
|
npcWX, npcWY = town.WorldX+npc.OffsetX, town.WorldY+npc.OffsetY
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dx := npcWX - hm.CurrentX
|
|
|
|
|
dy := npcWY - hm.CurrentY
|
|
|
|
|
standoff := cfg.TownNPCStandoffWorld
|
|
|
|
|
if standoff <= 0 {
|
|
|
|
|
standoff = tuning.DefaultValues().TownNPCStandoffWorld
|
|
|
|
|
}
|
|
|
|
|
toX, toY := townNPCStandPoint(npcWX, npcWY, hm.CurrentX, hm.CurrentY, standoff)
|
|
|
|
|
dx := toX - hm.CurrentX
|
|
|
|
|
dy := toY - hm.CurrentY
|
|
|
|
|
dist := math.Sqrt(dx*dx + dy*dy)
|
|
|
|
|
walkSpeed := cfg.TownNPCWalkSpeed
|
|
|
|
|
if walkSpeed <= 0 {
|
|
|
|
|
@ -1812,15 +1860,15 @@ func ProcessSingleHeroMovementTick(
|
|
|
|
|
hm.TownNPCWalkTargetID = npcID
|
|
|
|
|
hm.TownNPCWalkFromX = hm.CurrentX
|
|
|
|
|
hm.TownNPCWalkFromY = hm.CurrentY
|
|
|
|
|
hm.TownNPCWalkToX = npcWX
|
|
|
|
|
hm.TownNPCWalkToY = npcWY
|
|
|
|
|
hm.TownNPCWalkToX = toX
|
|
|
|
|
hm.TownNPCWalkToY = toY
|
|
|
|
|
hm.TownNPCWalkStart = now
|
|
|
|
|
hm.TownNPCWalkArrive = now.Add(walkDur)
|
|
|
|
|
if sender != nil {
|
|
|
|
|
heading := math.Atan2(dy, dx)
|
|
|
|
|
sender.SendToHero(heroID, "hero_move", model.HeroMovePayload{
|
|
|
|
|
X: hm.CurrentX, Y: hm.CurrentY,
|
|
|
|
|
TargetX: npcWX, TargetY: npcWY,
|
|
|
|
|
TargetX: toX, TargetY: toY,
|
|
|
|
|
Speed: walkSpeed, Heading: heading,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|