; ; /* ============================================================================ ORDER FEEDBACK — the route, drawn on the ground, the moment you give it. ---------------------------------------------------------------------------- A move order used to answer with one particle puff at the destination. On a map with lakes and cliffs that is a lie of omission: the flow field may be about to walk the army AROUND terrain the player cannot see from here, and the first they learn of it is their tanks arriving from the wrong side of a fight. The fix is not a tutorial line, it is showing the route. So every move order now traces the SAME flow field the units will follow — not an idealised straight line — and draws it as a ribbon of chevrons marching toward the destination. The chevrons flash and drift in the travel direction, because a static line reads as terrain decoration; motion is what says "this is where they are going". Colour is doctrine, not decoration: attack-move orders draw amber (they will stop and fight), plain moves draw cyan (they will not). That makes the one toggle whose state players most often forget — A-MOVE vs MOVE — visible on the ground at the moment it matters, instead of only in a button label. Tracing happens ONCE, at order time (a few hundred field lookups); per-frame work is just placing instanced lines along a stored polyline. Nothing here allocates during draw. ============================================================================ */ const MOVE_FX_MAX=3; // newest orders win; older ones keep fading const MOVE_FX_LIFE=2600, MOVE_FX_FADE=750; const moveFxList=[]; /* Follow the field cell-by-cell from the army's centroid. k===8 marks the goal cell (or unreachable ground, where a beeline is the honest fallback). The step is under one cell so the trace cannot tunnel a wall diagonal the field itself forbids, and two smoothing passes turn the 8-way staircase into the route the steering will actually approximate. */ function moveFxTrace(sx,sy,wx,wy,fi){ const pts=[{x:sx,y:sy}]; const F=(typeof fields!=='undefined')?fields[fi]:null; const cell=MAP/PGS, step=cell*0.9; let x=sx,y=sy; if(F&&F.dirs){ for(let n=0;n<300;n++){ const d=Math.hypot(wx-x,wy-y); if(d=8){ vx=(wx-x)/d; vy=(wy-y)/d; } else { const inv=1/Math.hypot(DIRX[k],DIRY[k]); vx=DIRX[k]*inv; vy=DIRY[k]*inv; } x+=vx*step; y+=vy*step; pts.push({x,y}); } } pts.push({x:wx,y:wy}); for(let pass=0;pass<2;pass++) for(let i=1;iMOVE_FX_MAX) moveFxList.shift(); } /* Point at arc position s. `hint` avoids restarting the segment search for every chevron on a path we are walking front-to-back anyway. */ function moveFxAt(fx,s,out,hint){ const a=fx.arc,p=fx.pts; let i=hint||1; while(it0?(s-t0)/(t1-t0):0; out.x=p[i-1].x+(p[i].x-p[i-1].x)*f; out.y=p[i-1].y+(p[i].y-p[i-1].y)*f; out.ang=Math.atan2(p[i].y-p[i-1].y,p[i].x-p[i-1].x); return i; } const moveFxP={x:0,y:0,ang:0}; function moveFxDraw(vis,t){ if(!moveFxList.length) return; if(META&&META.settings&&META.settings.orderPaths===false){ moveFxList.length=0; return; } const now=performance.now(); /* Screen-constant sizing. World-unit geometry that reads fine zoomed in is a 1-pixel hairline at the strategic zoom this cue matters most at — the first build of this drew, flushed, verified as present in the instance stream, and was still invisible in the screenshot. Scale by the camera's ortho span so a chevron is ALWAYS a thumb-sized mark. */ const k=clamp((typeof orthoSpan!=='undefined'?orthoSpan:520)/470,1,3.2); for(let n=moveFxList.length-1;n>=0;n--){ const fx=moveFxList[n]; if(now>=fx.until){ moveFxList.splice(n,1); continue; } const fade=Math.min(1,(fx.until-now)/MOVE_FX_FADE); const amber=fx.kind===0; const r=amber?255:96, g=amber?198:230, b=amber?88:255; /* Continuous ribbon under the chevrons: the chevrons say "which way", the ribbon says "exactly where" when they are mid-flight between spots. */ const SEG=36; let hint=1; let px=fx.pts[0].x, py=fx.pts[0].y; for(let s=SEG;s<=fx.len;s+=SEG){ hint=moveFxAt(fx,Math.min(s,fx.len),moveFxP,hint); const mx=(px+moveFxP.x)*0.5,my=(py+moveFxP.y)*0.5; if(vis(mx,my,SEG)) FX.line.add(mx,my,terrainH(mx,my)+3.6,Math.hypot(moveFxP.x-px,moveFxP.y-py), Math.atan2(moveFxP.y-py,moveFxP.x-px),r,g,b,82*fade,1.6*k); px=moveFxP.x; py=moveFxP.y; } /* Chevrons march toward the destination. The flash ripples ALONG the path (phase offset by arc position) so the route pulses in sequence toward the destination — a directional cue that survives at any zoom. The flash floor stays high; a cue that spends half its cycle invisible is half a cue. */ const SP=Math.max(46,34*k), drift=(t*58)%SP; hint=1; for(let s=drift;s