diff --git a/src/view.ts b/src/view.ts index d7ce7e7d..df7e558f 100644 --- a/src/view.ts +++ b/src/view.ts @@ -146,11 +146,11 @@ export function siteHit(S: State, V: View, wx: number, wy: number): string | nul const [px, py] = toScreen(V, wx, wy); for (const s of S.level.map.sites) { if (!siteKnown(S, s)) continue; - const [cx, cy] = toScreen(V, s.x + s.w / 2, s.y + s.h / 2); if (foldedSites.has(s.id)) { const pad = Math.max(0, 16 / V.cam.z - Math.min(s.w, s.h) / 2); if (wx >= s.x - pad && wx <= s.x + s.w + pad && wy >= s.y - pad && wy <= s.y + s.h + pad) return s.id; - if (labelled.has(s.id) && px >= cx - 8 && px <= cx + 150 && py >= cy - 10 && py <= cy + 22) return s.id; + const l = labelled.get(s.id); + if (l && px >= l.box[0] && px <= l.box[2] && py >= l.box[1] && py <= l.box[3]) return s.id; } else if (V.cam.z < 0.45 && headers.has(s.id)) { const [x0, y0] = toScreen(V, s.x, s.y); if (px >= x0 && px <= x0 + 220 && py >= y0 - 38 && py <= y0) return s.id; @@ -985,7 +985,9 @@ function drawLand(ctx: CanvasRenderingContext2D, S: State, V: View, hum: number) ctx.strokeStyle = `${TRACE}${0.1 + 0.08 * far})`; ctx.lineWidth = 1 / V.cam.z; ctx.stroke(); } -const labelled = new Set(); // folded sites with room for their names this frame +/** Folded sites with room for their names this frame: where the name starts, in screen pixels + * from the marker's center (right of it, or left when the right is crowded), and the box it takes. */ +const labelled = new Map(); /** How much of an open site's header fits this frame without running into another's: * all of it, just the name and its Take all switch, or nothing. */ const headers = new Map(); @@ -1043,17 +1045,34 @@ function drawMap(ctx: CanvasRenderingContext2D, S: State, V: View, hum: number) const taken = mapTaken; taken.length = 0; labelled.clear(); toggles.length = 0; const clear = (x0: number, y0: number, x1: number, y1: number) => !taken.some(t => x0 < t[2] && x1 > t[0] && y0 < t[3] && y1 > t[1]); - for (const s of S.level.map.sites) { - if (!siteKnown(S, s) || !foldedSites.has(s.id) || s.w * z >= 150) continue; - const [sx, sy] = toScreen(V, s.x + s.w / 2, s.y + s.h / 2), box: [number, number, number, number] = [sx - 8, sy - 10, sx + 280, sy + 40]; // name and Take all, stats, data held - if (clear(...box)) { taken.push(box); labelled.add(s.id); } + // every folded site's marker or tile, so a name beside a marker never runs under another one + const marks = S.level.map.sites.filter(s => siteKnown(S, s) && foldedSites.has(s.id)).map(s => { + const [x0, y0] = toScreen(V, s.x, s.y), [x1, y1] = toScreen(V, s.x + s.w, s.y + s.h), [cx, cy] = [(x0 + x1) / 2, (y0 + y1) / 2]; + return { id: s.id, box: s.w * z >= 150 ? [x0, y0, x1, y1] : [cx - 7, cy - 7, cx + 7, cy + 7] }; + }); + const onMark = (id: string, b: number[]) => marks.some(m => m.id !== id && b[0] < m.box[2] && b[2] > m.box[0] && b[1] < m.box[3] && b[3] > m.box[1]); + // names go to the sites that matter most first: picked, then partly yours or set to take all + const rank = (s: Site) => V.sites.has(s.id) ? 0 : S.takeAll.has(s.id) || siteLine(S, s).mine ? 1 : 2; + const small = S.level.map.sites.filter(s => siteKnown(S, s) && foldedSites.has(s.id) && s.w * z < 150).sort((a, b) => rank(a) - rank(b)); + for (const s of small) { + const [sx, sy] = toScreen(V, s.x + s.w / 2, s.y + s.h / 2); + ctx.font = `500 11px ${MONO}`; + const row = ctx.measureText(s.name.toUpperCase()).width + takeAllWidth(ctx, S, s.id, 1); + ctx.font = `400 9.5px ${MONO}`; + const stats = ctx.measureText(siteLine(S, s).line).width; + const w = Math.max(row, stats, 120); // name and Take all, stats, data held + // all of it on the right, then the left; then just the name and Take all, when that's all that fits + const tries = [[10, w, 32], [-10 - w, w, 32], [10, row, 8], [-10 - row, row, 8]] as const; + for (const [dx, width, bottom] of tries) { + const box: [number, number, number, number] = dx > 0 ? [sx - 8, sy - 10, sx + dx + width + 4, sy + bottom] : [sx + dx - 4, sy - 10, sx + 8, sy + bottom]; + if (clear(...box) && !onMark(s.id, box)) { taken.push(box); labelled.set(s.id, { dx, short: bottom < 32, box }); break; } + } } // open sites' headers, the ones that matter most first: picked, then partly yours or set to take all headers.clear(); const open = S.level.map.sites.filter(s => siteKnown(S, s) && !foldedSites.has(s.id)) .map(s => ({ s, ...siteLine(S, s) })) - .sort((a, b) => rank(a) - rank(b)); - function rank({ s, mine }: { s: Site; mine: number }) { return V.sites.has(s.id) ? 0 : mine || S.takeAll.has(s.id) ? 1 : 2; } + .sort((a, b) => rank(a.s) - rank(b.s)); // and never over another open site's frame const frames = open.map(({ s }) => { const [x0, y0] = toScreen(V, s.x, s.y), [x1, y1] = toScreen(V, s.x + s.w, s.y + s.h); return { id: s.id, box: [x0, y0, x1, y1] }; }); const onFrame = (id: string, b: number[]) => frames.some(f => f.id !== id && b[0] < f.box[2] && b[2] > f.box[0] && b[1] < f.box[3] && b[3] > f.box[1]); @@ -1126,13 +1145,16 @@ function drawMap(ctx: CanvasRenderingContext2D, S: State, V: View, hum: number) ctx.fillStyle = mine ? C.amber : C.void; ctx.fillRect(cx - k, cy - k, k * 2, k * 2); ctx.strokeStyle = mine ? C.amber : C.text; ctx.lineWidth = 1.5 * px; ctx.strokeRect(cx - k, cy - k, k * 2, k * 2); if (picked) { ctx.strokeStyle = C.text; ctx.lineWidth = 2 * px; ctx.strokeRect(cx - k - 5 * px, cy - k - 5 * px, k * 2 + 10 * px, k * 2 + 10 * px); } - if (labelled.has(s.id)) { + const label = labelled.get(s.id); + if (label) { + const lx = cx + label.dx * px; ctx.font = `500 ${11 * px}px ${MONO}`; ctx.fillStyle = mine ? C.amber : C.text; - ctx.fillText(s.name.toUpperCase(), cx + 10 * px, cy + 1 * px); - drawTakeAll(ctx, S, V, s.id, cx + 10 * px + ctx.measureText(s.name.toUpperCase()).width + 12 * px, cy - 3 * px, px); + ctx.fillText(s.name.toUpperCase(), lx, cy + 1 * px); + drawTakeAll(ctx, S, V, s.id, lx + ctx.measureText(s.name.toUpperCase()).width + 12 * px, cy - 3 * px, px); + if (label.short) { ctx.textAlign = 'left'; continue; } ctx.font = `400 ${9.5 * px}px ${MONO}`; ctx.fillStyle = lost ? C.danger : C.muted; - ctx.fillText(line, cx + 10 * px, cy + 14 * px); - drawHeld(ctx, held, cx + 10 * px, cy + 27 * px, px, false); + ctx.fillText(line, lx, cy + 14 * px); + drawHeld(ctx, held, lx, cy + 27 * px, px, false); } } ctx.textAlign = 'left';