File size: 7,137 Bytes
1aaa2ca
 
 
 
 
 
 
 
 
 
 
 
592bfd0
b870df3
1aaa2ca
592bfd0
 
 
 
6bd2c72
 
 
1aaa2ca
 
 
6bd2c72
1aaa2ca
6e6bac2
 
 
 
 
 
 
 
1aaa2ca
 
b870df3
 
592bfd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a53030d
592bfd0
1aaa2ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e6bac2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1aaa2ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/* ============================================================
   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.
   ============================================================ */

"use strict";

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;
}