@@ -8,6 +8,9 @@ import type { Layer } from "./Layer";
8
8
export const isNukeType = ( t : UnitType ) =>
9
9
t === UnitType . AtomBomb || t === UnitType . HydrogenBomb || t === UnitType . MIRV ;
10
10
11
+ /**
12
+ * Renders a deterministic preview for single-blast nukes and MIRV scatter.
13
+ */
11
14
export class NukePreview implements Layer {
12
15
constructor (
13
16
private game : GameView ,
@@ -25,8 +28,8 @@ export class NukePreview implements Layer {
25
28
return false ;
26
29
}
27
30
28
- // deterministic hash
29
- private h32 = ( x : number ) => {
31
+ // 32-bit deterministic hash
32
+ private h32 = ( x : number ) : number => {
30
33
x ^= x >>> 16 ;
31
34
x = Math . imul ( x , 0x7feb352d ) ;
32
35
x ^= x >>> 15 ;
@@ -35,76 +38,77 @@ export class NukePreview implements Layer {
35
38
return x >>> 0 ;
36
39
} ;
37
40
41
+ private rand01 = ( x : number , y : number , seed : number ) : number =>
42
+ ( this . h32 ( this . h32 ( x ) ^ this . h32 ( y ) ^ seed ) & 0xffff ) / 0x10000 ;
43
+
38
44
renderLayer ( ctx : CanvasRenderingContext2D ) : void {
39
45
const p = this . ui . nukePreview ;
40
46
const anchor = this . ui . nukeAnchor ;
41
47
if ( ! p ?. active || ! anchor ) return ;
42
48
43
- // seed stability per (type, anchor)
49
+ // Stable seed per (type, anchor)
44
50
const sig = `${ p . nukeType } |${ anchor . x } |${ anchor . y } ` ;
45
51
if ( this . _npSig !== sig ) {
46
52
this . _npSig = sig ;
47
53
this . _npSeed = this . game . ticks ( ) ;
48
54
}
49
-
50
55
const seed = this . _npSeed ;
51
56
52
- // MIRV branch (scatter a bunch of mini-warheads)
53
57
if ( p . nukeType === "MIRV" ) {
54
58
this . renderMirvPreview ( ctx , anchor . x , anchor . y , seed ) ;
55
59
return ;
56
60
}
57
61
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 ) ;
62
75
const s = this . transform . scale ;
63
- const rInner = inner * s ;
64
- const rOuter = outer * s ;
65
76
66
77
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 ;
72
84
73
85
ctx . save ( ) ;
74
86
ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
75
87
ctx . globalCompositeOperation = "source-over" ;
76
88
ctx . imageSmoothingEnabled = false ;
77
89
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
+ } ) ;
85
96
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 ;
98
100
const tileStep = Math . max ( 1 , Math . floor ( 2 / Math . max ( 0.5 , s ) ) ) ;
99
101
ctx . fillStyle = "rgba(220, 20, 60, 0.14)" ;
100
102
101
103
for ( let dy = - outer ; dy <= outer ; dy += tileStep ) {
102
- const wy = anchor . y + dy ;
104
+ const wy = ay + dy ;
103
105
for ( let dx = - outer ; dx <= outer ; dx += tileStep ) {
104
- const wx = anchor . x + dx ;
106
+ const wx = ax + dx ;
105
107
const d2 = dx * dx + dy * dy ;
106
108
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 ) {
108
112
const pt = this . transform . worldToScreenCoordinates ( new Cell ( wx , wy ) ) ;
109
113
const px = pt . x - rect . left ;
110
114
const py = pt . y - rect . top ;
@@ -114,93 +118,52 @@ export class NukePreview implements Layer {
114
118
}
115
119
}
116
120
117
- // safety line just inside real outer
121
+ // Static safety line just inside the outer radius
118
122
const halfTilePx = s * tileStep * 0.5 ;
119
123
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)" ) ;
149
126
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 ( [ ] ) ;
159
127
ctx . restore ( ) ;
160
128
}
161
129
130
+ // ---------- MIRV scatter preview ----------
131
+
162
132
private renderMirvPreview (
163
133
ctx : CanvasRenderingContext2D ,
164
134
ax : number ,
165
135
ay : number ,
166
136
seed : number ,
167
- ) {
137
+ ) : void {
168
138
const s = this . transform . scale ;
169
139
const rect = this . transform . boundingRect ( ) ;
170
140
171
- // Use the actual MIRV warhead magnitudes (no extra scaling)
141
+ // Warhead magnitudes
172
142
const wh = this . game . config ( ) . nukeMagnitudes ( UnitType . MIRVWarhead ) ;
173
143
const rInnerPx = wh . inner * s ;
174
144
const rOuterPx = wh . outer * s ;
175
145
176
- // Match MirvExecution.mirvRange exactly
146
+ // MIRV parameters
177
147
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
180
150
181
- // Owner gate: preview only on tiles with the same owner as the anchor (player or TerraNullius)
182
151
const anchorOwner = this . game . owner ( this . game . ref ( ax , ay ) ) ;
183
152
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" } ` ;
186
156
if ( this . _mirvSig !== sig ) {
187
157
this . _mirvSig = sig ;
188
158
this . _mirvTargets = [ ] ;
189
159
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 ;
193
160
let attempts = 0 ;
194
161
const MAX_ATTEMPTS = 15000 ;
195
162
196
- while (
197
- this . _mirvTargets . length < PREVIEW_COUNT &&
198
- attempts < MAX_ATTEMPTS
199
- ) {
163
+ while ( this . _mirvTargets . length < PREVIEW_COUNT && attempts < MAX_ATTEMPTS ) {
200
164
attempts ++ ;
201
165
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
204
167
const r01 = ( this . h32 ( seed ^ attempts ) & 0xffff ) / 0x10000 ;
205
168
const t01 = ( this . h32 ( seed ^ ( attempts * 2654435761 ) ) & 0xffff ) / 0x10000 ;
206
169
@@ -218,10 +181,9 @@ export class NukePreview implements Layer {
218
181
if ( ! this . game . isLand ( tile ) ) continue ;
219
182
220
183
const owner = this . game . owner ( tile ) ;
221
- // must match the same owner object (player or TerraNullius)
222
184
if ( owner !== anchorOwner ) continue ;
223
185
224
- // respect min Manhattan spacing among already selected targets
186
+ // Enforce minimum Manhattan spacing
225
187
let tooClose = false ;
226
188
for ( const t of this . _mirvTargets ) {
227
189
if ( Math . abs ( t . x - tx ) + Math . abs ( t . y - ty ) < MIN_SPACING ) {
@@ -236,14 +198,6 @@ export class NukePreview implements Layer {
236
198
}
237
199
}
238
200
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
-
247
201
ctx . save ( ) ;
248
202
ctx . setTransform ( 1 , 0 , 0 , 1 , 0 , 0 ) ;
249
203
ctx . globalCompositeOperation = "source-over" ;
@@ -253,36 +207,66 @@ export class NukePreview implements Layer {
253
207
let cx = pt . x - rect . left + s * 0.5 ;
254
208
let cy = pt . y - rect . top + s * 0.5 ;
255
209
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
+ ) ;
283
230
}
284
231
285
- ctx . setLineDash ( [ ] ) ;
286
232
ctx . restore ( ) ;
287
233
}
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
+ }
288
272
}
0 commit comments