Point'n Click 2D pathFinding

Share your advanced PureBasic knowledge/code with the community.
benubi
Enthusiast
Enthusiast
Posts: 219
Joined: Tue Mar 29, 2005 4:01 pm

Re: Point'n Click 2D pathFinding

Post by benubi »

Hello,

this looks impressive, good job!

I believe the point is stuck because it lands on the line itself and there is a collision 360° no matter in which direction you "shoot". I'd try to keep the "dot" at a minimum distance from the collider lines (dot radius + 0.00000000001). I haven't looked through the entire code, I may not understand it either but I believe this could be the issue. :wink:
User avatar
IceSoft
Addict
Addict
Posts: 1694
Joined: Thu Jun 24, 2004 8:51 am
Location: Germany

Re: Point'n Click 2D pathFinding

Post by IceSoft »

thyphoon wrote: Sat Aug 16, 2025 6:03 pm Thans yes ... Not easy ... but i search how to correct it 😉
For a better debuging maybe it is usefull you can add the start-/end points as variables (not mousepointer position).
Belive! C++ version of Puzzle of Mystralia
Bug Planet
<Wrapper>4PB, PB<game>, =QONK=, PetriDish, Movie2Image, PictureManager,...
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 352
Joined: Sat Dec 25, 2004 2:37 pm

Re: Point'n Click 2D pathFinding

Post by thyphoon »

benubi wrote: Tue Aug 19, 2025 10:22 pm Hello,

this looks impressive, good job!

I believe the point is stuck because it lands on the line itself and there is a collision 360° no matter in which direction you "shoot". I'd try to keep the "dot" at a minimum distance from the collider lines (dot radius + 0.00000000001). I haven't looked through the entire code, I may not understand it either but I believe this could be the issue. :wink:
Yes, I agree with your analysis. As soon as I have a little time, I'll dive back into it to try to resolve this last point.
IceSoft wrote: Wed Aug 20, 2025 6:00 am For a better debuging maybe it is usefull you can add the start-/end points as variables (not mousepointer position).
Yes, I simplified my code a bit to put it here. Because I have somewhat large logs that allow me to analyze each step of the path search.
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 352
Joined: Sat Dec 25, 2004 2:37 pm

Re: Point'n Click 2D pathFinding

Post by thyphoon »

Some improvements and new features. I managed to fix the bug when you were on the square obstacle. In fact, if you're on a segment of an obstacle, you move back one pixel before starting pathfinding; it's the best solution I've found.
You can now activate and deactivate a wall with F1.
I also tested it so you can change the sprite size to give an impression of depth if needed.
There's still a small bug to fix, will you find it? 😜

Code: Select all

; =======================================================================
; Point'n Click 2D pathFinding — Single-file demo (optimised & documented)
; =======================================================================
; FR: Démo autonome de pathfinding 2D pour jeu point&click.
; EN: Self-contained 2D pathfinding demo for point-and-click games.
;
; Notes
; - Code prêt à diffuser : commentaires FR/EN, logs optionnels, gardes anti cas
;   dégénérés, et fonctions documentées (params/retours).
; - Aucune dépendance externe — PureBasic seulement.
; - Les maths restent en entiers pour la compatibilité du rendu PB 2D,
;   les distances utilisent des flottants là où nécessaire.
; =======================================================================


; =============================
; Debug core (module + macro)
; =============================

DeclareModule trace
  EnableExplicit

  ; FR: Active/désactive les logs (#DBG) à la compilation.
  ; EN: Enable/disable build-time logs (#DBG).
  #EnableDebug = #True

  ; -------------------------------------------------------------------
  ; NewLog(msg, procName, moduleName, lineNumber, fileName)
  ; FR: Envoie un message formaté dans la console Debug.
  ;     - msg        : message libre
  ;     - procName   : #PB_Compiler_Procedure (fourni par macro DBG)
  ;     - moduleName : #PB_Compiler_Module    (idem)
  ;     - lineNumber : #PB_Compiler_Line      (idem)
  ;     - fileName   : #PB_Compiler_File      (idem)
  ; EN: Send a formatted log message to the Debug console.
  ; -------------------------------------------------------------------
  Declare NewLog(msg.s, procName.s, moduleName.s, lineNumber.i, fileName.s)

  ; -------------------------------------------------------------------
  ; Macro DBG(tag, msg)
  ; FR: Log compact avec tag facultatif ("" pour aucun).
  ; EN: Compact logger with optional tag ("" for none).
  ; -------------------------------------------------------------------
  Macro DBG(tag, msg)
    CompilerIf trace::#EnableDebug
      If tag <> ""
        trace::NewLog("[" + tag + "] " + msg, #PB_Compiler_Procedure, #PB_Compiler_Module, #PB_Compiler_Line, #PB_Compiler_File)
      Else
        trace::NewLog(msg,              #PB_Compiler_Procedure, #PB_Compiler_Module, #PB_Compiler_Line, #PB_Compiler_File)
      EndIf
    CompilerEndIf
  EndMacro
EndDeclareModule

Module trace
  EnableExplicit

  Procedure NewLog(msg.s, procName.s, moduleName.s, lineNumber.i, fileName.s)
    Protected tagText.s

    ; FR: Étiquette [Module::Proc] si dispo, sinon fallback.
    ; EN: Label [Module::Proc] if available, otherwise a fallback.
    If procName <> ""
      If moduleName <> ""
        tagText = "[" + moduleName + "::" + procName + "]"
      Else
        tagText = "[" + procName + "]"
      EndIf
    Else
      If moduleName <> ""
        tagText = "[" + moduleName + "]"
      Else
        tagText = "[top]"
      EndIf
    EndIf

    Protected shortName.s = GetFilePart(fileName)
    Debug tagText + " " + msg + "  (" + shortName + ":" + Str(lineNumber) + ")"
  EndProcedure
EndModule


; =======================================================================
; Module: vector
; =======================================================================
DeclareModule vector
  EnableExplicit

  ; FR: Point/Vector entier (x,y) — suffisant pour l’écran PB.
  ; EN: Integer point/vector (x,y) — enough for PB 2D screen.
  Structure pointf
    x.l
    y.l
  EndStructure

  ; -------------------------------------------------------------------
  ; new(*v, x, y)
  ; FR: Initialise *v = (x,y).
  ; EN: Init *v = (x,y).
  ; -------------------------------------------------------------------
  Declare new(*v.pointf, x.l, y.l)

  ; -------------------------------------------------------------------
  ; set(*from, *target)
  ; FR: Copie *from -> *target.
  ; EN: Copy *from -> *target.
  ; -------------------------------------------------------------------
  Declare set(*from.pointf, *target.pointf)

  ; -------------------------------------------------------------------
  ; toString(*v) -> s
  ; FR: Représentation lisible "<x, y>".
  ; EN: Human-readable string "<x, y>".
  ; -------------------------------------------------------------------
  Declare.s toString(*v.pointf)

  ; -------------------------------------------------------------------
  ; add(*out, *a, *b)
  ; FR: *out = *a + *b (composante par composante).
  ; EN: *out = *a + *b (component-wise).
  ; -------------------------------------------------------------------
  Declare add(*vResult.pointf, *v1.pointf, *v2.pointf)

  ; -------------------------------------------------------------------
  ; diff(*out, *a, *b)
  ; FR: *out = *b - *a (vecteur AB).
  ; EN: *out = *b - *a (vector AB).
  ; -------------------------------------------------------------------
  Declare diff(*vResult.pointf, *v1.pointf, *v2.pointf)

  ; -------------------------------------------------------------------
  ; dist(*a, *b) -> l
  ; FR: Distance Euclidienne |AB| arrondie (pour usage entier).
  ; EN: Euclidean distance |AB| rounded (integer use).
  ; -------------------------------------------------------------------
  Declare.l dist(*v1.pointf, *v2.pointf)

  ; -------------------------------------------------------------------
  ; length(*v) -> l
  ; FR: Norme Euclidienne |v| arrondie.
  ; EN: Euclidean norm |v| rounded.
  ; -------------------------------------------------------------------
  Declare.l length(*v.pointf)

  ; -------------------------------------------------------------------
  ; normalized(*out, *v)
  ; FR: Renvoie un *out unitaire approché (arrondi) — gardes contre 0.
  ; EN: Returns approximated unit vector *out (rounded) — zero-guarded.
  ; -------------------------------------------------------------------
  Declare normalized(*vResult.pointf, *v.pointf)
EndDeclareModule

Module vector
  EnableExplicit

  Procedure new(*v.pointf, x.l, y.l) : *v\x = x : *v\y = y : EndProcedure

  Procedure set(*from.pointf, *target.pointf)
    *target\x = *from\x : *target\y = *from\y
  EndProcedure

  Procedure.s toString(*v.pointf)
    ProcedureReturn "<" + Str(*v\x) + ", " + Str(*v\y) + ">"
  EndProcedure

  Procedure add(*vResult.pointf, *v1.pointf, *v2.pointf)
    *vResult\x = *v1\x + *v2\x : *vResult\y = *v1\y + *v2\y
  EndProcedure

  Procedure diff(*vResult.pointf, *v1.pointf, *v2.pointf)
    *vResult\x = *v2\x - *v1\x : *vResult\y = *v2\y - *v1\y
  EndProcedure

  Procedure.l dist(*v1.pointf, *v2.pointf)
    Protected vResult.pointf
    vResult\x = *v2\x - *v1\x : vResult\y = *v2\y - *v1\y
    ProcedureReturn length(@vResult)
  EndProcedure

  Procedure.l length(*v.pointf)
    ProcedureReturn Sqr(*v\x * *v\x + *v\y * *v\y)
  EndProcedure

  Procedure normalized(*vResult.pointf, *v.pointf)
    Protected len.l = length(*v)
    If len <> 0
      *vResult\x = *v\x / len
      *vResult\y = *v\y / len
    Else
      *vResult\x = 0 : *vResult\y = 0
    EndIf
  EndProcedure
EndModule


; =======================================================================
; Module: polygon
; =======================================================================
DeclareModule polygon
  EnableExplicit
  UseModule vector

  ; Const
  #Epsilon = 0.01

  ; FR: Un polygone activable avec nom + liste de sommets (horaire).
  ; EN: Toggleable polygon with name + vertex list (clockwise).
  Structure Polygon
    List Polygon.pointf()
    Name.s
    Enabled.b
  EndStructure

  ; ===================== API =====================
  ; addPoint(polygon(), x, y)
  ; FR: Ajoute un point (avec conversion Y de la démo).
  ; EN: Add a point (with demo Y transform).
  Declare addPoint(List polygon.pointf(), x.l, y.l)

  ; distanceBetwenPoint(x1,y1,x2,y2) -> f
  ; FR: Distance Euclidienne (float).
  ; EN: Euclidean distance (float).
  Declare.f distanceBetwenPoint(x1, y1, x2, y2)

  ; pointInside(*p, polygon()) -> b
  ; FR: Test point-in-polygon (ray casting).
  ; EN: Point-in-polygon (ray casting).
  Declare pointInside(*p.pointf, List polygon.pointf())

  ; distanceToSegment(Px,Py, x1,y1, x2,y2) -> f
  ; FR: Distance point-segment robuste (clamp + epsilon).
  ; EN: Robust point-segment distance (clamp + epsilon).
  Declare.f distanceToSegment(Px.l, Py.l, x1.l, y1.l, x2.l, y2.l)

  ; getClosestPointOnEdge(*out, *p, polygon())
  ; FR: Projeté de *p sur l’arête la plus proche du polygone.
  ; EN: Project *p onto the polygon’s nearest edge.
  Declare getClosestPointOnEdge(*resultV.pointf, *p3.pointf, List polygon.pointf())

  ; isVertexConcave(polygon(), Index) -> b
  ; FR: Retourne #True si le sommet est concave (orientation).
  ; EN: Returns #True if vertex is concave (orientation).
  Declare isVertexConcave(List polygon.pointf(), Index.i)

  ; nearestSegment(*p, polygon(), distMax=5) -> index
  ; FR: Index du segment le plus proche (ou -1).
  ; EN: Index of nearest segment (or -1).
  Declare nearestSegment(*p.pointf, List polygon.pointf(), distMax = 5)

  ; inLineOfSight(*p1,*p2, polygon(), obstacle=#False) -> b
  ; FR: Visibilité entre *p1 et *p2 en tenant compte du polygone.
  ; EN: LoS between *p1 and *p2 using polygon.
  Declare.b inLineOfSight(*p1.pointf, *p2.pointf, List polygon.pointf(), obstacle.b = #False)

  ; inLineOfSightGlobal(*p1,*p2, walkArea()) -> b
  ; FR: Visibilité globale (walk area + obstacles Enabled).
  ; EN: Global LoS (walk area + Enabled obstacles).
  Declare.b inLineOfSightGlobal(*p1.pointf, *p2.pointf, List walkArea.Polygon())

  ; drawPoly(polygon(), lineColor, pointColor)
  ; FR/EN: Render helper for debug.
  Declare drawPoly(List polygon.pointf(), lineColor = #White, pointColor = #White)

  ; SetMeta(*poly, label, enable=#True)
  ; FR/EN: Set name and enabled flag.
  Declare SetMeta(*poly.Polygon, label.s, enable.b = #True)

  ; firstHitOnObstacles(*A,*B, walkArea(), *hit) -> b
  ; FR: Première intersection AB avec obstacles actifs.
  ; EN: First AB hit against active obstacles.
  Declare.b firstHitOnObstacles(*A.pointf, *B.pointf, List walkArea.Polygon(), *hit.pointf)

  ; PushPerpFromNearestSegment(*pt, walk(), step=10, tol=24)
  ; FR: Recul perpendiculaire vers l’espace libre depuis le bord proche.
  ; EN: Perpendicular push away from nearest edge towards free space.
  Declare  PushPerpFromNearestSegment(*pt.vector::pointf, List walk.polygon::Polygon(), Stepi.f = 10.0, tol.f = 24.0)
EndDeclareModule

Module polygon
  EnableExplicit
  UseModule trace
  UseModule vector

  Procedure SetMeta(*poly.Polygon, label.s, enable.b = #True)
    *poly\Name = label : *poly\Enabled = enable
  EndProcedure

  Procedure addPoint(List polygon.pointf(), x.l, y.l)
    ; NB demo: mapping en coordonnées écran
    AddElement(polygon())
    polygon()\x = x * 2
    polygon()\y = y * 2 - 400
  EndProcedure

  Procedure.f distanceBetwenPoint(x1, y1, x2, y2)
    ProcedureReturn Sqr((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))
  EndProcedure

  Procedure pointInside(*p.pointf, List polygon.pointf())
    ; FR: Test par comptage d’intersections (crossing number).
    ; EN: Ray cast / crossing number test.
    Protected cn.l = 0
    Define pa.pointf, pb.pointf
    Protected i.l

    For i = 0 To ListSize(polygon()) - 1
      SelectElement(polygon(), i)
      pa\x = polygon()\x : pa\y = polygon()\y

      If i = ListSize(polygon()) - 1
        SelectElement(polygon(), 0)
      Else
        SelectElement(polygon(), i + 1)
      EndIf

      pb\x = polygon()\x : pb\y = polygon()\y

      If (((pa\y <= *p\y) And (pb\y > *p\y)) Or ((pa\y > *p\y) And (pb\y <= *p\y)))
        Protected vt.f = (*p\y - pa\y) / (pb\y - pa\y)
        If (*p\x < pa\x + vt * (pb\x - pa\x))
          cn + 1
        EndIf
      EndIf
    Next

    ProcedureReturn (cn & 1)
  EndProcedure

  Procedure.f distanceToSegment(Px.l, Py.l, x1.l, y1.l, x2.l, y2.l)
    Protected.f dx = x2 - x1, dy = y2 - y1
    Protected.f denom = dx * dx + dy * dy

    If denom <= #Epsilon
      ProcedureReturn distanceBetwenPoint(Px, Py, x1, y1)
    EndIf

    Protected.f ratio = ((Px - x1) * dx + (Py - y1) * dy) / denom
    If     ratio < 0.0 - #Epsilon : ratio = 0.0
    ElseIf ratio > 1.0 + #Epsilon : ratio = 1.0
    EndIf

    Protected.f qx = (1.0 - ratio) * x1 + ratio * x2
    Protected.f qy = (1.0 - ratio) * y1 + ratio * y2
    ProcedureReturn distanceBetwenPoint(Px, Py, qx, qy)
  EndProcedure

  Procedure getClosestPointOnEdge(*resultV.pointf, *p3.pointf, List polygon.pointf())
    ; FR: Renvoie dans *resultV le point du polygone (sur son arête la plus
    ;     proche) qui est au plus près de *p3. Robuste cas dégénérés.
    ; EN: Writes into *resultV the closest point to *p3 lying on the nearest
    ;     polygon edge. Robust to degenerate edges.

    If ListSize(polygon()) < 2
      vector::set(*p3, *resultV)
      DBG("polygon", "getClosestPointOnEdge: polygon <2 points, no-op")
      ProcedureReturn
    EndIf

    Protected.f tx = *p3\x, ty = *p3\y
    Protected vi1 = -1, vi2 = -1
    Protected.f mindist = 1.0e12

    Protected.l ia, ib
    For ia = 0 To ListSize(polygon()) - 1
      Protected.pointf va, vb
      SelectElement(polygon(), ia) : va\x = polygon()\x : va\y = polygon()\y
      If ia = ListSize(polygon()) - 1 : ib = 0 : Else : ib = ia + 1 : EndIf
      SelectElement(polygon(), ib) : vb\x = polygon()\x : vb\y = polygon()\y

      Protected dist.f = distanceToSegment(tx, ty, va\x, va\y, vb\x, vb\y)
      If dist < mindist
        mindist = dist : vi1 = ia : vi2 = ib
      EndIf
    Next

    If vi1 < 0 Or vi2 < 0
      vector::set(*p3, *resultV)
      DBG("polygon", "getClosestPointOnEdge: vi1/vi2 invalid, no-op")
      ProcedureReturn
    EndIf

    Protected.f x1, y1, x2, y2, x3, y3
    SelectElement(polygon(), vi1) : x1 = polygon()\x : y1 = polygon()\y
    SelectElement(polygon(), vi2) : x2 = polygon()\x : y2 = polygon()\y
    x3 = *p3\x : y3 = *p3\y

    Protected.f dx = x2 - x1, dy = y2 - y1
    Protected.f denom = dx * dx + dy * dy
    If denom <= polygon::#Epsilon
      Protected.f d1 = distanceBetwenPoint(x1, y1, x3, y3)
      Protected.f d2 = distanceBetwenPoint(x2, y2, x3, y3)
      If d1 <= d2 : vector::new(*resultV, x1, y1) : Else : vector::new(*resultV, x2, y2) : EndIf
      ProcedureReturn
    EndIf

    Protected.f u = (((x3 - x1) * dx) + ((y3 - y1) * dy)) / denom
    If u < 0.0 : u = 0.0 : ElseIf u > 1.0 : u = 1.0 : EndIf
    vector::new(*resultV, x1 + u * dx, y1 + u * dy)
  EndProcedure

  Procedure isVertexConcave(List polygon.pointf(), Index.i)
    ; FR: Test de concavité via produit vectoriel.
    ; EN: Concavity test using cross product.
    Protected.i nextIndex, prevIndex
    Protected.f currentX, currentY, nextX, nextY, previousX, previousY

    SelectElement(polygon(), Index)
    currentX = polygon()\x : currentY = polygon()\y

    nextIndex = Index + 1 : If nextIndex > ListSize(polygon()) - 1 : nextIndex = 0 : EndIf
    SelectElement(polygon(), nextIndex)
    nextX = polygon()\x : nextY = polygon()\y

    prevIndex = Index - 1 : If prevIndex < 0 : prevIndex = ListSize(polygon()) - 1 : EndIf
    SelectElement(polygon(), prevIndex)
    previousX = polygon()\x : previousY = polygon()\y

    Protected.f leftX = currentX - previousX
    Protected.f leftY = currentY - previousY
    Protected.f rightX = nextX - currentX
    Protected.f rightY = nextY - currentY
    Protected.f cross = (leftX * rightY) - (leftY * rightX)
    ProcedureReturn Bool(cross < 0)
  EndProcedure

  Procedure Max(a, b) : If a > b : ProcedureReturn a : EndIf : ProcedureReturn b : EndProcedure
  Procedure Min(a, b) : If a < b : ProcedureReturn a : EndIf : ProcedureReturn b : EndProcedure

  Procedure nearestSegment(*p.pointf, List polygon.pointf(), distMax = 5)
    ; FR: Renvoie l’index du segment le plus proche si à <= distMax.
    ; EN: Returns the nearest segment index if within <= distMax.
    Protected pa.pointf, pb.pointf
    Protected segment.l = -1
    Protected minDist.l = 4096
    Protected i.l

    For i = 0 To ListSize(polygon()) - 1
      SelectElement(polygon(), i)
      pa\x = polygon()\x : pa\y = polygon()\y

      If i = ListSize(polygon()) - 1
        SelectElement(polygon(), 0)
      Else
        SelectElement(polygon(), i + 1)
      EndIf

      pb\x = polygon()\x : pb\y = polygon()\y

      Protected dist.l = distanceToSegment(*p\x, *p\y, pa\x, pa\y, pb\x, pb\y)
      If dist <= distMax And dist < minDist
        segment = ListIndex(polygon())
        DBG("polygon", "Found segment: " + Str(segment))
        minDist = dist
      EndIf
    Next
    ProcedureReturn segment
  EndProcedure

  ; -------------------------------------------------------------------
  ; LineIntersection(L1:[PA,PB], L2:[PA,PB], *Cross) -> 0/1/2
  ; FR: 1 si intersection sur les 2 segments, 0 sinon, 2 si droites
  ;     sécantes mais en dehors des segments. *Cross rempli.
  ; EN: 1 if the segments intersect, 0 no, 2 if infinite lines cross
  ;     but the hit is outside segments. *Cross filled.
  ; -------------------------------------------------------------------
  Procedure LineIntersection(*L1PA.pointf, *L1PB.pointf, *L2PA.pointf, *L2PB.pointf, *Cross.pointf)
    Protected.f A1, B1, C1, A2, B2, C2, det, tx, ty

    A1 = *L1PB\y - *L1PA\y : B1 = *L1PA\x - *L1PB\x : C1 = A1 * *L1PA\x + B1 * *L1PA\y
    A2 = *L2PB\y - *L2PA\y : B2 = *L2PA\x - *L2PB\x : C2 = A2 * *L2PA\x + B2 * *L2PA\y

    det = A1 * B2 - A2 * B1
    If Abs(det) <= #Epsilon
      Protected.f dpa = Abs(A1 * *L2PA\x + B1 * *L2PA\y - C1)
      Protected.f dpb = Abs(A1 * *L2PB\x + B1 * *L2PB\y - C1)

      If dpa <= #Epsilon And dpb <= #Epsilon
        Protected.f min1x = Min(*L1PA\x, *L1PB\x), max1x = Max(*L1PA\x, *L1PB\x)
        Protected.f min2x = Min(*L2PA\x, *L2PB\x), max2x = Max(*L2PA\x, *L2PB\x)
        Protected.f min1y = Min(*L1PA\y, *L1PB\y), max1y = Max(*L1PA\y, *L1PB\y)
        Protected.f min2y = Min(*L2PA\y, *L2PB\y), max2y = Max(*L2PA\y, *L2PB\y)

        If (Min(max1x, max2x) + #Epsilon >= Max(min1x, min2x)) And (Min(max1y, max2y) + #Epsilon >= Max(min1y, min2y))
          *Cross\x = *L2PA\x : *Cross\y = *L2PA\y
          ProcedureReturn 1
        Else
          ProcedureReturn 0
        EndIf
      EndIf
      ProcedureReturn 0
    EndIf

    tx = (B2 * C1 - B1 * C2) / det
    ty = (A1 * C2 - A2 * C1) / det
    *Cross\x = tx : *Cross\y = ty

    If tx < Min(*L1PA\x, *L1PB\x) - #Epsilon Or tx > Max(*L1PA\x, *L1PB\x) + #Epsilon Or ty < Min(*L1PA\y, *L1PB\y) - #Epsilon Or ty > Max(*L1PA\y, *L1PB\y) + #Epsilon
      ProcedureReturn 2
    EndIf
    If tx < Min(*L2PA\x, *L2PB\x) - #Epsilon Or tx > Max(*L2PA\x, *L2PB\x) + #Epsilon Or ty < Min(*L2PA\y, *L2PB\y) - #Epsilon Or ty > Max(*L2PA\y, *L2PB\y) + #Epsilon
      ProcedureReturn 2
    EndIf

    ProcedureReturn 1
  EndProcedure

  Procedure.b inLineOfSight(*p1.pointf, *p2.pointf, List polygon.pointf(), obstacle.b = #False)
    ; FR: LoS contre un polygone (edges bloquants) + correction pour
    ;     cas de points sur sommets/segments (epsilon).
    ; EN: LoS against one polygon, robust near/at vertices & edges.
    Protected.pointf tmp, tmp2, c, d, mid
    vector::set(*p1, @tmp) : vector::set(*p2, @tmp2)

    Protected.l i, ni
    For i = 0 To ListSize(polygon()) - 1
      SelectElement(polygon(), i) : vector::set(@polygon(), @c)
      ni = i + 1 : If ni > ListSize(polygon()) - 1 : ni = 0 : EndIf
      SelectElement(polygon(), ni) : vector::set(@polygon(), @d)

      If (*p1\x = c\x And *p1\y = c\y And *p2\x = d\x And *p2\y = d\y) Or (*p1\x = d\x And *p1\y = d\y And *p2\x = c\x And *p2\y = c\y)
        ProcedureReturn #True
      EndIf

      If (*p1\x = c\x And *p1\y = c\y And distanceToSegment(*p2\x, *p2\y, c\x, c\y, d\x, d\y) <= 3.0 + #Epsilon) Or
         (*p1\x = d\x And *p1\y = d\y And distanceToSegment(*p2\x, *p2\y, c\x, c\y, d\x, d\y) <= 3.0 + #Epsilon)
        ProcedureReturn #True
      EndIf

      Protected cross.pointf
      If LineIntersection(@tmp, @tmp2, @c, @d, @cross) = 1
        If distanceToSegment(tmp\x, tmp\y, c\x, c\y, d\x, d\y) > #Epsilon And distanceToSegment(tmp2\x, tmp2\y, c\x, c\y, d\x, d\y) > #Epsilon
          ProcedureReturn #False
        EndIf
      EndIf
    Next

    vector::add(@mid, @tmp, @tmp2) : mid\x / 2 : mid\y / 2
    Protected.b result = pointInside(@mid, polygon())
    If obstacle : result = 1 - result : EndIf
    ProcedureReturn result
  EndProcedure

  Procedure.b inLineOfSightGlobal(*p1.pointf, *p2.pointf, List walkArea.Polygon())
    ; FR: walkArea(0)=zone libre, autres=obstacles (#Enabled).
    ; EN: walkArea(0)=free area, others=obstacles (#Enabled).
    Protected.b result = #False, obstacle = #False

    ForEach walkArea()
      If ListIndex(walkArea()) > 0
        If walkArea()\Enabled = #False : Continue : EndIf
        obstacle = #True
      Else
        obstacle = #False
      EndIf

      result = inLineOfSight(*p1, *p2, walkArea()\Polygon(), obstacle)
      If result = #False : Break : EndIf
    Next
    ProcedureReturn result
  EndProcedure

  Procedure.b firstHitOnObstacles(*A.pointf, *B.pointf, List walkArea.Polygon(), *hit.pointf)
    ; FR: Première intersection du segment AB avec obstacles actifs.
    ;     Retourne #True si trouvée et remplit *hit.
    ; EN: First hit of AB against active obstacles. Returns #True and
    ;     fills *hit if found.
    Protected.f bestT = 1.0e12
    Protected found.b = #False

    Protected.f ax = *A\x, ay = *A\y
    Protected.f bx = *B\x, by = *B\y
    Protected.f dx = bx - ax, dy = by - ay
    Protected.f denom = dx * dx + dy * dy
    If denom = 0 : ProcedureReturn #False : EndIf

    Protected.pointf c, d, cross
    Protected i.l, ni.l

    ForEach walkArea()
      If ListIndex(walkArea()) = 0 : Continue : EndIf
      If walkArea()\Enabled = #False : Continue : EndIf

      For i = 0 To ListSize(walkArea()\Polygon()) - 1
        SelectElement(walkArea()\Polygon(), i) : c\x = walkArea()\Polygon()\x : c\y = walkArea()\Polygon()\y
        ni = i + 1 : If ni > ListSize(walkArea()\Polygon()) - 1 : ni = 0 : EndIf
        SelectElement(walkArea()\Polygon(), ni) : d\x = walkArea()\Polygon()\x : d\y = walkArea()\Polygon()\y

        If LineIntersection(*A, *B, @c, @d, @cross) = 1
          Protected.f t = ((cross\x - ax) * dx + (cross\y - ay) * dy) / denom
          If t >= 0.0 And t <= 1.0 And t < bestT
            bestT = t : *hit\x = cross\x : *hit\y = cross\y : found = #True
          EndIf
        EndIf
      Next
    Next
    ProcedureReturn found
  EndProcedure

  ; ------------------------------------------------------------
  ; IsFreeAt(x,y, walk()) -> b
  ; FR: Vrai si (x,y) est dans la zone walkArea(0) et hors obstacles
  ;     Enabled. Préserve le curseur de liste.
  ; EN: True if (x,y) is inside walkArea(0) and outside Enabled obstacles.
  ; ------------------------------------------------------------
  Procedure.b IsFreeAt(x.f, y.f, List walk.polygon::Polygon())
    Protected pt.vector::pointf : pt\x = x : pt\y = y
    Protected i.l, curIdx.l = ListIndex(walk())

    If ListSize(walk()) = 0 : ProcedureReturn #False : EndIf

    SelectElement(walk(), 0)
    If polygon::pointInside(@pt, walk()\Polygon()) = #False
      SelectElement(walk(), curIdx) : ProcedureReturn #False
    EndIf

    For i = 1 To ListSize(walk()) - 1
      SelectElement(walk(), i)
      If walk()\Enabled
        If polygon::pointInside(@pt, walk()\Polygon())
          SelectElement(walk(), curIdx) : ProcedureReturn #False
        EndIf
      EndIf
    Next

    SelectElement(walk(), curIdx)
    ProcedureReturn #True
  EndProcedure

  ; ------------------------------------------------------------
  ; PushPerpFromNearestSegment(*pt, walk(), step, tol)
  ; FR: Recul perpendiculaire depuis le segment le plus proche, en
  ;     choisissant la normale qui mène vers l’espace libre (tests IsFreeAt).
  ; EN: Perpendicular push from nearest edge, choosing the normal that
  ;     goes toward free space (IsFreeAt checks).
  ; ------------------------------------------------------------
  Procedure PushPerpFromNearestSegment(*pt.vector::pointf, List walk.polygon::Polygon(), Stepi.f = 10.0, tol.f = 24.0)
    Protected found.b = #False
    Protected.f bestDist = 1.0e12
    Protected.f ax, ay, bx, by, qx, qy, dx, dy, denom, u, d
    Protected bestPolyIdx.l = -1, bestPbIdx.l = -1

    ForEach walk()
      If ListIndex(walk()) > 0 And walk()\Enabled = #False : Continue : EndIf

      Protected seg = polygon::nearestSegment(*pt, walk()\Polygon(), tol)
      If seg >= 0
        Protected n = ListSize(walk()\Polygon())
        Protected pbIndex = seg
        Protected paIndex = pbIndex - 1 : If paIndex < 0 : paIndex = n - 1 : EndIf

        SelectElement(walk()\Polygon(), paIndex) : ax = walk()\Polygon()\x : ay = walk()\Polygon()\y
        SelectElement(walk()\Polygon(), pbIndex) : bx = walk()\Polygon()\x : by = walk()\Polygon()\y

        d = polygon::distanceToSegment(*pt\x, *pt\y, ax, ay, bx, by)
        If d < bestDist
          bestDist = d : bestPolyIdx = ListIndex(walk()) : bestPbIdx = pbIndex : found = #True
        EndIf
      EndIf
    Next

    If found = #False : ProcedureReturn : EndIf

    SelectElement(walk(), bestPolyIdx)
    n = ListSize(walk()\Polygon())
    pbIndex = bestPbIdx
    paIndex = pbIndex - 1 : If paIndex < 0 : paIndex = n - 1 : EndIf

    SelectElement(walk()\Polygon(), paIndex) : ax = walk()\Polygon()\x : ay = walk()\Polygon()\y
    SelectElement(walk()\Polygon(), pbIndex) : bx = walk()\Polygon()\x : by = walk()\Polygon()\y

    dx = bx - ax : dy = by - ay
    denom = dx*dx + dy*dy
    If denom <= polygon::#Epsilon
      qx = ax : qy = ay
    Else
      u = ((*pt\x - ax) * dx + (*pt\y - ay) * dy) / denom
      If u < 0.0 : u = 0.0 : ElseIf u > 1.0 : u = 1.0 : EndIf
      qx = ax + u * dx : qy = ay + u * dy
    EndIf

    Protected.f nx1 = dy, ny1 = -dx
    Protected.f len = Sqr(nx1*nx1 + ny1*ny1)
    If len <= polygon::#Epsilon : ProcedureReturn : EndIf
    nx1 / len : ny1 / len
    Protected.f nx2 = -nx1, ny2 = -ny1

    Protected probe.f = 2.0
    Protected curIdx.l = ListIndex(walk())
    Protected useNX.f, useNY.f

    If IsFreeAt(qx + nx1 * probe, qy + ny1 * probe, walk())
      useNX = nx1 : useNY = ny1
    ElseIf IsFreeAt(qx + nx2 * probe, qy + ny2 * probe, walk())
      useNX = nx2 : useNY = ny2
    Else
      useNX = *pt\x - qx : useNY = *pt\y - qy
      len = Sqr(useNX*useNX + useNY*useNY)
      If len <= polygon::#Epsilon : ProcedureReturn : EndIf
      useNX / len : useNY / len
    EndIf
    SelectElement(walk(), curIdx)

    Protected.f tryStep = Stepi
    Protected.f tx = *pt\x + useNX * tryStep
    Protected.f ty = *pt\y + useNY * tryStep

    If IsFreeAt(tx, ty, walk()) = #False
      Protected i
      For i = 0 To 6
        tryStep * 0.5
        tx = *pt\x + useNX * tryStep
        ty = *pt\y + useNY * tryStep
        If IsFreeAt(tx, ty, walk()) Or tryStep < 0.5 : Break : EndIf
      Next
    EndIf

    If IsFreeAt(tx, ty, walk())
      *pt\x = tx : *pt\y = ty
    EndIf
  EndProcedure

  Procedure drawPoly(List polygon.pointf(), lineColor = #White, pointColor = #White)
    Protected pa.pointf, pb.pointf
    Protected i.l
    For i = 0 To ListSize(polygon()) - 1
      SelectElement(polygon(), i)
      pa\x = polygon()\x : pa\y = polygon()\y
      If i = ListSize(polygon()) - 1
        SelectElement(polygon(), 0)
      Else
        SelectElement(polygon(), i + 1)
      EndIf
      pb\x = polygon()\x : pb\y = polygon()\y
      LineXY(pa\x, pa\y, pb\x, pb\y, lineColor)
      Circle(pa\x, pa\y, 5, pointColor)
    Next
  EndProcedure
EndModule


; ======================================================================
; Module: scale — KNN(3) + Shepard IDW
; ======================================================================
DeclareModule scale
  EnableExplicit
  UseModule vector
  UseModule trace

  #EnableDebug = #True

  #ScaleDefault = 1.0
  #ScaleMin     = 0.20
  #ScaleMax     = 4.00
  #Epsilon      = 0.0001

  Structure Anchor
    pos.vector::pointf
    size.f
    name.s
  EndStructure

  ; AddScalePoint(x,y,size[,name])
  Declare AddScalePoint(x.l, y.l, size.f, name.s = "")
  ; Clear()
  Declare Clear()
  ; At(x,y) -> f
  Declare.f At(x.l, y.l)
  ; Ease(current,target[,amount]) -> f
  Declare.f Ease(current.f, target.f, amount.f = 0.15)
  ; draw([color])
  Declare draw(color = #Yellow)
EndDeclareModule

Module scale
  EnableExplicit
  UseModule vector
  UseModule trace

  Global NewList gAnchors.Anchor()

  Procedure AddScalePoint(x.l, y.l, size.f, name.s = "")
    AddElement(gAnchors())
    gAnchors()\pos\x = x : gAnchors()\pos\y = y
    gAnchors()\size  = size
    gAnchors()\name  = name
    DBG("scale", "Add anchor '" + name + "' at (" + Str(x) + "," + Str(y) + ") size=" + StrF(size, 2))
  EndProcedure

  Procedure Clear()
    ClearList(gAnchors())
    DBG("scale", "Clear anchors")
  EndProcedure

  Procedure.f At(x.l, y.l)
    ; FR: Shepard IDW sur K=3 plus proches, clamp min/max.
    ; EN: Shepard IDW using the 3 nearest anchors, clamped.
    If ListSize(gAnchors()) = 0 : ProcedureReturn #ScaleDefault : EndIf
    If ListSize(gAnchors()) = 1 : FirstElement(gAnchors()) : ProcedureReturn gAnchors()\size : EndIf

    Protected.f px = x, py = y

    Protected.f bestD2_0 = 1.0e30, bestD2_1 = 1.0e30, bestD2_2 = 1.0e30
    Protected.f bestS_0  = #ScaleDefault, bestS_1  = #ScaleDefault, bestS_2  = #ScaleDefault
    Protected found0.b = #False, found1.b = #False, found2.b = #False

    ForEach gAnchors()
      Protected.f dx = px - gAnchors()\pos\x
      Protected.f dy = py - gAnchors()\pos\y
      Protected.f d2 = dx*dx + dy*dy
      If d2 < #Epsilon : d2 = #Epsilon : EndIf

      If d2 < bestD2_0
        bestD2_2 = bestD2_1 : bestS_2 = bestS_1 : found2 = found1
        bestD2_1 = bestD2_0 : bestS_1 = bestS_0 : found1 = found0
        bestD2_0 = d2       : bestS_0 = gAnchors()\size : found0 = #True
      ElseIf d2 < bestD2_1
        bestD2_2 = bestD2_1 : bestS_2 = bestS_1 : found2 = found1
        bestD2_1 = d2       : bestS_1 = gAnchors()\size : found1 = #True
      ElseIf d2 < bestD2_2
        bestD2_2 = d2       : bestS_2 = gAnchors()\size : found2 = #True
      EndIf
    Next

    Protected.f num = 0.0, den = 0.0, w.f
    If found0 : w = 1.0 / (bestD2_0 + #Epsilon) : num + w * bestS_0 : den + w : EndIf
    If found1 : w = 1.0 / (bestD2_1 + #Epsilon) : num + w * bestS_1 : den + w : EndIf
    If found2 : w = 1.0 / (bestD2_2 + #Epsilon) : num + w * bestS_2 : den + w : EndIf

    Protected.f s : If den > 0.0 : s = num / den : Else : s = #ScaleDefault : EndIf
    If #ScaleMin > 0.0 And s < #ScaleMin : s = #ScaleMin : EndIf
    If #ScaleMax > 0.0 And s > #ScaleMax : s = #ScaleMax : EndIf
    ProcedureReturn s
  EndProcedure

  Procedure.f Ease(current.f, target.f, amount.f = 0.15)
    If amount <= 0.0 : amount = 0.15 : EndIf
    If amount >  1.0 : amount = 1.0  : EndIf
    ProcedureReturn current + (target - current) * amount
  EndProcedure

  Procedure draw(color = #Yellow)
    ForEach gAnchors()
      Circle(gAnchors()\pos\x, gAnchors()\pos\y, 4, color)
      DrawText(gAnchors()\pos\x + 6, gAnchors()\pos\y - 6, StrF(gAnchors()\size, 2) + " " + gAnchors()\name, color)
    Next
  EndProcedure
EndModule


; =======================================================================
; Module: Pathfinding
; =======================================================================
DeclareModule Pathfinding
  EnableExplicit
  UseModule vector

  ; dijkstra(Graph(2), walkPath(), srcIndex, destIndex)
  Declare dijkstra(Array graph.l(2), List walkPath.l(), srcIndex.i, destIndex.i)

  ; getWalkPath(*Start, *Target, walkArea(), walkPath())
  Declare getWalkPath(*Start.vector::pointf, *Target.vector::pointf, List walkArea.polygon::Polygon(), List walkPath.vector::pointf())

  ; getPositionOnPath(walkPath(), Distance, *Position)
  Declare getPositionOnPath(List walkPath.vector::pointf(), Distance.l, *Position.vector::pointf)

  ; drawPath(walkPath())
  Declare drawPath(List walkPath.vector::pointf())

  ; getPathLength(walkPath()) -> l
  Declare.l getPathLength(List walkPath.vector::pointf())
EndDeclareModule

Module Pathfinding
  #EnableDebug=#True
  EnableExplicit
  UseModule vector
  UseModule trace

  #M = 999999

  Procedure dijkstra(Array graph.l(2), List walkPath.l(), srcIndex.i, destIndex.i)
    ; FR: Dijkstra simple sur matrice d’adjacence (coûts entiers).
    ; EN: Basic Dijkstra on adjacency matrix (integer costs).
    Protected Nb.l = ArraySize(graph(), 1)

    Dim Distance(Nb - 1)
    Dim Predecessor(Nb - 1)
    Dim Mark(Nb - 1)

    Protected z, MinK, MinD
    For z = 0 To Nb - 1 : Mark(z) = #False : Next

    Mark(srcIndex) = #True
    Distance(srcIndex) = 0
    Predecessor(srcIndex) = srcIndex

    For z = 0 To Nb - 1
      If z <> srcIndex
        Distance(z) = graph(srcIndex, z)
        Predecessor(z) = srcIndex
      EndIf
    Next

    While Mark(destIndex) = #False
      MinK = -1 : MinD = #M
      For z = 0 To Nb - 1
        If Mark(z) = #False
          If Distance(z) < MinD : MinK = z : MinD = Distance(z) : EndIf
        EndIf
      Next

      If MinD = #M
        DBG("path", "No path between src and dest") : Break
      ElseIf MinK = destIndex
        DBG("path", "Walk path found") : Break
      Else
        Mark(MinK) = #True
      EndIf

      For z = 0 To Nb - 1
        If Mark(z) = #False
          If Distance(MinK) + graph(MinK, z) < Distance(z)
            Distance(z) = Distance(MinK) + graph(MinK, z)
            Predecessor(z) = MinK
          EndIf
        EndIf
      Next
    Wend

    If MinK = destIndex
      ClearList(walkPath())
      AddElement(walkPath()) : walkPath() = destIndex
      z = MinK
      While Predecessor(z) <> srcIndex
        FirstElement(walkPath()) : InsertElement(walkPath()) : walkPath() = Predecessor(z)
        z = Predecessor(z)
      Wend
      FirstElement(walkPath()) : InsertElement(walkPath()) : walkPath() = Predecessor(z)
    EndIf
  EndProcedure

  Procedure drawWalkPath(Array graph.l(2), List walkPointCoord.pointf())
    ; FR: Debug: affiche le graphe de visibilité.
    ; EN: Debug: draws visibility graph.
    Protected pa.pointf, pb.pointf
    Protected.l i, j, Nb = ArraySize(graph(), 1)

    For i = 0 To Nb - 1
      For j = 0 To Nb - 1
        If graph(i, j) <> 0 And graph(i, j) <> #M
          SelectElement(walkPointCoord(), i) : pa\x = walkPointCoord()\x : pa\y = walkPointCoord()\y
          SelectElement(walkPointCoord(), j) : pb\x = walkPointCoord()\x : pb\y = walkPointCoord()\y
          If polygon::distanceToSegment(MouseX(), MouseY(), pa\x, pa\y, pb\x, pb\y) < 1
            LineXY(pa\x, pa\y, pb\x, pb\y, #Green)
          Else
            LineXY(pa\x, pa\y, pb\x, pb\y, #Gray)
          EndIf
          Circle(pa\x, pa\y, 5, #Yellow)
          Circle(pb\x, pb\y, 5, #Yellow)
        EndIf
      Next
    Next
  EndProcedure

  Procedure getWalkPath(*Start.pointf, *Target.pointf, List walkArea.polygon::Polygon(), List walkPath.pointf())
    ; FR: Construit un chemin Start->Target:
    ;   1) recule Start si collé à un bord,
    ;   2) projette Target sur le bord valide le plus proche si hors zone,
    ;   3) graphe de visibilité (sommets concaves/c onvexes),
    ;   4) Dijkstra,
    ;   5) fallback : nœud approché + point d’approche avant impact.
    ;
    ; EN: Build path Start->Target:
    ;   1) push Start if too close to an edge,
    ;   2) project Target to nearest valid edge if out of area,
    ;   3) visibility graph (concave/convex vertices),
    ;   4) Dijkstra,
    ;   5) fallback: closest reachable node + approach point before hit.

    polygon::PushPerpFromNearestSegment(*Start, walkArea(), 1, 2)

    Protected pa.pointf, pb.pointf, p.pointf
    Protected i.l, j.l, k.l, pathIndex.l, nodeCount.l
    Protected.b result, usedFallback = #False

    NewList walkPointCoord.pointf()
    NewList walkPointIndex.l()

    Protected clicked.pointf
    clicked\x = *Target\x : clicked\y = *Target\y

    Protected best.pointf
    Protected.f bestDist = 1.0e12
    Protected bestFound.b = #False

    ForEach walkArea()
      If ListIndex(walkArea()) > 0 And walkArea()\Enabled = #False : Continue : EndIf

      result = polygon::pointInside(@clicked, walkArea()\Polygon())
      If ListIndex(walkArea()) = 0 : result = 1 - result : EndIf

      If result = #True
        polygon::getClosestPointOnEdge(@p, @clicked, walkArea()\Polygon())
        Protected.f d = polygon::distanceBetwenPoint(clicked\x, clicked\y, p\x, p\y)
        If d < bestDist
          bestDist = d : bestFound = #True : best\x = p\x : best\y = p\y
        EndIf
      EndIf
    Next

    If bestFound
      *Target\x = best\x : *Target\y = best\y
    EndIf

    AddElement(walkPointCoord()) : walkPointCoord()\x = *Start\x  : walkPointCoord()\y = *Start\y
    AddElement(walkPointCoord()) : walkPointCoord()\x = *Target\x : walkPointCoord()\y = *Target\y

    If polygon::inLineOfSightGlobal(*Start, *Target, walkArea())
      Dim Graph.l(1, 1)
      Graph(0, 0) = 0 : Graph(1, 1) = 0
      Graph(0, 1) = polygon::distanceBetwenPoint(*Start\x, *Start\y, *Target\x, *Target\y)
      Graph(1, 0) = Graph(0, 1)
      drawWalkPath(Graph(), walkPointCoord())

      ClearList(walkPath())
      AddElement(walkPath()) : walkPath()\x = *Start\x : walkPath()\y = *Start\y
      AddElement(walkPath()) : walkPath()\x = *Target\x : walkPath()\y = *Target\y
      ProcedureReturn
    EndIf

    ForEach walkArea()
      If ListIndex(walkArea()) > 0 And walkArea()\Enabled = #False : Continue : EndIf

      For i = 0 To ListSize(walkArea()\Polygon()) - 1
        SelectElement(walkArea()\Polygon(), i)
        pa\x = walkArea()\Polygon()\x : pa\y = walkArea()\Polygon()\y

        If ListIndex(walkArea()) = 0
          If polygon::isVertexConcave(walkArea()\Polygon(), i) = #True
            AddElement(walkPointCoord()) : walkPointCoord()\x = pa\x : walkPointCoord()\y = pa\y
          EndIf
        Else
          If polygon::isVertexConcave(walkArea()\Polygon(), i) = #False
            AddElement(walkPointCoord()) : walkPointCoord()\x = pa\x : walkPointCoord()\y = pa\y
          EndIf
        EndIf
      Next
    Next

    nodeCount = ListSize(walkPointCoord())
    Dim Graph.l(nodeCount, nodeCount)

    For i = 0 To nodeCount - 1
      SelectElement(walkPointCoord(), i) : pa\x = walkPointCoord()\x : pa\y = walkPointCoord()\y
      For j = 0 To nodeCount - 1
        If i <> j
          SelectElement(walkPointCoord(), j) : pb\x = walkPointCoord()\x : pb\y = walkPointCoord()\y
          If polygon::inLineOfSightGlobal(@pa, @pb, walkArea()) = #True
            Graph(i, j) = polygon::distanceBetwenPoint(pa\x, pa\y, pb\x, pb\y)
          Else
            Graph(i, j) = 999999
          EndIf
        Else
          Graph(i, j) = 0
        EndIf
      Next
    Next

    Pathfinding::dijkstra(Graph(), walkPointIndex(), 0, 1)
    drawWalkPath(Graph(), walkPointCoord())

    If ListSize(walkPointIndex()) = 0
      usedFallback = #True
      DBG("path", "Fallback: cible bloquée, recherche du nœud atteignable le plus proche")
      Protected.f bestScore = 1.0e12
      Protected bestFound2.b = #False
      NewList bestPath.l()

      For k = 0 To nodeCount - 1
        NewList tmpPath.l()
        Pathfinding::dijkstra(Graph(), tmpPath(), 0, k)
        If ListSize(tmpPath()) > 0
          SelectElement(walkPointCoord(), k)
          Protected.f score = polygon::distanceBetwenPoint(walkPointCoord()\x, walkPointCoord()\y, *Target\x, *Target\y)
          If score < bestScore
            bestScore = score : bestFound2 = #True
            ClearList(bestPath())
            ForEach tmpPath() : AddElement(bestPath()) : bestPath() = tmpPath() : Next
          EndIf
        EndIf
      Next

      If bestFound2
        ClearList(walkPointIndex())
        ForEach bestPath() : AddElement(walkPointIndex()) : walkPointIndex() = bestPath() : Next
      EndIf
    EndIf

    ClearList(walkPath())
    For pathIndex = 0 To ListSize(walkPointIndex()) - 1
      SelectElement(walkPointIndex(), pathIndex)
      SelectElement(walkPointCoord(), walkPointIndex())
      AddElement(walkPath())
      walkPath()\x = walkPointCoord()\x : walkPath()\y = walkPointCoord()\y
    Next

    If usedFallback And ListSize(walkPath()) > 0
      LastElement(walkPath()) : pa\x = walkPath()\x : pa\y = walkPath()\y
      Protected hit.pointf
      If polygon::firstHitOnObstacles(@pa, *Target, walkArea(), @hit)
        Protected.f approachMargin = 4.0
        Protected.f dx = *Target\x - pa\x, dy = *Target\y - pa\y
        Protected.f len = Sqr(dx * dx + dy * dy)
        If len <> 0 : dx / len : dy / len : EndIf
        Protected approach.pointf
        approach\x = hit\x - dx * approachMargin
        approach\y = hit\y - dy * approachMargin
        AddElement(walkPath()) : walkPath()\x = approach\x : walkPath()\y = approach\y
        DBG("path", "Ajout point d’approche (" + Str(Int(approach\x)) + "," + Str(Int(approach\y)) + ")")
      EndIf
    EndIf
  EndProcedure

  Procedure getPositionOnPath(List walkPath.pointf(), Distance.l, *Position.pointf)
    ; FR: Place *Position à une Distance le long du chemin (linéaire),
    ;     ignore segments de longueur ~0 pour éviter div/0.
    ; EN: Places *Position at a Distance along the path (linear),
    ;     skipping near-zero segments to avoid div/0.
    Protected n.l
    Protected.l x1, y1, x2, y2
    Protected.f segLen, pathLenSoFar.f, oldPathLen.f, segDist.f

    If ListSize(walkPath()) = 0 : ProcedureReturn : EndIf

    SelectElement(walkPath(), 0)
    *Position\x = walkPath()\x : *Position\y = walkPath()\y
    pathLenSoFar = 0.0

    For n = 0 To ListSize(walkPath()) - 1
      SelectElement(walkPath(), n)
      x1 = walkPath()\x : y1 = walkPath()\y

      If SelectElement(walkPath(), n + 1)
        x2 = walkPath()\x : y2 = walkPath()\y
        oldPathLen = pathLenSoFar
        segLen = polygon::distanceBetwenPoint(x1, y1, x2, y2)
        If segLen <= polygon::#Epsilon : Continue : EndIf
        pathLenSoFar + segLen

        If Distance >= oldPathLen And Distance <= pathLenSoFar
          segDist = Distance - oldPathLen
          *Position\x = x1 + (segDist / segLen) * (x2 - x1)
          *Position\y = y1 + (segDist / segLen) * (y2 - y1)
          Break
        EndIf
      EndIf
    Next
  EndProcedure

  Procedure drawPath(List walkPath.pointf())
    ; FR/EN: Debug draw of the current path.
    Protected.l x1, y1, x2, y2, hypothenus, n
    For n = 0 To ListSize(walkPath()) - 1
      SelectElement(walkPath(), n)
      x1 = walkPath()\x : y1 = walkPath()\y
      If SelectElement(walkPath(), n + 1)
        x2 = walkPath()\x : y2 = walkPath()\y
        hypothenus = polygon::distanceBetwenPoint(x1, y1, x2, y2)
        LineXY(x1, y1, x2, y2, #White)
        Circle(x1, y1, 5, #Red)
        Circle(x2, y2, 5, #Red)
      EndIf
    Next
  EndProcedure

  Procedure.l getPathLength(List walkPath.pointf())
    ; FR/EN: Sum of segment lengths (integer).
    Protected.l PathDistance = 0
    Protected.l x1, y1, x2, y2, hypothenus, n
    For n = 0 To ListSize(walkPath()) - 1
      SelectElement(walkPath(), n)
      x1 = walkPath()\x : y1 = walkPath()\y
      If SelectElement(walkPath(), n + 1)
        x2 = walkPath()\x : y2 = walkPath()\y
        hypothenus = polygon::distanceBetwenPoint(x1, y1, x2, y2)
        PathDistance + hypothenus
      EndIf
    Next
    ProcedureReturn PathDistance
  EndProcedure
EndModule


; =======================================================================
;- Demo / Test harness
; =======================================================================

Structure room
  List walkArea.polygon::Polygon()
EndStructure

Structure chara
  coord.vector::pointf
  target.vector::pointf
  List Path.vector::pointf()
  pathLength.l
  move.b
EndStructure

Define mainChara.chara
mainChara\coord\x = 260 : mainChara\coord\y = 120

Define room.room

; Walkable area (index 0)
AddElement(room\walkArea())
polygon::SetMeta(@room\walkArea(), "Walk Area", #True)
polygon::addPoint(room\walkArea()\Polygon(), 5, 248)
polygon::addPoint(room\walkArea()\Polygon(), 5, 248)    ; (dup pour la forme de la démo)
polygon::addPoint(room\walkArea()\Polygon(), 235, 248)
polygon::addPoint(room\walkArea()\Polygon(), 252, 277)
polygon::addPoint(room\walkArea()\Polygon(), 214, 283)
polygon::addPoint(room\walkArea()\Polygon(), 217, 300)
polygon::addPoint(room\walkArea()\Polygon(), 235, 319)
polygon::addPoint(room\walkArea()\Polygon(), 265, 339)
polygon::addPoint(room\walkArea()\Polygon(), 275, 352)
polygon::addPoint(room\walkArea()\Polygon(), 310, 350)
polygon::addPoint(room\walkArea()\Polygon(), 309, 312)
polygon::addPoint(room\walkArea()\Polygon(), 322, 308)
polygon::addPoint(room\walkArea()\Polygon(), 304, 279)
polygon::addPoint(room\walkArea()\Polygon(), 307, 249)
polygon::addPoint(room\walkArea()\Polygon(), 419, 248)
polygon::addPoint(room\walkArea()\Polygon(), 431, 262)
polygon::addPoint(room\walkArea()\Polygon(), 389, 274)
polygon::addPoint(room\walkArea()\Polygon(), 378, 295)
polygon::addPoint(room\walkArea()\Polygon(), 408, 311)
polygon::addPoint(room\walkArea()\Polygon(), 397, 316)
polygon::addPoint(room\walkArea()\Polygon(), 378, 309)
polygon::addPoint(room\walkArea()\Polygon(), 365, 323)
polygon::addPoint(room\walkArea()\Polygon(), 342, 360)
polygon::addPoint(room\walkArea()\Polygon(), 358, 379)
polygon::addPoint(room\walkArea()\Polygon(), 205, 379)
polygon::addPoint(room\walkArea()\Polygon(), 206, 338)
polygon::addPoint(room\walkArea()\Polygon(), 212, 320)
polygon::addPoint(room\walkArea()\Polygon(), 198, 316)
polygon::addPoint(room\walkArea()\Polygon(), 162, 298)
polygon::addPoint(room\walkArea()\Polygon(), 119, 305)
polygon::addPoint(room\walkArea()\Polygon(), 99, 338)
polygon::addPoint(room\walkArea()\Polygon(), 91, 362)
polygon::addPoint(room\walkArea()\Polygon(), 79, 372)
polygon::addPoint(room\walkArea()\Polygon(), 90, 380)
polygon::addPoint(room\walkArea()\Polygon(), 4, 379)

; Obstacle #1
AddElement(room\walkArea())
polygon::SetMeta(@room\walkArea(), "Bloc A", #True)
polygon::addPoint(room\walkArea()\Polygon(), 120, 280)
polygon::addPoint(room\walkArea()\Polygon(), 120, 260)
polygon::addPoint(room\walkArea()\Polygon(), 140, 260)
polygon::addPoint(room\walkArea()\Polygon(), 140, 280)

; Obstacle #2 (circle)
AddElement(room\walkArea())
polygon::SetMeta(@room\walkArea(), "Obstacle Cercle", #True)
Define z
For z = 0 To 360 Step 36
  polygon::addPoint(room\walkArea()\Polygon(), 60 + 20 * Cos(Radian(z)), 300 + 20 * Sin(Radian(z)))
Next

; Obstacle #3 (vertical wall)
AddElement(room\walkArea())
polygon::SetMeta(@room\walkArea(), "Mur vertical", #True)
polygon::addPoint(room\walkArea()\Polygon(), 220, 50)
polygon::addPoint(room\walkArea()\Polygon(), 220, 400)
polygon::addPoint(room\walkArea()\Polygon(), 225, 400)
polygon::addPoint(room\walkArea()\Polygon(), 225, 50)
Define *wall.polygon::Polygon = @room\walkArea()

;-scale (depth anchors)
scale::AddScalePoint( 80,  80, 1, "Test")
scale::AddScalePoint(440, 120, 1, "Test")
scale::AddScalePoint(640, 260, 0.25, "Test")
scale::AddScalePoint(330, 200, 1, "Test")
scale::AddScalePoint(200, 360, 1, "Test")


; -----------------------------------------------------------------------
; PB 2D init
; -----------------------------------------------------------------------
If InitSprite()
  If InitKeyboard() And InitMouse()
    Define winMain = OpenWindow(#PB_Any, 0, 0, 1024, 800, "Press [Esc] to close", #PB_Window_ScreenCentered | #PB_Window_SystemMenu)
    OpenWindowedScreen(WindowID(winMain), 0, 0, 1024, 800, 1, 0, 0)
  Else
    MessageRequester("Error", "Unable to init keyboard or mouse") : End
  EndIf
Else
  MessageRequester("Error", "Unable to init sprite") : End
EndIf

; -----------------------------------------------------------------------
; Main loop (+ F1 debounce to toggle wall)
; -----------------------------------------------------------------------
Define EventID.l, Distance.l
Define f1Latch.b = #False

Repeat
  Delay(1)
  Repeat : Until WindowEvent() = 0
  ExamineKeyboard() : ExamineMouse()
  ClearScreen(0)

  StartDrawing(ScreenOutput())
  mainChara\target\x = MouseX()
  mainChara\target\y = MouseY()

  ; Draw polygons (grey if disabled)
  ForEach room\walkArea()
    Define color
    If ListIndex(room\walkArea()) = 0
      color = RGB(0, 0, 255)
    ElseIf room\walkArea()\Enabled
      color = RGB(100, 100, 255)
    Else
      color = RGB(80, 80, 80)
    EndIf
    polygon::drawPoly(room\walkArea()\Polygon(), color)
  Next

  ; Left click: compute path
  If MouseButton(#PB_MouseButton_Left)
    Pathfinding::getWalkPath(@mainChara\coord, @mainChara\target, room\walkArea(), mainChara\Path())
    mainChara\move = #True
    mainChara\pathLength = Pathfinding::getPathLength(mainChara\Path())
    Distance = 0
  EndIf

  ; Right click: (hook dispo si besoin)
  If MouseButton(#PB_MouseButton_Right)
    ; reserved for alternate action
  EndIf

  ; Move along the path
  If mainChara\move = #True
    Pathfinding::getPositionOnPath(mainChara\Path(), Distance, @mainChara\coord)
    Distance + 5
    If Distance >= mainChara\pathLength
      mainChara\move = #False : Distance = 0
    EndIf
  EndIf

  ; Draw current path
  Pathfinding::drawPath(mainChara\Path())

  ; Size from depth anchors
  Define s.f = scale::At(mainChara\coord\x, mainChara\coord\y)
  Circle(mainChara\coord\x, mainChara\coord\y, 10 * s, #Green)
  DrawText(150, 20, "taille: " + StrF(s))
  scale::draw(#Yellow)

  DrawText(20,  0, " Chara : " + Str(mainChara\coord\x) + "," + Str(mainChara\coord\y))
  DrawText(20, 20, "Distance: " + Str(Distance))
  DrawText(20, 40, "F1 - Toggle Obstacle")

  ; F1 edge: toggle Enabled
  If KeyboardPushed(#PB_Key_F1) And f1Latch = #False
    f1Latch = #True
    If *wall
      *wall\Enabled = 1 - *wall\Enabled
      trace::DBG("demo", "Mur vertical Enabled=" + Str(*wall\Enabled))
    EndIf
  EndIf
  If KeyboardPushed(#PB_Key_F1) = 0 : f1Latch = #False : EndIf

  DrawingMode(#PB_2DDrawing_XOr)
  Line(MouseX() - 1, MouseY(), -10, 1, #White)
  Line(MouseX() + 1, MouseY(), 10, 1, #White)
  Line(MouseX(), MouseY() - 1, 1, -10, #White)
  Line(MouseX(), MouseY() + 1, 1, 10, #White)
  DrawingMode(#PB_2DDrawing_Default)

  StopDrawing()
  FlipBuffers()
Until KeyboardReleased(#PB_Key_Escape) Or EventID = #PB_Event_CloseWindow
User avatar
minimy
Enthusiast
Enthusiast
Posts: 599
Joined: Mon Jul 08, 2013 8:43 pm
Location: off world

Re: Point'n Click 2D pathFinding

Post by minimy »

Thank you Thyphoon! I was trying to block/stuck the 'player' and I can't.
I think you solved the stuck error that sometimes occurred.
Not perfect yet, there are improvements to be made... But I'm already quite happy with the result
This is perfect for me. Tested in 6.02 and 6.2 (Dont be critic with your self) :P
Sorry for my bad english, this hot weather is not good for the thinkers :lol:

But i found this solution:
Image

Beer may not be a good solution either... :mrgreen:
If translation=Error: reply="Sorry, Im Spanish": Endif
Post Reply