gabraken commited on
Commit
7f61fe6
ยท
1 Parent(s): accf9ef
frontend/src/lib/components/Map.svelte CHANGED
@@ -3,7 +3,7 @@
3
  import { backendUrl } from '$lib/socket';
4
  import { gameState, myPlayerId, selectedUnit, selectedBuilding, mapViewport, mapVisibleCells, mapExploredCells, mapCenterRequest, mapTextureUrl, isTutorial } from '$lib/stores/game';
5
  import { BUILDING_SIZES } from '$lib/types';
6
- import type { Unit, Building, UnitType, BuildingType } from '$lib/types';
7
 
8
  // โ”€โ”€ Per-unit smooth interpolation state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
9
  const TICK_MS = 250; // must match server TICK_INTERVAL * 1000
@@ -339,6 +339,9 @@
339
  }
340
 
341
  unitRenderStates = { ...unitRenderStates };
 
 
 
342
  }
343
 
344
  function lerp(a: number, b: number, t: number) { return a + (b - a) * t; }
@@ -471,13 +474,19 @@
471
  if (next.size !== exploredCells.size) exploredCells = next;
472
  }
473
 
474
- // Fog of war: circle-based vision sources for SVG mask rendering
 
 
 
475
  type FogSource = { x: number; y: number; r: number };
476
 
477
- $: visibleSources = (() => {
 
 
 
478
  const sources: FogSource[] = [];
479
- if (!gs || !myId) return sources;
480
- const myP = gs.players[myId];
481
  if (!myP) return sources;
482
  for (const u of Object.values(myP.units)) {
483
  sources.push({ x: u.x, y: u.y, r: visionRadiusUnit(u) });
@@ -487,22 +496,37 @@
487
  sources.push({ x: b.x, y: b.y, r: visionRadiusBuilding(b) });
488
  }
489
  return sources;
490
- })();
491
 
492
- // Accumulate explored sources (deduped by snapped grid position)
493
  let exploredSources: FogSource[] = [];
494
  let _exploredSourceKeys = new Set<string>();
495
- $: {
 
 
 
 
496
  let changed = false;
497
- for (const s of visibleSources) {
498
- const key = `${Math.round(s.x * 2)},${Math.round(s.y * 2)},${s.r}`;
 
 
 
 
 
 
 
 
 
 
 
499
  if (!_exploredSourceKeys.has(key)) {
500
  _exploredSourceKeys.add(key);
501
- exploredSources.push(s);
502
  changed = true;
503
  }
504
  }
505
- if (changed) exploredSources = exploredSources;
506
  }
507
 
508
  // Expose viewport and fog for Minimap
@@ -867,9 +891,12 @@
867
  {/each}
868
  {/if}
869
 
870
- <!-- Fog of war: single black rect covering the viewport, with vision circles
871
- masked out via SVG <mask>. Two layers: dark (unexplored) and dim (explored). -->
 
 
872
  {#if !isObserver}
 
873
  <defs>
874
  <mask id="fog-explored-mask" maskUnits="userSpaceOnUse" x="0" y="0" width={MAP_W} height={MAP_H}>
875
  <rect x="0" y="0" width={MAP_W} height={MAP_H} fill="white"/>
@@ -879,7 +906,7 @@
879
  </mask>
880
  <mask id="fog-visible-mask" maskUnits="userSpaceOnUse" x="0" y="0" width={MAP_W} height={MAP_H}>
881
  <rect x="0" y="0" width={MAP_W} height={MAP_H} fill="white"/>
882
- {#each visibleSources as s}
883
  <circle cx={s.x} cy={s.y} r={s.r} fill="black"/>
884
  {/each}
885
  </mask>
 
3
  import { backendUrl } from '$lib/socket';
4
  import { gameState, myPlayerId, selectedUnit, selectedBuilding, mapViewport, mapVisibleCells, mapExploredCells, mapCenterRequest, mapTextureUrl, isTutorial } from '$lib/stores/game';
5
  import { BUILDING_SIZES } from '$lib/types';
6
+ import type { Unit, Building, UnitType, BuildingType, GameState } from '$lib/types';
7
 
8
  // โ”€โ”€ Per-unit smooth interpolation state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
9
  const TICK_MS = 250; // must match server TICK_INTERVAL * 1000
 
339
  }
340
 
341
  unitRenderStates = { ...unitRenderStates };
342
+
343
+ // Accumulate fog explored sources (reliable here: explicit subscription, not $: block)
344
+ accumulateExploredSources(newGs as GameState);
345
  }
346
 
347
  function lerp(a: number, b: number, t: number) { return a + (b - a) * t; }
 
474
  if (next.size !== exploredCells.size) exploredCells = next;
475
  }
476
 
477
+ // Fog of war: circle-based vision sources for SVG mask rendering.
478
+ // getVisibleSources is called INLINE in the template (not via $: block) so that
479
+ // Svelte's template renderer tracks gs/myId as dependencies โ€” same pattern as the
480
+ // original buildFogRects, which avoids the production-build reactive scheduling issue.
481
  type FogSource = { x: number; y: number; r: number };
482
 
483
+ function getVisibleSources(
484
+ _gs: typeof gs,
485
+ _myId: typeof myId,
486
+ ): FogSource[] {
487
  const sources: FogSource[] = [];
488
+ if (!_gs || !_myId) return sources;
489
+ const myP = _gs.players[_myId];
490
  if (!myP) return sources;
491
  for (const u of Object.values(myP.units)) {
492
  sources.push({ x: u.x, y: u.y, r: visionRadiusUnit(u) });
 
496
  sources.push({ x: b.x, y: b.y, r: visionRadiusBuilding(b) });
497
  }
498
  return sources;
499
+ }
500
 
501
+ // Explored sources: accumulated in onGameStateUpdate (explicit subscription, reliable in prod).
502
  let exploredSources: FogSource[] = [];
503
  let _exploredSourceKeys = new Set<string>();
504
+
505
+ function accumulateExploredSources(newGs: GameState) {
506
+ if (!myId) return;
507
+ const myP = newGs.players[myId];
508
+ if (!myP) return;
509
  let changed = false;
510
+ for (const u of Object.values(myP.units) as Unit[]) {
511
+ const r = visionRadiusUnit(u);
512
+ const key = `${Math.round(u.x * 2)},${Math.round(u.y * 2)},${r}`;
513
+ if (!_exploredSourceKeys.has(key)) {
514
+ _exploredSourceKeys.add(key);
515
+ exploredSources.push({ x: u.x, y: u.y, r });
516
+ changed = true;
517
+ }
518
+ }
519
+ for (const b of Object.values(myP.buildings) as Building[]) {
520
+ if (b.status === 'destroyed') continue;
521
+ const r = visionRadiusBuilding(b);
522
+ const key = `${Math.round(b.x * 2)},${Math.round(b.y * 2)},${r}`;
523
  if (!_exploredSourceKeys.has(key)) {
524
  _exploredSourceKeys.add(key);
525
+ exploredSources.push({ x: b.x, y: b.y, r });
526
  changed = true;
527
  }
528
  }
529
+ if (changed) exploredSources = [...exploredSources];
530
  }
531
 
532
  // Expose viewport and fog for Minimap
 
891
  {/each}
892
  {/if}
893
 
894
+ <!-- Fog of war: single black rect covering the viewport, with vision circles masked out.
895
+ getVisibleSources is called INLINE so Svelte's template renderer tracks gs/myId as
896
+ dependencies โ€” avoids the production-build $: scheduling issue (same pattern as the
897
+ original buildFogRects). exploredSources is populated via onGameStateUpdate. -->
898
  {#if !isObserver}
899
+ {@const _visSrc = getVisibleSources(gs, myId)}
900
  <defs>
901
  <mask id="fog-explored-mask" maskUnits="userSpaceOnUse" x="0" y="0" width={MAP_W} height={MAP_H}>
902
  <rect x="0" y="0" width={MAP_W} height={MAP_H} fill="white"/>
 
906
  </mask>
907
  <mask id="fog-visible-mask" maskUnits="userSpaceOnUse" x="0" y="0" width={MAP_W} height={MAP_H}>
908
  <rect x="0" y="0" width={MAP_W} height={MAP_H} fill="white"/>
909
+ {#each _visSrc as s}
910
  <circle cx={s.x} cy={s.y} r={s.r} fill="black"/>
911
  {/each}
912
  </mask>