-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
1364 lines (1252 loc) · 63.9 KB
/
Copy pathindex.html
File metadata and controls
1364 lines (1252 loc) · 63.9 KB
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="logo-wordmark.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Sentinel Node Dash — dVPN Runner</title>
<meta name="description" content="Arcade runner powered by live Sentinel dVPN node data from Node Scorecard. Collect real nodes, dodge trackers, earn $P2P.">
<style>
:root { --cyan:#00e5ff; --bg:#0a0a1f; --danger:#ff3366; --good:#22ff99; --gold:#ffd54a; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); overflow:hidden; font-family:'Courier New',monospace; color:var(--cyan); }
canvas { display:block; touch-action:none; }
#ui { position:absolute; top:8px; left:12px; pointer-events:none; z-index:10; font-size:clamp(11px, 1.4vw, 16px); text-shadow:0 0 10px var(--cyan); line-height:1.45; max-width:60vw; }
#muteBtn, #pauseBtn { position:absolute; top:10px; z-index:20; background:rgba(0,229,255,0.12); color:var(--cyan); border:1px solid var(--cyan); border-radius:8px; padding:7px 13px; font-size:16px; cursor:pointer; font-family:inherit; }
#muteBtn { right:12px; }
#pauseBtn { right:70px; }
/* Panels are centred but can NEVER exceed the viewport: they cap at 90vh
and scroll internally. This is what stops the title being cut off on
short screens (e.g. 1366x690 laptops) or in landscape on mobile. */
.panel { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
background:rgba(8,8,26,0.97); padding:24px 32px; border:3px solid var(--cyan);
text-align:center; border-radius:16px; z-index:30;
width:560px; max-width:92vw;
max-height:90vh; overflow-y:auto; overscroll-behavior:contain;
-webkit-overflow-scrolling:touch;
box-shadow:0 0 60px rgba(0,229,255,0.55); }
.panel::-webkit-scrollbar { width:8px; }
.panel::-webkit-scrollbar-thumb { background:rgba(0,229,255,0.35); border-radius:4px; }
.panel h1 { margin:0 0 10px; color:var(--cyan); font-size:clamp(20px, 3.4vw, 28px); letter-spacing:1px; line-height:1.2; }
.panel h2 { margin:0 0 8px; color:var(--cyan); font-size:clamp(17px, 2.6vw, 22px); }
.panel p { font-size:clamp(13px, 1.6vw, 15px); line-height:1.5; color:#bfefff; margin:8px 0; }
button.btn { padding:12px 28px; margin:6px 5px; font-size:17px; background:var(--cyan); color:#001018; border:none; border-radius:10px; cursor:pointer; font-weight:bold; font-family:inherit; transition:transform .08s; }
button.btn:hover { transform:scale(1.05); }
button.btn.secondary { background:transparent; color:var(--cyan); border:2px solid var(--cyan); }
button.quizOpt { display:block; width:100%; margin:8px 0; padding:13px; font-size:16px; background:rgba(0,229,255,0.1); color:var(--cyan); border:2px solid var(--cyan); border-radius:10px; cursor:pointer; font-family:inherit; text-align:left; }
button.quizOpt:hover { background:rgba(0,229,255,0.28); }
a { color:var(--cyan); }
/* Very short viewports (laptops ~700px tall, phones in landscape) */
@media (max-height: 760px) {
.panel { padding:16px 22px; }
.panel p { font-size:13px; line-height:1.4; margin:6px 0; }
.panel h1 { font-size:22px; margin-bottom:6px; }
#startBtn { font-size:19px !important; padding:13px 44px !important; }
.skin canvas { width:48px; height:48px; }
.hint, .small { font-size:11px !important; }
}
/* Narrow phones */
@media (max-width: 520px) {
.panel { padding:16px 18px; width:94vw; }
button.btn { padding:11px 20px; font-size:15px; }
#skinPicker { gap:8px !important; }
}
.small { font-size:13px; color:#7fd8e8; }
.hint { font-size:14px; color:#9fe8f5; margin-top:16px; }
#nodeStatus { font-size:13px; margin-top:14px; color:#7fd8e8; }
.skin { cursor:pointer; padding:5px 7px; border:2px solid transparent; border-radius:12px; font-size:12px; color:#7fd8e8; transition:.15s; }
.skin:hover { border-color:rgba(0,229,255,0.4); }
.skin.selected { border-color:var(--cyan); background:rgba(0,229,255,0.08); color:var(--cyan); }
.skin canvas { display:block; margin:0 auto 2px; width:56px; height:56px; }
.tag { display:inline-block; padding:4px 12px; border-radius:20px; font-size:12px; letter-spacing:1px; border:1px solid var(--gold); color:var(--gold); margin-bottom:10px; }
.badge { display:inline-block; padding:3px 10px; border-radius:20px; font-size:12px; border:1px solid var(--gold); color:var(--gold); margin-left:6px; }
#menuLogo { display:block; margin:0 auto 8px; width:min(220px,52vw); height:auto; opacity:0.92; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
<div>SCORE <span id="score">0</span> | $P2P <span id="lives">10</span>/12 | COMBO <span id="combo">0</span>x</div>
<div>SPEED <span id="speed">1.0</span>x | NODES <span id="streak">0</span> | BEST <span id="best">0</span></div>
<div>ECOSYSTEM <span id="eco">0</span>/<span id="ecoTot">0</span> | ★<span id="resi">0</span> res ⚡<span id="dc">0</span> dc</div>
<div id="contractLine">CONTRACT —</div>
</div>
<button id="pauseBtn" title="Pause (P)">II</button>
<button id="muteBtn" title="Sound on/off">🔊</button>
<!-- MAIN MENU -->
<div id="menu" class="panel">
<img id="menuLogo" src="logo-wordmark.png" alt="Node Dash">
<h1>SENTINEL NODE DASH</h1>
<p>You are a data packet running through the <b>Sentinel dVPN network</b> — a decentralized bandwidth marketplace.</p>
<p style="text-align:left;display:inline-block;">
⚡ Collect <b style="color:var(--good)">real nodes</b> — live from Node Scorecard.<br>
⭐ <b style="color:var(--gold)">Residential</b> x2.4 · datacenter x1.2 · combo & near-miss pay extra.<br>
⚠ Dodge <b style="color:var(--danger)">DPI / trackers / geo-blocks</b> — each moves differently.<br>
⭐ Stars unlock the ecosystem once. Each run has a short <b>contract</b>.
</p>
<p class="hint">TAP / SPACE / ↑ = jump (double jump!) · P = pause</p>
<div style="margin:6px 0 4px;font-size:14px;color:#7fd8e8;">CHOOSE YOUR RUNNER</div>
<div id="skinPicker" style="display:flex;justify-content:center;gap:14px;margin-bottom:14px;">
<div class="skin" data-skin="0"><canvas width="70" height="70"></canvas><div>Data Packet</div><div class="hint">higher jump</div></div>
<div class="skin selected" data-skin="1"><canvas width="70" height="70"></canvas><div>dVPN Shield</div><div class="hint">+0.4s WG shield</div></div>
<div class="skin" data-skin="2"><canvas width="70" height="70"></canvas><div>$P2P Coin</div><div class="hint">+10% score</div></div>
</div>
<button class="btn" id="startBtn" style="font-size:24px;padding:20px 70px;">START RUN</button>
<div id="nodeStatus">Connecting to Node Scorecard…</div>
<p class="small">Live data: <a href="https://superpios.github.io/node-scorecard/" target="_blank" rel="noopener">Node Scorecard</a> • <a href="https://sentinel.co" target="_blank" rel="noopener">sentinel.co</a></p>
</div>
<!-- GAME OVER -->
<div id="gameover" class="panel" style="display:none;">
<h1 style="color:var(--danger);">CONNECTION LOST</h1>
<p id="finalScore" style="font-size:20px;"></p>
<p class="small">Want real censorship-resistant bandwidth? Explore the <a href="https://sentinel.co" target="_blank" rel="noopener">Sentinel ecosystem</a>.</p>
<p class="small">The nodes you collected are real - <a href="https://superpios.github.io/node-scorecard/" target="_blank" rel="noopener">inspect them on Node Scorecard</a>.</p>
<button class="btn" id="againBtn">PLAY AGAIN</button>
<button class="btn secondary" id="menuBtn">MAIN MENU</button>
</div>
<!-- DAPP INFO -->
<div id="infoPanel" class="panel" style="display:none;">
<div class="tag" id="dappTag"></div>
<h2 id="dappTitle"></h2>
<p id="dappDesc"></p>
<a id="dappLink" href="#" target="_blank" rel="noopener" style="font-size:17px;">→ Learn more</a>
<div id="topicProgress" class="small" style="margin-top:14px;"></div>
<button class="btn" id="quizBtn">QUIZ for +$P2P</button>
<button class="btn secondary" id="continueBtn">CONTINUE RUN</button>
<div class="small" style="margin-top:6px;">Each topic appears only once per run.</div>
</div>
<!-- PAUSE -->
<div id="pausePanel" class="panel" style="display:none;">
<h2>⏸ PAUSED</h2>
<p id="pauseStats" class="small"></p>
<button class="btn" id="resumeBtn">RESUME</button><br>
<button class="btn secondary" id="restartBtn">RESTART RUN</button>
<button class="btn secondary" id="quitBtn">MAIN MENU</button>
<p class="small" style="margin-top:10px;">P = resume · R = restart · M = menu</p>
</div>
<!-- QUIZ -->
<div id="quizPanel" class="panel" style="display:none;">
<div class="tag" id="quizTag"></div>
<h2>🧠 SENTINEL QUIZ</h2>
<p id="quizQ" style="font-size:18px;"></p>
<div id="quizOpts"></div>
<p id="quizResult" style="font-size:17px;min-height:24px;"></p>
</div>
<script>
'use strict';
/* ============================================================
SENTINEL NODE DASH — arcade runner promoting the Sentinel
dVPN ecosystem. Uses live node data from Node Scorecard:
https://superpios.github.io/node-scorecard/latest.json
============================================================ */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W, H, groundY;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
groundY = H - 130;
}
window.addEventListener('resize', () => { resize(); if (typeof seedStars === 'function') seedStars(); });
resize();
/* ---------------- Live node data (Node Scorecard) ---------- */
const SCORECARD_URL = 'https://superpios.github.io/node-scorecard/latest.json';
let nodes = [];
let dataSource = 'demo';
function fallbackNodes() {
const countries = ['Germany','United States','France','Italy','Japan','Brazil','Netherlands','Singapore'];
return Array.from({length:120}, (_, i) => ({
moniker: 'DemoNode-' + (i+1),
dl_mbps: 50 + Math.random()*1500,
country: countries[i % countries.length],
hosting: Math.random() > 0.25,
protocol: Math.random() > 0.5 ? 'wireguard' : 'v2ray'
}));
}
async function loadNodes() {
try {
const res = await fetch(SCORECARD_URL, {cache:'no-store'});
if (!res.ok) throw new Error('HTTP ' + res.status);
const all = await res.json();
const active = all.filter(n => n.status === 'active' && (n.dl_mbps || 0) > 8);
if (!active.length) throw new Error('no active nodes');
nodes = active;
dataSource = 'live';
setNodeStatus('● LIVE — ' + nodes.length + ' active Sentinel nodes loaded', '#22ff99');
} catch (e) {
nodes = fallbackNodes();
dataSource = 'demo';
setNodeStatus('○ Offline — demo node data (Scorecard unreachable)', '#ffaa44');
}
}
function setNodeStatus(html, color) {
const el = document.getElementById('nodeStatus');
el.innerHTML = html;
el.style.color = color;
}
/* ---------------- Ecosystem content -------------------------
Each TOPIC = 1 info panel + 1 quiz, shown AT MOST ONCE per run.
Sources: sentinel.co (official dVPN apps) + Node Scorecard.
------------------------------------------------------------ */
const TOPICS = [
// --- Official dVPN apps built on Sentinel (sentinel.co/#use-dvpn) ---
{id:'shield', tag:'dVPN APP', name:'Sentinel Shield',
desc:'The flagship open-source dVPN by Sentinel P2P. WireGuard & V2Ray, on Android and iOS.',
link:'https://shield.sentinel.co/',
quiz:{q:'Sentinel Shield is...', opts:['A closed-source corporate VPN','The flagship OPEN-SOURCE dVPN by Sentinel P2P','A crypto wallet'], correct:1}},
{id:'independent', tag:'dVPN APP', name:'Independent VPN',
desc:'A free decentralized VPN on Sentinel, focused on digital rights and open internet access. WireGuard & V2Ray, Android & iOS.',
link:'https://independentdvpn.com/',
quiz:{q:'What is Independent VPN focused on?', opts:['Selling user data','Digital rights and free internet access','Mining Bitcoin'], correct:1}},
{id:'ryn', tag:'dVPN APP', name:'Ryn VPN',
desc:'Sentinel-powered privacy VPN with a clean minimalist interface and a user base of over 10 million people.',
link:'https://www.rynvpn.com/',
quiz:{q:'Ryn VPN runs its bandwidth on...', opts:['Its own private datacenters','The decentralized Sentinel network','A single cloud provider'], correct:1}},
{id:'norse', tag:'dVPN APP', name:'DVPN by NORSE',
desc:'A decentralized VPN wrapped into user-friendly apps for every platform: Android, iOS, macOS, Windows and Linux.',
link:'https://norselabs.io/',
quiz:{q:'DVPN by NORSE is available on...', opts:['Android only','All major platforms: mobile AND desktop','Smart fridges only'], correct:1}},
{id:'valt', tag:'dVPN APP', name:'VALT',
desc:'VALT lets you capture and protect the data you create every day, with dVPN bandwidth from the Sentinel network.',
link:'https://valtdata.com/',
quiz:{q:'What does VALT help you do?', opts:['Sell your data to advertisers','Capture and PROTECT the data you create','Delete the internet'], correct:1}},
{id:'meile', tag:'dVPN APP', name:'Meile dVPN',
desc:'Sentinel-powered desktop dVPN client for macOS, Linux and Windows, built by MathNodes.',
link:'https://mathnodes.com/index.php/meile-dvpn-client-linux-os-x/',
quiz:{q:'Meile dVPN is built primarily for...', opts:['Desktop: macOS, Linux, Windows','Smartwatches','Game consoles'], correct:0}},
// --- Core network concepts ---
{id:'network', tag:'NETWORK', name:'The Sentinel Network',
desc:'A Layer-1 decentralized bandwidth marketplace: 1500+ independent node operators across 90+ countries. No company owns the servers, no single jurisdiction governs the network.',
link:'https://sentinel.co',
quiz:{q:'Why can a dVPN not simply "promise" not to log you?', opts:['Because it does log you','Because no company exists that COULD log your traffic — it is technical, not a promise','Because logs are illegal'], correct:1}},
{id:'p2p', tag:'TOKEN', name:'$P2P Token',
desc:'The token powering the bandwidth marketplace: users pay node operators directly on-chain — no invoices, no middleman. Operators earn $P2P for the bandwidth they share.',
link:'https://sentinel.co',
quiz:{q:'Who earns $P2P on Sentinel?', opts:['Only the Sentinel team','Node operators who share their bandwidth','Nobody, it is free'], correct:1}},
{id:'onchain', tag:'PROTOCOL', name:'On-Chain Sessions',
desc:'A signed transaction creates an immutable session on the blockchain. The node queries the chain to verify it BEFORE generating VPN credentials. Authorization is cryptographic, not corporate. The blockchain IS the backend.',
link:'https://p2pscan.com/transactions',
quiz:{q:'What authorizes your VPN session on Sentinel?', opts:['A support ticket','An on-chain transaction verified by the node','An email confirmation'], correct:1}},
// --- Nodes: residential vs datacenter (the game mechanic!) ---
{id:'residential', tag:'NODE TYPE', name:'\u2605 Residential Nodes',
desc:'Nodes running on a real home internet line instead of a datacenter. Their IP looks like an ordinary user, so they are far harder to block or fingerprint — the highest privacy tier on the network. In this game they are GOLD and worth x2.4 score.',
link:'https://sentinel.co',
quiz:{q:'Why is a residential node harder to block?', opts:['It is faster','Its IP looks like a normal home user, not a datacenter','It is invisible'], correct:1}},
{id:'datacenter', tag:'NODE TYPE', name:'\u26A1 Datacenter Nodes',
desc:'Nodes hosted in datacenters (hosting = true on the Scorecard). Huge bandwidth, rock-solid uptime, cheap to scale — but their IP ranges are known and easier for services to block. They are the backbone of raw network capacity.',
link:'https://sentinel.co',
quiz:{q:'What is the trade-off of a datacenter node?', opts:['Fast and stable, but its IP range is easier to block','It has no bandwidth','It cannot be paid'], correct:0}},
{id:'hostnode', tag:'EARN', name:'Host Your Own Node',
desc:'Anyone can run a Sentinel node on Windows, macOS, Linux — or a Raspberry Pi at home — share bandwidth and earn $P2P. 1500+ operators already do.',
link:'https://docs.sentinel.co/dvpn-node-setup',
quiz:{q:'What do you need to become a node operator?', opts:['A corporate licence','A machine with bandwidth to share — even a home Pi','Approval from a CEO'], correct:1}},
// --- Protocols: live + incoming ---
{id:'protocols', tag:'PROTOCOLS', name:'WireGuard & V2Ray',
desc:'Live today on Sentinel nodes. WireGuard = kernel-fast Curve25519 tunnels. V2Ray (VMess/VLESS) = anti-censorship, disguises traffic where WireGuard gets blocked. Keys never leave your device.',
link:'https://sentinel.co',
quiz:{q:'When do you reach for V2Ray instead of WireGuard?', opts:['When you want maximum raw speed','When WireGuard is blocked and you need anti-censorship','Never, they are identical'], correct:1}},
{id:'nextproto', tag:'PROTOCOLS', name:'\u{1F6E0} The New Protocol Wave \u2014 LIVE',
desc:'It landed. Node software v9 ships Xray (VLESS + REALITY, traffic that looks like ordinary TLS), Hysteria2 (fast UDP that thrives on lossy links) and AmneziaWG (WireGuard with an obfuscated handshake DPI cannot fingerprint) \u2014 and the first pioneer nodes running them are already live on the network.',
link:'https://sentinel.co',
quiz:{q:'What is the status of Xray, Hysteria2 and AmneziaWG on Sentinel?', opts:['Still only a roadmap idea','LIVE \u2014 shipped in node software v9, first pioneer nodes online','They were cancelled'], correct:1}},
// --- Agents / x402 ---
{id:'x402', tag:'AI AGENTS', name:'\u{1F916} dVPN for AI Agents (x402)',
desc:'The first dVPN an AI agent can buy by itself: it calls the endpoint, gets a price, signs a USDC payment on Base or Solana, and the tunnel comes up. No API keys, no accounts, no human. A full day costs about $0.033.',
link:'https://x402.sentinel.co/',
quiz:{q:'How does an AI agent pay for Sentinel bandwidth?', opts:['It emails an invoice','It autonomously signs a USDC payment via x402','It asks its owner for a credit card'], correct:1}},
// --- More dVPN clients & apps ---
{id:'chiba', tag:'dVPN APP', name:'ChibaTunnel',
desc:'A cyberpunk-inspired, open-source dVPN client powered by the Sentinel protocol. Trustless, no-log, multi-platform — privacy guaranteed by cryptography, not promises.',
link:'https://chibatunnel.xyz/',
quiz:{q:'ChibaTunnel guarantees privacy through...', opts:['A legal no-log promise','Cryptography and an open-source, trustless design','Insurance'], correct:1}},
{id:'bluecli', tag:'BUILDERS', name:'⌨ BlueCLI',
desc:'An interactive, self-contained command-line client for the Sentinel dVPN network, built by Bitveil — the same crew behind Bluefrens. Connect from a terminal, a server or a homelab: no GUI, no bloat.',
link:'https://github.com/Bitveil/bluecli',
quiz:{q:'What is BlueCLI?', opts:['An NFT collection','A self-contained command-line dVPN client for Sentinel','A hardware router'], correct:1}},
// --- Explorers & network tools ---
{id:'p2pscan', tag:'EXPLORER', name:'\u{1F50E} P2PScan',
desc:'The Sentinel block explorer. Every session, payment and node registration is a public transaction you can look up yourself. Nothing hidden behind a company dashboard — verify, do not trust.',
link:'https://p2pscan.com/',
quiz:{q:'What can you check on P2PScan?', opts:['Nothing, it is private','Every on-chain session, payment and validator — publicly verifiable','Only the team wallet'], correct:1}},
{id:'suchnode', tag:'EXPLORER', name:'\u{1F5FA} SuchNode',
desc:'A community-built node explorer and live map of the network: browse every dVPN node worldwide with its country, protocol, speed and price.',
link:'https://nodes.suchnode.net/',
quiz:{q:'What does SuchNode let you browse?', opts:['Every dVPN node on the network, worldwide','Only nodes in one country','Cat pictures'], correct:0}},
{id:'stats', tag:'DATA', name:'\u{1F4C8} Network Stats',
desc:'The official live dashboard: active nodes, sessions, users and total bandwidth served across the network — more than 6 petabytes so far.',
link:'https://stats.sentinel.co/',
quiz:{q:'Roughly how much data has the Sentinel network served?', opts:['A few megabytes','Over 6 petabytes','Exactly one gigabyte'], correct:1}},
// --- Community & culture ---
{id:'bluefrens', tag:'COMMUNITY', name:'\u{1F438} Bluefrens',
desc:'1,420 pixel-art NFTs on Stargaze, built by Bitveil — a tribute to the whole Sentinel ecosystem, with traits nodding to the dVPN apps and contributors. A friendly face on peer-to-peer tech.',
link:'https://bluefrens.xyz/',
quiz:{q:'What are Bluefrens?', opts:['A new VPN protocol','A pixel-art NFT collection celebrating the Sentinel community','A datacenter'], correct:1}},
// --- Privacy: why decentralized (v4, user-approved) ---
{id:'trustproblem', tag:'PRIVACY', name:'\u{1F513} The Trust Problem',
desc:'A centralized VPN sees EVERYTHING: your traffic, your payment, your email \u2014 one company holding the full picture, and "no-log" is a promise written in a policy. On Sentinel that company does not exist: traffic flows through independent operators who do not know who you are, and no single party ever holds the full picture.',
link:'https://sentinel.co',
quiz:{q:'What really protects your privacy?', opts:['A legal promise in a policy document','An architecture where no single party CAN hold all your data','A bigger company'], correct:1}},
{id:'keysnever', tag:'PRIVACY', name:'\u{1F511} Your Keys Never Leave',
desc:'The encryption keys (Curve25519 for WireGuard) are generated ON YOUR DEVICE. The node only ever receives your public key \u2014 not even the node operator can decrypt your traffic. End-to-end encryption by construction, not by courtesy.',
link:'https://sentinel.co',
quiz:{q:'Who can decrypt your tunnel?', opts:['The node operator','The Sentinel team','Only you \u2014 the node only has your public key'], correct:2}},
{id:'nohq', tag:'PRIVACY', name:'\u{1F3F0} No Headquarters to Raid',
desc:'A centralized VPN has known datacenters and a legal HQ: one court order, one block or one seizure can hit everything at once. Sentinel is 1,500+ independent operators across 80+ countries \u2014 there is no central switch to flip and no office to raid. The network survives the loss of any single node.',
link:'https://sentinel.co',
quiz:{q:'Why is a decentralized network harder to shut down?', opts:['It has better lawyers','There is no single point of failure to hit','It is invisible'], correct:1}},
{id:'noaccount', tag:'PRIVACY', name:'\u{1F3AD} No Account, No Identity',
desc:'Traditional VPNs ask for your email and credit card: your identity is welded to your VPN account. On Sentinel you pay per session on-chain from a wallet: no account, no email, no card. Pseudonymous by design \u2014 on-chain transactions are public, but they point to a wallet address, not to your name.',
link:'https://p2pscan.com/',
quiz:{q:'What do you need to use the Sentinel network?', opts:['An email and a credit card','A wallet \u2014 not an identity','A government ID'], correct:1}},
// --- Security: already secure by design (v5, user direction) ---
{id:'securebydesign', tag:'SECURITY', name:'\u{1F6E1} Secure by Design',
desc:'Using a Sentinel dApp is secure out of the box: encryption is automatic and end-to-end, session authorization happens on-chain, and there are no accounts or passwords to steal \u2014 because none exist. You do not configure security. It is built into the protocol.',
link:'https://sentinel.co',
quiz:{q:'What must a user configure to be secure on a Sentinel dApp?', opts:['A firewall and three plugins','Nothing \u2014 security is built into the protocol','A weekly password change'], correct:1}},
{id:'builtinguard', tag:'SECURITY', name:'\u{1F6D1} Built-in Protections',
desc:'Modern Sentinel clients carry the protections with them: a kill-switch that blocks traffic the instant the tunnel drops, and DNS resolved inside the tunnel so nothing leaks to your ISP. The client stands guard \u2014 not you.',
link:'https://sentinel.co',
quiz:{q:'The tunnel drops mid-session. What protects you?', opts:['You must react fast','The client\u0027s built-in kill-switch, automatically','Nothing can'], correct:1}},
{id:'seedsafe', tag:'SECURITY', name:'\u{1F510} The Only Rule',
desc:'The network never asks anything of you \u2014 except this: NEVER share your seed phrase or private key. Anyone asking for it \u2014 "support", airdrops, DMs \u2014 is ALWAYS a scam. That is the single user-side rule; the protocol handles everything else.',
link:'https://sentinel.co',
quiz:{q:'Someone from "support" asks for your seed phrase\u2026', opts:['Give it, they are support','It is ALWAYS a scam \u2014 no one legitimate ever asks','Only give half of it'], correct:1}},
{id:'whitelabel', tag:'BUILDERS', name:'\u{1F411} Become the Shepherd',
desc:'Launching your own VPN has never been easier. Creators, businesses and communities already promote VPNs through sponsorships \u2014 now they can BUILD the product instead. The Sentinel Network removes the biggest barrier: decentralized infrastructure anyone can build on, with independently operated nodes across 80+ countries. You integrate payments, subscriptions and your own brand experience; the network does the rest. The next generation of VPN companies won\u0027t all build their own networks \u2014 many will build on open infrastructure.',
link:'https://docs.sentinel.co',
quiz:{q:'What do you need to launch your own VPN brand on Sentinel?', opts:['Your own global server fleet','Just your brand and your app \u2014 the network provides the global infrastructure','A banking licence'], correct:1}},
// --- More real ecosystem (added v3, all verifiable) ---
{id:'slatests', tag:'NETWORK', name:'\u{1F9EA} Official SLA Tests',
desc:'Sentinel runs official SLA tests on the whole network: a real tunnel is opened through each node and its true speed is measured. Around 1,500 nodes are tested per run \u2014 the strongest proof a node actually serves clients. The SLA\u2713 badge on nodes in THIS game comes from these tests.',
link:'https://test.sentinel.co',
quiz:{q:'What does an SLA test actually do?', opts:['Reads the node description','Opens a REAL tunnel through the node and measures speed','Asks the operator politely'], correct:1}},
// --- The data behind this game ---
{id:'scorecard', tag:'DATA', name:'\u{1F4CA} Node Scorecard',
desc:'The open dashboard tracking speed, uptime, protocol and reliability of every Sentinel node — and the exact LIVE data powering the nodes you are collecting in this run.',
link:'https://superpios.github.io/node-scorecard/',
quiz:{q:'The \u26A1 nodes you collect in this game are...', opts:['Randomly invented','Real Sentinel nodes, live from Node Scorecard','Old screenshots'], correct:1}},
{id:'veil', tag:'dVPN APP', name:'Veil DVPN',
desc:'A native Android dVPN app built on Sentinel, designed to make decentralized privacy simple, fast, and usable for everyone. Instead of relying on a single corporate VPN provider, users can route traffic through independent nodes operated by a global community.',
link:'https://docs.sentinel.co/get-started/apps',
quiz:{q:'Veil DVPN, as listed in official Sentinel docs, is\u2026',
opts:['A hardware wallet','A native Android dVPN on community Sentinel nodes','A Cosmos faucet'],
correct:1}},
{id:'katacomb', tag:'dVPN APP', name:'Katacomb VPN',
desc:'An open source dVPN client built by Trinity, core community member and creator of the Sentinel docs website. It supports 6 protocols: WireGuard, AmneziaWG, OpenVPN, V2Ray, XRAY, and Hysteria2.',
link:'https://docs.sentinel.co/get-started/apps',
quiz:{q:'How many protocol families does Katacomb list in official docs?',
opts:['2','4','6'],
correct:2}},
{id:'scorecardxyz', tag:'AI AGENTS', name:'nodescorecard.xyz (x402 intelligence)',
desc:'Machine-payable Node Scorecard API for agents. No account and no API key. An agent calls an endpoint, receives HTTP 402, signs a USDC payment, retries, and gets the measurement. This is not the VPN tunnel shop at x402.sentinel.co \u2014 this endpoint sells node intelligence (scores, recommendations, snapshots).',
link:'https://nodescorecard.xyz',
quiz:{q:'What does nodescorecard.xyz sell over x402?',
opts:['A monthly consumer VPN subscription','Independent Sentinel node measurements','Physical SIM cards'],
correct:1}},
{id:'xyzfacilitator', tag:'AI AGENTS', name:'x402 facilitator (PayAI + Solana gasless)',
desc:'Settlement uses facilitator https://facilitator.payai.network. Supported networks in the live manifest: Base, Solana, Polygon, Arbitrum, Avalanche. Asset is USDC. On Solana the facilitator sponsors the transaction fee, so the agent needs USDC only \u2014 no SOL. Failed requests (HTTP status >= 400) are never settled. Receipts use X-Receipt-Id and canonicalization profile jcs-strings-v0.2.',
link:'https://nodescorecard.xyz/manifest',
quiz:{q:'On Solana, a nodescorecard.xyz x402 payment\u2026',
opts:['Requires SOL for every call','Is gas-sponsored by the facilitator; the agent pays USDC','Settles only in $P2P'],
correct:1}},
{id:'p2psolana', tag:'TOKEN', name:'$P2P migration to Solana',
desc:'$P2P remains the Sentinel ecosystem token. Official communication: work continues on a migration to Solana; two designs under consideration are a phased burn-and-mint and a Merkle-tree claim after a snapshot. This is described as Sentinel\u2019s second migration after Ethereum \u2192 Cosmos in 2021. Timing stated by Sentinel: later in the year / before the end of the year. No official calendar day is asserted here.',
link:'https://sentinel.co',
quiz:{q:'After the planned migration, the Sentinel token is\u2026',
opts:['Replaced by SOL','Still $P2P','Renamed back to $DVPN only'],
correct:1}}
];
// Progress: each topic shows its panel + quiz AT MOST ONCE per run.
let seenTopics = new Set(); // info panel already shown
let quizzedTopics = new Set(); // quiz already answered
function remainingTopics() { return TOPICS.filter(t => !seenTopics.has(t.id)); }
/* ---------------- Audio (WebAudio synth, zero deps) -------- */
let audioCtx = null, muted = false, musicTimer = null, musicStep = 0;
function ac() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function beep(freq, dur, type, vol) {
if (muted) return;
try {
const a = ac(), o = a.createOscillator(), g = a.createGain();
o.type = type || 'square'; o.frequency.value = freq;
g.gain.setValueAtTime(vol || 0.06, a.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, a.currentTime + dur);
o.connect(g); g.connect(a.destination);
o.start(); o.stop(a.currentTime + dur);
} catch (e) {}
}
const MELODY = [220, 277, 330, 277, 220, 330, 392, 330, 220, 277, 330, 440, 392, 330, 277, 220];
function startMusic() {
stopMusic();
musicTimer = setInterval(() => {
if (state !== 'running' || muted) return;
beep(MELODY[musicStep % MELODY.length], 0.14, 'triangle', 0.035);
if (musicStep % 4 === 0) beep(MELODY[musicStep % MELODY.length] / 2, 0.2, 'sine', 0.045);
musicStep++;
}, 190);
}
function stopMusic() { if (musicTimer) { clearInterval(musicTimer); musicTimer = null; } }
/* ---------------- Game state ------------------------------- */
const MAX_LIVES = 12, START_LIVES = 10, BASE_SPEED = 3.2, MAX_SPEED = 5.6;
let state = 'menu'; // menu | running | paused | info | quiz | over
let lives, score, speed, streak, distance, level, jumpsLeft, bgTheme;
let obstacles, powerups, specials, particles, floaters;
let resiCount = 0, dcCount = 0, italyCount = 0, slaCount = 0;
let lastSpawn = 0, lastRegen = 0, rafId = null;
let invincibleUntil = 0, boostUntil = 0, tutUntil = 0, pendingPower = null;
let combo = 0, comboMul = 1, shake = 0, hitStop = 0;
let contract = null, contractDone = false;
let highScore = 0;
try { highScore = parseFloat(localStorage.getItem('sentinelHigh')) || 0; } catch (e) {}
document.getElementById('best').textContent = Math.floor(highScore);
const themes = [
{bg:'#05081c', bg2:'#0d1638', ground:'#0a1024'}, // deep chain blue
{bg:'#100722', bg2:'#241041', ground:'#160c2c'}, // violet
{bg:'#04160f', bg2:'#0a2b20', ground:'#08201a'}, // emerald
{bg:'#1a0d05', bg2:'#33200c', ground:'#22150a'} // amber
];
const player = {x: 170, y: 0, vy: 0, w: 62, h: 58};
class Entity {
constructor(kind) { // 'obstacle' | 'power' | 'special'
this.kind = kind;
this.x = W + 120 + Math.random() * 240;
this.wobble = Math.random() * Math.PI * 2;
this.node = nodes.length ? nodes[Math.floor(Math.random() * nodes.length)] : {};
this.brick = Math.floor(Math.random() * 6);
this.missed = false;
this.speedMul = 1;
this.size = 46;
if (kind === 'obstacle') {
this.obType = this.brick % 3; // 0 DPI tall, 1 tracker low/fast, 2 geoblock mid
if (this.obType === 0) { this.baseY = groundY - 158; this.size = 66; this.speedMul = 1.00; }
else if (this.obType === 1) { this.baseY = groundY - 44; this.size = 38; this.speedMul = 1.38; }
else { this.baseY = groundY - 168; this.size = 50; this.speedMul = 0.88; }
} else {
this.obType = -1;
this.baseY = groundY - 55 - Math.random() * 240;
}
this.y = this.baseY;
}
}
class Particle {
constructor(x, y, color) {
this.x = x; this.y = y;
this.vx = Math.random() * 10 - 5;
this.vy = Math.random() * 8 - 11;
this.life = 34;
this.color = color;
}
update() { this.x += this.vx; this.y += this.vy; this.vy += 0.35; this.life--; }
draw() {
ctx.globalAlpha = Math.max(this.life / 34, 0);
ctx.fillStyle = this.color;
ctx.fillRect(this.x - 4, this.y - 4, 8, 8);
ctx.globalAlpha = 1;
}
}
class Floater { // floating text (node moniker etc.)
constructor(x, y, text, color) {
this.x = x; this.y = y; this.text = text; this.color = color; this.life = 80;
}
update() { this.y -= 0.9; this.life--; }
draw() {
ctx.globalAlpha = Math.min(1, this.life / 40);
ctx.fillStyle = this.color;
ctx.font = 'bold 15px monospace';
ctx.fillText(this.text, this.x, this.y);
ctx.globalAlpha = 1;
}
}
const CONTRACTS = [
{id:'italy', text:'Collect 3 Italy nodes', ok:() => italyCount >= 3},
{id:'sla', text:'Collect 2 SLA\u2713 nodes', ok:() => slaCount >= 2},
{id:'combo', text:'Reach an 8-node combo', ok:() => combo >= 8},
{id:'topics', text:'Discover 4 ecosystem topics', ok:() => seenTopics.size >= 4}
];
function scoreMul() {
const coin = (skin === 2) ? 1.1 : 1;
const boost = (Date.now() < boostUntil) ? 2 : 1;
return comboMul * coin * boost;
}
function checkContract() {
if (!contract || contractDone) return;
if (contract.ok()) {
contractDone = true;
lives = Math.min(MAX_LIVES, lives + 1);
boostUntil = Math.max(boostUntil, Date.now() + 8000);
floaters.push(new Floater(player.x - 40, player.y - 36, 'CONTRACT DONE +1 $P2P', '#ffd54a'));
beep(1240, 0.22, 'triangle', 0.07);
}
}
/* ---------------- Core flow -------------------------------- */
function resetGame() {
lives = START_LIVES; score = 0; speed = BASE_SPEED; streak = 0;
distance = 0; level = 0; jumpsLeft = 2; bgTheme = 0;
obstacles = []; powerups = []; specials = []; particles = []; floaters = [];
lastSpawn = Date.now(); lastRegen = 0;
invincibleUntil = 0; boostUntil = 0; pendingPower = null;
seenTopics = new Set(); quizzedTopics = new Set(); currentTopic = null;
resiCount = 0; dcCount = 0; italyCount = 0; slaCount = 0;
combo = 0; comboMul = 1; shake = 0; hitStop = 0;
contract = CONTRACTS[Math.floor(Math.random() * CONTRACTS.length)];
contractDone = false;
player.y = groundY - player.h; player.vy = 0;
}
function startGame() {
hide('menu'); hide('gameover'); hide('pausePanel');
ac(); // unlock audio on user gesture
resetGame();
state = 'running';
tutUntil = Date.now() + 5000; // tutorial shows every run, first jump dismisses it
startMusic();
}
function endGame() {
state = 'over';
hide('pausePanel');
stopMusic();
beep(140, 0.5, 'sawtooth', 0.08);
if (score > highScore) {
highScore = score;
try { localStorage.setItem('sentinelHigh', String(highScore)); } catch (e) {}
}
document.getElementById('best').textContent = Math.floor(highScore);
const mastered = seenTopics.size === TOPICS.length;
document.getElementById('finalScore').innerHTML =
'Score: <b>' + Math.floor(score) + '</b><br>' +
'Nodes collected: <b>' + streak + '</b> ' +
'(\u2605 ' + resiCount + ' residential \u00B7 \u26A1 ' + dcCount + ' datacenter)<br>' +
'Ecosystem discovered: <b>' + seenTopics.size + '/' + TOPICS.length + '</b>' +
(mastered ? ' \u{1F3C6} MASTERED!' : '') + '<br>' +
'Quizzes answered: <b>' + quizzedTopics.size + '</b><br>' +
'Contract: <b>' + (contract ? contract.text : '-') + '</b> ' + (contractDone ? 'DONE' : 'open') + '<br>' +
'High score: <b>' + Math.floor(highScore) + '</b>' +
(dataSource === 'live' ? '<br><span class="small">powered by live Sentinel node data</span>' : '');
show('gameover');
}
function togglePause() {
if (state === 'running') {
state = 'paused';
document.getElementById('pauseStats').textContent =
'Score ' + Math.floor(score) + ' \u00b7 $P2P ' + lives +
' \u00b7 Nodes ' + streak + ' \u00b7 Ecosystem ' + seenTopics.size + '/' + TOPICS.length;
show('pausePanel');
} else if (state === 'paused') {
hide('pausePanel');
state = 'running';
}
}
// Leave any state (running, paused, info, quiz, game over) and go back to the menu.
function goToMenu() {
['pausePanel', 'gameover', 'infoPanel', 'quizPanel'].forEach(hide);
state = 'menu';
stopMusic();
resetGame();
show('menu');
}
// Restart immediately from anywhere.
function restartRun() {
['pausePanel', 'gameover', 'infoPanel', 'quizPanel'].forEach(hide);
startGame();
}
function show(id) { document.getElementById(id).style.display = 'block'; }
function hide(id) { document.getElementById(id).style.display = 'none'; }
/* ---------------- Input ------------------------------------ */
function jump() {
if (state !== 'running' || jumpsLeft <= 0) return;
player.vy = (skin === 0) ? -21.4 : -19;
jumpsLeft--;
tutUntil = 0;
beep(jumpsLeft === 1 ? 520 : 660, 0.12, 'square', 0.05);
for (let i = 0; i < 12; i++) particles.push(new Particle(player.x + 20, player.y + player.h, '#00e5ff'));
}
canvas.addEventListener('pointerdown', jump);
document.addEventListener('keydown', e => {
const k = e.key;
if ([' ', 'ArrowUp', 'w', 'W'].includes(k)) { e.preventDefault(); jump(); }
if (k === 'p' || k === 'P' || k === 'Escape') {
if (state === 'running' || state === 'paused') togglePause();
}
// R / M work while paused or on the game-over screen
if ((k === 'r' || k === 'R') && (state === 'paused' || state === 'over')) restartRun();
if ((k === 'm' || k === 'M') && (state === 'paused' || state === 'over')) goToMenu();
if ((k === 'Enter' || k === ' ') && state === 'menu') startGame();
});
/* ---------------- Ecosystem discovery (once per topic) -------
A star only interrupts the run if there is a NEW topic left.
Once every topic has been seen, stars become pure bonus:
no panel, no pause, just points. No repeated interruptions.
------------------------------------------------------------ */
let currentTopic = null;
function collectStar(x, y) {
const left = remainingTopics();
if (left.length === 0) {
// Ecosystem fully explored -> silent bonus, game never pauses again
score += 1000 * scoreMul();
lives = Math.min(MAX_LIVES, lives + 1);
boostUntil = Math.max(boostUntil, Date.now() + 8000);
floaters.push(new Floater(x - 70, y - 20, '\u2605 ECOSYSTEM MASTERED +1000 + BOOST', '#ffd54a'));
beep(1200, 0.18, 'triangle', 0.06);
return;
}
currentTopic = left[Math.floor(Math.random() * left.length)];
seenTopics.add(currentTopic.id);
checkContract();
state = 'info';
document.getElementById('dappTag').textContent = currentTopic.tag;
document.getElementById('dappTitle').textContent = currentTopic.name;
document.getElementById('dappDesc').textContent = currentTopic.desc;
document.getElementById('dappLink').href = currentTopic.link;
document.getElementById('topicProgress').textContent =
'Ecosystem discovered: ' + seenTopics.size + '/' + TOPICS.length;
// quiz button only if this topic's quiz hasn't been answered yet
const qb = document.getElementById('quizBtn');
qb.style.display = quizzedTopics.has(currentTopic.id) ? 'none' : 'inline-block';
lives = Math.min(MAX_LIVES, lives + 1);
score += 600;
beep(880, 0.25, 'triangle', 0.07);
show('infoPanel');
}
function closeInfo() { hide('infoPanel'); state = 'running'; }
function openQuiz() {
if (!currentTopic || quizzedTopics.has(currentTopic.id)) { closeInfo(); return; }
hide('infoPanel');
state = 'quiz';
quizzedTopics.add(currentTopic.id); // one quiz per topic, no matter the outcome
const q = currentTopic.quiz;
document.getElementById('quizTag').textContent = currentTopic.name;
document.getElementById('quizQ').textContent = q.q;
document.getElementById('quizResult').textContent = '';
const box = document.getElementById('quizOpts');
box.innerHTML = '';
q.opts.forEach((opt, i) => {
const b = document.createElement('button');
b.className = 'quizOpt';
b.textContent = String.fromCharCode(65 + i) + ') ' + opt;
b.addEventListener('click', () => answerQuiz(i));
box.appendChild(b);
});
show('quizPanel');
}
function answerQuiz(i) {
const q = currentTopic.quiz;
const res = document.getElementById('quizResult');
document.querySelectorAll('#quizOpts button').forEach(b => { b.disabled = true; });
if (i === q.correct) {
const TECH_TAGS = ['PROTOCOL','PROTOCOLS','ROADMAP','AI AGENTS','NETWORK','NODE TYPE','TOKEN','EARN','PRIVACY','SECURITY'];
const isTech = TECH_TAGS.indexOf(currentTopic.tag) >= 0;
res.style.color = '#22ff99';
if (isTech) {
pendingPower = Math.random() < 0.5 ? 'wg' : 'boost';
score += 800;
res.textContent = pendingPower === 'wg'
? '\u2705 Correct! POWER-UP: \u26E8 WireGuard Mode \u2014 invincible 5s! (+800)'
: '\u2705 Correct! POWER-UP: \u26A1 P2P Boost \u2014 double score 12s! (+800)';
beep(1180, 0.3, 'triangle', 0.08);
} else {
lives = Math.min(MAX_LIVES, lives + 1);
score += 1500;
res.textContent = '\u2705 Correct! +1 $P2P, +1500 score';
beep(1040, 0.3, 'triangle', 0.08);
}
} else {
res.style.color = '#ff3366';
res.textContent = '\u274C The answer was: ' + q.opts[q.correct];
beep(180, 0.3, 'sawtooth', 0.07);
}
setTimeout(() => { hide('quizPanel'); state = 'running';
if (pendingPower === 'wg') { invincibleUntil = Date.now() + 5000 + (skin === 1 ? 400 : 0); floaters.push(new Floater(player.x, player.y - 30, '\u26E8 WIREGUARD MODE ON', '#ffd54a')); }
if (pendingPower === 'boost') { boostUntil = Date.now() + 12000; floaters.push(new Floater(player.x, player.y - 30, '\u26A1 P2P BOOST x2 ON', '#22ff99')); }
pendingPower = null;
}, 1800);
}
/* ---------------- Spawning & update ------------------------- */
function checkCollision(a, b) {
const pad = (b.size || 46) * 0.92;
return Math.abs((a.x + 15) - b.x) < pad && Math.abs((a.y + 25) - b.y) < pad;
}
function spawn() {
const interval = 520 / (speed / BASE_SPEED);
if (Date.now() - lastSpawn < interval) return;
lastSpawn = Date.now();
const r = Math.random();
if (r < 0.46) obstacles.push(new Entity('obstacle'));
else if (r < 0.82) powerups.push(new Entity('power'));
else specials.push(new Entity('special'));
}
function update() {
if (state !== 'running') return;
if (hitStop > 0) { hitStop--; return; }
if (shake > 0) shake *= 0.84;
distance += speed;
score += speed * 1.15 * scoreMul();
comboMul = 1 + Math.min(4, Math.floor(combo / 3)) * 0.5;
// physics
player.vy += 1.0;
player.y += player.vy;
if (player.y >= groundY - player.h) {
player.y = groundY - player.h;
player.vy = 0;
jumpsLeft = 2;
}
spawn();
// level from distance only — collecting must not accelerate the run
if (Math.floor(distance / 3200) > level) {
level++;
bgTheme = (bgTheme + 1) % themes.length;
speed = Math.min(MAX_SPEED, speed + 0.16);
floaters.push(new Floater(W / 2 - 60, H / 2 - 60, 'LEVEL ' + (level + 1), '#00e5ff'));
beep(700, 0.2, 'triangle', 0.06);
}
// slow $P2P regen: +1 every 9000 score
if (Math.floor(score / 9000) > lastRegen) {
lastRegen = Math.floor(score / 9000);
if (lives < START_LIVES) {
lives++;
floaters.push(new Floater(player.x, player.y - 20, '+1 $P2P (regen)', '#22ff99'));
}
}
// obstacles (trackers / censorship)
for (let i = obstacles.length - 1; i >= 0; i--) {
const o = obstacles[i];
o.x -= speed * 6.5 * (o.speedMul || 1);
o.wobble += 0.12;
if (o.obType === 2) o.y = o.baseY + Math.sin(o.wobble * 1.4) * 38;
else if (o.obType === 0) o.y = o.baseY + Math.sin(o.wobble) * 6;
else o.y = o.baseY;
if (!o.missed && o.x + 20 < player.x) {
o.missed = true;
const dy = Math.abs((player.y + 25) - o.y);
if (dy < 72) {
const bonus = Math.floor(90 * scoreMul());
score += bonus;
floaters.push(new Floater(o.x - 20, o.y - 28, 'NEAR MISS +' + bonus, '#00e5ff'));
beep(1400, 0.07, 'square', 0.04);
}
}
if (o.x < -100) { obstacles.splice(i, 1); continue; }
if (checkCollision(player, o)) {
if (Date.now() < invincibleUntil) {
obstacles.splice(i, 1);
for (let k = 0; k < 12; k++) particles.push(new Particle(o.x, o.y, '#ffd54a'));
floaters.push(new Floater(o.x - 46, o.y - 24, '\u26E8 WG SHIELD!', '#ffd54a'));
beep(920, 0.1, 'triangle', 0.05);
continue;
}
lives--;
combo = 0; comboMul = 1;
shake = 10; hitStop = 3;
speed = Math.max(BASE_SPEED, speed - 0.22);
floaters.push(new Floater(o.x - 30, o.y - 30, '-1 $P2P blocked!', '#ff3366'));
for (let k = 0; k < 18; k++) particles.push(new Particle(o.x, o.y, '#ff3366'));
beep(160, 0.25, 'sawtooth', 0.08);
obstacles.splice(i, 1);
if (lives <= 0) { endGame(); return; }
}
}
// node powerups (real Scorecard nodes)
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.x -= speed * 6.5;
p.wobble += 0.2;
p.y = p.baseY + Math.sin(p.wobble) * 12;
if (p.x < -100) { powerups.splice(i, 1); continue; }
if (checkCollision(player, p)) {
const n = p.node || {};
const mbps = Math.round(n.dl_mbps || 300);
const residential = n.hosting === false; // Scorecard hosting=false: not classified as datacenter ASN
const gain = Math.floor(mbps * (residential ? 2.4 : 1.2) * scoreMul());
score += gain;
streak++; combo++;
if (residential) resiCount++; else dcCount++;
if ((n.country || '') === 'Italy') italyCount++;
if (n.sla_pass === true) slaCount++;
hitStop = 2;
checkContract();
const proto = (n.protocol || '').toUpperCase();
const label = (n.moniker || 'node')
+ (n.country ? ' \u00B7 ' + n.country : '')
+ ' \u00B7 ' + mbps + ' Mbps'
+ (proto ? ' \u00B7 ' + proto : '')
+ (n.sla_pass === true ? ' \u00B7 SLA\u2713' : '');
const tier = residential ? '\u2605 RESIDENTIAL x2.4' : '\u26A1 DATACENTER x1.2';
const col = residential ? '#ffd54a' : '#22ff99';
floaters.push(new Floater(p.x - 70, p.y - 42, label, col));
floaters.push(new Floater(p.x - 40, p.y - 24, tier, col));
floaters.push(new Floater(p.x, p.y - 4, '+' + gain, col));
for (let k = 0; k < 26; k++) particles.push(new Particle(p.x, p.y, col));
beep(residential ? 990 : 780, 0.15, 'square', 0.06);
powerups.splice(i, 1);
}
}
// special stars (dApp discovery)
for (let i = specials.length - 1; i >= 0; i--) {
const s = specials[i];
s.x -= speed * 6.5;
s.wobble += 0.18;
s.y = s.baseY + Math.sin(s.wobble) * 12;
if (s.x < -100) { specials.splice(i, 1); continue; }
if (checkCollision(player, s)) {
for (let k = 0; k < 32; k++) particles.push(new Particle(s.x, s.y, '#ffd54a'));
const sx = s.x, sy = s.y;
specials.splice(i, 1);
collectStar(sx, sy);
break;
}
}
particles.forEach(p => p.update());
particles = particles.filter(p => p.life > 0);
floaters.forEach(f => f.update());
floaters = floaters.filter(f => f.life > 0);
}
/* ---------------- Rendering ---------------------------------
NOTE ON BRANDING: every glyph below is ORIGINAL vector art drawn
with canvas paths — a shield/packet emblem inspired by the dVPN
theme. No third-party logo or trademark file is embedded or traced.
Sentinel brand blue (#0156FC) is used as an accent colour only.
------------------------------------------------------------ */
const BRAND_BLUE = '#0156fc';
/* --- Original emblem: hex shield + P2P chevron (the player) --- */
function drawShieldEmblem(x, y, r, glow, g) {
const c = g || ctx;
c.save();
c.translate(x, y);
// hex shield body
c.shadowBlur = glow ? 26 : 0;
c.shadowColor = '#00e5ff';
c.beginPath();
c.moveTo(0, -r);
c.lineTo(r * 0.87, -r * 0.5);
c.lineTo(r * 0.87, r * 0.34);
c.lineTo(0, r);
c.lineTo(-r * 0.87, r * 0.34);
c.lineTo(-r * 0.87, -r * 0.5);
c.closePath();
const grad = c.createLinearGradient(0, -r, 0, r);
grad.addColorStop(0, '#2ee9ff');
grad.addColorStop(1, BRAND_BLUE);
c.fillStyle = grad;
c.fill();
c.shadowBlur = 0;
c.strokeStyle = '#bff6ff';
c.lineWidth = 2;
c.stroke();
// inner peer-to-peer chevron (two arrows meeting = P2P routing)
c.strokeStyle = '#eafcff';
c.lineWidth = Math.max(2, r * 0.16);
c.lineCap = 'round';
c.beginPath();
c.moveTo(-r * 0.42, -r * 0.10);
c.lineTo(0, -r * 0.46);
c.lineTo(r * 0.42, -r * 0.10);
c.stroke();
c.beginPath();
c.moveTo(-r * 0.42, r * 0.42);
c.lineTo(0, r * 0.06);
c.lineTo(r * 0.42, r * 0.42);
c.stroke();
c.restore();
}
/* --- Original $P2P coin glyph --- */
function drawCoinEmblem(x, y, r, glow, g) {
const c = g || ctx;
c.save();
c.translate(x, y);
c.shadowBlur = glow ? 26 : 0;
c.shadowColor = '#ffd54a';
c.beginPath(); c.arc(0, 0, r, 0, Math.PI * 2);
const grd = c.createLinearGradient(-r, -r, r, r);
grd.addColorStop(0, '#fff0b8'); grd.addColorStop(1, '#e8a900');
c.fillStyle = grd; c.fill();
c.shadowBlur = 0;
c.strokeStyle = '#fff8dd'; c.lineWidth = 2; c.stroke();
c.fillStyle = '#3a2500';
c.font = 'bold ' + Math.round(r * 0.78) + 'px monospace';
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText('P2P', 0, 1);
c.textAlign = 'left'; c.textBaseline = 'alphabetic';
c.restore();
}
/* --- Original data-packet glyph --- */
function drawPacketEmblem(x, y, r, glow, g) {
const c = g || ctx;
c.save();
c.translate(x, y);
c.shadowBlur = glow ? 24 : 0;
c.shadowColor = '#00e5ff';
c.fillStyle = '#00b8e0';
c.beginPath();
c.moveTo(-r, -r * 0.66); c.lineTo(r * 0.62, -r * 0.66);
c.lineTo(r, 0); c.lineTo(r * 0.62, r * 0.66); c.lineTo(-r, r * 0.66);
c.closePath(); c.fill();
c.shadowBlur = 0;
c.strokeStyle = '#bff6ff'; c.lineWidth = 2; c.stroke();
// encrypted payload bars
c.fillStyle = '#eafcff';
for (let i = 0; i < 3; i++) c.fillRect(-r * 0.72, -r * 0.4 + i * r * 0.35, r * 0.9, r * 0.16);
c.restore();
}
const SKINS = [
{id:0, name:'Data Packet', draw:drawPacketEmblem},
{id:1, name:'dVPN Shield', draw:drawShieldEmblem},
{id:2, name:'$P2P Coin', draw:drawCoinEmblem}
];
let skin = 1;
/* --- Background layer 1: node constellation (uses REAL node data) --- */
let stars = [];
function seedStars() {
stars = Array.from({length: 70}, () => ({
x: Math.random() * (W + 200),
y: Math.random() * (groundY - 40),
r: Math.random() * 1.8 + 0.6,
d: 0.25 + Math.random() * 0.7, // parallax depth
tw: Math.random() * Math.PI * 2 // twinkle phase
}));