Skip to content

Commit 394beaa

Browse files
committed
Make NukePreview more readable
1 parent 79ac40b commit 394beaa

File tree

1 file changed

+116
-132
lines changed

1 file changed

+116
-132
lines changed

src/client/graphics/layers/NukePreview.ts

Lines changed: 116 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type { Layer } from "./Layer";
88
export const isNukeType = (t: UnitType) =>
99
t === UnitType.AtomBomb || t === UnitType.HydrogenBomb || t === UnitType.MIRV;
1010

11+
/**
12+
* Renders a deterministic preview for single-blast nukes and MIRV scatter.
13+
*/
1114
export class NukePreview implements Layer {
1215
constructor(
1316
private game: GameView,
@@ -25,8 +28,8 @@ export class NukePreview implements Layer {
2528
return false;
2629
}
2730

28-
// deterministic hash
29-
private h32 = (x: number) => {
31+
// 32-bit deterministic hash
32+
private h32 = (x: number): number => {
3033
x ^= x >>> 16;
3134
x = Math.imul(x, 0x7feb352d);
3235
x ^= x >>> 15;
@@ -35,76 +38,77 @@ export class NukePreview implements Layer {
3538
return x >>> 0;
3639
};
3740

41+
private rand01 = (x: number, y: number, seed: number): number =>
42+
(this.h32(this.h32(x) ^ this.h32(y) ^ seed) & 0xffff) / 0x10000;
43+
3844
renderLayer(ctx: CanvasRenderingContext2D): void {
3945
const p = this.ui.nukePreview;
4046
const anchor = this.ui.nukeAnchor;
4147
if (!p?.active || !anchor) return;
4248

43-
// seed stability per (type, anchor)
49+
// Stable seed per (type, anchor)
4450
const sig = `${p.nukeType}|${anchor.x}|${anchor.y}`;
4551
if (this._npSig !== sig) {
4652
this._npSig = sig;
4753
this._npSeed = this.game.ticks();
4854
}
49-
5055
const seed = this._npSeed;
5156

52-
// MIRV branch (scatter a bunch of mini-warheads)
5357
if (p.nukeType === "MIRV") {
5458
this.renderMirvPreview(ctx, anchor.x, anchor.y, seed);
5559
return;
5660
}
5761

58-
// === existing single-blast code (atom/hydrogen) ===
59-
const { inner, outer } = this.game
60-
.config()
61-
.nukeMagnitudes(p.nukeType as NukeType);
62+
this.renderSingleBlastPreview(ctx, anchor.x, anchor.y, p.nukeType as NukeType, seed);
63+
}
64+
65+
// ---------- Single-blast (Atom/Hydrogen) ----------
66+
67+
private renderSingleBlastPreview(
68+
ctx: CanvasRenderingContext2D,
69+
ax: number,
70+
ay: number,
71+
nukeType: NukeType,
72+
seed: number,
73+
): void {
74+
const { inner, outer } = this.game.config().nukeMagnitudes(nukeType);
6275
const s = this.transform.scale;
63-
const rInner = inner * s;
64-
const rOuter = outer * s;
6576

6677
const rect = this.transform.boundingRect();
67-
const tl = this.transform.worldToScreenCoordinates(
68-
new Cell(anchor.x, anchor.y),
69-
);
70-
const cx = tl.x - rect.left + s * 0.5;
71-
const cy = tl.y - rect.top + s * 0.5;
78+
const topLeftPx = this.transform.worldToScreenCoordinates(new Cell(ax, ay));
79+
const cx = topLeftPx.x - rect.left + s * 0.5;
80+
const cy = topLeftPx.y - rect.top + s * 0.5;
81+
82+
const rInnerPx = inner * s;
83+
const rOuterPx = outer * s;
7284

7385
ctx.save();
7486
ctx.setTransform(1, 0, 0, 1, 0, 0);
7587
ctx.globalCompositeOperation = "source-over";
7688
ctx.imageSmoothingEnabled = false;
7789

78-
// inner ring + fill
79-
ctx.beginPath();
80-
ctx.setLineDash([]);
81-
ctx.lineWidth = 2;
82-
ctx.strokeStyle = "rgba(220, 20, 60, 0.65)";
83-
ctx.arc(cx, cy, rInner, 0, Math.PI * 2);
84-
ctx.stroke();
90+
// Inner circle stroke + fill
91+
this.drawCircle(ctx, cx, cy, rInnerPx, {
92+
strokeWidth: 2,
93+
stroke: "rgba(220, 20, 60, 0.65)",
94+
fill: "rgba(220, 20, 60, 0.30)",
95+
});
8596

86-
ctx.beginPath();
87-
ctx.fillStyle = "rgba(220, 20, 60, 0.30)";
88-
ctx.arc(cx, cy, rInner, 0, Math.PI * 2);
89-
ctx.fill();
90-
91-
92-
const rand01 = (x: number, y: number) =>
93-
(this.h32(this.h32(x) ^ this.h32(y) ^ seed) & 0xffff) / 0x10000;
94-
95-
// probabilistic band
96-
const outer2 = outer * outer,
97-
inner2 = inner * inner;
97+
// Probabilistic band between inner and outer radii
98+
const outer2 = outer * outer;
99+
const inner2 = inner * inner;
98100
const tileStep = Math.max(1, Math.floor(2 / Math.max(0.5, s)));
99101
ctx.fillStyle = "rgba(220, 20, 60, 0.14)";
100102

101103
for (let dy = -outer; dy <= outer; dy += tileStep) {
102-
const wy = anchor.y + dy;
104+
const wy = ay + dy;
103105
for (let dx = -outer; dx <= outer; dx += tileStep) {
104-
const wx = anchor.x + dx;
106+
const wx = ax + dx;
105107
const d2 = dx * dx + dy * dy;
106108
if (d2 > outer2) continue;
107-
if (d2 <= inner2 || rand01(wx, wy) < 0.5) {
109+
110+
// Fill if inside inner or by probability
111+
if (d2 <= inner2 || this.rand01(wx, wy, seed) < 0.5) {
108112
const pt = this.transform.worldToScreenCoordinates(new Cell(wx, wy));
109113
const px = pt.x - rect.left;
110114
const py = pt.y - rect.top;
@@ -114,93 +118,52 @@ export class NukePreview implements Layer {
114118
}
115119
}
116120

117-
// safety line just inside real outer
121+
// Static safety line just inside the outer radius
118122
const halfTilePx = s * tileStep * 0.5;
119123
const visualPad = Math.max(halfTilePx + 1, 3);
120-
const rOuterVisual = Math.max(rInner + 2, rOuter - visualPad);
121-
ctx.beginPath();
122-
ctx.setLineDash([]);
123-
ctx.lineWidth = 1;
124-
ctx.strokeStyle = "rgba(220,20,60,0.35)";
125-
ctx.arc(cx, cy, rOuterVisual, 0, Math.PI * 2);
126-
ctx.stroke();
127-
128-
// spinning rings outside the band
129-
const bandPx = Math.max(0, rOuter - rInner);
130-
const offsetOut = Math.max(4, Math.min(24, bandPx * 0.2));
131-
const sepOut = Math.max(6, Math.min(18, bandPx * 0.18));
132-
const rRing1 = rOuter + offsetOut;
133-
const rRing2 = rRing1 + sepOut;
134-
135-
const dash = 12,
136-
gap = 10,
137-
speed = 15;
138-
const t = performance.now() / 1000;
139-
const cycle = dash + gap;
140-
const spin = (t * speed) % cycle;
141-
142-
ctx.beginPath();
143-
ctx.setLineDash([dash, gap]);
144-
ctx.lineDashOffset = -spin;
145-
ctx.lineWidth = 1.5;
146-
ctx.strokeStyle = "rgba(220, 20, 60, 0.95)";
147-
ctx.arc(cx, cy, rRing1, 0, Math.PI * 2);
148-
ctx.stroke();
124+
const rOuterVisual = Math.max(rInnerPx + 2, rOuterPx - visualPad);
125+
this.strokeCircle(ctx, cx, cy, rOuterVisual, 1, "rgba(220,20,60,0.35)");
149126

150-
ctx.beginPath();
151-
ctx.setLineDash([dash, gap]);
152-
ctx.lineDashOffset = spin;
153-
ctx.lineWidth = 1;
154-
ctx.strokeStyle = "rgba(220, 20, 60, 0.9)";
155-
ctx.arc(cx, cy, rRing2, 0, Math.PI * 2);
156-
ctx.stroke();
157-
158-
ctx.setLineDash([]);
159127
ctx.restore();
160128
}
161129

130+
// ---------- MIRV scatter preview ----------
131+
162132
private renderMirvPreview(
163133
ctx: CanvasRenderingContext2D,
164134
ax: number,
165135
ay: number,
166136
seed: number,
167-
) {
137+
): void {
168138
const s = this.transform.scale;
169139
const rect = this.transform.boundingRect();
170140

171-
// Use the actual MIRV warhead magnitudes (no extra scaling)
141+
// Warhead magnitudes
172142
const wh = this.game.config().nukeMagnitudes(UnitType.MIRVWarhead);
173143
const rInnerPx = wh.inner * s;
174144
const rOuterPx = wh.outer * s;
175145

176-
// Match MirvExecution.mirvRange exactly
146+
// MIRV parameters
177147
const MIRV_RANGE = 1500; // tiles
178-
const MIN_SPACING = 25; // tiles (Manhattan), like proximityCheck
179-
const PREVIEW_COUNT = 100; // draw ~100 for clarity/perf
148+
const MIN_SPACING = 25; // Manhattan distance
149+
const PREVIEW_COUNT = 100; // count for perf/clarity
180150

181-
// Owner gate: preview only on tiles with the same owner as the anchor (player or TerraNullius)
182151
const anchorOwner = this.game.owner(this.game.ref(ax, ay));
183152

184-
// Recompute targets only when signature changes
185-
const sig = `MIRV|${ax}|${ay}|${anchorOwner.isPlayer() ? (anchorOwner as any).id() : "TN"}`;
153+
// Recompute candidate targets when signature changes
154+
const sig =
155+
`MIRV|${ax}|${ay}|${anchorOwner.isPlayer() ? (anchorOwner as any).id() : "TN"}`;
186156
if (this._mirvSig !== sig) {
187157
this._mirvSig = sig;
188158
this._mirvTargets = [];
189159

190-
// We’ll pick PREVIEW_COUNT positions uniformly from the circle (radius 1500),
191-
// respecting owner/land and min spacing, with a bounded attempts budget.
192-
const range2 = MIRV_RANGE * MIRV_RANGE;
193160
let attempts = 0;
194161
const MAX_ATTEMPTS = 15000;
195162

196-
while (
197-
this._mirvTargets.length < PREVIEW_COUNT &&
198-
attempts < MAX_ATTEMPTS
199-
) {
163+
while (this._mirvTargets.length < PREVIEW_COUNT && attempts < MAX_ATTEMPTS) {
200164
attempts++;
201165

202-
// Uniform sample in circle via polar
203-
// Use a deterministic PRNG from seed + attempts
166+
// Uniform sample in circle via polar with deterministic PRNG
204167
const r01 = (this.h32(seed ^ attempts) & 0xffff) / 0x10000;
205168
const t01 = (this.h32(seed ^ (attempts * 2654435761)) & 0xffff) / 0x10000;
206169

@@ -218,10 +181,9 @@ export class NukePreview implements Layer {
218181
if (!this.game.isLand(tile)) continue;
219182

220183
const owner = this.game.owner(tile);
221-
// must match the same owner object (player or TerraNullius)
222184
if (owner !== anchorOwner) continue;
223185

224-
// respect min Manhattan spacing among already selected targets
186+
// Enforce minimum Manhattan spacing
225187
let tooClose = false;
226188
for (const t of this._mirvTargets) {
227189
if (Math.abs(t.x - tx) + Math.abs(t.y - ty) < MIN_SPACING) {
@@ -236,14 +198,6 @@ export class NukePreview implements Layer {
236198
}
237199
}
238200

239-
// Spin/dash (tiny rings)
240-
const dash = 8,
241-
gap = 8,
242-
speed = 18;
243-
const time = performance.now() / 1000;
244-
const cycle = dash + gap;
245-
const spin = (time * speed) % cycle;
246-
247201
ctx.save();
248202
ctx.setTransform(1, 0, 0, 1, 0, 0);
249203
ctx.globalCompositeOperation = "source-over";
@@ -253,36 +207,66 @@ export class NukePreview implements Layer {
253207
let cx = pt.x - rect.left + s * 0.5;
254208
let cy = pt.y - rect.top + s * 0.5;
255209

256-
// slight intra-tile jitter so they don’t sit perfectly centered
257-
const jx = ((w & 0xff) / 255 - 0.5) * (s * 0.45);
258-
const jy = (((w >> 8) & 0xff) / 255 - 0.5) * (s * 0.45);
259-
cx += jx;
260-
cy += jy;
261-
262-
// mini inner stroke + fill
263-
ctx.beginPath();
264-
ctx.setLineDash([]);
265-
ctx.lineWidth = 1;
266-
ctx.strokeStyle = "rgba(220, 20, 60, 0.7)";
267-
ctx.arc(cx, cy, rInnerPx, 0, Math.PI * 2);
268-
ctx.stroke();
269-
270-
ctx.beginPath();
271-
ctx.fillStyle = "rgba(220, 20, 60, 0.22)";
272-
ctx.arc(cx, cy, rInnerPx, 0, Math.PI * 2);
273-
ctx.fill();
274-
275-
// tiny spinning ring right outside inner
276-
ctx.beginPath();
277-
ctx.setLineDash([dash, gap]);
278-
ctx.lineDashOffset = w & 1 ? -spin : spin; // alternate direction for variety
279-
ctx.lineWidth = 1;
280-
ctx.strokeStyle = "rgba(220, 20, 60, 0.9)";
281-
ctx.arc(cx, cy, Math.max(rInnerPx + 2, rOuterPx - 1), 0, Math.PI * 2);
282-
ctx.stroke();
210+
// Sub-tile jitter for visual variety
211+
cx += ((w & 0xff) / 255 - 0.5) * (s * 0.45);
212+
cy += (((w >> 8) & 0xff) / 255 - 0.5) * (s * 0.45);
213+
214+
// Inner circle stroke + fill
215+
this.drawCircle(ctx, cx, cy, rInnerPx, {
216+
strokeWidth: 1,
217+
stroke: "rgba(220, 20, 60, 0.7)",
218+
fill: "rgba(220, 20, 60, 0.22)",
219+
});
220+
221+
// Static outer boundary hint
222+
this.strokeCircle(
223+
ctx,
224+
cx,
225+
cy,
226+
Math.max(rInnerPx + 2, rOuterPx - 1),
227+
1,
228+
"rgba(220, 20, 60, 0.35)",
229+
);
283230
}
284231

285-
ctx.setLineDash([]);
286232
ctx.restore();
287233
}
234+
235+
// ---------- Drawing helpers ----------
236+
237+
private drawCircle(
238+
ctx: CanvasRenderingContext2D,
239+
cx: number,
240+
cy: number,
241+
r: number,
242+
opts: { strokeWidth: number; stroke: string; fill: string },
243+
): void {
244+
ctx.beginPath();
245+
ctx.setLineDash([]);
246+
ctx.lineWidth = opts.strokeWidth;
247+
ctx.strokeStyle = opts.stroke;
248+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
249+
ctx.stroke();
250+
251+
ctx.beginPath();
252+
ctx.fillStyle = opts.fill;
253+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
254+
ctx.fill();
255+
}
256+
257+
private strokeCircle(
258+
ctx: CanvasRenderingContext2D,
259+
cx: number,
260+
cy: number,
261+
r: number,
262+
width: number,
263+
color: string,
264+
): void {
265+
ctx.beginPath();
266+
ctx.setLineDash([]);
267+
ctx.lineWidth = width;
268+
ctx.strokeStyle = color;
269+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
270+
ctx.stroke();
271+
}
288272
}

0 commit comments

Comments
 (0)