fix movement

master
Denis Ranneft 1 month ago
parent 32caac9e55
commit 13c7e65515

@ -403,8 +403,8 @@ func (hm *HeroMovement) pickDestination(graph *RoadGraph) {
// assignRoad finds and configures the road from CurrentTownID to DestinationTownID.
// If no road exists (hero is mid-road), it finds the nearest town and routes from there.
// startAtFirstWaypoint: place hero at jittered waypoint 0 (departure town) without nearest-point snap —
// use when leaving town so an off-road NPC position does not snap to an arbitrary polyline point.
// startAtFirstWaypoint: force hero to jittered waypoint 0 (departure town). Otherwise
// snapProgressToNearestPointOnRoad projects CurrentX/Y onto the polyline (used after LeaveTown).
func (hm *HeroMovement) assignRoad(graph *RoadGraph, startAtFirstWaypoint bool) {
road := graph.FindRoad(hm.CurrentTownID, hm.DestinationTownID)
if road == nil {
@ -962,8 +962,9 @@ func (hm *HeroMovement) LeaveTown(graph *RoadGraph, now time.Time) {
// Prevent a huge movement step on the first tick after town: AdvanceTick uses now - LastMoveTick.
hm.LastMoveTick = now
hm.pickDestination(graph)
// Start exactly at the road origin (current town); snap from an NPC-tile would jump to the wrong spine point.
hm.assignRoad(graph, true)
// Project CurrentX/Y onto the outbound road polyline. The normal town flow walks the hero
// to the plaza first; forcing waypoint 0 caused a visible teleport away from that spot.
hm.assignRoad(graph, false)
hm.refreshSpeed(now)
}

@ -1,6 +1,7 @@
package game
import (
"math"
"testing"
"time"
@ -33,6 +34,44 @@ func testGraph() *RoadGraph {
}
}
// TestLeaveTown_ProjectsPlazaOntoRoad ensures we do not snap the hero to waypoint 0 when leaving
// town from a plaza offset from the graph origin (regression: assignRoad(..., true) teleported).
func TestLeaveTown_ProjectsPlazaOntoRoad(t *testing.T) {
graph := testGraph()
town1 := int64(1)
hero := &model.Hero{
ID: 1, State: model.StateInTown, HP: 10, MaxHP: 10, Level: 1,
CurrentTownID: &town1,
PositionX: 2,
PositionY: 1,
}
now := time.Now()
hm := NewHeroMovement(hero, graph, now)
if hm.State != model.StateInTown {
t.Fatalf("expected StateInTown from NewHeroMovement, got %v", hm.State)
}
hm.CurrentTownID = 1
hm.CurrentX = 2
hm.CurrentY = 1
hm.Road = nil
hm.LeaveTown(graph, now)
if hm.State != model.StateWalking {
t.Fatalf("expected StateWalking after LeaveTown, got %v", hm.State)
}
if hm.Road == nil {
t.Fatal("expected Road assigned after LeaveTown")
}
// Waypoint 0 is (0,0); (2,1) should project closer to the outbound polyline than to the origin.
dFromPlaza := math.Hypot(hm.CurrentX-2, hm.CurrentY-1)
dFromOrigin := math.Hypot(2, 1)
if dFromPlaza >= dFromOrigin-0.05 {
t.Fatalf("expected position projected toward plaza (2,1), got (%g,%g) distPlaza=%g distOrigin=%g",
hm.CurrentX, hm.CurrentY, dFromPlaza, dFromOrigin)
}
}
func testHeroOnRoad(id int64, hp, maxHP int) *model.Hero {
townID := int64(1)
destID := int64(2)

@ -371,9 +371,6 @@ export class GameEngine {
}
}
const newX = hero.position.x || 0;
const newY = hero.position.y || 0;
const activity = hero.serverActivityState?.toLowerCase();
const isDead = hero.hp <= 0 || activity === 'dead';
@ -405,16 +402,9 @@ export class GameEngine {
}
}
{
const tdx = newX - this._targetPositionX;
const tdy = newY - this._targetPositionY;
if (
tdx * tdx + tdy * tdy >
POSITION_DRIFT_SNAP_THRESHOLD * POSITION_DRIFT_SNAP_THRESHOLD
) {
this._snapHeroWorldPositionTo(newX, newY);
}
}
// Display position: follow hero_move interpolation and applyPositionSync only.
// Do not snap here on hero_state vs last move — server position includes
// excursion/rest offsets and ordering with hero_move caused visible teleports.
this._notifyStateChange();
}
@ -923,24 +913,6 @@ export class GameEngine {
// ---- Private: Helpers ----
/**
* Snap render/interpolation state and camera to a world position (teleport, town arrival, etc.).
*/
private _snapHeroWorldPositionTo(x: number, y: number): void {
this._heroDisplayX = x;
this._heroDisplayY = y;
this._prevPositionX = x;
this._prevPositionY = y;
this._targetPositionX = x;
this._targetPositionY = y;
this._moveTargetX = x;
this._moveTargetY = y;
this._lastMoveUpdateTime = performance.now();
const heroScreen = worldToScreen(x, y);
this.camera.setTarget(heroScreen.x, heroScreen.y);
this.camera.snapToTarget();
}
private _notifyStateChange(): void {
this._onStateChange?.(this._gameState);
}

Loading…
Cancel
Save