core/npc/NpcMovement.cs
public class NpcMovement : Component {
	[Property] public Vector3 Goal {get; set;}
	public Action OnReachTarget {get; set;}
	[Property] public Vector3 OnReachTargetPosition {get; set;}
	[Button] public bool Go(bool force = true) {
		Goal = Grid.Snap(Goal);
		if (Path.Count > 0) {
			Path.Clear();
			WaitingForGo = true;
			HitGo = 0.1f;
			return false;
		}
		Path.Clear();
		if (Grid.IsBlocked(Goal)) {
			if (Goal == OnReachTargetPosition)
				OnReachTarget.Invoke();
			return false;
		}
		Path.Add(Grid.Snap(WorldPosition));
		Advance(new());
		if (Path.Count == 0) {
			if (Goal == OnReachTargetPosition)
				OnReachTarget.Invoke();
			return false;
		}
		if (!Path.Last().AlmostEqual(Goal)) {
			if (Goal == OnReachTargetPosition)
				OnReachTarget.Invoke();
			Path.Clear();
			return false;
		}
		//DebugOverlay.Line(Path, Color.Red, 5);
		Run = LastGo < 0.4f;
		LastGo = 0;
		return true;
	}
	[Property, ReadOnly] public bool WaitingForGo {get ; set;}
	private TimeSince LastGo {get; set;} = 1000;
	private TimeUntil HitGo {get; set;}
	[Property] public bool DontUpdateActivity {get; set;}
	[Property, ReadOnly] public bool Run {get; set;} = false;
	[Property, ReadOnly] public List<Vector3> Path {get; set;} = new();

	public void Advance(List<Vector3> invalid, bool useangle = false, float angle = 0f) {
		if (Path.Last() == Goal)
			return;
		if (useangle) {
			var canidate = Grid.Snap(Path.Last() + Rotation.FromYaw(angle).Forward * 30f);
			if (Grid.IsBlocked(canidate) || Path.Last().Distance(Goal) <= canidate.Distance(Goal) + Path.Last().Distance(canidate) * 0.5f)
				useangle = false;
		}
		if (!useangle) {
			var best = -1f;
			for (int i = 0; i < 6; i++) {
				var canidate = Grid.Snap(Path.Last() + Rotation.FromYaw(i * 60f - 30f).Forward * 30f);
				if (Grid.IsBlocked(canidate))
					continue;
				var dot = Vector3.Direction(Path.Last(), canidate).Dot(Vector3.Direction(Path.Last(), Goal));
				if (dot < best)
					continue;
				angle = i * 60f - 30f;
				best = dot;
			}
		}
		var point = Grid.Snap(Path.Last() + Rotation.FromYaw(angle).Forward * 30f);
		Path.Add(point);
		if (Path.Count > 40)
			return;
		Advance(invalid, true, angle);
	}

	public Vector3 EntityRoot {get; set;}
	protected override void OnUpdate() {
		if (WaitingForGo && HitGo) {
			Go();
			WaitingForGo = false;
		}
		if (!Grid.Snap(OnReachTargetPosition).AlmostEqual(Grid.Snap(Goal)))
			OnReachTarget = null;
		if (!WaitingForGo && Path.Count == 0 && Grid.Snap(EntityRoot).AlmostEqual(Grid.Snap(Goal)))
			OnReachTarget?.Invoke();
		base.OnUpdate();
		var a = Components.Get<NpcAnimator>();
		if (!a.IsValid())
			return;
		Path ??= new();
		UpdateActivity(a);
		if (Path.Count == 0)
			a.Owner.SnapToGrid();
		var root = a.Owner.Renderer.GetAttachment("root").Value.Position;
		root.z = 0;
		root = WorldTransform.PointToWorld(WorldTransform.PointToLocal(root).WithY(0f));
		if (Grid.Snap(root).Distance(root) < 5)
			root = Grid.Snap(root);
		//DebugOverlay.Sphere(new(root, 2), Color.Green, 0.5f);
		EntityRoot = root;
		if (Path.Count == 0)
			return;
		if (root.AlmostEqual(Path[0]))
			Path.RemoveAt(0);
	}

	private void UpdateActivity(NpcAnimator a) {
		if (!DontUpdateActivity && !a.ActivitySet.NonInteruptActivities.Contains(a.Activity)) {
			a.Activity = "walk";
			if (Run)
				a.Activity = "jog";
			if (Path.Count == 0) {
				a.Activity = "idle";
				return;
			}
			if (Path[0].AlmostEqual(WorldPosition))
				Path.RemoveAt(0);
			if (Path.Count == 0) {
				a.Activity = "idle";
				return;
			}
		}
		if (Path.Count == 0)
			return;
		var newrot = Rotation.LookAt(Vector3.Direction(EntityRoot, Path[0]), Vector3.Up);
		if (newrot != WorldRotation) {
			WorldPosition = WorldPosition.RotateAround(EntityRoot, Rotation.Difference(WorldRotation, newrot));
			WorldRotation = newrot;
			a.Owner.SnapToGrid();
		}
	}
}