File size: 7,499 Bytes
b51e03a
3597f5a
6468c17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597f5a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
;
;
/* ============================================================================
   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<cell*1.25) break;
      const k=F.dirs[ffCell(x,y)];
      let vx,vy;
      if(k>=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;i<pts.length-1;i++){
      pts[i].x=(pts[i-1].x+pts[i].x*2+pts[i+1].x)*0.25;
      pts[i].y=(pts[i-1].y+pts[i].y*2+pts[i+1].y)*0.25;
    }
  /* Cumulative arc length, so draw can march chevrons at constant spacing
     without re-measuring the polyline every frame. */
  let len=0;
  const arc=[0];
  for(let i=1;i<pts.length;i++){
    len+=Math.hypot(pts[i].x-pts[i-1].x,pts[i].y-pts[i-1].y);
    arc.push(len);
  }
  return {pts,arc,len};
}

/* kind: 0 attack-move, 1 move-only. Patrol has its own persistent route
   drawing and never comes through here. */
function moveFxOrder(sel,wx,wy,fi,kind){
  if(!sel||!sel.length) return;
  let cx=0,cy=0;
  for(const i of sel){ cx+=ux[i]; cy+=uy[i]; }
  cx/=sel.length; cy/=sel.length;
  if(Math.hypot(wx-cx,wy-cy)<24) return;          // shuffle in place: no route to show
  const tr=moveFxTrace(cx,cy,wx,wy,fi);
  moveFxList.push({pts:tr.pts,arc:tr.arc,len:tr.len,kind:kind|0,
                   born:performance.now(),until:performance.now()+MOVE_FX_LIFE});
  if(moveFxList.length>MOVE_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(i<a.length-1&&a[i]<s) i++;
  const t0=a[i-1],t1=a[i],f=t1>t0?(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<fx.len-6;s+=SP){
      hint=moveFxAt(fx,s,moveFxP,hint);
      if(!vis(moveFxP.x,moveFxP.y,14*k)) continue;
      const flash=0.74+0.26*Math.sin(t*7.2-s*0.045);
      const a=225*fade*flash;
      for(const side of [-1,1]){
        const aa=moveFxP.ang+Math.PI+side*0.46;
        const mx=moveFxP.x+Math.cos(aa)*4.4*k, my=moveFxP.y+Math.sin(aa)*4.4*k;
        FX.line.add(mx,my,terrainH(mx,my)+4.4,9.0*k,aa,r,g,b,a,2.6*k);
      }
    }

    /* Destination: three chevrons stacked along the approach, collapsing onto
       the point β€” an arrow that visibly lands where the army will. */
    const last=fx.pts[fx.pts.length-1], prev=fx.pts[Math.max(0,fx.pts.length-2)];
    const fa=Math.atan2(last.y-prev.y,last.x-prev.x);
    if(vis(last.x,last.y,60*k)){
      const pulse=((t*1.8)%1);
      for(let q=0;q<3;q++){
        const ph=(pulse+q/3)%1;
        const dBack=34*k*(1-ph), a=235*fade*ph*ph;
        const hx=last.x-Math.cos(fa)*dBack, hy=last.y-Math.sin(fa)*dBack;
        for(const side of [-1,1]){
          const aa=fa+Math.PI+side*0.52;
          const mx=hx+Math.cos(aa)*5.5*k, my=hy+Math.sin(aa)*5.5*k;
          FX.line.add(mx,my,terrainH(mx,my)+4.8,11.5*k,aa,r,g,b,a,3.0*k);
        }
      }
      FX.ring.add(last.x,last.y,terrainH(last.x,last.y)+4.0,(7.5+2.5*Math.sin(t*5))*k,
        t*.6,r,g,b,180*fade);
    }
  }
}