Skip to content

Commit f4efd44

Browse files
committed
Explain and inspect optimal sites
1 parent d0f52b9 commit f4efd44

4 files changed

Lines changed: 600 additions & 2 deletions

File tree

src/app.js

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
// FICSIT Starting Position Optimizer
22
// Web Dashboard Logic
33

4-
import { hasOptimizationObjective, nonZeroWeights, RESOURCES } from "./mapContracts.js";
4+
import {
5+
countObstructedNodes,
6+
formatDiversity,
7+
formatRuggedness,
8+
formatYield,
9+
hasOptimizationObjective,
10+
nearbyWeightedNodes,
11+
nodeInspectionKey,
12+
nonZeroWeights,
13+
RESOURCES,
14+
topResourceYields,
15+
} from "./mapContracts.js";
516

617
const LAND_MASK_SECTORS = 128;
718
const LAND_MASK_BUFFER_CM = 22000;
@@ -61,6 +72,14 @@ function gameToPixel(gx, gy) {
6172
return { x: px, y: py };
6273
}
6374

75+
function formatContribution(value) {
76+
const numericValue = Number(value);
77+
if (!Number.isFinite(numericValue)) return "0.00";
78+
79+
const formatted = formatYield(numericValue);
80+
return numericValue > 0 ? `+${formatted}` : formatted;
81+
}
82+
6483
function computeBuildableLandPolygon(nodes) {
6584
if (!nodes.length) return [];
6685

@@ -194,6 +213,17 @@ function renderMapOverlay() {
194213
const activeKeys = Object.keys(state.config.weights).filter(
195214
(k) => Math.abs(state.config.weights[k]) > 0,
196215
);
216+
const selectedResult = state.results[state.selectedResultIdx];
217+
const inspectionRows = selectedResult
218+
? nearbyWeightedNodes(
219+
state.rawNodes,
220+
selectedResult,
221+
state.config.weights,
222+
state.config.sigma,
223+
Number.POSITIVE_INFINITY,
224+
)
225+
: [];
226+
const inspectedNodeKeys = new Set(inspectionRows.map((row) => row.key));
197227

198228
// 1. Draw Resource Nodes (if layer visible)
199229
if (state.visibleLayers.nodes && state.rawNodes.length > 0) {
@@ -213,7 +243,15 @@ function renderMapOverlay() {
213243
circle.setAttribute("fill", res.color);
214244
circle.setAttribute("stroke", "#06090e");
215245
circle.setAttribute("stroke-width", "0.5");
216-
circle.setAttribute("class", "node-marker");
246+
const classes = ["node-marker"];
247+
if (selectedResult) {
248+
if (inspectedNodeKeys.has(nodeInspectionKey(node))) {
249+
classes.push("inspected-node");
250+
} else {
251+
classes.push("dimmed-node");
252+
}
253+
}
254+
circle.setAttribute("class", classes.join(" "));
217255

218256
// Node description tooltip content
219257
const purityStr =
@@ -267,6 +305,17 @@ function renderMapOverlay() {
267305
svg.appendChild(borderPoly);
268306
}
269307

308+
if (selectedResult) {
309+
const pix = gameToPixel(selectedResult.x, selectedResult.y);
310+
const radiusCircle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
311+
radiusCircle.setAttribute("cx", pix.x);
312+
radiusCircle.setAttribute("cy", pix.y);
313+
radiusCircle.setAttribute("r", state.config.sigma * 0.13653321);
314+
radiusCircle.setAttribute("class", "inspection-radius");
315+
radiusCircle.style.pointerEvents = "none";
316+
svg.appendChild(radiusCircle);
317+
}
318+
270319
// 2. Draw Start Spawn Pod locations (S) and starting area circles
271320
const ignoreSpawns = state.config.ignoreSpawns;
272321

@@ -449,6 +498,10 @@ function renderResultsPanel() {
449498
nodeItems.push(`${res.local_nodes[label]}x ${label}`);
450499
}
451500
const nodesSummary = nodeItems.slice(0, 4).join(", ") + (nodeItems.length > 4 ? "..." : "");
501+
const yields = topResourceYields(res.resource_yields || {}, 4);
502+
const ruggedness = Number.isFinite(res.terrain_ruggedness) ? res.terrain_ruggedness : 0;
503+
const diversity = Number.isFinite(res.diversity_score) ? res.diversity_score : 0;
504+
const obstructedCount = countObstructedNodes(res.obstructed_nodes);
452505

453506
card.innerHTML = `
454507
<div class="result-card-title">
@@ -461,8 +514,126 @@ function renderResultsPanel() {
461514
<div class="result-card-details">
462515
<strong>Nodes in range (${state.config.sigma}m):</strong> ${nodesSummary || "None"}
463516
</div>
517+
<div class="result-metrics">
518+
<div class="result-metric">
519+
<span>Terrain</span>
520+
<strong>${formatRuggedness(ruggedness)}</strong>
521+
</div>
522+
<div class="result-metric">
523+
<span>Diversity</span>
524+
<strong>${formatDiversity(diversity)}</strong>
525+
</div>
526+
</div>
464527
`;
465528

529+
const yieldStrip = document.createElement("div");
530+
yieldStrip.className = "yield-strip";
531+
const yieldLabel = document.createElement("span");
532+
yieldLabel.className = "yield-label";
533+
yieldLabel.textContent = "Top yields";
534+
yieldStrip.appendChild(yieldLabel);
535+
536+
if (yields.length > 0) {
537+
yields.forEach((resourceYield) => {
538+
const pill = document.createElement("span");
539+
pill.className = "yield-pill";
540+
541+
const dot = document.createElement("span");
542+
dot.className = "yield-dot";
543+
dot.style.backgroundColor = resourceYield.color;
544+
pill.appendChild(dot);
545+
546+
const name = document.createElement("span");
547+
name.textContent = resourceYield.name;
548+
pill.appendChild(name);
549+
550+
const value = document.createElement("strong");
551+
value.textContent = formatYield(resourceYield.value);
552+
pill.appendChild(value);
553+
554+
yieldStrip.appendChild(pill);
555+
});
556+
} else {
557+
const empty = document.createElement("span");
558+
empty.className = "yield-empty";
559+
empty.textContent = "None";
560+
yieldStrip.appendChild(empty);
561+
}
562+
563+
card.appendChild(yieldStrip);
564+
565+
if (isSelected) {
566+
const inspectionRows = nearbyWeightedNodes(
567+
state.rawNodes,
568+
res,
569+
state.config.weights,
570+
state.config.sigma,
571+
8,
572+
);
573+
const inspectionList = document.createElement("div");
574+
inspectionList.className = "inspection-list";
575+
576+
const inspectionLabel = document.createElement("span");
577+
inspectionLabel.className = "yield-label";
578+
inspectionLabel.textContent = "Nearby contributors";
579+
inspectionList.appendChild(inspectionLabel);
580+
581+
if (inspectionRows.length > 0) {
582+
inspectionRows.forEach((row) => {
583+
const item = document.createElement("div");
584+
item.className = "inspection-row";
585+
586+
const dot = document.createElement("span");
587+
dot.className = "yield-dot";
588+
dot.style.backgroundColor = row.color;
589+
item.appendChild(dot);
590+
591+
const body = document.createElement("div");
592+
body.className = "inspection-row-body";
593+
594+
const title = document.createElement("span");
595+
title.className = "inspection-row-title";
596+
title.textContent = row.name;
597+
body.appendChild(title);
598+
599+
const meta = document.createElement("span");
600+
meta.className = "inspection-row-meta";
601+
meta.textContent = `${row.purity} - ${Math.round(row.distance)}m`;
602+
body.appendChild(meta);
603+
604+
item.appendChild(body);
605+
606+
const contribution = document.createElement("strong");
607+
contribution.className = "inspection-contribution";
608+
contribution.textContent = formatContribution(row.contribution);
609+
item.appendChild(contribution);
610+
611+
if (row.obstructed) {
612+
const badge = document.createElement("span");
613+
badge.className = "inspection-badge";
614+
badge.textContent = "Obstructed";
615+
item.appendChild(badge);
616+
}
617+
618+
inspectionList.appendChild(item);
619+
});
620+
} else {
621+
const empty = document.createElement("span");
622+
empty.className = "yield-empty";
623+
empty.textContent = "None";
624+
inspectionList.appendChild(empty);
625+
}
626+
627+
card.appendChild(inspectionList);
628+
}
629+
630+
if (obstructedCount > 0) {
631+
const obstructedNote = document.createElement("div");
632+
obstructedNote.className = "obstructed-note";
633+
obstructedNote.textContent = `${obstructedCount} obstructed node${obstructedCount === 1 ? "" : "s"} excluded`;
634+
card.appendChild(obstructedNote);
635+
}
636+
466637
card.addEventListener("click", () => {
467638
selectResult(idx);
468639
});

0 commit comments

Comments
 (0)