some fixes for heroes and local testing

master
Denis Ranneft 11 hours ago
parent 867a00bf8c
commit 1fdfdbfcda

@ -89,8 +89,8 @@ func main() {
}
engine.RegisterHeroMovement(hero)
}
hub.OnDisconnect = func(heroID int64) {
engine.UnregisterHeroMovement(heroID)
hub.OnDisconnect = func(heroID int64, remainingSameHero int) {
engine.HeroSocketDetached(heroID, remainingSameHero == 0)
}
// Bridge hub incoming client messages to engine's command channel.

@ -329,6 +329,31 @@ func (e *Engine) RegisterHeroMovement(hero *model.Hero) {
}
now := time.Now()
// Reconnect while the previous socket is still tearing down: keep live movement so we
// do not replace (x,y) and route with a stale DB snapshot.
if existing, ok := e.movements[hero.ID]; ok {
existing.Hero.RefreshDerivedCombatStats(now)
e.logger.Info("hero movement reattached (existing session)",
"hero_id", hero.ID,
"state", existing.State,
"pos_x", existing.CurrentX,
"pos_y", existing.CurrentY,
)
if e.sender != nil {
e.sender.SendToHero(hero.ID, "hero_state", existing.Hero)
if route := existing.RoutePayload(); route != nil {
e.sender.SendToHero(hero.ID, "route_assigned", route)
}
if cs, ok := e.combats[hero.ID]; ok {
e.sender.SendToHero(hero.ID, "combat_start", model.CombatStartPayload{
Enemy: enemyToInfo(&cs.Enemy),
})
}
}
return
}
hm := NewHeroMovement(hero, e.roadGraph, now)
e.movements[hero.ID] = hm
@ -357,24 +382,33 @@ func (e *Engine) RegisterHeroMovement(hero *model.Hero) {
}
}
// UnregisterHeroMovement removes movement state and persists hero to DB.
// Called when a WS client disconnects.
func (e *Engine) UnregisterHeroMovement(heroID int64) {
// HeroSocketDetached persists hero state on every WS disconnect and removes in-memory
// movement only when lastConnection is true (no other tabs/sockets for this hero).
func (e *Engine) HeroSocketDetached(heroID int64, lastConnection bool) {
e.mu.Lock()
hm, ok := e.movements[heroID]
if ok {
hm.SyncToHero()
if lastConnection {
delete(e.movements, heroID)
}
}
var heroSnap *model.Hero
if ok {
heroSnap = hm.Hero
}
e.mu.Unlock()
if ok && e.heroStore != nil {
if ok && e.heroStore != nil && heroSnap != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.heroStore.Save(ctx, hm.Hero); err != nil {
e.logger.Error("failed to save hero on disconnect", "hero_id", heroID, "error", err)
if err := e.heroStore.Save(ctx, heroSnap); err != nil {
e.logger.Error("failed to save hero on ws disconnect", "hero_id", heroID, "error", err)
} else {
e.logger.Info("hero state persisted on disconnect", "hero_id", heroID)
e.logger.Info("hero state persisted on ws disconnect",
"hero_id", heroID,
"last_connection", lastConnection,
)
}
}
}

@ -89,15 +89,24 @@ func NewHeroMovement(hero *model.Hero, graph *RoadGraph, now time.Time) *HeroMov
dir = -1
}
// Add per-hero position offset so heroes on the same road don't overlap.
// Use hero ID to create a stable lateral offset of ±1.5 tiles.
// Persisted (x,y) already include any in-world offset from prior sessions; do not add
// lateral jitter again on reconnect (that doubled the shift every reload). Only spread
// new heroes that still sit at the default origin.
var curX, curY float64
if hero.PositionX == 0 && hero.PositionY == 0 {
lateralOffset := (float64(hero.ID%7) - 3.0) * 0.5
curX = lateralOffset * 0.3
curY = lateralOffset * 0.7
} else {
curX = hero.PositionX
curY = hero.PositionY
}
hm := &HeroMovement{
HeroID: hero.ID,
Hero: hero,
CurrentX: hero.PositionX + lateralOffset*0.3,
CurrentY: hero.PositionY + lateralOffset*0.7,
CurrentX: curX,
CurrentY: curY,
State: hero.State,
LastMoveTick: now,
Direction: dir,
@ -278,18 +287,56 @@ func (hm *HeroMovement) assignRoad(graph *RoadGraph) {
}
hm.Road = jitteredRoad
// Restore progress along this hero's jittered polyline from saved world position.
// Otherwise WaypointIndex stays 0 and the next AdvanceTick snaps (x,y) to waypoint[0]
// (departure town), which looks like "teleport back to the city" on reload.
hm.snapProgressToNearestPointOnRoad()
}
// snapProgressToNearestPointOnRoad sets WaypointIndex, WaypointFraction, and CurrentX/Y
// to the closest point on the current road polyline to the incoming position.
func (hm *HeroMovement) snapProgressToNearestPointOnRoad() {
if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
hm.WaypointIndex = 0
hm.WaypointFraction = 0
// Position the hero at the start of the road if they're very close to the origin town.
if len(jitteredWaypoints) > 0 {
start := jitteredWaypoints[0]
dist := math.Hypot(hm.CurrentX-start.X, hm.CurrentY-start.Y)
if dist < 5.0 {
hm.CurrentX = start.X
hm.CurrentY = start.Y
return
}
hx, hy := hm.CurrentX, hm.CurrentY
bestIdx := 0
bestT := 0.0
bestDistSq := math.MaxFloat64
bestX, bestY := hx, hy
for i := 0; i < len(hm.Road.Waypoints)-1; i++ {
ax, ay := hm.Road.Waypoints[i].X, hm.Road.Waypoints[i].Y
bx, by := hm.Road.Waypoints[i+1].X, hm.Road.Waypoints[i+1].Y
dx, dy := bx-ax, by-ay
segLenSq := dx*dx + dy*dy
var t float64
if segLenSq < 1e-12 {
t = 0
} else {
t = ((hx-ax)*dx + (hy-ay)*dy) / segLenSq
if t < 0 {
t = 0
}
if t > 1 {
t = 1
}
}
px := ax + t*dx
py := ay + t*dy
dSq := (hx-px)*(hx-px) + (hy-py)*(hy-py)
if dSq < bestDistSq {
bestDistSq = dSq
bestIdx = i
bestT = t
bestX, bestY = px, py
}
}
hm.WaypointIndex = bestIdx
hm.WaypointFraction = bestT
hm.CurrentX = bestX
hm.CurrentY = bestY
}
// refreshSpeed recalculates the effective movement speed using hero buffs/debuffs.

@ -39,8 +39,9 @@ type Hub struct {
OnConnect func(heroID int64)
// OnDisconnect is called when a client is unregistered.
// Set by the engine to persist state and remove movement. May be nil.
OnDisconnect func(heroID int64)
// remainingSameHero is how many other WS clients for this hero are still connected.
// Set by the engine to persist state; may be nil.
OnDisconnect func(heroID int64, remainingSameHero int)
}
// Client represents a single WebSocket connection.
@ -87,17 +88,27 @@ func (h *Hub) Run() {
}
case client := <-h.unregister:
heroID := client.heroID
h.mu.Lock()
existed := false
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
existed = true
close(client.send)
}
remaining := 0
for c := range h.clients {
if c.heroID == heroID {
remaining++
}
}
h.mu.Unlock()
h.logger.Info("client disconnected", "hero_id", client.heroID)
h.logger.Info("client disconnected", "hero_id", heroID, "remaining_same_hero", remaining)
// Notify engine of disconnection.
if h.OnDisconnect != nil {
go h.OnDisconnect(client.heroID)
// Always persist; engine drops in-memory movement only when remaining == 0.
// Synchronous so a reconnect that loads from DB sees the latest save.
if existed && h.OnDisconnect != nil {
h.OnDisconnect(heroID, remaining)
}
case env := <-h.broadcast:

Loading…
Cancel
Save