Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions src/lib/components/MiniMap.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

$: mapWidth = $gameConfig.mapSize || 75;
$: mapHeight = $gameConfig.mapSize || 75;
$: minimapWidth = mapHeight;
$: minimapHeight = mapWidth;

const terrainColor = (type: TerrainType) => {
switch (type) {
Expand All @@ -49,15 +51,19 @@

const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));

const toMinimap = (x: number, y: number) => ({ x: mapHeight - 1 - y, y: x });
const fromMinimap = (x: number, y: number) => ({ x: y, y: mapHeight - 1 - x });

const minimapBounds = () => {
if (dragBounds) return dragBounds;
const cols = mapWidth / minimapZoom;
const rows = mapHeight / minimapZoom;
const cols = minimapWidth / minimapZoom;
const rows = minimapHeight / minimapZoom;
const center = toMinimap($mapCenter.x, $mapCenter.y);
return {
cols,
rows,
left: clamp($mapCenter.x - cols / 2, 0, mapWidth - cols),
top: clamp($mapCenter.y - rows / 2, 0, mapHeight - rows)
left: clamp(center.x - cols / 2, 0, minimapWidth - cols),
top: clamp(center.y - rows / 2, 0, minimapHeight - rows)
};
};

Expand All @@ -75,19 +81,20 @@

if ($tiles.size) {
const firstCol = Math.max(0, Math.floor(left));
const lastCol = Math.min(mapWidth, Math.ceil(left + cols));
const lastCol = Math.min(minimapWidth, Math.ceil(left + cols));
const firstRow = Math.max(0, Math.floor(top));
const lastRow = Math.min(mapHeight, Math.ceil(top + rows));
for (let y = firstRow; y < lastRow; y++) {
for (let x = firstCol; x < lastCol; x++) {
const lastRow = Math.min(minimapHeight, Math.ceil(top + rows));
for (let minimapY = firstRow; minimapY < lastRow; minimapY++) {
for (let minimapX = firstCol; minimapX < lastCol; minimapX++) {
const { x, y } = fromMinimap(minimapX, minimapY);
const visibility = visibilityAt(x, y);
if (visibility === TileVisibilityState.UNEXPLORED) continue;
const color = terrainColor($tiles.get(tileKey(x, y))?.terrain ?? TerrainType.GRASSLAND);
ctx.fillStyle = visibility === TileVisibilityState.VISIBLE ? color : `${color}88`;
const x1 = Math.floor((x - left) * scaleX);
const y1 = Math.floor((y - top) * scaleY);
const x2 = Math.ceil((x + 1 - left) * scaleX);
const y2 = Math.ceil((y + 1 - top) * scaleY);
const x1 = Math.floor((minimapX - left) * scaleX);
const y1 = Math.floor((minimapY - top) * scaleY);
const x2 = Math.ceil((minimapX + 1 - left) * scaleX);
const y2 = Math.ceil((minimapY + 1 - top) * scaleY);
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
}
}
Expand All @@ -98,33 +105,37 @@
if (!c.start) continue;
const own = c.owner?.value === $userId;
if (!own && visibilityAt(c.start.x, c.start.y) !== TileVisibilityState.VISIBLE) continue;
const position = toMinimap(c.start.x, c.start.y + c.size - 1);
ctx.fillStyle = own ? 'rgba(68,153,255,0.5)' : c.owner ? 'rgba(221,68,68,0.5)' : 'rgba(153,153,153,0.4)';
ctx.fillRect(Math.floor((c.start.x - left) * scaleX), Math.floor((c.start.y - top) * scaleY), Math.ceil(c.size * scaleX), Math.ceil(c.size * scaleY));
ctx.fillRect(Math.floor((position.x - left) * scaleX), Math.floor((position.y - top) * scaleY), Math.ceil(c.size * scaleX), Math.ceil(c.size * scaleY));
}

// Buildings as bright pixels.
ctx.fillStyle = '#e8c37a';
for (const b of $buildings) {
if (!b.coords) continue;
if (visibilityAt(b.coords.x, b.coords.y) !== TileVisibilityState.VISIBLE) continue;
ctx.fillRect(Math.floor((b.coords.x - left) * scaleX), Math.floor((b.coords.y - top) * scaleY), Math.max(1, scaleX), Math.max(1, scaleY));
const position = toMinimap(b.coords.x, b.coords.y);
ctx.fillRect(Math.floor((position.x - left) * scaleX), Math.floor((position.y - top) * scaleY), Math.max(1, scaleX), Math.max(1, scaleY));
}

// Armies use owner colors and a slightly larger dot than buildings.
for (const army of $armies) {
if (!army.coords) continue;
if (visibilityAt(army.coords.x, army.coords.y) !== TileVisibilityState.VISIBLE) continue;
const position = toMinimap(army.coords.x, army.coords.y);
ctx.fillStyle = army.owner?.value === $userId ? '#60a5fa' : '#f87171';
ctx.fillRect(Math.floor((army.coords.x - left) * scaleX) - 1, Math.floor((army.coords.y - top) * scaleY) - 1, Math.max(2, scaleX + 1), Math.max(2, scaleY + 1));
ctx.fillRect(Math.floor((position.x - left) * scaleX) - 1, Math.floor((position.y - top) * scaleY) - 1, Math.max(2, scaleX + 1), Math.max(2, scaleY + 1));
}

// Viewport rectangle from the live map center + visible span.
if (viewCols > 0 && viewRows > 0) {
const vx = ($mapCenter.x - viewCols / 2 - left) * scaleX;
const vy = ($mapCenter.y - viewRows / 2 - top) * scaleY;
const center = toMinimap($mapCenter.x, $mapCenter.y);
const vx = (center.x - viewRows / 2 - left) * scaleX;
const vy = (center.y - viewCols / 2 - top) * scaleY;
ctx.strokeStyle = '#efe2c4';
ctx.lineWidth = 1;
ctx.strokeRect(Math.round(vx) + 0.5, Math.round(vy) + 0.5, Math.round(viewCols * scaleX), Math.round(viewRows * scaleY));
ctx.strokeRect(Math.round(vx) + 0.5, Math.round(vy) + 0.5, Math.round(viewRows * scaleX), Math.round(viewCols * scaleY));
}

ctx.strokeStyle = 'rgba(255,255,255,0.12)';
Expand All @@ -151,8 +162,9 @@
const panFromPointer = (clientX: number, clientY: number) => {
const rect = canvas.getBoundingClientRect();
const { cols, rows, left, top } = minimapBounds();
const col = clamp(Math.round(left + ((clientX - rect.left) / rect.width) * cols), 0, mapWidth - 1);
const row = clamp(Math.round(top + ((clientY - rect.top) / rect.height) * rows), 0, mapHeight - 1);
const position = fromMinimap(left + ((clientX - rect.left) / rect.width) * cols, top + ((clientY - rect.top) / rect.height) * rows);
const col = clamp(Math.round(position.x), 0, mapWidth - 1);
const row = clamp(Math.round(position.y), 0, mapHeight - 1);
onPan(col, row);
};

Expand Down
62 changes: 52 additions & 10 deletions src/routes/game/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@

// tiles
let loaded = new Map<string, Container>();
let visibleTileKeys = new Set<string>();
let visibleBoundsKey = '';
let tileLabels = new Map<string, Container>();
let tileData = new Map<string, { tile?: Tile; city?: City; building?: Building; armies?: Army[] }>();
let constructionGfx = new Map<string, { gfx: Graphics; startMs: number; endMs: number; cx: number; cy: number }>();
let starvingGfx = new Map<string, { gfx: Graphics; segs: number[][]; isCenter: boolean }>();
Expand Down Expand Up @@ -679,6 +682,12 @@
return screenToTile((-cont.x + cw / 2) / cont.scale.x, (-cont.y + ch / 2) / cont.scale.y);
};

const syncMapCenter = () => {
const center = getCenter();
if (center.x === $mapCenter.x && center.y === $mapCenter.y) return;
mapCenter.set(center);
};

// Clamp a container position to the iso map's screen-space AABB (pure).
const clampPos = (x: number, y: number, s: number) => {
const pad = 200;
Expand All @@ -700,7 +709,7 @@
cont.y = tgtY;
cont.scale.set(tgtScale);
loadVisible();
mapCenter.set(getCenter());
syncMapCenter();
};

const centerCam = (col: number, row: number, snap = false) => {
Expand Down Expand Up @@ -846,14 +855,17 @@
cont.y = ny;
cont.scale.set(ns);
loadVisible();
mapCenter.set(getCenter());
syncMapCenter();
};

// ── data actions ────────────────────────────────────────
const rebuildTiles = () => {
for (const [, c] of loaded) c.destroy({ children: true });
loaded.clear();
visibleTileKeys.clear();
visibleBoundsKey = '';
for (const label of labelLayer?.removeChildren() ?? []) label.destroy({ children: true });
tileLabels.clear();
constructionGfx.clear();
starvingGfx.clear();
trainingGfx.clear();
Expand Down Expand Up @@ -1484,6 +1496,7 @@
const c = clampPos(tgtX, tgtY, tgtScale);
tgtX = c.x;
tgtY = c.y;
visibleBoundsKey = '';
loadVisible();
};

Expand All @@ -1493,7 +1506,7 @@
app = new Application();
await app.init({ width: cw, height: ch, backgroundColor: 0x171c18, antialias: false, roundPixels: true, resolution: window.devicePixelRatio || 1, autoDensity: true });
el.appendChild(app.canvas);
cont = new Container();
cont = new Container({ isRenderGroup: true });
cont.sortableChildren = true;
cont.interactive = true;
cont.hitArea = new Rectangle(-1e5, -1e5, 2e5, 2e5);
Expand Down Expand Up @@ -1633,7 +1646,7 @@
};

// ── tile rendering ──────────────────────────────────────
const addCityLabel = (city: City, px: number, py: number) => {
const addCityLabel = (city: City, px: number, py: number, key: string) => {
const owned = city.owner?.value === $userId;
const ownerColor = owned ? 0x336ca8 : city.owner ? 0x9b3f38 : 0x66685f;
const wrapper = new Container();
Expand Down Expand Up @@ -1676,6 +1689,7 @@
wrapper.addChild(badge, population, name);
wrapper.pivot.x = (badgeWidth + 4 + name.width) / 2;
labelLayer.addChild(wrapper);
tileLabels.set(key, wrapper);
};

const addTrainingMarker = (building: Building, tile: Container, key: string) => {
Expand Down Expand Up @@ -1875,7 +1889,13 @@
const renderTile = (col: number, row: number) => {
if (col < 0 || row < 0 || col >= worldWidth || row >= worldHeight) return;
const k = tileKey(col, row);
if (loaded.has(k)) return;
const existing = loaded.get(k);
if (existing) {
existing.visible = true;
const label = tileLabels.get(k);
if (label) label.visible = true;
return;
}

const td = tileData.get(k);
const { sx: px, sy: py } = tileToScreen(col, row);
Expand Down Expand Up @@ -1912,7 +1932,7 @@
tc.addChild(memoryFog);
}
if (visible && td?.building) tc.addChild(getStructureSprite(structureKind(td.building.type)));
if (visible && td?.city && (td.building?.type === BuildingType.CITY_CENTER || td.building?.type === BuildingType.TOWN_CENTER)) addCityLabel(td.city, px, py);
if (visible && td?.city && (td.building?.type === BuildingType.CITY_CENTER || td.building?.type === BuildingType.TOWN_CENTER)) addCityLabel(td.city, px, py, k);

const visibleArmies = visible ? td?.armies : undefined;
if (visibleArmies?.length) {
Expand Down Expand Up @@ -2091,10 +2111,32 @@
colMax = Math.ceil(xMax) + 2;
const rowMin = Math.floor(yMin) - 2,
rowMax = Math.ceil(yMax) + 4;
const firstCol = Math.max(0, colMin);
const lastCol = Math.min(worldWidth - 1, colMax);
const firstRow = Math.max(0, rowMin);
const lastRow = Math.min(worldHeight - 1, rowMax);
const boundsKey = `${firstCol},${lastCol},${firstRow},${lastRow}`;
if (boundsKey === visibleBoundsKey) return;
visibleBoundsKey = boundsKey;
// Feed the minimap's viewport rectangle (tile-space AABB of the view).
viewTilesW = xMax - xMin;
viewTilesH = yMax - yMin;
for (let col = colMin; col <= colMax; col++) for (let row = rowMin; row <= rowMax; row++) renderTile(col, row);
const nextVisibleTileKeys = new Set<string>();
for (let col = firstCol; col <= lastCol; col++) {
for (let row = firstRow; row <= lastRow; row++) {
const key = tileKey(col, row);
nextVisibleTileKeys.add(key);
renderTile(col, row);
}
}
for (const key of visibleTileKeys) {
if (nextVisibleTileKeys.has(key)) continue;
const tile = loaded.get(key);
if (tile) tile.visible = false;
const label = tileLabels.get(key);
if (label) label.visible = false;
}
visibleTileKeys = nextVisibleTileKeys;
};

// ── selection ───────────────────────────────────────────
Expand Down Expand Up @@ -2164,7 +2206,7 @@
lastMoveX = p.x;
lastMoveY = p.y;
loadVisible();
mapCenter.set(getCenter());
syncMapCenter();
});

cont.on('pointerup', (e) => {
Expand Down Expand Up @@ -2204,7 +2246,7 @@
} else if (!easeMotion || performance.now() - lastMoveT > 80) {
// No flick (or motion easing off): stop where released.
velX = velY = 0;
mapCenter.set(getCenter());
syncMapCenter();
} else {
// Flick: cap the throw speed; the ticker coasts the target from here.
velX = Math.max(-4000, Math.min(4000, velX));
Expand All @@ -2214,7 +2256,7 @@
});

cont.on('pointerupoutside', () => {
if (drag) mapCenter.set(getCenter());
if (drag) syncMapCenter();
drag = false;
});

Expand Down
Loading