getpicked 1.0.0 → 1.1.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.
- package/index.js +83 -18
- package/package.json +4 -2
package/index.js
CHANGED
|
@@ -2,12 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
import blessed from "blessed";
|
|
4
4
|
import WebSocket from "ws";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
5
9
|
|
|
6
10
|
const isLocal = process.argv.includes("--local");
|
|
7
11
|
const SERVER_URL = process.env.PORTO_SERVER ||
|
|
8
12
|
(isLocal ? "ws://localhost:8111/ws" : "wss://shapely-insect.spcf.app/ws");
|
|
9
13
|
|
|
10
|
-
// ───
|
|
14
|
+
// ─── Device Identity ─────────────────────────────────────────
|
|
15
|
+
const IDENTITY_DIR = join(homedir(), ".getpicked");
|
|
16
|
+
const IDENTITY_FILE = join(IDENTITY_DIR, "identity.json");
|
|
17
|
+
|
|
18
|
+
function loadIdentity() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(IDENTITY_FILE, "utf-8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveIdentity(identity) {
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(IDENTITY_DIR, { recursive: true });
|
|
29
|
+
writeFileSync(IDENTITY_FILE, JSON.stringify(identity, null, 2));
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let identity = loadIdentity() || { device_id: randomUUID(), secret: null };
|
|
34
|
+
saveIdentity(identity);
|
|
35
|
+
|
|
36
|
+
// ─── Colors ──────────────────────────────────────────────────
|
|
11
37
|
const C = {
|
|
12
38
|
bg: "#0a0a0f",
|
|
13
39
|
panel: "#111118",
|
|
@@ -25,10 +51,10 @@ const C = {
|
|
|
25
51
|
dark: "#181820",
|
|
26
52
|
};
|
|
27
53
|
|
|
28
|
-
// ─── Screen
|
|
54
|
+
// ─── Screen ──────────────────────────────────────────────────
|
|
29
55
|
const screen = blessed.screen({
|
|
30
56
|
smartCSR: true,
|
|
31
|
-
title: "
|
|
57
|
+
title: "getpicked",
|
|
32
58
|
fullUnicode: true,
|
|
33
59
|
});
|
|
34
60
|
|
|
@@ -53,14 +79,20 @@ function clearSceneKeys() {
|
|
|
53
79
|
|
|
54
80
|
function clearScreen() {
|
|
55
81
|
clearSceneKeys();
|
|
56
|
-
// Detach all children
|
|
57
82
|
while (screen.children.length) {
|
|
58
83
|
screen.children[0].detach();
|
|
59
84
|
}
|
|
60
85
|
}
|
|
61
86
|
|
|
62
|
-
//
|
|
63
|
-
|
|
87
|
+
// Track active WebSocket for clean shutdown
|
|
88
|
+
let activeWs = null;
|
|
89
|
+
|
|
90
|
+
screen.key(["C-c"], () => {
|
|
91
|
+
if (activeWs) {
|
|
92
|
+
try { activeWs.close(); } catch {}
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
|
95
|
+
});
|
|
64
96
|
|
|
65
97
|
// ═══════════════════════════════════════════════════════════════════
|
|
66
98
|
// MAIN MENU
|
|
@@ -314,7 +346,6 @@ function showUsernameInput(thenJoin = false) {
|
|
|
314
346
|
else showMainMenu();
|
|
315
347
|
}
|
|
316
348
|
|
|
317
|
-
// Single keypress handler for all input — avoids conflicts with screen.key()
|
|
318
349
|
function onKeypress(ch, key) {
|
|
319
350
|
if (!key) return;
|
|
320
351
|
|
|
@@ -441,11 +472,14 @@ function showLobby() {
|
|
|
441
472
|
let myId = null;
|
|
442
473
|
let pingInterval = null;
|
|
443
474
|
let renderInterval = null;
|
|
475
|
+
let connectTimeout = null;
|
|
444
476
|
let dead = false;
|
|
445
477
|
|
|
446
478
|
function leave() {
|
|
447
479
|
clearInterval(pingInterval);
|
|
448
480
|
clearInterval(renderInterval);
|
|
481
|
+
clearTimeout(connectTimeout);
|
|
482
|
+
activeWs = null;
|
|
449
483
|
if (ws) {
|
|
450
484
|
try { ws.close(); } catch {}
|
|
451
485
|
}
|
|
@@ -458,10 +492,24 @@ function showLobby() {
|
|
|
458
492
|
headerBox.setContent(`{center}{${C.yellow}-fg}⟳ Connecting...{/}{/center}`);
|
|
459
493
|
screen.render();
|
|
460
494
|
|
|
461
|
-
const
|
|
462
|
-
|
|
495
|
+
const params = new URLSearchParams({
|
|
496
|
+
username,
|
|
497
|
+
device_id: identity.device_id,
|
|
498
|
+
});
|
|
499
|
+
if (identity.secret) params.set("secret", identity.secret);
|
|
500
|
+
|
|
501
|
+
ws = new WebSocket(`${SERVER_URL}?${params}`, { handshakeTimeout: 10000 });
|
|
502
|
+
activeWs = ws;
|
|
503
|
+
|
|
504
|
+
// Manual connection timeout fallback
|
|
505
|
+
connectTimeout = setTimeout(() => {
|
|
506
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
507
|
+
ws.terminate();
|
|
508
|
+
}
|
|
509
|
+
}, 12000);
|
|
463
510
|
|
|
464
511
|
ws.on("open", () => {
|
|
512
|
+
clearTimeout(connectTimeout);
|
|
465
513
|
pingInterval = setInterval(() => {
|
|
466
514
|
if (ws.readyState === WebSocket.OPEN) {
|
|
467
515
|
ws.send(JSON.stringify({ type: "ping" }));
|
|
@@ -470,7 +518,21 @@ function showLobby() {
|
|
|
470
518
|
});
|
|
471
519
|
|
|
472
520
|
ws.on("message", (raw) => {
|
|
473
|
-
|
|
521
|
+
let msg;
|
|
522
|
+
try {
|
|
523
|
+
msg = JSON.parse(raw.toString());
|
|
524
|
+
} catch {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (msg.type === "welcome") {
|
|
529
|
+
// Persist server-issued auth credentials
|
|
530
|
+
identity.device_id = msg.device_id;
|
|
531
|
+
identity.secret = msg.secret;
|
|
532
|
+
saveIdentity(identity);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
474
536
|
if (msg.type === "lobby_state") {
|
|
475
537
|
if (msg.your_id) myId = msg.your_id;
|
|
476
538
|
lobbyState = msg;
|
|
@@ -481,6 +543,9 @@ function showLobby() {
|
|
|
481
543
|
|
|
482
544
|
ws.on("close", () => {
|
|
483
545
|
clearInterval(pingInterval);
|
|
546
|
+
clearInterval(renderInterval);
|
|
547
|
+
clearTimeout(connectTimeout);
|
|
548
|
+
activeWs = null;
|
|
484
549
|
dead = true;
|
|
485
550
|
headerBox.setContent(`{center}{${C.red}-fg}● Disconnected{/}{/center}`);
|
|
486
551
|
footerBox.setContent(`{center}{${C.dim}-fg}Press Q or Esc to return{/}{/center}`);
|
|
@@ -489,6 +554,9 @@ function showLobby() {
|
|
|
489
554
|
|
|
490
555
|
ws.on("error", () => {
|
|
491
556
|
clearInterval(pingInterval);
|
|
557
|
+
clearInterval(renderInterval);
|
|
558
|
+
clearTimeout(connectTimeout);
|
|
559
|
+
activeWs = null;
|
|
492
560
|
dead = true;
|
|
493
561
|
headerBox.setContent(`{center}{${C.red}-fg}● Connection failed — is the server running?{/}{/center}`);
|
|
494
562
|
footerBox.setContent(`{center}{${C.dim}-fg}Press Q or Esc to return{/}{/center}`);
|
|
@@ -501,7 +569,6 @@ function showLobby() {
|
|
|
501
569
|
}, 200);
|
|
502
570
|
|
|
503
571
|
// 256-color index 231 = true #ffffff white, NOT remappable by terminal themes
|
|
504
|
-
// (palette indices 0-15 like SGR 97 are theme-dependent and render as grey)
|
|
505
572
|
const W = "\x1b[38;5;231m";
|
|
506
573
|
const B = "\x1b[1m";
|
|
507
574
|
const R = "\x1b[0m";
|
|
@@ -513,7 +580,7 @@ function showLobby() {
|
|
|
513
580
|
|
|
514
581
|
const elapsed = (Date.now() - stateReceivedAt) / 1000;
|
|
515
582
|
|
|
516
|
-
// ── Header ──
|
|
583
|
+
// ── Header ──
|
|
517
584
|
if (state === "waiting") {
|
|
518
585
|
const t = Math.max(0, Math.ceil(time_remaining - elapsed));
|
|
519
586
|
const lid = lobbyState.lobby_id;
|
|
@@ -521,7 +588,7 @@ function showLobby() {
|
|
|
521
588
|
`{${C.green}-fg}● LOBBY{/} {${C.dim}-fg}#${lid}{/}{|}${W}${player_count}${R}{${C.dim}-fg}/${max_players}{/} {${C.yellow}-fg}⏱ ${t}s{/}`
|
|
522
589
|
);
|
|
523
590
|
const barW = screen.width;
|
|
524
|
-
const filled = Math.round((player_count / max_players) * barW);
|
|
591
|
+
const filled = max_players > 0 ? Math.round((player_count / max_players) * barW) : 0;
|
|
525
592
|
progressBox.setContent(
|
|
526
593
|
`{${C.green}-fg}${"▀".repeat(Math.min(filled, barW))}{/}` +
|
|
527
594
|
`{${C.dark}-fg}${"▀".repeat(Math.max(0, barW - filled))}{/}`
|
|
@@ -547,7 +614,7 @@ function showLobby() {
|
|
|
547
614
|
progressBox.setContent(`{${C.green}-fg}${"▀".repeat(screen.width)}{/}`);
|
|
548
615
|
}
|
|
549
616
|
|
|
550
|
-
// ── Grid ──
|
|
617
|
+
// ── Grid ──
|
|
551
618
|
const colW = 26;
|
|
552
619
|
const availW = gridBox.width - 4;
|
|
553
620
|
const cols = Math.max(1, Math.floor(availW / colW));
|
|
@@ -567,7 +634,6 @@ function showLobby() {
|
|
|
567
634
|
nameAnsi = `{${C.dim}-fg}`;
|
|
568
635
|
}
|
|
569
636
|
} else if (state === "countdown") {
|
|
570
|
-
// All dots dark during countdown — suspense, no winner known
|
|
571
637
|
dot = `{${C.dark}-fg}●{/}`;
|
|
572
638
|
nameAnsi = `{${C.dark}-fg}`;
|
|
573
639
|
} else if (state === "reveal") {
|
|
@@ -593,7 +659,6 @@ function showLobby() {
|
|
|
593
659
|
: player.username;
|
|
594
660
|
|
|
595
661
|
const pad = " ".repeat(Math.max(1, 22 - name.length));
|
|
596
|
-
// Use {/} reset for tag-based styles, \x1b[0m for ANSI-based
|
|
597
662
|
const resetStr = nameAnsi.startsWith("\x1b") ? R : "{/}";
|
|
598
663
|
row += ` ${dot} ${nameAnsi}${name}${resetStr}${pad}`;
|
|
599
664
|
c++;
|
|
@@ -612,7 +677,7 @@ function showLobby() {
|
|
|
612
677
|
|
|
613
678
|
gridBox.setContent(lines.join("\n\n"));
|
|
614
679
|
|
|
615
|
-
// ── Footer ──
|
|
680
|
+
// ── Footer ──
|
|
616
681
|
if (state === "waiting") {
|
|
617
682
|
footerBox.setContent(
|
|
618
683
|
`{${C.text}-fg}You: ${W}${B}${username}${R}{|}{${C.dim}-fg}Q{/} {${C.text}-fg}leave lobby{/}`
|
|
@@ -651,5 +716,5 @@ function showLobby() {
|
|
|
651
716
|
}
|
|
652
717
|
}
|
|
653
718
|
|
|
654
|
-
// ─── Go
|
|
719
|
+
// ─── Go ──────────────────────────────────────────────────────
|
|
655
720
|
showMainMenu();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getpicked",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Join a lobby, enter the raffle, win the draw",
|
|
5
5
|
"bin": {
|
|
6
6
|
"getpicked": "./index.js"
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
"keywords": ["raffle", "game", "multiplayer", "terminal", "cli"],
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"files": ["index.js"],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18.0.0"
|
|
14
|
+
},
|
|
12
15
|
"dependencies": {
|
|
13
16
|
"blessed": "^0.1.81",
|
|
14
|
-
"blessed-contrib": "^4.11.0",
|
|
15
17
|
"ws": "^8.16.0"
|
|
16
18
|
}
|
|
17
19
|
}
|