buffs fix + adventure update

master
Denis Ranneft 1 month ago
parent cae397a7d8
commit cbab3dbe3b

@ -451,6 +451,7 @@ func (hm *HeroMovement) ShiftGameDeadlines(d time.Duration, now time.Time) {
hm.TownLeaveAt = shift(hm.TownLeaveAt) hm.TownLeaveAt = shift(hm.TownLeaveAt)
hm.WanderingMerchantDeadline = shift(hm.WanderingMerchantDeadline) hm.WanderingMerchantDeadline = shift(hm.WanderingMerchantDeadline)
hm.Excursion.StartedAt = shift(hm.Excursion.StartedAt) hm.Excursion.StartedAt = shift(hm.Excursion.StartedAt)
hm.Excursion.OutUntil = shift(hm.Excursion.OutUntil)
hm.Excursion.WildUntil = shift(hm.Excursion.WildUntil) hm.Excursion.WildUntil = shift(hm.Excursion.WildUntil)
hm.Excursion.ReturnUntil = shift(hm.Excursion.ReturnUntil) hm.Excursion.ReturnUntil = shift(hm.Excursion.ReturnUntil)
hm.LastExcursionEndedAt = shift(hm.LastExcursionEndedAt) hm.LastExcursionEndedAt = shift(hm.LastExcursionEndedAt)
@ -665,7 +666,7 @@ func (hm *HeroMovement) AdminStopRest(now time.Time) bool {
if hm.ActiveRestKind != model.RestKindRoadside && hm.ActiveRestKind != model.RestKindAdventureInline { if hm.ActiveRestKind != model.RestKindRoadside && hm.ActiveRestKind != model.RestKindAdventureInline {
return false return false
} }
if hm.ActiveRestKind == model.RestKindAdventureInline && hm.Excursion.Active() { if hm.Excursion.Active() {
hm.endExcursion(now) hm.endExcursion(now)
} }
hm.ActiveRestKind = model.RestKindNone hm.ActiveRestKind = model.RestKindNone
@ -751,13 +752,12 @@ func (hm *HeroMovement) roadForwardUnit() (float64, float64) {
func (hm *HeroMovement) displayOffset(now time.Time) (float64, float64) { func (hm *HeroMovement) displayOffset(now time.Time) (float64, float64) {
exc := &hm.Excursion exc := &hm.Excursion
if exc.Active() { if exc.Active() {
cfg := tuning.Get()
perpX, perpY := hm.roadPerpendicularUnit() perpX, perpY := hm.roadPerpendicularUnit()
depth := exc.DepthWorldUnits depth := exc.DepthWorldUnits
var t float64 var t float64
switch exc.Phase { switch exc.Phase {
case model.ExcursionOut: case model.ExcursionOut:
outMs := float64(cfg.AdventureOutDurationMs) outMs := float64(exc.OutUntil.Sub(exc.StartedAt).Milliseconds())
if outMs > 0 { if outMs > 0 {
elapsed := float64(now.Sub(exc.StartedAt).Milliseconds()) elapsed := float64(now.Sub(exc.StartedAt).Milliseconds())
t = smoothstep(clamp01(elapsed / outMs)) t = smoothstep(clamp01(elapsed / outMs))
@ -765,10 +765,9 @@ func (hm *HeroMovement) displayOffset(now time.Time) (float64, float64) {
case model.ExcursionWild: case model.ExcursionWild:
t = 1.0 t = 1.0
case model.ExcursionReturn: case model.ExcursionReturn:
retMs := float64(cfg.AdventureReturnDurationMs) retMs := float64(exc.ReturnUntil.Sub(exc.WildUntil).Milliseconds())
if retMs > 0 { if retMs > 0 {
returnStart := exc.ReturnUntil.Add(-time.Duration(cfg.AdventureReturnDurationMs) * time.Millisecond) elapsed := float64(now.Sub(exc.WildUntil).Milliseconds())
elapsed := float64(now.Sub(returnStart).Milliseconds())
t = 1.0 - smoothstep(clamp01(elapsed / retMs)) t = 1.0 - smoothstep(clamp01(elapsed / retMs))
} }
} }
@ -776,12 +775,6 @@ func (hm *HeroMovement) displayOffset(now time.Time) (float64, float64) {
return perpX * d, perpY * d return perpX * d, perpY * d
} }
if hm.State == model.StateResting && hm.ActiveRestKind == model.RestKindRoadside {
perpX, perpY := hm.roadPerpendicularUnit()
const roadsideDepth = 2.0
return perpX * roadsideDepth, perpY * roadsideDepth
}
return 0, 0 return 0, 0
} }
@ -1004,6 +997,10 @@ func (hm *HeroMovement) excursionPersisted() *model.ExcursionPersisted {
t := s.StartedAt t := s.StartedAt
ep.StartedAt = &t ep.StartedAt = &t
} }
if !s.OutUntil.IsZero() {
t := s.OutUntil
ep.OutUntil = &t
}
if !s.WildUntil.IsZero() { if !s.WildUntil.IsZero() {
t := s.WildUntil t := s.WildUntil
ep.WildUntil = &t ep.WildUntil = &t
@ -1062,6 +1059,9 @@ func (hm *HeroMovement) applyExcursionFromBlob(ep *model.ExcursionPersisted) {
if ep.StartedAt != nil { if ep.StartedAt != nil {
hm.Excursion.StartedAt = *ep.StartedAt hm.Excursion.StartedAt = *ep.StartedAt
} }
if ep.OutUntil != nil {
hm.Excursion.OutUntil = *ep.OutUntil
}
if ep.WildUntil != nil { if ep.WildUntil != nil {
hm.Excursion.WildUntil = *ep.WildUntil hm.Excursion.WildUntil = *ep.WildUntil
} }
@ -1269,32 +1269,50 @@ func (hm *HeroMovement) mayStartExcursion(now time.Time) bool {
func (hm *HeroMovement) beginExcursion(now time.Time) { func (hm *HeroMovement) beginExcursion(now time.Time) {
cfg := tuning.Get() cfg := tuning.Get()
depth := cfg.AdventureDepthWorldUnits
hm.refreshSpeed(now)
speed := hm.Speed
if speed < 0.1 {
speed = 0.1
}
outDur := time.Duration(depth / speed * float64(time.Second))
outEnd := now.Add(outDur)
wildDur := randomDurationBetweenMs(cfg.AdventureWildMinMs, cfg.AdventureWildMaxMs)
wildEnd := outEnd.Add(wildDur)
returnDur := time.Duration(depth / speed * float64(time.Second))
hm.Excursion = model.ExcursionSession{ hm.Excursion = model.ExcursionSession{
Phase: model.ExcursionOut, Phase: model.ExcursionOut,
StartedAt: now, StartedAt: now,
DepthWorldUnits: cfg.AdventureDepthWorldUnits, OutUntil: outEnd,
WildUntil: wildEnd,
ReturnUntil: wildEnd.Add(returnDur),
DepthWorldUnits: depth,
RoadFreezeWaypoint: hm.WaypointIndex, RoadFreezeWaypoint: hm.WaypointIndex,
RoadFreezeFraction: hm.WaypointFraction, RoadFreezeFraction: hm.WaypointFraction,
} }
outEnd := now.Add(time.Duration(cfg.AdventureOutDurationMs) * time.Millisecond)
wildDur := randomDurationBetweenMs(cfg.AdventureWildMinMs, cfg.AdventureWildMaxMs)
wildEnd := outEnd.Add(wildDur)
hm.Excursion.WildUntil = wildEnd
hm.Excursion.ReturnUntil = wildEnd.Add(time.Duration(cfg.AdventureReturnDurationMs) * time.Millisecond)
} }
// advanceExcursionPhases progresses through out->wild->return and returns true when complete. // advanceExcursionPhases progresses through out->wild->return and returns true when complete.
func (hm *HeroMovement) advanceExcursionPhases(now time.Time) (ended bool) { func (hm *HeroMovement) advanceExcursionPhases(now time.Time) (ended bool) {
exc := &hm.Excursion exc := &hm.Excursion
cfg := tuning.Get() if exc.Phase == model.ExcursionOut && !now.Before(exc.OutUntil) {
if exc.Phase == model.ExcursionOut { exc.Phase = model.ExcursionWild
outEnd := exc.StartedAt.Add(time.Duration(cfg.AdventureOutDurationMs) * time.Millisecond)
if !now.Before(outEnd) {
exc.Phase = model.ExcursionWild
}
} }
if exc.Phase == model.ExcursionWild && !now.Before(exc.WildUntil) { if exc.Phase == model.ExcursionWild && !now.Before(exc.WildUntil) {
exc.Phase = model.ExcursionReturn exc.Phase = model.ExcursionReturn
// Only recalculate return duration if we haven't already passed the original deadline
// (handles large time jumps from offline catch-up or timer-based exits).
if now.Before(exc.ReturnUntil) {
speed := hm.Speed
if speed < 0.1 {
speed = 0.1
}
exc.WildUntil = now
exc.ReturnUntil = now.Add(time.Duration(exc.DepthWorldUnits / speed * float64(time.Second)))
}
} }
if exc.Phase == model.ExcursionReturn && !now.Before(exc.ReturnUntil) { if exc.Phase == model.ExcursionReturn && !now.Before(exc.ReturnUntil) {
return true return true
@ -1321,8 +1339,34 @@ func (hm *HeroMovement) beginRoadsideRest(now time.Time) {
hm.Hero.State = model.StateResting hm.Hero.State = model.StateResting
hm.ActiveRestKind = model.RestKindRoadside hm.ActiveRestKind = model.RestKindRoadside
hm.RestHealRemainder = 0 hm.RestHealRemainder = 0
dur := randomDurationBetweenMs(cfg.RoadsideRestMinMs, cfg.RoadsideRestMaxMs)
hm.RestUntil = now.Add(dur) depth := cfg.RoadsideRestDepthWorldUnits
if depth <= 0 {
depth = 12.0
}
hm.refreshSpeed(now)
speed := hm.Speed
if speed < 0.1 {
speed = 0.1
}
moveDur := time.Duration(depth / speed * float64(time.Second))
outUntil := now.Add(moveDur)
restDur := randomDurationBetweenMs(cfg.RoadsideRestMinMs, cfg.RoadsideRestMaxMs)
wildUntil := outUntil.Add(restDur)
returnUntil := wildUntil.Add(moveDur)
hm.Excursion = model.ExcursionSession{
Phase: model.ExcursionOut,
StartedAt: now,
OutUntil: outUntil,
WildUntil: wildUntil,
ReturnUntil: returnUntil,
DepthWorldUnits: depth,
RoadFreezeWaypoint: hm.WaypointIndex,
RoadFreezeFraction: hm.WaypointFraction,
}
hm.RestUntil = returnUntil
} }
func (hm *HeroMovement) beginAdventureInlineRest(now time.Time) { func (hm *HeroMovement) beginAdventureInlineRest(now time.Time) {
@ -1422,16 +1466,30 @@ func ProcessSingleHeroMovementTick(
switch hm.ActiveRestKind { switch hm.ActiveRestKind {
case model.RestKindRoadside: case model.RestKindRoadside:
hm.applyRestHealTick(dt) excursionEnded := hm.advanceExcursionPhases(now)
cfg := tuning.Get() if hm.Excursion.Phase == model.ExcursionWild {
hpFrac := float64(hm.Hero.HP) / float64(hm.Hero.MaxHP) hm.applyRestHealTick(dt)
if now.After(hm.RestUntil) || hpFrac >= cfg.RoadsideRestExitHp { }
if excursionEnded {
hm.endExcursion(now)
hm.ActiveRestKind = model.RestKindNone hm.ActiveRestKind = model.RestKindNone
hm.RestUntil = time.Time{} hm.RestUntil = time.Time{}
hm.RestHealRemainder = 0 hm.RestHealRemainder = 0
hm.State = model.StateWalking hm.State = model.StateWalking
hm.Hero.State = model.StateWalking hm.Hero.State = model.StateWalking
hm.refreshSpeed(now) hm.refreshSpeed(now)
} else if hm.Excursion.Phase == model.ExcursionWild {
cfg := tuning.Get()
hpFrac := float64(hm.Hero.HP) / float64(hm.Hero.MaxHP)
if hpFrac >= cfg.RoadsideRestExitHp {
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)))
}
} }
hm.SyncToHero() hm.SyncToHero()
if sender != nil && hm.Hero != nil { if sender != nil && hm.Hero != nil {

@ -144,11 +144,12 @@ func TestRoadsideRest_ExitsByHPThreshold(t *testing.T) {
hm := NewHeroMovement(hero, graph, now) hm := NewHeroMovement(hero, graph, now)
hm.beginRoadsideRest(now) hm.beginRoadsideRest(now)
tick := now.Add(time.Second) // Tick past the Out phase so the hero is in Wild phase where HP threshold is checked.
tick := hm.Excursion.OutUntil.Add(time.Second)
ProcessSingleHeroMovementTick(hero.ID, hm, graph, tick, nil, nil, nil, nil, nil) ProcessSingleHeroMovementTick(hero.ID, hm, graph, tick, nil, nil, nil, nil, nil)
if hm.State != model.StateWalking { if hm.Excursion.Phase != model.ExcursionReturn {
t.Fatalf("expected StateWalking after HP threshold, got %s", hm.State) t.Fatalf("expected excursion Return phase after HP threshold exit, got %s", hm.Excursion.Phase)
} }
} }
@ -160,9 +161,11 @@ func TestRoadsideRest_DisplayOffset(t *testing.T) {
hm := NewHeroMovement(hero, graph, now) hm := NewHeroMovement(hero, graph, now)
hm.beginRoadsideRest(now) hm.beginRoadsideRest(now)
ox, oy := hm.displayOffset(now) // Check offset partway through the Out phase (smoothstep should be non-zero).
outMid := hm.Excursion.StartedAt.Add(hm.Excursion.OutUntil.Sub(hm.Excursion.StartedAt) / 2)
ox, oy := hm.displayOffset(outMid)
if ox == 0 && oy == 0 { if ox == 0 && oy == 0 {
t.Fatal("expected non-zero display offset during roadside rest") t.Fatal("expected non-zero display offset during roadside rest out phase")
} }
} }
@ -181,7 +184,7 @@ func TestAdventureInlineRest_TriggersOnLowHP(t *testing.T) {
hm.Hero.State = model.StateWalking hm.Hero.State = model.StateWalking
hm.beginExcursion(now) hm.beginExcursion(now)
tick := now.Add(time.Duration(cfg.AdventureOutDurationMs+1000) * time.Millisecond) tick := hm.Excursion.OutUntil.Add(time.Second)
ProcessSingleHeroMovementTick(hero.ID, hm, graph, tick, nil, nil, nil, nil, nil) ProcessSingleHeroMovementTick(hero.ID, hm, graph, tick, nil, nil, nil, nil, nil)
if hm.State != model.StateResting { if hm.State != model.StateResting {

@ -29,6 +29,8 @@ type ExcursionSession struct {
Phase ExcursionPhase Phase ExcursionPhase
StartedAt time.Time StartedAt time.Time
// OutUntil marks the end of the out phase (hero reached full depth); derived from depth/speed.
OutUntil time.Time
// WildUntil marks the end of the wild phase; once reached the hero begins returning. // WildUntil marks the end of the wild phase; once reached the hero begins returning.
WildUntil time.Time WildUntil time.Time
// ReturnUntil marks the deadline for the return phase; once reached the hero is back on road. // ReturnUntil marks the deadline for the return phase; once reached the hero is back on road.
@ -53,6 +55,7 @@ func (s *ExcursionSession) Active() bool {
type ExcursionPersisted struct { type ExcursionPersisted struct {
Phase string `json:"phase,omitempty"` Phase string `json:"phase,omitempty"`
StartedAt *time.Time `json:"startedAt,omitempty"` StartedAt *time.Time `json:"startedAt,omitempty"`
OutUntil *time.Time `json:"outUntil,omitempty"`
WildUntil *time.Time `json:"wildUntil,omitempty"` WildUntil *time.Time `json:"wildUntil,omitempty"`
ReturnUntil *time.Time `json:"returnUntil,omitempty"` ReturnUntil *time.Time `json:"returnUntil,omitempty"`
DepthWorldUnits float64 `json:"depthWorldUnits,omitempty"` DepthWorldUnits float64 `json:"depthWorldUnits,omitempty"`

@ -190,6 +190,9 @@ type Values struct {
RoadsideRestHpPerS float64 `json:"roadsideRestHpPerSecond"` RoadsideRestHpPerS float64 `json:"roadsideRestHpPerSecond"`
// AdventureRestHpPerS is the HP/MaxHP fraction healed per second during adventure inline rest. // AdventureRestHpPerS is the HP/MaxHP fraction healed per second during adventure inline rest.
AdventureRestHpPerS float64 `json:"adventureRestHpPerSecond"` AdventureRestHpPerS float64 `json:"adventureRestHpPerSecond"`
// RoadsideRestDepthWorldUnits is the perpendicular offset from road during roadside rest.
RoadsideRestDepthWorldUnits float64 `json:"roadsideRestDepthWorldUnits"`
} }
func DefaultValues() Values { func DefaultValues() Values {
@ -322,7 +325,7 @@ func DefaultValues() Values {
AdventureWildMinMs: 560_000, AdventureWildMinMs: 560_000,
AdventureWildMaxMs: 2_960_000, AdventureWildMaxMs: 2_960_000,
AdventureReturnDurationMs: 20_000, AdventureReturnDurationMs: 20_000,
AdventureDepthWorldUnits: 20.0, AdventureDepthWorldUnits: 40.0,
AdventureEncounterCooldownMs: 6_000, AdventureEncounterCooldownMs: 6_000,
AdventureReturnEncounterEnabled: true, AdventureReturnEncounterEnabled: true,
@ -333,6 +336,8 @@ func DefaultValues() Values {
RoadsideRestMaxMs: 600_000, RoadsideRestMaxMs: 600_000,
RoadsideRestHpPerS: 0.003, RoadsideRestHpPerS: 0.003,
AdventureRestHpPerS: 0.004, AdventureRestHpPerS: 0.004,
RoadsideRestDepthWorldUnits: 12.0,
} }
} }

@ -852,9 +852,9 @@ export function App() {
return next; return next;
}); });
// Optimistic decrement of per-buff charge (subscribers skip server-side consumption) // Optimistic decrement of per-buff charge (server always consumes via ConsumeBuffCharge)
const currentCharge = hero.buffCharges?.[type]; const currentCharge = hero.buffCharges?.[type];
if (!hero.subscriptionActive && currentCharge != null && currentCharge.remaining > 0) { if (currentCharge != null && currentCharge.remaining > 0) {
const updatedCharges: Partial<Record<BuffType, BuffChargeState>> = { const updatedCharges: Partial<Record<BuffType, BuffChargeState>> = {
...hero.buffCharges, ...hero.buffCharges,
[type]: { ...currentCharge, remaining: currentCharge.remaining - 1 }, [type]: { ...currentCharge, remaining: currentCharge.remaining - 1 },

Loading…
Cancel
Save