Spaces:
Paused
Paused
Claude
Claude Opus 5
Tier 3g: name the three render rules that were copied instead
b870df3 unverified Download renderNodes.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 7.14 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/renderNodes.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities/renderNodes.js
-
curl -L -o renderNodes.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/renderNodes.js
7.14 kB
| /* ============================================================ | |
| Resource-deposit rendering β the charted ore/crystal/gas silhouettes on the | |
| map. Deposits are charted map knowledge (data.js), not battlefield intel, so | |
| they draw at full visibility regardless of fog (a hidden cache only appears | |
| once scouted). Each rock's irregular outline is stable frame-to-frame (seeded | |
| off its id). Split out of render.js; drawFrame calls drawNodes. | |
| ============================================================ */ | |
| ; | |
| import { COM } from "./data.js"; | |
| import { isNodeDiscovered } from "./engine/fog.js"; | |
| import { UNITS } from "./engine/entities.js"; | |
| import { hashStr, seededRng, pathPoints, inView, centeredText } from "./renderShared.js"; | |
| // The same amber "running, just not at full rate" tone renderBuildings.js's producer-concern | |
| // badge uses (CONCERN_STYLE.warn) β a saturated node isn't dead, just diminishing. | |
| const SATURATION_COLOR = "#fbbf24"; | |
| // observerMode (default false, every existing caller unaffected): a hidden cache is charted | |
| // knowledge to a spectator too β see render.js's drawFrame header comment. | |
| export function drawNodes(ctx, state, view, observerMode = false) { | |
| for (const n of state.map.nodes) { | |
| if (n.amount <= 0) continue; | |
| if (view && !inView(view, n.x, n.y, 20)) continue; // off-screen deposit | |
| if (!observerMode && !isNodeDiscovered(state.fog, n)) continue; // a hidden cache stays dark until scouted | |
| const r = 7 + 9 * (n.amount / n.max); | |
| if (n.wreck) { | |
| drawWreckNode(ctx, n, r); | |
| } else { | |
| const extract = COM[n.com]?.extract; | |
| if (extract === "forage") drawOrganicNode(ctx, n, r); | |
| else if (extract === "capture") drawGasNode(ctx, n, r); | |
| else drawRockyNode(ctx, n, r); // mine / exploit β ore, crystals, radioactives, ice, relics | |
| } | |
| ctx.fillStyle = "#05070f"; | |
| // baseline "alphabetic": this glyph sits on a hand-tuned +3 offset, not vertically centered. | |
| centeredText(ctx, COM[n.com]?.ico || "?", n.x, n.y + 3, "10px sans-serif", "alphabetic"); | |
| // Saturation cue: node.miners is retallied every tick by sim.js countMiners, but nothing used | |
| // to render it, so a node six workers deep just looked like ordinary slow income with no | |
| // visible cause. Once the live headcount crosses the worker's soft cap (miningEfficiency, | |
| // engine/gather.js β the point where an extra miner starts pulling a diminishing share), ring | |
| // it and post the live "miners/cap" count, so "spread out, then expand" reads at a glance | |
| // without opening the panel. | |
| if ((n.miners || 0) > UNITS.worker.minerSoftCap) { | |
| ctx.beginPath(); | |
| ctx.arc(n.x, n.y, r + 6, 0, Math.PI * 2); | |
| ctx.strokeStyle = SATURATION_COLOR; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.font = "9px sans-serif"; | |
| ctx.fillStyle = SATURATION_COLOR; | |
| ctx.fillText(`${n.miners}/${UNITS.worker.minerSoftCap}`, n.x, n.y - r - 9); | |
| ctx.textAlign = "left"; // restore the canvas default, like textBaseline elsewhere | |
| } | |
| } | |
| } | |
| // Cached per-node unit silhouette (offsets at radius 1), so the seeded-PRNG | |
| // polygon jitter is computed once per node instead of every single frame β the | |
| // shape is static, only its radius changes as the deposit depletes. | |
| const nodeShapeCache = new Map(); | |
| function rockyShape(id) { | |
| let shape = nodeShapeCache.get(id); | |
| if (!shape) { | |
| const rng = seededRng(hashStr(id)); | |
| const rot = rng() * Math.PI * 2; // same rng draw order as the original, so silhouettes are unchanged | |
| shape = []; | |
| for (let i = 0; i < 8; i++) { | |
| const a = rot + (i / 8) * Math.PI * 2; | |
| const jitter = 0.72 + rng() * 0.5; | |
| shape.push([Math.cos(a) * jitter, Math.sin(a) * jitter]); | |
| } | |
| nodeShapeCache.set(id, shape); | |
| } | |
| return shape; | |
| } | |
| // Mined/exploited deposits (ore, crystals, radioactives, ice, relics) read | |
| // as a faceted asteroid chunk β an irregular polygon beats a perfect circle | |
| // at selling "rock" at a glance, and the per-node seed keeps it stable. | |
| function drawRockyNode(ctx, n, r) { | |
| const pts = rockyShape(n.id).map(([ux, uy]) => [n.x + ux * r, n.y + uy * r]); | |
| pathPoints(ctx, pts); | |
| ctx.fillStyle = "#ffd166"; | |
| ctx.globalAlpha = 0.85; | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| ctx.strokeStyle = "#b9822f"; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| // Foraged deposits (biomass, spice) read as a soft cluster of overlapping | |
| // blobs β organic growth rather than mineral facets. | |
| function drawOrganicNode(ctx, n, r) { | |
| const rng = seededRng(hashStr(n.id) ^ 0x9e3779b9); | |
| ctx.fillStyle = "#ffd166"; | |
| ctx.globalAlpha = 0.85; | |
| const lobes = 3; | |
| for (let i = 0; i < lobes; i++) { | |
| const a = (i / lobes) * Math.PI * 2 + rng() * 0.6; | |
| const dist = r * 0.32 * rng(); | |
| const lr = r * (0.55 + rng() * 0.15); | |
| ctx.beginPath(); | |
| ctx.arc(n.x + Math.cos(a) * dist, n.y + Math.sin(a) * dist, lr, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.globalAlpha = 1; | |
| } | |
| // Battle wreckage (engine/wreckage.js `wreck: true`) reads as jagged scrap β a sharper, | |
| // more irregular silhouette than a natural deposit's faceted-but-round asteroid look, in | |
| // a cool grey/rust palette instead of the warm gold every mined/foraged/captured deposit | |
| // shares β so a battlefield's mineable aftermath reads at a glance as wreckage, not an | |
| // ore seam that happened to be sitting there. Regardless of its actual commodity (a | |
| // wreck node can be any raw good, or the Phase-2 battle bonus's metals/electronics/ | |
| // relics β none of which have a distinct `extract` shape of their own), every wreck node | |
| // shares this one look. | |
| function drawWreckNode(ctx, n, r) { | |
| const pts = wreckShape(n.id).map(([ux, uy]) => [n.x + ux * r, n.y + uy * r]); | |
| pathPoints(ctx, pts); | |
| ctx.fillStyle = "#8a8f98"; | |
| ctx.globalAlpha = 0.85; | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| ctx.strokeStyle = "#c65a3a"; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| // Cached per-node silhouette, same idea as rockyShape above: computed once, reused every | |
| // frame. One fewer point and a wider jitter band than a rocky node's 8-point shape, so it | |
| // reads as jagged debris rather than a smooth asteroid facet. | |
| const wreckShapeCache = new Map(); | |
| function wreckShape(id) { | |
| let shape = wreckShapeCache.get(id); | |
| if (!shape) { | |
| const rng = seededRng(hashStr(id) ^ 0x51ed270b); | |
| const rot = rng() * Math.PI * 2; | |
| shape = []; | |
| const points = 7; | |
| for (let i = 0; i < points; i++) { | |
| const a = rot + (i / points) * Math.PI * 2; | |
| const jitter = 0.55 + rng() * 0.85; | |
| shape.push([Math.cos(a) * jitter, Math.sin(a) * jitter]); | |
| } | |
| wreckShapeCache.set(id, shape); | |
| } | |
| return shape; | |
| } | |
| // Captured deposits (Helium-3 gas) read as a soft drifting cloud β nested | |
| // translucent rings instead of a hard edge. | |
| function drawGasNode(ctx, n, r) { | |
| const rings = [ | |
| { rr: r * 1.3, a: 0.18 }, | |
| { rr: r * 0.95, a: 0.35 }, | |
| { rr: r * 0.55, a: 0.7 }, | |
| ]; | |
| for (const { rr, a } of rings) { | |
| ctx.beginPath(); | |
| ctx.arc(n.x, n.y, rr, 0, Math.PI * 2); | |
| ctx.fillStyle = "#ffd166"; | |
| ctx.globalAlpha = a; | |
| ctx.fill(); | |
| } | |
| ctx.globalAlpha = 1; | |
| } | |