Spaces:
Runtime error
Runtime error
Fix
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
| 478 |
const sources: FogSource[] = [];
|
| 479 |
-
if (!
|
| 480 |
-
const myP =
|
| 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 |
-
//
|
| 493 |
let exploredSources: FogSource[] = [];
|
| 494 |
let _exploredSourceKeys = new Set<string>();
|
| 495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
let changed = false;
|
| 497 |
-
for (const
|
| 498 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
if (!_exploredSourceKeys.has(key)) {
|
| 500 |
_exploredSourceKeys.add(key);
|
| 501 |
-
exploredSources.push(
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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>
|