@nebularstreams/overlay-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +44 -0
  2. package/preview/assets/images/favicon.ico +0 -0
  3. package/preview/assets/images/favicon.png +0 -0
  4. package/preview/assets/images/login-right.webp +0 -0
  5. package/preview/assets/images/nebular.color.100h.png +0 -0
  6. package/preview/assets/images/nebular.color.white.100h.png +0 -0
  7. package/preview/assets/images/nebular.medallion.transparent..png +0 -0
  8. package/preview/assets/images/testcard.png +0 -0
  9. package/preview/assets/webfonts/fa-brands-400.woff2 +0 -0
  10. package/preview/assets/webfonts/fa-solid-900.woff2 +0 -0
  11. package/preview/bundle.min.js +6 -0
  12. package/preview/index.css +28 -0
  13. package/preview/index.html +23 -0
  14. package/readme.md +132 -0
  15. package/skill/nebular-overlay/SKILL.md +56 -0
  16. package/skill/nebular-overlay/agents/openai.yaml +4 -0
  17. package/skill/nebular-overlay/references/examples.md +327 -0
  18. package/skill/nebular-overlay/references/manifest.md +153 -0
  19. package/skill/nebular-overlay/references/runtime-and-design.md +88 -0
  20. package/skill/nebular-overlay/scripts/validate-templates.mjs +244 -0
  21. package/templates/all/broadcast/breaking-news/index.html +100 -0
  22. package/templates/all/broadcast/breaking-news/manifest.json +9 -0
  23. package/templates/all/broadcast/empty/index.html +46 -0
  24. package/templates/all/broadcast/empty/manifest.json +7 -0
  25. package/templates/all/broadcast/empty/nebular.medallion.transparent.png +0 -0
  26. package/templates/all/broadcast/event-countdown/index.html +160 -0
  27. package/templates/all/broadcast/event-countdown/logo.png +0 -0
  28. package/templates/all/broadcast/event-countdown/manifest.json +33 -0
  29. package/templates/all/broadcast/lower-third/index.html +77 -0
  30. package/templates/all/broadcast/lower-third/manifest.json +8 -0
  31. package/templates/all/broadcast/newsticker/index.html +153 -0
  32. package/templates/all/broadcast/newsticker/manifest.json +21 -0
  33. package/templates/all/broadcast/newsticker-frames/background-label-green.png +0 -0
  34. package/templates/all/broadcast/newsticker-frames/background-label-orange.png +0 -0
  35. package/templates/all/broadcast/newsticker-frames/banner.png +0 -0
  36. package/templates/all/broadcast/newsticker-frames/index.html +229 -0
  37. package/templates/all/broadcast/newsticker-frames/manifest.json +20 -0
  38. package/templates/all/broadcast/weather/index.html +382 -0
  39. package/templates/all/broadcast/weather/manifest.json +104 -0
  40. package/templates/all/engagement/call-to-action/index.html +116 -0
  41. package/templates/all/engagement/call-to-action/manifest.json +12 -0
  42. package/templates/all/engagement/chat/index.html +121 -0
  43. package/templates/all/engagement/chat/manifest.json +32 -0
  44. package/templates/all/engagement/post/index.html +132 -0
  45. package/templates/all/engagement/post/manifest.json +31 -0
  46. package/templates/all/gambling/bingo/index.html +227 -0
  47. package/templates/all/gambling/bingo/manifest.json +144 -0
  48. package/templates/all/gambling/keno/index.html +202 -0
  49. package/templates/all/gambling/keno/manifest.json +129 -0
  50. package/templates/all/gambling/lottery/index.html +159 -0
  51. package/templates/all/gambling/lottery/manifest.json +95 -0
  52. package/templates/all/gambling/prize-wall/index.html +256 -0
  53. package/templates/all/gambling/prize-wall/manifest.json +108 -0
  54. package/templates/all/gambling/roulette/index.html +200 -0
  55. package/templates/all/gambling/roulette/manifest.json +78 -0
  56. package/templates/all/gaming/battleship/index.html +578 -0
  57. package/templates/all/gaming/battleship/manifest.json +279 -0
  58. package/templates/all/gaming/gta/mission/index.html +121 -0
  59. package/templates/all/gaming/gta/mission/manifest.json +38 -0
  60. package/templates/all/sports/football/scoreboard/index.html +115 -0
  61. package/templates/all/sports/football/scoreboard/manifest.json +54 -0
  62. package/templates/all/sports/tennis/scoreboard/index.html +120 -0
  63. package/templates/all/sports/tennis/scoreboard/manifest.json +92 -0
  64. package/templates/all/trivia/audience-poll/index.html +60 -0
  65. package/templates/all/trivia/audience-poll/manifest.json +26 -0
  66. package/templates/all/trivia/classic-quiz/index.html +49 -0
  67. package/templates/all/trivia/classic-quiz/manifest.json +15 -0
  68. package/templates/all/trivia/lightning-round/index.html +126 -0
  69. package/templates/all/trivia/lightning-round/manifest.json +27 -0
  70. package/templates/all/trivia/pasapalabra/index.html +249 -0
  71. package/templates/all/trivia/pasapalabra/manifest.json +68 -0
  72. package/templates/all/trivia/quiz-deck/index.html +58 -0
  73. package/templates/all/trivia/quiz-deck/manifest.json +25 -0
  74. package/templates/all/trivia/true-or-false/index.html +81 -0
  75. package/templates/all/trivia/true-or-false/manifest.json +27 -0
  76. package/templates/basic/broadcast/empty/index.html +46 -0
  77. package/templates/basic/broadcast/empty/manifest.json +7 -0
  78. package/templates/basic/broadcast/empty/nebular.medallion.transparent.png +0 -0
  79. package/templates/basic/broadcast/event-countdown/index.html +160 -0
  80. package/templates/basic/broadcast/event-countdown/logo.png +0 -0
  81. package/templates/basic/broadcast/event-countdown/manifest.json +33 -0
  82. package/templates/basic/broadcast/lower-third/index.html +77 -0
  83. package/templates/basic/broadcast/lower-third/manifest.json +8 -0
  84. package/templates/basic/broadcast/newsticker/index.html +153 -0
  85. package/templates/basic/broadcast/newsticker/manifest.json +21 -0
  86. package/templates/basic/broadcast/weather/index.html +382 -0
  87. package/templates/basic/broadcast/weather/manifest.json +104 -0
  88. package/templates/basic/engagement/call-to-action/index.html +116 -0
  89. package/templates/basic/engagement/call-to-action/manifest.json +12 -0
  90. package/templates/basic/engagement/chat/index.html +121 -0
  91. package/templates/basic/engagement/chat/manifest.json +32 -0
  92. package/templates/basic/engagement/post/index.html +132 -0
  93. package/templates/basic/engagement/post/manifest.json +31 -0
  94. package/templates/basic/sports/football/scoreboard/index.html +115 -0
  95. package/templates/basic/sports/football/scoreboard/manifest.json +54 -0
  96. package/templates/basic/sports/tennis/scoreboard/index.html +120 -0
  97. package/templates/basic/sports/tennis/scoreboard/manifest.json +92 -0
  98. package/templates/debug/testing/security-probe/forbidden.js +1 -0
  99. package/templates/debug/testing/security-probe/forbidden.json +1 -0
  100. package/templates/debug/testing/security-probe/forbidden.mp4 +1 -0
  101. package/templates/debug/testing/security-probe/forbidden.svg +1 -0
  102. package/templates/debug/testing/security-probe/index.html +367 -0
  103. package/templates/debug/testing/security-probe/manifest.json +13 -0
  104. package/templates/empty/broadcast/empty/index.html +46 -0
  105. package/templates/empty/broadcast/empty/manifest.json +7 -0
  106. package/templates/empty/broadcast/empty/nebular.medallion.transparent.png +0 -0
  107. package/templates/games/gambling/bingo/index.html +227 -0
  108. package/templates/games/gambling/bingo/manifest.json +56 -0
  109. package/templates/games/gambling/keno/index.html +202 -0
  110. package/templates/games/gambling/keno/manifest.json +129 -0
  111. package/templates/games/gambling/lottery/index.html +159 -0
  112. package/templates/games/gambling/lottery/manifest.json +95 -0
  113. package/templates/games/gambling/prize-wall/index.html +256 -0
  114. package/templates/games/gambling/prize-wall/manifest.json +108 -0
  115. package/templates/games/gambling/roulette/index.html +200 -0
  116. package/templates/games/gambling/roulette/manifest.json +78 -0
  117. package/templates/games/gaming/battleship/index.html +578 -0
  118. package/templates/games/gaming/battleship/manifest.json +279 -0
  119. package/templates/games/gaming/gta/mission/index.html +121 -0
  120. package/templates/games/gaming/gta/mission/manifest.json +38 -0
  121. package/templates/games/trivia/audience-poll/index.html +60 -0
  122. package/templates/games/trivia/audience-poll/manifest.json +26 -0
  123. package/templates/games/trivia/classic-quiz/index.html +49 -0
  124. package/templates/games/trivia/classic-quiz/manifest.json +15 -0
  125. package/templates/games/trivia/lightning-round/index.html +126 -0
  126. package/templates/games/trivia/lightning-round/manifest.json +27 -0
  127. package/templates/games/trivia/pasapalabra/index.html +249 -0
  128. package/templates/games/trivia/pasapalabra/manifest.json +68 -0
  129. package/templates/games/trivia/quiz-deck/index.html +58 -0
  130. package/templates/games/trivia/quiz-deck/manifest.json +25 -0
  131. package/templates/games/trivia/true-or-false/index.html +81 -0
  132. package/templates/games/trivia/true-or-false/manifest.json +27 -0
  133. package/tool/cli.cjs +7 -0
@@ -0,0 +1,578 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Battleship Tactical Board</title>
6
+ <style>
7
+ * {
8
+ box-sizing: border-box
9
+ }
10
+
11
+ html, body {
12
+ width: 100%;
13
+ height: 100%;
14
+ margin: 0;
15
+ overflow: hidden;
16
+ background: transparent
17
+ }
18
+
19
+ body {
20
+ font-family: Inter, system-ui, sans-serif;
21
+ color: #fff
22
+ }
23
+
24
+ .tactical {
25
+ position: absolute;
26
+ inset: auto 5% 9% auto;
27
+ width: 66rem;
28
+ display: grid;
29
+ grid-template-columns: 1fr 18rem;
30
+ gap: 1rem;
31
+ }
32
+
33
+ .radar, .intel {
34
+ background: #061822ed;
35
+ border: 1px solid #61e7ff4d;
36
+ border-radius: 1rem;
37
+ box-shadow: 0 1.4rem 4rem #0008
38
+ }
39
+
40
+ .radar {
41
+ padding: 1rem
42
+ }
43
+
44
+ .header {
45
+ display: flex;
46
+ justify-content: space-between;
47
+ align-items: center;
48
+ margin-bottom: .8rem
49
+ }
50
+
51
+ .header strong {
52
+ color: #75edff;
53
+ letter-spacing: .13em
54
+ }
55
+
56
+ .header span {
57
+ color: #77aab4;
58
+ font-size: .72rem
59
+ }
60
+
61
+ .grid {
62
+ display: grid;
63
+ grid-template-columns:repeat(10, minmax(0, 1fr));
64
+ gap: .3rem;
65
+ background-image: linear-gradient(#55d9ef0b 1px, transparent 1px), linear-gradient(90deg, #55d9ef0b 1px, transparent 1px);
66
+ background-size: 1.2rem 1.2rem
67
+ }
68
+
69
+ .cell {
70
+ aspect-ratio: 1;
71
+ display: grid;
72
+ place-items: center;
73
+ border: 1px solid #62dff02e;
74
+ border-radius: .25rem;
75
+ background: #0d2b386b;
76
+ color: #8bc8d2;
77
+ font-size: 1rem;
78
+ font-weight: 900;
79
+ letter-spacing: .015em;
80
+ text-shadow: 0 .08rem .18rem #000, 0 0 .45rem #41dff044;
81
+ position: relative
82
+ }
83
+
84
+ .cell.shot:after {
85
+ content: "";
86
+ width: .65rem;
87
+ height: .65rem;
88
+ border: .16rem solid #78eaff;
89
+ border-radius: 50%
90
+ }
91
+
92
+ .cell.fleet {
93
+ background: #145064c9;
94
+ border-color: #70dff0a8;
95
+ color: #c6f8ff
96
+ }
97
+
98
+ .cell.hit {
99
+ background: #641f2bd6;
100
+ border-color: #ff657c
101
+ }
102
+
103
+ .cell.hit:after {
104
+ content: "×";
105
+ width: auto;
106
+ height: auto;
107
+ border: 0;
108
+ color: #ff8799;
109
+ font-size: 2rem;
110
+ font-weight: 950
111
+ }
112
+
113
+ .cell.latest {
114
+ box-shadow: inset 0 0 0 .2rem #ffe466, 0 0 1.2rem #ffe46677;
115
+ transform: scale(1.08);
116
+ z-index: 2
117
+ }
118
+
119
+ .cell.hit-impact {
120
+ z-index: 5;
121
+ animation: direct-hit .9s cubic-bezier(.2, .85, .25, 1)
122
+ }
123
+
124
+ .radar.hit-impact {
125
+ animation: radar-hit .8s ease-out
126
+ }
127
+
128
+ .intel {
129
+ position: relative;
130
+ overflow: hidden;
131
+ display: flex;
132
+ flex-direction: column;
133
+ align-items: center;
134
+ justify-content: flex-start;
135
+ padding: 1.15rem;
136
+ text-align: center;
137
+ background: radial-gradient(circle at 50% 16%, #164b5a, #071923 50%, #041119 100%)
138
+ }
139
+
140
+ .intel:before {
141
+ content: "";
142
+ position: absolute;
143
+ width: 18rem;
144
+ height: 18rem;
145
+ top: -8.5rem;
146
+ border: 1px solid #64eaff1f;
147
+ border-radius: 50%;
148
+ box-shadow: 0 0 0 2.8rem #64eaff09, 0 0 0 5.6rem #64eaff08;
149
+ pointer-events: none
150
+ }
151
+
152
+ .command-head {
153
+ width: 100%;
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: space-between;
157
+ position: relative;
158
+ z-index: 1
159
+ }
160
+
161
+ .mission-title {
162
+ color: #d9faff;
163
+ font-size: 1rem;
164
+ font-weight: 900;
165
+ letter-spacing: .09em;
166
+ text-transform: uppercase
167
+ }
168
+
169
+ .live {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: .35rem;
173
+ color: #68eaff;
174
+ font-size: .72rem;
175
+ font-weight: 900;
176
+ letter-spacing: .12em
177
+ }
178
+
179
+ .live:before {
180
+ content: "";
181
+ width: .42rem;
182
+ height: .42rem;
183
+ border-radius: 50%;
184
+ background: #42f5cf;
185
+ box-shadow: 0 0 .7rem #42f5cf
186
+ }
187
+
188
+ .status {
189
+ margin-top: 1.4rem;
190
+ color: #6feaff;
191
+ font-size: .82rem;
192
+ font-weight: 850;
193
+ letter-spacing: .16em
194
+ }
195
+
196
+ .coordinate {
197
+ margin: .3rem 0;
198
+ font-size: 4rem;
199
+ font-weight: 950;
200
+ color: #fff
201
+ }
202
+
203
+ .result {
204
+ padding: .38rem .9rem;
205
+ border-radius: 2rem;
206
+ background: #194450;
207
+ color: #91efff;
208
+ font-size: 1rem;
209
+ font-weight: 900
210
+ }
211
+
212
+ .result.hit {
213
+ background: #9e2638;
214
+ color: #fff
215
+ }
216
+
217
+ .player-card {
218
+ width: 100%;
219
+ display: grid;
220
+ grid-template-columns: 3rem 1fr;
221
+ grid-template-rows: auto auto auto;
222
+ column-gap: .7rem;
223
+ margin-top: 1rem;
224
+ padding: .75rem;
225
+ text-align: left;
226
+ border: 1px solid #5ce6fb24;
227
+ border-radius: .65rem;
228
+ background: linear-gradient(135deg, #123542b8, #0a202b99)
229
+ }
230
+
231
+ .player-mark {
232
+ grid-row: 1 / 4;
233
+ display: grid;
234
+ place-items: center;
235
+ width: 3rem;
236
+ height: 3rem;
237
+ border-radius: 50%;
238
+ color: #061822;
239
+ background: #69eaff;
240
+ font-weight: 950;
241
+ box-shadow: 0 0 1.2rem #69eaff4d
242
+ }
243
+
244
+ .player-card span, .fleet-head span {
245
+ color: #61dff3;
246
+ font-size: .72rem;
247
+ font-weight: 900;
248
+ letter-spacing: .14em
249
+ }
250
+
251
+ .player-card strong {
252
+ overflow: hidden;
253
+ color: #fff;
254
+ font-size: 1.25rem;
255
+ text-overflow: ellipsis;
256
+ white-space: nowrap
257
+ }
258
+
259
+ .player-card small {
260
+ overflow: hidden;
261
+ color: #82aab3;
262
+ font-size: .85rem;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap
265
+ }
266
+
267
+ .metrics {
268
+ width: 100%;
269
+ display: grid;
270
+ grid-template-columns: repeat(3, 1fr);
271
+ margin: .75rem 0;
272
+ border-block: 1px solid #62dff01f
273
+ }
274
+
275
+ .metric {
276
+ padding: .8rem .15rem
277
+ }
278
+
279
+ .metric + .metric {
280
+ border-left: 1px solid #62dff01f
281
+ }
282
+
283
+ .metric strong {
284
+ display: block;
285
+ color: #e9fcff;
286
+ font-size: 2.2rem;
287
+ font-weight: 950;
288
+ line-height: 1
289
+ }
290
+
291
+ .metric span {
292
+ color: #7199a2;
293
+ display: block;
294
+ margin-top: .4rem;
295
+ font-size: .72rem;
296
+ font-weight: 900;
297
+ letter-spacing: .08em
298
+ }
299
+
300
+ .fleet-status {
301
+ width: 100%;
302
+ text-align: left
303
+ }
304
+
305
+ .fleet-head {
306
+ display: flex;
307
+ align-items: baseline;
308
+ justify-content: space-between;
309
+ margin-bottom: .5rem
310
+ }
311
+
312
+ .fleet-head strong {
313
+ color: #fff;
314
+ font-size: 1.15rem;
315
+ font-weight: 950
316
+ }
317
+
318
+ .fleet-roster {
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: .52rem
322
+ }
323
+
324
+ .ship-row {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: space-between;
328
+ min-height: 1.2rem;
329
+ transition: opacity .25s ease
330
+ }
331
+
332
+ .ship {
333
+ display: flex;
334
+ gap: .14rem
335
+ }
336
+
337
+ .ship i {
338
+ display: block;
339
+ width: 1.3rem;
340
+ height: .68rem;
341
+ border: 1px solid #7cecff99;
342
+ border-radius: .12rem;
343
+ background: linear-gradient(#5de6fa, #16758a);
344
+ box-shadow: 0 0 .45rem #53e8ff45
345
+ }
346
+
347
+ .ship-row em {
348
+ color: #6d98a1;
349
+ font-size: 1rem;
350
+ font-style: normal;
351
+ font-weight: 900;
352
+ letter-spacing: .08em
353
+ }
354
+
355
+ .ship-row.sunk {
356
+ opacity: .35
357
+ }
358
+
359
+ .ship-row.sunk .ship i {
360
+ border-color: #ff657c88;
361
+ background: #762535;
362
+ box-shadow: none
363
+ }
364
+
365
+ .ship-row.sunk em {
366
+ color: #ff8295
367
+ }
368
+
369
+ .intel.hit-impact {
370
+ animation: intel-hit .85s cubic-bezier(.2, .85, .25, 1)
371
+ }
372
+
373
+ .intel.hit-impact .coordinate,
374
+ .intel.hit-impact .result {
375
+ animation: result-hit .85s cubic-bezier(.2, .85, .25, 1)
376
+ }
377
+
378
+ @keyframes direct-hit {
379
+ 0% {
380
+ transform: scale(1.08);
381
+ box-shadow: inset 0 0 0 .2rem #fff, 0 0 0 0 #ff526fdd
382
+ }
383
+ 35% {
384
+ transform: scale(1.42) rotate(4deg);
385
+ box-shadow: inset 0 0 0 .2rem #fff, 0 0 0 1.4rem #ff526f00, 0 0 2.5rem #ff284fe8
386
+ }
387
+ 100% {
388
+ transform: scale(1.08);
389
+ box-shadow: inset 0 0 0 .2rem #ffe466, 0 0 1.2rem #ffe46677
390
+ }
391
+ }
392
+
393
+ @keyframes radar-hit {
394
+ 0%, 100% { border-color: #61e7ff4d }
395
+ 24% { border-color: #ff6d84; box-shadow: 0 0 3rem #ff294c99, inset 0 0 2.5rem #ff294c22 }
396
+ }
397
+
398
+ @keyframes intel-hit {
399
+ 0%, 100% { transform: translateX(0) }
400
+ 18% { transform: translateX(.55rem); box-shadow: 0 0 3rem #ff294c99 }
401
+ 34% { transform: translateX(-.35rem) }
402
+ 50% { transform: translateX(.18rem) }
403
+ }
404
+
405
+ @keyframes result-hit {
406
+ 0% { transform: scale(.82); filter: brightness(2.2) }
407
+ 38% { transform: scale(1.22); filter: brightness(1.5) }
408
+ 100% { transform: scale(1); filter: brightness(1) }
409
+ }
410
+
411
+ @media (prefers-reduced-motion: reduce) {
412
+ .cell.hit-impact, .radar.hit-impact, .intel.hit-impact,
413
+ .intel.hit-impact .coordinate, .intel.hit-impact .result {
414
+ animation: none
415
+ }
416
+ }
417
+ </style>
418
+ </head>
419
+ <body>
420
+ <main class="tactical">
421
+ <section class="radar" id="radar">
422
+ <div class="header"><strong>TACTICAL GRID</strong><span>BLUE · FLEET&nbsp;&nbsp; ○ MISS&nbsp;&nbsp; × HIT&nbsp;&nbsp; GOLD · LATEST</span></div>
423
+ <div class="grid" id="grid"></div>
424
+ </section>
425
+ <aside class="intel" id="intel">
426
+ <div class="command-head">
427
+ <div class="mission-title" id="title">Fleet Command</div>
428
+ <div class="live">TACTICAL LIVE</div>
429
+ </div>
430
+ <div class="status">LATEST SHOT</div>
431
+ <div class="coordinate" id="coordinate">J8</div>
432
+ <div class="result hit" id="result">HIT</div>
433
+ <div class="player-card">
434
+ <div class="player-mark">P1</div>
435
+ <span>NOW PLAYING</span>
436
+ <strong id="player">John Carter</strong>
437
+ <small id="location">New York · NY</small>
438
+ </div>
439
+ <div class="metrics">
440
+ <div class="metric"><strong id="shotCount">6</strong><span>SHOTS</span></div>
441
+ <div class="metric"><strong id="hitCount">3</strong><span>HITS</span></div>
442
+ <div class="metric"><strong id="accuracy">50%</strong><span>ACCURACY</span></div>
443
+ </div>
444
+ <section class="fleet-status">
445
+ <div class="fleet-head"><span>ENEMY FLEET</span><strong id="remaining">5 ACTIVE</strong></div>
446
+ <div class="fleet-roster" id="fleetRoster"></div>
447
+ </section>
448
+ </aside>
449
+ </main>
450
+ <script>
451
+
452
+ const ROWS = "ABCDEFGHIJ".split("");
453
+ const ALL = ROWS.flatMap(row => Array.from({length: 10}, (_, i) => `${row}${i + 1}`));
454
+ const cells = new Map();
455
+ const dom = {
456
+ grid: document.getElementById("grid"),
457
+ radar: document.getElementById("radar"),
458
+ intel: document.getElementById("intel"),
459
+ coordinate: document.getElementById("coordinate"),
460
+ result: document.getElementById("result"),
461
+ title: document.getElementById("title"),
462
+ player: document.getElementById("player"),
463
+ location: document.getElementById("location"),
464
+ shotCount: document.getElementById("shotCount"),
465
+ hitCount: document.getElementById("hitCount"),
466
+ accuracy: document.getElementById("accuracy"),
467
+ remaining: document.getElementById("remaining"),
468
+ fleetRoster: document.getElementById("fleetRoster")
469
+ };
470
+ let fleet = ["A2", "A3", "A4", "A5", "C3", "C4", "C5", "F7", "F8", "F9", "H2", "H3", "J8", "J9"];
471
+ let shots = ["A3", "B8", "D4", "F8", "H9", "J8"];
472
+ let fleetVisible = false;
473
+ let latest = shots.at(-1);
474
+
475
+ ALL.forEach(id => {
476
+ const cell = document.createElement("div");
477
+ cell.className = "cell";
478
+ cell.textContent = id;
479
+ dom.grid.append(cell);
480
+ cells.set(id, cell)
481
+ });
482
+
483
+ function findShips(fleetSet) {
484
+ const unvisited = new Set(fleetSet);
485
+ const ships = [];
486
+ while (unvisited.size) {
487
+ const ship = [];
488
+ const pending = [unvisited.values().next().value];
489
+ while (pending.length) {
490
+ const id = pending.pop();
491
+ if (!unvisited.delete(id)) continue;
492
+ ship.push(id);
493
+ const row = ROWS.indexOf(id[0]);
494
+ const column = Number(id.slice(1));
495
+ [[row - 1, column], [row + 1, column], [row, column - 1], [row, column + 1]]
496
+ .map(([r, c]) => `${ROWS[r] ?? ""}${c}`)
497
+ .filter(next => rValid(next) && unvisited.has(next))
498
+ .forEach(next => pending.push(next))
499
+ }
500
+ ships.push(ship)
501
+ }
502
+ return ships.sort((a, b) => b.length - a.length)
503
+ }
504
+
505
+ function rValid(id) {
506
+ return ALL.includes(id)
507
+ }
508
+
509
+ function renderFleetRoster(fleetSet, shotSet) {
510
+ const ships = findShips(fleetSet);
511
+ const active = ships.filter(ship => !ship.every(id => shotSet.has(id))).length;
512
+ dom.remaining.textContent = `${active} ACTIVE`;
513
+ dom.fleetRoster.replaceChildren(...ships.slice(0, 7).map((ship) => {
514
+ const sunk = ship.every(id => shotSet.has(id));
515
+ const row = document.createElement("div");
516
+ row.className = `ship-row${sunk ? " sunk" : ""}`;
517
+ const hull = document.createElement("div");
518
+ hull.className = "ship";
519
+ hull.replaceChildren(...ship.map(() => document.createElement("i")));
520
+ const status = document.createElement("em");
521
+ status.textContent = sunk ? "SUNK" : "ACTIVE";
522
+ row.append(hull, status);
523
+ return row
524
+ }))
525
+ }
526
+
527
+ function render() {
528
+ const fleetSet = new Set(fleet);
529
+ const shotSet = new Set(shots);
530
+ const hitSet = new Set(shots.filter(id => fleetSet.has(id)));
531
+ cells.forEach((cell, id) => {
532
+ cell.classList.toggle("fleet", fleetVisible && fleetSet.has(id));
533
+ cell.classList.toggle("shot", shotSet.has(id));
534
+ cell.classList.toggle("hit", hitSet.has(id));
535
+ cell.classList.toggle("latest", id === latest)
536
+ });
537
+ dom.coordinate.textContent = latest ?? "—";
538
+ const isHit = latest != null && hitSet.has(latest);
539
+ dom.result.textContent = latest == null ? "AWAITING" : isHit ? "HIT" : "MISS";
540
+ dom.result.classList.toggle("hit", isHit);
541
+ dom.shotCount.textContent = shotSet.size;
542
+ dom.hitCount.textContent = hitSet.size;
543
+ dom.accuracy.textContent = `${shotSet.size ? Math.round(hitSet.size / shotSet.size * 100) : 0}%`;
544
+ renderFleetRoster(fleetSet, shotSet)
545
+ }
546
+
547
+ function celebrateHit(id) {
548
+ const animated = [cells.get(id), dom.radar, dom.intel].filter(Boolean);
549
+ animated.forEach(element => element.classList.remove("hit-impact"));
550
+ void dom.radar.offsetWidth;
551
+ animated.forEach(element => element.classList.add("hit-impact"));
552
+ setTimeout(() => animated.forEach(element => element.classList.remove("hit-impact")), 950)
553
+ }
554
+
555
+ window.addEventListener("nebular-overlay:update", ({detail = {}}) => {
556
+ let newHit;
557
+ if (detail.content?.title != null) dom.title.textContent = detail.content.title;
558
+ if (detail.content?.player != null) dom.player.textContent = detail.content.player;
559
+ if (detail.content?.location != null) dom.location.textContent = detail.content.location;
560
+ if (Array.isArray(detail.board?.fleet)) fleet = detail.board.fleet.map(String);
561
+ if (detail.board?.fleetVisibility != null) fleetVisible = detail.board.fleetVisibility === "revealed";
562
+ if (Array.isArray(detail.board?.shots)) {
563
+ const nextShots = detail.board.shots.map(String);
564
+ const previousShots = new Set(shots);
565
+ const added = nextShots.filter(id => !previousShots.has(id));
566
+ latest = added.at(-1) ?? (nextShots.includes(latest) ? latest : nextShots.at(-1));
567
+ if (added.length && fleet.includes(latest)) newHit = latest;
568
+ shots = nextShots;
569
+ }
570
+ if (detail.board?.fleet != null || detail.board?.fleetVisibility != null || detail.board?.shots != null) render();
571
+ if (newHit) celebrateHit(newHit)
572
+ });
573
+
574
+ render();
575
+
576
+ </script>
577
+ </body>
578
+ </html>