@pdc-test/chat-io 1.1.3 → 1.1.5

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 (3) hide show
  1. package/bin/cli.js +175 -175
  2. package/package.json +2 -2
  3. package/src/server/index.js +181 -181
package/bin/cli.js CHANGED
@@ -35,16 +35,16 @@ let isMultilinePrompt = false;
35
35
 
36
36
  // Helpers VISUALES
37
37
  function getTime() {
38
- const d = new Date();
39
- const hs = String(d.getHours()).padStart(2, '0');
40
- const ms = String(d.getMinutes()).padStart(2, '0');
41
- return `\x1b[90m${hs}:${ms}\x1b[0m`;
38
+ const d = new Date();
39
+ const hs = String(d.getHours()).padStart(2, '0');
40
+ const ms = String(d.getMinutes()).padStart(2, '0');
41
+ return `\x1b[90m${hs}:${ms}\x1b[0m`;
42
42
  }
43
43
 
44
44
  function clearTopLine() {
45
- readline.moveCursor(process.stdout, 0, -1);
46
- readline.clearLine(process.stdout, 0);
47
- readline.moveCursor(process.stdout, 0, 1);
45
+ readline.moveCursor(process.stdout, 0, -1);
46
+ readline.clearLine(process.stdout, 0);
47
+ readline.moveCursor(process.stdout, 0, 1);
48
48
  }
49
49
 
50
50
  // Interfaz del Teclado Local
@@ -52,30 +52,30 @@ const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all',
52
52
  let knownUsersList = [];
53
53
 
54
54
  function completer(line) {
55
- const tokens = line.split(" ");
56
- let completions = [];
57
-
58
- if (line.startsWith("@")) {
59
- completions = knownUsersList.map(u => "@" + u);
60
- } else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
61
- completions = knownUsersList.map(u => tokens[0] + " " + u + " ");
62
- } else if (line.toLowerCase().startsWith("/rps ")) {
63
- if (tokens.length === 2) {
64
- completions = knownUsersList.map(u => "/rps " + u + " ");
65
- } else if (tokens.length === 3) {
66
- const rpsOpts = ["piedra", "papel", "tijera"];
67
- completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
68
- }
69
- } else if (line.includes("@")) {
70
- const lastAt = line.lastIndexOf("@");
71
- const prefix = line.substring(0, lastAt);
72
- completions = knownUsersList.map(u => prefix + "@" + u + " ");
73
- } else {
74
- completions = [...commands, ...knownUsersList.map(u => "/w " + u)];
75
- }
76
-
77
- const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
78
- return [hits.length ? hits : [], line];
55
+ const tokens = line.split(" ");
56
+ let completions = [];
57
+
58
+ if (line.startsWith("@")) {
59
+ completions = knownUsersList.map(u => "@" + u);
60
+ } else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
61
+ completions = knownUsersList.map(u => tokens[0] + " " + u + " ");
62
+ } else if (line.toLowerCase().startsWith("/rps ")) {
63
+ if (tokens.length === 2) {
64
+ completions = knownUsersList.map(u => "/rps " + u + " ");
65
+ } else if (tokens.length === 3) {
66
+ const rpsOpts = ["piedra", "papel", "tijera"];
67
+ completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
68
+ }
69
+ } else if (line.includes("@")) {
70
+ const lastAt = line.lastIndexOf("@");
71
+ const prefix = line.substring(0, lastAt);
72
+ completions = knownUsersList.map(u => prefix + "@" + u + " ");
73
+ } else {
74
+ completions = [...commands, ...knownUsersList.map(u => "/w " + u)];
75
+ }
76
+
77
+ const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
78
+ return [hits.length ? hits : [], line];
79
79
  }
80
80
 
81
81
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ', removeHistoryDuplicates: true });
@@ -88,15 +88,15 @@ rl.on('SIGINT', () => {
88
88
 
89
89
  process.stdin.on('keypress', (str, key) => {
90
90
  if (key && key.name !== 'return' && key.name !== 'enter' && key.name !== 'c' && myName) {
91
- if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
92
- currentIsTyping = true;
93
- ws.send(JSON.stringify({ type: "typing" }));
94
- }
95
- clearTimeout(typingTimeout);
96
- typingTimeout = setTimeout(() => {
97
- currentIsTyping = false;
98
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "typing_stop" }));
99
- }, 1500);
91
+ if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
92
+ currentIsTyping = true;
93
+ ws.send(JSON.stringify({ type: "typing" }));
94
+ }
95
+ clearTimeout(typingTimeout);
96
+ typingTimeout = setTimeout(() => {
97
+ currentIsTyping = false;
98
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "typing_stop" }));
99
+ }, 1500);
100
100
  }
101
101
  });
102
102
 
@@ -120,74 +120,74 @@ setInterval(() => {
120
120
 
121
121
  // Flujo Principal Socket
122
122
  ws.on('open', () => {
123
- console.log("\x1b[36m====================================\x1b[0m");
124
- console.log(" 🚀 BIENVENIDO AL JSON-CHAT-CLI 🚀");
125
- console.log("\x1b[36m====================================\x1b[0m");
126
- console.log("🤖 Sistema: Escribe tu nombre y presiona Enter para comenzar:");
127
- rl.prompt();
123
+ console.log("\x1b[36m====================================\x1b[0m");
124
+ console.log(" 🚀 BIENVENIDO AL CHAT-IO 🚀");
125
+ console.log("\x1b[36m====================================\x1b[0m");
126
+ console.log("🤖 Sistema: Escribe tu nombre y presiona Enter para comenzar:");
127
+ rl.prompt();
128
128
  });
129
129
 
130
130
  // Encargado de Pintar Todo en la Terminal limpiamente
131
131
  function printMessage(msgStr) {
132
- if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
133
- readline.clearLine(process.stdout, 0);
134
- readline.cursorTo(process.stdout, 0);
135
- console.log(msgStr);
136
- rl.setPrompt('> ');
137
- rl.prompt(true);
132
+ if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
133
+ readline.clearLine(process.stdout, 0);
134
+ readline.cursorTo(process.stdout, 0);
135
+ console.log(msgStr);
136
+ rl.setPrompt('> ');
137
+ rl.prompt(true);
138
138
  }
139
139
 
140
140
  // Receptor de la Nube (Solo procesa comandos JSON Puros, nada de basura String)
141
141
  ws.on('message', (data) => {
142
- let res;
143
- try { res = JSON.parse(data.toString()); } catch(e) { return; }
144
-
145
- switch (res.type) {
146
- case "error": printMessage(`❌ \x1b[31m${res.msg}\x1b[0m`); break;
147
-
148
- case "system": printMessage(`\n[${getTime()}] ${res.msg}`); break;
149
-
150
- case "registered":
151
- myName = res.name;
152
- printMessage(`\n✅ ¡Listo ${res.name}! Estás con el avatar ${res.emoji}. Escribe /help para los comandos nativos.`);
153
- break;
154
-
155
- case "typing_event":
156
- // El servidor nos entrega el arreglo COMPLETO y VIVO de quién tipea
157
- // Tu computadora solo limpia e ignora inteligentemente si está tu mismo nombre de pila
158
- activeTypers = res.users.filter(u => u !== myName);
159
- break;
160
-
161
- case "users_update":
162
- if (res.plain) knownUsersList = res.plain.filter(u => u !== myName);
163
- break;
164
-
165
- case "ding":
166
- playSound('ding');
167
- break;
168
-
169
- case "users_list":
170
- printMessage(`\n👥 \x1b[36mCONECTADOS AHORA (${res.users.length}):\x1b[0m ` + res.users.join(", ") + "\n");
171
- break;
172
-
173
- case "chat":
174
- // Todo el poder de Parsear Regex se procesa en TU PC, ahorrándole billones de cálculos al Server
175
- let safeMsg = res.msg.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
176
- if (res.isWhisper) {
177
- const dir = res.from === myName ? `Private → ${res.to}` : `${res.emoji} Secreto de ${res.from}`;
178
- printMessage(`\n[${getTime()}] 🔒 [${dir}]: \x1b[35m${safeMsg}\x1b[0m`);
179
- if (res.from !== myName) playSound('ding');
180
- } else {
181
- printMessage(`\n[${getTime()}] 🌍 [${res.emoji} ${res.from}]: ${safeMsg}`);
182
- if (res.from !== myName) playSound('msg');
183
- }
184
- break;
185
-
186
- case "animation":
187
- if (res.name === "tiendita") renderTiendita(res.user);
188
- else if (res.name === "flip") renderFlip(res.user, res.emoji, res.result);
189
- break;
190
- }
142
+ let res;
143
+ try { res = JSON.parse(data.toString()); } catch (e) { return; }
144
+
145
+ switch (res.type) {
146
+ case "error": printMessage(`❌ \x1b[31m${res.msg}\x1b[0m`); break;
147
+
148
+ case "system": printMessage(`\n[${getTime()}] ${res.msg}`); break;
149
+
150
+ case "registered":
151
+ myName = res.name;
152
+ printMessage(`\n✅ ¡Listo ${res.name}! Estás con el avatar ${res.emoji}. Escribe /help para los comandos nativos.`);
153
+ break;
154
+
155
+ case "typing_event":
156
+ // El servidor nos entrega el arreglo COMPLETO y VIVO de quién tipea
157
+ // Tu computadora solo limpia e ignora inteligentemente si está tu mismo nombre de pila
158
+ activeTypers = res.users.filter(u => u !== myName);
159
+ break;
160
+
161
+ case "users_update":
162
+ if (res.plain) knownUsersList = res.plain.filter(u => u !== myName);
163
+ break;
164
+
165
+ case "ding":
166
+ playSound('ding');
167
+ break;
168
+
169
+ case "users_list":
170
+ printMessage(`\n👥 \x1b[36mCONECTADOS AHORA (${res.users.length}):\x1b[0m ` + res.users.join(", ") + "\n");
171
+ break;
172
+
173
+ case "chat":
174
+ // Todo el poder de Parsear Regex se procesa en TU PC, ahorrándole billones de cálculos al Server
175
+ let safeMsg = res.msg.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
176
+ if (res.isWhisper) {
177
+ const dir = res.from === myName ? `Private → ${res.to}` : `${res.emoji} Secreto de ${res.from}`;
178
+ printMessage(`\n[${getTime()}] 🔒 [${dir}]: \x1b[35m${safeMsg}\x1b[0m`);
179
+ if (res.from !== myName) playSound('ding');
180
+ } else {
181
+ printMessage(`\n[${getTime()}] 🌍 [${res.emoji} ${res.from}]: ${safeMsg}`);
182
+ if (res.from !== myName) playSound('msg');
183
+ }
184
+ break;
185
+
186
+ case "animation":
187
+ if (res.name === "tiendita") renderTiendita(res.user);
188
+ else if (res.name === "flip") renderFlip(res.user, res.emoji, res.result);
189
+ break;
190
+ }
191
191
  });
192
192
 
193
193
  // ANIMACIONES RENDERIZADAS LOCALMENTE CON CPU CLIENT (LATENCIA CERO, NO SUCIA LA RED)
@@ -195,15 +195,15 @@ function renderTiendita(user) {
195
195
  if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
196
196
  readline.clearLine(process.stdout, 0);
197
197
  readline.cursorTo(process.stdout, 0);
198
-
198
+
199
199
  console.log(`\n[${getTime()}] 🏪 \x1b[1m${user}\x1b[0m convoca a la tiendita...\n`);
200
-
200
+
201
201
  const asciiArt = `_____ _ _ _ _ _ _ _ \n |_ _(_) ___ _ __ __| (_) |_ __ _| | | |\n | | | |/ _ \\ '_ \\ / _ | | __/ _ | | | |\n | | | | __/ | | | (_ | | || (_ |_|_|_|\n |_| |_|\\___|_| |_|\\__,_|_|\\__\\__,_(_|_|_)\n `;
202
202
  const colors = ["\x1b[31m", "\x1b[33m", "\x1b[32m", "\x1b[36m", "\x1b[1m\x1b[35m"];
203
-
203
+
204
204
  // Dejar huella Base
205
205
  process.stdout.write(`${colors[0]}${asciiArt}\x1b[0m\n`);
206
-
206
+
207
207
  let frames = 0;
208
208
  const anim = setInterval(() => {
209
209
  frames++;
@@ -222,10 +222,10 @@ function renderFlip(user, emoji, result) {
222
222
  readline.clearLine(process.stdout, 0);
223
223
  readline.cursorTo(process.stdout, 0);
224
224
  console.log(`\n[${getTime()}] 🪙 [${emoji} ${user}] lanza una moneda al aire...`);
225
-
225
+
226
226
  const asciiCoins = ["\x1b[33m ( o ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m", "\x1b[38;5;220m ( 0 ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m"];
227
227
  let i = 0;
228
-
228
+
229
229
  process.stdout.write(" \n");
230
230
  const anim = setInterval(() => {
231
231
  process.stdout.write(`\r\x1b[1A\x1b[K ${asciiCoins[i % 4]} zumbando...\n`);
@@ -235,42 +235,42 @@ function renderFlip(user, emoji, result) {
235
235
  process.stdout.write(`\r\x1b[1A\x1b[K[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${user}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n\n`);
236
236
  rl.prompt(true);
237
237
  }
238
- }, 100);
238
+ }, 100);
239
239
  }
240
240
 
241
241
  // Control General del Botón Enter (Distribuidor Estructurado)
242
242
  rl.on('line', (input) => {
243
- // Destruimos el "echo" por defecto para no dejar basura arriba del prompt
244
- readline.moveCursor(process.stdout, 0, -1);
245
- readline.clearLine(process.stdout, 0);
246
-
247
- const line = input.trim();
248
- if (!line) { rl.prompt(true); return; }
249
-
250
- // Flujo Inicial Obligatorio
251
- if (!myName) {
252
- ws.send(JSON.stringify({ type: "register", name: line }));
253
- return;
254
- }
255
-
256
- // Local Helper Menú
257
- if (line.toLowerCase() === "/version" || line.toLowerCase() === "/v") {
258
- printMessage(`\nℹ️ Versión actual de Chat-IO: \x1b[1m\x1b[33m${pkgVersion}\x1b[0m\n`);
259
- return;
260
- }
261
- if (line.toLowerCase().startsWith("/sound")) {
262
- soundsEnabled = !soundsEnabled;
263
- printMessage(`\n🔊 Sonidos ahora están: \x1b[1m\x1b[33m${soundsEnabled ? "ACTIVADOS" : "DESACTIVADOS"}\x1b[0m\n`);
264
- return;
265
- }
266
- if (line.toLowerCase() === "/clear") { console.clear(); rl.setPrompt('> '); rl.prompt(true); return; }
267
- if (line.toLowerCase() === "/exit") {
268
- printMessage("\x1b[33mSaliendo...\x1b[0m");
269
- ws.close();
270
- process.exit(0);
271
- }
272
- if (line.toLowerCase() === "/help") {
273
- printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
243
+ // Destruimos el "echo" por defecto para no dejar basura arriba del prompt
244
+ readline.moveCursor(process.stdout, 0, -1);
245
+ readline.clearLine(process.stdout, 0);
246
+
247
+ const line = input.trim();
248
+ if (!line) { rl.prompt(true); return; }
249
+
250
+ // Flujo Inicial Obligatorio
251
+ if (!myName) {
252
+ ws.send(JSON.stringify({ type: "register", name: line }));
253
+ return;
254
+ }
255
+
256
+ // Local Helper Menú
257
+ if (line.toLowerCase() === "/version" || line.toLowerCase() === "/v") {
258
+ printMessage(`\nℹ️ Versión actual de Chat-IO: \x1b[1m\x1b[33m${pkgVersion}\x1b[0m\n`);
259
+ return;
260
+ }
261
+ if (line.toLowerCase().startsWith("/sound")) {
262
+ soundsEnabled = !soundsEnabled;
263
+ printMessage(`\n🔊 Sonidos ahora están: \x1b[1m\x1b[33m${soundsEnabled ? "ACTIVADOS" : "DESACTIVADOS"}\x1b[0m\n`);
264
+ return;
265
+ }
266
+ if (line.toLowerCase() === "/clear") { console.clear(); rl.setPrompt('> '); rl.prompt(true); return; }
267
+ if (line.toLowerCase() === "/exit") {
268
+ printMessage("\x1b[33mSaliendo...\x1b[0m");
269
+ ws.close();
270
+ process.exit(0);
271
+ }
272
+ if (line.toLowerCase() === "/help") {
273
+ printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
274
274
  👥 /users → Ver conectados remotamente
275
275
  🪙 /flip → Lanzar moneda 60FPS
276
276
  🎮 /rps <usr> <j> → Piedra-Papel-Tijera
@@ -284,43 +284,43 @@ rl.on('line', (input) => {
284
284
  ❌ /exit → Salir del chat
285
285
  ℹ️ /help → Te ayuda desde la memoria Caché
286
286
  ─────────────────────────────────────────────\n`);
287
- return;
288
- }
289
-
290
- // Constructor Arquitectónico y Limpio de Comandos a Servidor
291
- if (line.startsWith("/")) {
292
- const lowerLine = line.toLowerCase();
293
- if (lowerLine === "/users" || lowerLine === "/flip" || lowerLine === "/tiendita") {
294
- ws.send(JSON.stringify({ type: "command", cmd: lowerLine.substring(1) }));
295
- } else if (lowerLine === "/all") {
296
- ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
297
- } else if (lowerLine.startsWith("/w ")) {
298
- const p = line.split(" ");
299
- if (p.length === 2) {
300
- ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
301
- } else if (p.length > 2) {
302
- const inlineMsg = p.slice(2).join(" ").trim();
303
- if (inlineMsg) {
304
- ws.send(JSON.stringify({ type: "chat", target: p[1], msg: inlineMsg }));
305
- } else {
306
- ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
307
- }
308
- }
309
- } else if (lowerLine.startsWith("/rps ")) {
310
- const p = line.split(" ");
311
- if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2].toLowerCase() }));
312
- } else if (lowerLine.startsWith("/blink ")) {
313
- ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
314
- } else {
315
- printMessage(`❌ Comando desconocido. Intenta /help`);
316
- }
317
- } else {
318
- // Mensaje Casual
319
- ws.send(JSON.stringify({ type: "chat", msg: line }));
320
- }
321
-
322
- currentIsTyping = false;
323
- ws.send(JSON.stringify({ type: "typing_stop" }));
287
+ return;
288
+ }
289
+
290
+ // Constructor Arquitectónico y Limpio de Comandos a Servidor
291
+ if (line.startsWith("/")) {
292
+ const lowerLine = line.toLowerCase();
293
+ if (lowerLine === "/users" || lowerLine === "/flip" || lowerLine === "/tiendita") {
294
+ ws.send(JSON.stringify({ type: "command", cmd: lowerLine.substring(1) }));
295
+ } else if (lowerLine === "/all") {
296
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
297
+ } else if (lowerLine.startsWith("/w ")) {
298
+ const p = line.split(" ");
299
+ if (p.length === 2) {
300
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
301
+ } else if (p.length > 2) {
302
+ const inlineMsg = p.slice(2).join(" ").trim();
303
+ if (inlineMsg) {
304
+ ws.send(JSON.stringify({ type: "chat", target: p[1], msg: inlineMsg }));
305
+ } else {
306
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
307
+ }
308
+ }
309
+ } else if (lowerLine.startsWith("/rps ")) {
310
+ const p = line.split(" ");
311
+ if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2].toLowerCase() }));
312
+ } else if (lowerLine.startsWith("/blink ")) {
313
+ ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
314
+ } else {
315
+ printMessage(`❌ Comando desconocido. Intenta /help`);
316
+ }
317
+ } else {
318
+ // Mensaje Casual
319
+ ws.send(JSON.stringify({ type: "chat", msg: line }));
320
+ }
321
+
322
+ currentIsTyping = false;
323
+ ws.send(JSON.stringify({ type: "typing_stop" }));
324
324
  });
325
325
 
326
326
  // Salvavidas de Desconexión Rápida
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdc-test/chat-io",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "main": "./src/index.js",
5
5
  "bin": {
6
6
  "chat-io": "bin/cli.js"
@@ -9,7 +9,7 @@
9
9
  "start": "node src/server/index.js"
10
10
  },
11
11
  "dependencies": {
12
- "@pdc-test/chat-io": "^1.1.3",
12
+ "@pdc-test/chat-io": "^1.1.5",
13
13
  "crypto-js": "^4.2.0",
14
14
  "ws": "^8.0.0"
15
15
  }
@@ -10,45 +10,45 @@ let pendingRPS = new Map(); // target -> { from, miJugada }
10
10
 
11
11
  // Helpers
12
12
  function broadcast(jsonObj, ignoreWs = null) {
13
- const payload = JSON.stringify(jsonObj);
14
- for (const [s, data] of sessions.entries()) {
15
- if (s !== ignoreWs && s.readyState === WebSocket.OPEN && data.name) {
16
- try { s.send(payload); } catch(e) {}
17
- }
18
- }
13
+ const payload = JSON.stringify(jsonObj);
14
+ for (const [s, data] of sessions.entries()) {
15
+ if (s !== ignoreWs && s.readyState === WebSocket.OPEN && data.name) {
16
+ try { s.send(payload); } catch (e) { }
17
+ }
18
+ }
19
19
  }
20
20
 
21
21
  function sendToTarget(targetName, jsonObj) {
22
- if (!targetName) return;
23
- const payload = JSON.stringify(jsonObj);
24
- for (const [s, data] of sessions.entries()) {
25
- if (data.name && data.name.toLowerCase() === targetName.toLowerCase() && s.readyState === WebSocket.OPEN) {
26
- try { s.send(payload); } catch(e) {}
27
- }
28
- }
22
+ if (!targetName) return;
23
+ const payload = JSON.stringify(jsonObj);
24
+ for (const [s, data] of sessions.entries()) {
25
+ if (data.name && data.name.toLowerCase() === targetName.toLowerCase() && s.readyState === WebSocket.OPEN) {
26
+ try { s.send(payload); } catch (e) { }
27
+ }
28
+ }
29
29
  }
30
30
 
31
31
  function targetExists(name) {
32
- if (!name) return false;
33
- for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return true;
34
- return false;
32
+ if (!name) return false;
33
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return true;
34
+ return false;
35
35
  }
36
36
 
37
37
  function getExactName(name) {
38
- if (!name) return null;
39
- for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return d.name;
40
- return name;
38
+ if (!name) return null;
39
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return d.name;
40
+ return name;
41
41
  }
42
42
 
43
43
  // State para Tiping
44
44
  // typingMap trackea timeouts y a quién se le está escribiendo
45
- let typingMap = new Map();
45
+ let typingMap = new Map();
46
46
 
47
47
  function broadcastUsers() {
48
48
  const list = [];
49
49
  const plainList = [];
50
- for(const d of sessions.values()) {
51
- if(d.name) {
50
+ for (const d of sessions.values()) {
51
+ if (d.name) {
52
52
  list.push(`${d.emoji} ${d.name}`);
53
53
  plainList.push(d.name);
54
54
  }
@@ -66,186 +66,186 @@ function broadcastTyping() {
66
66
  visibleTypers.push(typerName);
67
67
  }
68
68
  }
69
- try { s.send(JSON.stringify({ type: "typing_event", users: visibleTypers })); } catch(e) {}
69
+ try { s.send(JSON.stringify({ type: "typing_event", users: visibleTypers })); } catch (e) { }
70
70
  }
71
71
  }
72
72
  }
73
73
 
74
74
  wss.on("connection", (ws) => {
75
- sessions.set(ws, { name: null, emoji: null, privateTarget: null });
76
-
77
- ws.on("message", (raw) => {
78
- let req;
79
- try { req = JSON.parse(raw); } catch(e) { return; } // Ignorar todo lo que no sea JSON moderno
80
-
81
- const user = sessions.get(ws);
82
-
83
- // 1. REGISTRO
84
- if (req.type === "register") {
85
- const desiredName = req.name.trim();
86
- if (targetExists(desiredName)) {
87
- ws.send(JSON.stringify({ type: "error", msg: "Nombre en uso. Elige otro." }));
88
- return;
89
- }
90
- const taken = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
91
- const available = userEmojis.filter(e => !taken.has(e));
92
- const emoji = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : "⭐";
93
-
94
- user.name = desiredName;
95
- user.emoji = emoji;
96
-
97
- ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
98
- broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
99
- broadcastUsers();
100
- return;
101
- }
102
-
103
- // Seguridad: Debe estar registrado para ejecutar comandos
104
- if (!user.name) return;
105
-
106
- const myName = user.name;
107
-
108
- // Limpiar typing automático si envía un mensaje real
109
- if (req.type === "chat" || req.type === "command") {
110
- if (typingMap.has(myName)) {
111
- clearTimeout(typingMap.get(myName).timer);
112
- typingMap.delete(myName);
113
- broadcastTyping();
114
- }
115
- }
116
-
117
- switch (req.type) {
118
- case "typing":
119
- if (typingMap.has(myName)) clearTimeout(typingMap.get(myName).timer);
120
- typingMap.set(myName, {
121
- target: user.privateTarget,
122
- timer: setTimeout(() => {
123
- typingMap.delete(myName);
124
- broadcastTyping();
125
- }, 5000)
126
- });
127
- broadcastTyping();
128
- break;
75
+ sessions.set(ws, { name: null, emoji: null, privateTarget: null });
76
+
77
+ ws.on("message", (raw) => {
78
+ let req;
79
+ try { req = JSON.parse(raw); } catch (e) { return; } // Ignorar todo lo que no sea JSON moderno
129
80
 
130
- case "typing_stop":
81
+ const user = sessions.get(ws);
82
+
83
+ // 1. REGISTRO
84
+ if (req.type === "register") {
85
+ const desiredName = req.name.trim();
86
+ if (targetExists(desiredName)) {
87
+ ws.send(JSON.stringify({ type: "error", msg: "Nombre en uso. Elige otro." }));
88
+ return;
89
+ }
90
+ const taken = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
91
+ const available = userEmojis.filter(e => !taken.has(e));
92
+ const emoji = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : "⭐";
93
+
94
+ user.name = desiredName;
95
+ user.emoji = emoji;
96
+
97
+ ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
98
+ broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
99
+ broadcastUsers();
100
+ return;
101
+ }
102
+
103
+ // Seguridad: Debe estar registrado para ejecutar comandos
104
+ if (!user.name) return;
105
+
106
+ const myName = user.name;
107
+
108
+ // Limpiar typing automático si envía un mensaje real
109
+ if (req.type === "chat" || req.type === "command") {
131
110
  if (typingMap.has(myName)) {
132
111
  clearTimeout(typingMap.get(myName).timer);
133
112
  typingMap.delete(myName);
134
113
  broadcastTyping();
135
114
  }
136
- break;
115
+ }
137
116
 
138
- case "chat":
139
- const isPrivate = req.target || user.privateTarget;
140
- const target = req.target || user.privateTarget;
117
+ switch (req.type) {
118
+ case "typing":
119
+ if (typingMap.has(myName)) clearTimeout(typingMap.get(myName).timer);
120
+ typingMap.set(myName, {
121
+ target: user.privateTarget,
122
+ timer: setTimeout(() => {
123
+ typingMap.delete(myName);
124
+ broadcastTyping();
125
+ }, 5000)
126
+ });
127
+ broadcastTyping();
128
+ break;
141
129
 
142
- if (isPrivate) {
143
- if (!targetExists(target)) {
144
- ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
145
- return;
146
- }
147
- const exactTarget = getExactName(target);
148
- const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
149
- ws.send(JSON.stringify(pMsg));
150
- sendToTarget(exactTarget, pMsg);
151
- } else {
152
- const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
153
- ws.send(JSON.stringify(globalMsg));
154
- broadcast(globalMsg, ws);
155
-
156
- // Menciones (ding!) enviamos comando JSON puro de ping a clientes
157
- const mentions = [...req.msg.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
158
- for (const m of mentions) {
159
- if (targetExists(m) && m !== myName) {
160
- sendToTarget(m, { type: "ding" });
161
- }
130
+ case "typing_stop":
131
+ if (typingMap.has(myName)) {
132
+ clearTimeout(typingMap.get(myName).timer);
133
+ typingMap.delete(myName);
134
+ broadcastTyping();
162
135
  }
163
- }
164
- break;
136
+ break;
165
137
 
166
- case "command":
167
- if (req.cmd === "users") {
168
- const list = [];
169
- for(const d of sessions.values()) if(d.name) list.push(`${d.emoji} ${d.name}`);
170
- ws.send(JSON.stringify({ type: "users_list", users: list }));
171
- }
172
- else if (req.cmd === "flip") {
173
- const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
174
- // Delega el procesado nativo intensivo al cliente! Envía la órden e historial en 1 Ms.
175
- const animData = { type: "animation", name: "flip", result: result, user: myName, emoji: user.emoji };
176
- ws.send(JSON.stringify(animData));
177
- broadcast(animData, ws);
178
- }
179
- else if (req.cmd === "tiendita") {
180
- const animData = { type: "animation", name: "tiendita", user: myName, emoji: user.emoji };
181
- ws.send(JSON.stringify(animData));
182
- broadcast(animData, ws);
183
- }
184
- else if (req.cmd === "rps") {
185
- const target = req.target;
186
- const choice = req.choice;
187
- if (!targetExists(target)) { ws.send(JSON.stringify({ type: "error", msg: "Usuario no existe" })); return; }
188
- if (target === myName) { ws.send(JSON.stringify({ type: "error", msg: "No contigo mismo" })); return; }
189
-
190
- if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
191
- const reto = pendingRPS.get(myName);
192
- pendingRPS.delete(myName);
193
- let win = "😐 Empate";
194
- if (choice !== reto.miJugada) {
195
- if ((choice === "piedra" && reto.miJugada === "tijera") || (choice === "papel" && reto.miJugada === "piedra") || (choice === "tijera" && reto.miJugada === "papel")) win = `🎉 Ganador: \x1b[32m${myName}\x1b[0m`;
196
- else win = `🎉 Ganador: \x1b[32m${target}\x1b[0m`;
138
+ case "chat":
139
+ const isPrivate = req.target || user.privateTarget;
140
+ const target = req.target || user.privateTarget;
141
+
142
+ if (isPrivate) {
143
+ if (!targetExists(target)) {
144
+ ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
145
+ return;
197
146
  }
198
- const resMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${reto.miJugada}) VS \x1b[36m${myName}\x1b[0m (${choice})\n🏆 ${win}\n`;
199
- const resPayload = { type: "system", msg: resMsg };
200
- ws.send(JSON.stringify(resPayload));
201
- broadcast(resPayload, ws);
147
+ const exactTarget = getExactName(target);
148
+ const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
149
+ ws.send(JSON.stringify(pMsg));
150
+ sendToTarget(exactTarget, pMsg);
202
151
  } else {
203
- pendingRPS.set(target, { from: myName, miJugada: choice });
204
- ws.send(JSON.stringify({ type: "system", msg: `🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m` }));
205
- sendToTarget(target, { type: "system", msg: `\x1b[5m\x1b[35m🚨 ¡${myName} te reta a muerte en RPS!\x1b[0m\n👉 Responde tu golpe con: /rps ${myName} <piedra|papel|tijera>` });
152
+ const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
153
+ ws.send(JSON.stringify(globalMsg));
154
+ broadcast(globalMsg, ws);
155
+
156
+ // Menciones (ding!) enviamos comando JSON puro de ping a clientes
157
+ const mentions = [...req.msg.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
158
+ for (const m of mentions) {
159
+ if (targetExists(m) && m !== myName) {
160
+ sendToTarget(m, { type: "ding" });
161
+ }
162
+ }
206
163
  }
207
- }
208
- else if (req.cmd === "mode_whisper") {
209
- const exactTarget = getExactName(req.target);
210
- if (!targetExists(exactTarget)) {
211
- ws.send(JSON.stringify({ type: "error", msg: `${req.target} no está conectado.` }));
212
- return;
164
+ break;
165
+
166
+ case "command":
167
+ if (req.cmd === "users") {
168
+ const list = [];
169
+ for (const d of sessions.values()) if (d.name) list.push(`${d.emoji} ${d.name}`);
170
+ ws.send(JSON.stringify({ type: "users_list", users: list }));
213
171
  }
214
- user.privateTarget = exactTarget;
215
- ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${exactTarget}.\x1b[0m Usa /all para salir.` }));
216
- }
217
- else if (req.cmd === "mode_all") {
218
- user.privateTarget = null;
219
- ws.send(JSON.stringify({ type: "system", msg: `🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m` }));
172
+ else if (req.cmd === "flip") {
173
+ const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
174
+ // Delega el procesado nativo intensivo al cliente! Envía la órden e historial en 1 Ms.
175
+ const animData = { type: "animation", name: "flip", result: result, user: myName, emoji: user.emoji };
176
+ ws.send(JSON.stringify(animData));
177
+ broadcast(animData, ws);
178
+ }
179
+ else if (req.cmd === "tiendita") {
180
+ const animData = { type: "animation", name: "tiendita", user: myName, emoji: user.emoji };
181
+ ws.send(JSON.stringify(animData));
182
+ broadcast(animData, ws);
183
+ }
184
+ else if (req.cmd === "rps") {
185
+ const target = req.target;
186
+ const choice = req.choice;
187
+ if (!targetExists(target)) { ws.send(JSON.stringify({ type: "error", msg: "Usuario no existe" })); return; }
188
+ if (target === myName) { ws.send(JSON.stringify({ type: "error", msg: "No contigo mismo" })); return; }
189
+
190
+ if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
191
+ const reto = pendingRPS.get(myName);
192
+ pendingRPS.delete(myName);
193
+ let win = "😐 Empate";
194
+ if (choice !== reto.miJugada) {
195
+ if ((choice === "piedra" && reto.miJugada === "tijera") || (choice === "papel" && reto.miJugada === "piedra") || (choice === "tijera" && reto.miJugada === "papel")) win = `🎉 Ganador: \x1b[32m${myName}\x1b[0m`;
196
+ else win = `🎉 Ganador: \x1b[32m${target}\x1b[0m`;
197
+ }
198
+ const resMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${reto.miJugada}) VS \x1b[36m${myName}\x1b[0m (${choice})\n🏆 ${win}\n`;
199
+ const resPayload = { type: "system", msg: resMsg };
200
+ ws.send(JSON.stringify(resPayload));
201
+ broadcast(resPayload, ws);
202
+ } else {
203
+ pendingRPS.set(target, { from: myName, miJugada: choice });
204
+ ws.send(JSON.stringify({ type: "system", msg: `🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m` }));
205
+ sendToTarget(target, { type: "system", msg: `\x1b[5m\x1b[35m🚨 ¡${myName} te reta a muerte en RPS!\x1b[0m\n👉 Responde tu golpe con: /rps ${myName} <piedra|papel|tijera>` });
206
+ }
207
+ }
208
+ else if (req.cmd === "mode_whisper") {
209
+ const exactTarget = getExactName(req.target);
210
+ if (!targetExists(exactTarget)) {
211
+ ws.send(JSON.stringify({ type: "error", msg: `${req.target} no está conectado.` }));
212
+ return;
213
+ }
214
+ user.privateTarget = exactTarget;
215
+ ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${exactTarget}.\x1b[0m Usa /all para salir.` }));
216
+ }
217
+ else if (req.cmd === "mode_all") {
218
+ user.privateTarget = null;
219
+ ws.send(JSON.stringify({ type: "system", msg: `🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m` }));
220
+ }
221
+ else if (req.cmd === "blink") {
222
+ const bMsg = { type: "system", msg: `🌟 [${user.emoji} ${myName}]: \x1b[5m${req.msg}\x1b[0m` };
223
+ ws.send(JSON.stringify(bMsg));
224
+ broadcast(bMsg, ws);
225
+ }
226
+ break;
227
+ }
228
+ });
229
+
230
+ ws.on("close", () => {
231
+ const u = sessions.get(ws);
232
+ if (u && u.name) {
233
+ broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
234
+ if (typingMap.has(u.name)) {
235
+ clearTimeout(typingMap.get(u.name).timer);
236
+ typingMap.delete(u.name);
237
+ broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
220
238
  }
221
- else if (req.cmd === "blink") {
222
- const bMsg = { type: "system", msg: `🌟 [${user.emoji} ${myName}]: \x1b[5m${req.msg}\x1b[0m` };
223
- ws.send(JSON.stringify(bMsg));
224
- broadcast(bMsg, ws);
239
+ }
240
+ sessions.delete(ws);
241
+ if (u && u.name) {
242
+ broadcastUsers();
243
+ pendingRPS.delete(u.name);
244
+ for (const [t, data] of pendingRPS.entries()) {
245
+ if (data.from === u.name) pendingRPS.delete(t);
225
246
  }
226
- break;
227
- }
228
- });
229
-
230
- ws.on("close", () => {
231
- const u = sessions.get(ws);
232
- if (u && u.name) {
233
- broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
234
- if (typingMap.has(u.name)) {
235
- clearTimeout(typingMap.get(u.name).timer);
236
- typingMap.delete(u.name);
237
- broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
238
- }
239
- }
240
- sessions.delete(ws);
241
- if (u && u.name) {
242
- broadcastUsers();
243
- pendingRPS.delete(u.name);
244
- for(const [t, data] of pendingRPS.entries()) {
245
- if(data.from === u.name) pendingRPS.delete(t);
246
- }
247
- }
248
- });
247
+ }
248
+ });
249
249
  });
250
250
 
251
251
  console.log(`📡 Servidor JSON-Core Central corriendo en puerto ${PORT}`);