@pdc-test/chat-io 1.0.2 → 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/bin/cli.js +199 -88
- package/package.json +2 -2
- package/src/server/index.js +174 -273
package/bin/cli.js
CHANGED
|
@@ -1,140 +1,251 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
const WebSocket = require('ws');
|
|
4
3
|
const readline = require('readline');
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// ============================================
|
|
6
|
+
// CLIENTE MODULAR RÁPIDO (Carga NATIVA JSON)
|
|
7
|
+
// ============================================
|
|
7
8
|
const SERVER_URL = 'wss://chat-io-production.up.railway.app/';
|
|
8
|
-
|
|
9
9
|
const ws = new WebSocket(SERVER_URL);
|
|
10
|
-
const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/blink'];
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
let myName = null;
|
|
12
|
+
let currentIsTyping = false;
|
|
13
|
+
let typingTimeout = null;
|
|
14
|
+
|
|
15
|
+
// Control de UI Local
|
|
16
|
+
let activeTypers = [];
|
|
17
|
+
let tIdx = 0;
|
|
18
|
+
const typingFrames = ['.', '..', '...'];
|
|
19
|
+
let isMultilinePrompt = false;
|
|
20
|
+
|
|
21
|
+
// Helpers VISUALES
|
|
22
|
+
function getTime() {
|
|
23
|
+
const d = new Date();
|
|
24
|
+
const hs = String(d.getHours()).padStart(2, '0');
|
|
25
|
+
const ms = String(d.getMinutes()).padStart(2, '0');
|
|
26
|
+
return `\x1b[90m${hs}:${ms}\x1b[0m`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearTopLine() {
|
|
30
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
31
|
+
readline.clearLine(process.stdout, 0);
|
|
32
|
+
readline.moveCursor(process.stdout, 0, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Interfaz del Teclado Local
|
|
36
|
+
const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all', '/blink'];
|
|
13
37
|
function completer(line) {
|
|
14
38
|
const hits = commands.filter((c) => c.startsWith(line));
|
|
15
39
|
return [hits.length ? hits : [], line];
|
|
16
40
|
}
|
|
17
41
|
|
|
18
|
-
const rl = readline.createInterface({
|
|
19
|
-
input: process.stdin,
|
|
20
|
-
output: process.stdout,
|
|
21
|
-
completer,
|
|
22
|
-
prompt: '\r> ' // Prefix visual del input
|
|
23
|
-
});
|
|
42
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ' });
|
|
24
43
|
|
|
25
|
-
let isTyping = false;
|
|
26
|
-
let typingTimeout = null;
|
|
27
|
-
let typingUsers = new Set();
|
|
28
|
-
let userTypingTimers = {};
|
|
29
|
-
let typingFrames = ['.', '..', '...'];
|
|
30
|
-
let tIdx = 0;
|
|
31
|
-
let isMultilinePrompt = false;
|
|
32
|
-
|
|
33
|
-
// Escuchador de teclado constante (Raw Mode Mágico)
|
|
34
44
|
process.stdin.on('keypress', (str, key) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
40
|
-
ws.send("/typ"); // Ping silencioso
|
|
41
|
-
}
|
|
45
|
+
if (key && key.name !== 'return' && key.name !== 'enter' && myName) {
|
|
46
|
+
if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
|
|
47
|
+
currentIsTyping = true;
|
|
48
|
+
ws.send(JSON.stringify({ type: "typing" }));
|
|
42
49
|
}
|
|
43
|
-
// Reiniciar el timeout si el usuario sigue tecleando
|
|
44
50
|
clearTimeout(typingTimeout);
|
|
45
51
|
typingTimeout = setTimeout(() => {
|
|
46
|
-
|
|
47
|
-
if (ws.readyState === WebSocket.OPEN) ws.send("
|
|
52
|
+
currentIsTyping = false;
|
|
53
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "typing_stop" }));
|
|
48
54
|
}, 1500);
|
|
49
55
|
}
|
|
50
56
|
});
|
|
51
57
|
|
|
52
|
-
//
|
|
58
|
+
// Actualizador de UI Typing Background -> Puesto a 300ms, Cero Network Latency porque es local
|
|
53
59
|
setInterval(() => {
|
|
54
|
-
if (
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
rl.setPrompt(`\x1b[90m[${users} ${suffix} ${typingFrames[tIdx]}]\x1b[0m\n> `);
|
|
60
|
+
if (activeTypers.length > 0) {
|
|
61
|
+
const usersInfo = activeTypers.join(", ") + (activeTypers.length > 1 ? " están" : " está") + " escribiendo";
|
|
62
|
+
rl.setPrompt(`\x1b[90m[${usersInfo} ${typingFrames[tIdx]}]\x1b[0m\n> `);
|
|
59
63
|
tIdx = (tIdx + 1) % typingFrames.length;
|
|
60
64
|
rl.prompt(true);
|
|
61
65
|
isMultilinePrompt = true;
|
|
62
66
|
} else {
|
|
63
67
|
if (isMultilinePrompt) {
|
|
64
68
|
rl.setPrompt('> ');
|
|
65
|
-
|
|
66
|
-
readline.moveCursor(process.stdout, 0, -1);
|
|
67
|
-
readline.clearLine(process.stdout, 0);
|
|
68
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
69
|
+
clearTopLine();
|
|
69
70
|
rl.prompt(true);
|
|
70
71
|
isMultilinePrompt = false;
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
},
|
|
74
|
+
}, 300);
|
|
74
75
|
|
|
75
|
-
// Flujo
|
|
76
|
+
// Flujo Principal Socket
|
|
76
77
|
ws.on('open', () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
console.log("\x1b[36m====================================\x1b[0m");
|
|
79
|
+
console.log(" 🚀 BIENVENIDO AL JSON-CHAT-CLI 🚀");
|
|
80
|
+
console.log("\x1b[36m====================================\x1b[0m");
|
|
81
|
+
console.log("🤖 Sistema: Escribe tu nombre y presiona Enter para comenzar:");
|
|
82
|
+
rl.prompt();
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Si el servidor nos avisa secretamente que alguien tipeó:
|
|
86
|
-
if (msg.startsWith("/typ_event ")) {
|
|
87
|
-
const u = msg.replace("/typ_event ", "").trim();
|
|
88
|
-
typingUsers.add(u);
|
|
89
|
-
clearTimeout(userTypingTimers[u]);
|
|
90
|
-
userTypingTimers[u] = setTimeout(() => typingUsers.delete(u), 3500);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (msg.startsWith("/typ_stop_event ")) {
|
|
95
|
-
const u = msg.replace("/typ_stop_event ", "").trim();
|
|
96
|
-
typingUsers.delete(u);
|
|
97
|
-
clearTimeout(userTypingTimers[u]);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Si recibimos texto normal, limpiamos todas las señales tácticas
|
|
102
|
-
typingUsers.clear();
|
|
103
|
-
|
|
104
|
-
// Purgamos toda visualización flotante o entrada corrupta antes de dibujar texto nuevo
|
|
105
|
-
if (isMultilinePrompt) {
|
|
106
|
-
readline.moveCursor(process.stdout, 0, -1);
|
|
107
|
-
readline.clearLine(process.stdout, 0);
|
|
108
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
109
|
-
isMultilinePrompt = false;
|
|
110
|
-
}
|
|
85
|
+
// Encargado de Pintar Todo en la Terminal limpiamente
|
|
86
|
+
function printMessage(msgStr) {
|
|
87
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
111
88
|
readline.clearLine(process.stdout, 0);
|
|
112
89
|
readline.cursorTo(process.stdout, 0);
|
|
113
|
-
|
|
114
|
-
console.log(msg);
|
|
90
|
+
console.log(msgStr);
|
|
115
91
|
rl.setPrompt('> ');
|
|
116
|
-
rl.prompt(true);
|
|
92
|
+
rl.prompt(true);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Receptor de la Nube (Solo procesa comandos JSON Puros, nada de basura String)
|
|
96
|
+
ws.on('message', (data) => {
|
|
97
|
+
let res;
|
|
98
|
+
try { res = JSON.parse(data.toString()); } catch(e) { return; }
|
|
99
|
+
|
|
100
|
+
switch (res.type) {
|
|
101
|
+
case "error": printMessage(`❌ \x1b[31m${res.msg}\x1b[0m`); break;
|
|
102
|
+
|
|
103
|
+
case "system": printMessage(`\n[${getTime()}] ${res.msg}`); break;
|
|
104
|
+
|
|
105
|
+
case "registered":
|
|
106
|
+
myName = res.name;
|
|
107
|
+
printMessage(`\n✅ ¡Listo ${res.name}! Estás con el avatar ${res.emoji}. Escribe /help para los comandos nativos.`);
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case "typing_event":
|
|
111
|
+
// El servidor nos entrega el arreglo COMPLETO y VIVO de quién tipea
|
|
112
|
+
// Tu computadora solo limpia e ignora inteligentemente si está tu mismo nombre de pila
|
|
113
|
+
activeTypers = res.users.filter(u => u !== myName);
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case "ding":
|
|
117
|
+
process.stdout.write("\x07"); // ASCII Bell físico disparado remotamente
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case "users_list":
|
|
121
|
+
printMessage(`\n👥 \x1b[36mCONECTADOS AHORA (${res.users.length}):\x1b[0m ` + res.users.join(", ") + "\n");
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case "chat":
|
|
125
|
+
// Todo el poder de Parsear Regex se procesa en TU PC, ahorrándole billones de cálculos al Server
|
|
126
|
+
let safeMsg = res.msg.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
|
|
127
|
+
if (res.isWhisper) {
|
|
128
|
+
const dir = res.from === myName ? `Private → ${res.to}` : `${res.emoji} Secreto de ${res.from}`;
|
|
129
|
+
printMessage(`\n[${getTime()}] 🔒 [${dir}]: \x1b[35m${safeMsg}\x1b[0m`);
|
|
130
|
+
} else {
|
|
131
|
+
printMessage(`\n[${getTime()}] 🌍 [${res.emoji} ${res.from}]: ${safeMsg}`);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case "animation":
|
|
136
|
+
if (res.name === "tiendita") renderTiendita(res.user);
|
|
137
|
+
else if (res.name === "flip") renderFlip(res.user, res.emoji, res.result);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
117
140
|
});
|
|
118
141
|
|
|
119
|
-
//
|
|
142
|
+
// ANIMACIONES RENDERIZADAS LOCALMENTE CON CPU CLIENT (LATENCIA CERO, NO SUCIA LA RED)
|
|
143
|
+
function renderTiendita(user) {
|
|
144
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
145
|
+
readline.clearLine(process.stdout, 0);
|
|
146
|
+
readline.cursorTo(process.stdout, 0);
|
|
147
|
+
|
|
148
|
+
console.log(`\n[${getTime()}] 🏪 \x1b[1m${user}\x1b[0m convoca a la tiendita...\n`);
|
|
149
|
+
|
|
150
|
+
const asciiArt = `_____ _ _ _ _ _ _ _ \n |_ _(_) ___ _ __ __| (_) |_ __ _| | | |\n | | | |/ _ \\ '_ \\ / _ | | __/ _ | | | |\n | | | | __/ | | | (_ | | || (_ |_|_|_|\n |_| |_|\\___|_| |_|\\__,_|_|\\__\\__,_(_|_|_)\n `;
|
|
151
|
+
const colors = ["\x1b[31m", "\x1b[33m", "\x1b[32m", "\x1b[36m", "\x1b[1m\x1b[35m"];
|
|
152
|
+
|
|
153
|
+
// Dejar huella Base
|
|
154
|
+
process.stdout.write(`${colors[0]}${asciiArt}\x1b[0m\n`);
|
|
155
|
+
|
|
156
|
+
let frames = 0;
|
|
157
|
+
const anim = setInterval(() => {
|
|
158
|
+
frames++;
|
|
159
|
+
const c = colors[frames % colors.length];
|
|
160
|
+
// Bucle mágico hiper-veloz usando RAM Local
|
|
161
|
+
process.stdout.write(`\r\x1b[6A\x1b[0J${c}${asciiArt}\x1b[0m\n`);
|
|
162
|
+
if (frames > 15) {
|
|
163
|
+
clearInterval(anim);
|
|
164
|
+
rl.prompt(true);
|
|
165
|
+
}
|
|
166
|
+
}, 150); // Mismo efecto, pero procesado x100 más eficiente
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderFlip(user, emoji, result) {
|
|
170
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
171
|
+
readline.clearLine(process.stdout, 0);
|
|
172
|
+
readline.cursorTo(process.stdout, 0);
|
|
173
|
+
console.log(`\n[${getTime()}] 🪙 [${emoji} ${user}] lanza una moneda al aire...`);
|
|
174
|
+
|
|
175
|
+
const asciiCoins = ["\x1b[33m ( o ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m", "\x1b[38;5;220m ( 0 ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m"];
|
|
176
|
+
let i = 0;
|
|
177
|
+
|
|
178
|
+
process.stdout.write(" \n");
|
|
179
|
+
const anim = setInterval(() => {
|
|
180
|
+
process.stdout.write(`\r\x1b[1A\x1b[K ${asciiCoins[i % 4]} zumbando...\n`);
|
|
181
|
+
i++;
|
|
182
|
+
if (i > 15) {
|
|
183
|
+
clearInterval(anim);
|
|
184
|
+
process.stdout.write(`\r\x1b[1A\x1b[K[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${user}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n\n`);
|
|
185
|
+
rl.prompt(true);
|
|
186
|
+
}
|
|
187
|
+
}, 100);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Control General del Botón Enter (Distribuidor Estructurado)
|
|
120
191
|
rl.on('line', (input) => {
|
|
121
|
-
//
|
|
122
|
-
// con el mensaje que el servidor nos devolverá en azul
|
|
192
|
+
// Destruimos el "echo" por defecto para no dejar basura arriba del prompt
|
|
123
193
|
readline.moveCursor(process.stdout, 0, -1);
|
|
124
194
|
readline.clearLine(process.stdout, 0);
|
|
125
195
|
|
|
126
196
|
const line = input.trim();
|
|
127
|
-
if (line) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
197
|
+
if (!line) { rl.prompt(true); return; }
|
|
198
|
+
|
|
199
|
+
// Flujo Inicial Obligatorio
|
|
200
|
+
if (!myName) {
|
|
201
|
+
ws.send(JSON.stringify({ type: "register", name: line }));
|
|
202
|
+
return;
|
|
131
203
|
}
|
|
132
|
-
|
|
204
|
+
|
|
205
|
+
// Local Helper Menú
|
|
206
|
+
if (line === "/help") {
|
|
207
|
+
printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
|
|
208
|
+
👥 /users → Ver conectados remotamente
|
|
209
|
+
🪙 /flip → Lanzar moneda 60FPS
|
|
210
|
+
🎮 /rps <usr> <j> → Piedra-Papel-Tijera
|
|
211
|
+
🌟 /blink <msj> → Envía un texto parpadeante ANSI
|
|
212
|
+
🔒 /w <usr> [msj] → Modo Susurro directo a una persona
|
|
213
|
+
🌍 /all → Volver a la Sala Global Abierta
|
|
214
|
+
🏪 /tiendita → Letrero de Neón de colores acelerado
|
|
215
|
+
ℹ️ /help → Te ayuda desde la memoria Caché
|
|
216
|
+
─────────────────────────────────────────────\n`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Constructor Arquitectónico y Limpio de Comandos a Servidor
|
|
221
|
+
if (line.startsWith("/")) {
|
|
222
|
+
if (line === "/users" || line === "/flip" || line === "/tiendita") {
|
|
223
|
+
ws.send(JSON.stringify({ type: "command", cmd: line.substring(1) }));
|
|
224
|
+
} else if (line === "/all") {
|
|
225
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
|
|
226
|
+
} else if (line.startsWith("/w ")) {
|
|
227
|
+
const p = line.split(" ");
|
|
228
|
+
if (p.length === 2) ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
|
|
229
|
+
else if (p.length > 2) ws.send(JSON.stringify({ type: "chat", target: p[1], msg: p.slice(2).join(" ") }));
|
|
230
|
+
} else if (line.startsWith("/rps ")) {
|
|
231
|
+
const p = line.split(" ");
|
|
232
|
+
if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2] }));
|
|
233
|
+
} else if (line.startsWith("/blink ")) {
|
|
234
|
+
ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
|
|
235
|
+
} else {
|
|
236
|
+
printMessage(`❌ Comando desconocido. Intenta /help`);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// Mensaje Casual
|
|
240
|
+
ws.send(JSON.stringify({ type: "chat", msg: line }));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isTyping = false;
|
|
244
|
+
ws.send(JSON.stringify({ type: "typing_stop" }));
|
|
133
245
|
});
|
|
134
246
|
|
|
247
|
+
// Salvavidas de Desconexión Rápida
|
|
135
248
|
ws.on('close', () => {
|
|
136
|
-
|
|
137
|
-
readline.cursorTo(process.stdout, 0);
|
|
138
|
-
console.log("\n\x1b[31m[!] Se ha perdido la conexión con la Nube.\x1b[0m");
|
|
249
|
+
printMessage("\x1b[31m[!] Oh no... Acabas de perder la conexión con la Matriz (Nube principal caída).\x1b[0m");
|
|
139
250
|
process.exit(0);
|
|
140
251
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdc-test/chat-io",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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.0
|
|
12
|
+
"@pdc-test/chat-io": "^1.1.0",
|
|
13
13
|
"crypto-js": "^4.2.0",
|
|
14
14
|
"ws": "^8.0.0"
|
|
15
15
|
}
|
package/src/server/index.js
CHANGED
|
@@ -1,305 +1,206 @@
|
|
|
1
|
-
const WebSocket = require(
|
|
2
|
-
|
|
1
|
+
const WebSocket = require('ws');
|
|
3
2
|
const PORT = process.env.PORT || 3000;
|
|
4
3
|
const wss = new WebSocket.Server({ port: PORT });
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const sessions = new Map(); // socket -> { name: string, emoji: string }
|
|
9
|
-
const pendingRPS = new Map(); // targetName -> { from: name, miJugada: "piedra" }
|
|
10
|
-
|
|
5
|
+
// --- ESTADO GLOBAL ---
|
|
6
|
+
const sessions = new Map(); // ws -> { name, emoji, privateTarget }
|
|
11
7
|
const userEmojis = ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🦄", "🐙", "🦋", "🦖", "🐧", "🦉", "👽", "🤖", "👻", "🥑", "🍕"];
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
const d = new Date();
|
|
15
|
-
const hs = String(d.getHours()).padStart(2, '0');
|
|
16
|
-
const ms = String(d.getMinutes()).padStart(2, '0');
|
|
17
|
-
return `\x1b[90m${hs}:${ms}\x1b[0m`;
|
|
18
|
-
}
|
|
9
|
+
let pendingRPS = new Map(); // target -> { from, miJugada }
|
|
19
10
|
|
|
20
|
-
|
|
11
|
+
// Helpers
|
|
12
|
+
function broadcast(jsonObj, ignoreWs = null) {
|
|
13
|
+
const payload = JSON.stringify(jsonObj);
|
|
21
14
|
for (const [s, data] of sessions.entries()) {
|
|
22
15
|
if (s !== ignoreWs && s.readyState === WebSocket.OPEN && data.name) {
|
|
23
|
-
try { s.send(
|
|
16
|
+
try { s.send(payload); } catch(e) {}
|
|
24
17
|
}
|
|
25
18
|
}
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
function sendToTarget(
|
|
21
|
+
function sendToTarget(targetName, jsonObj) {
|
|
22
|
+
const payload = JSON.stringify(jsonObj);
|
|
29
23
|
for (const [s, data] of sessions.entries()) {
|
|
30
|
-
if (data.name ===
|
|
31
|
-
try { s.send(
|
|
24
|
+
if (data.name === targetName && s.readyState === WebSocket.OPEN) {
|
|
25
|
+
try { s.send(payload); } catch(e) {}
|
|
32
26
|
}
|
|
33
27
|
}
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
function targetExists(name) {
|
|
37
|
-
for (const
|
|
31
|
+
for (const d of sessions.values()) if (d.name === name) return true;
|
|
38
32
|
return false;
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
server.on("message", (message) => {
|
|
49
|
-
if (globalTienditaTimer) {
|
|
50
|
-
clearInterval(globalTienditaTimer);
|
|
51
|
-
globalTienditaTimer = null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const socketData = sessions.get(server);
|
|
55
|
-
let text = message.toString().trim();
|
|
56
|
-
|
|
57
|
-
// Registro de Smart Client Silencioso
|
|
58
|
-
if (text === "/iam_smart") {
|
|
59
|
-
socketData.smart = true;
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// REGISTRO ÚNICO
|
|
64
|
-
if (!socketData.name) {
|
|
65
|
-
if (!text || text === "/typ") return;
|
|
66
|
-
const desiredName = text.replace("/name ", "").trim();
|
|
67
|
-
if (targetExists(desiredName)) {
|
|
68
|
-
server.send("❌ Nombre en uso. Escribe otro distinto:");
|
|
69
|
-
} else {
|
|
70
|
-
// Lógica de Emojis Únicos sin repetir (Evitamos colisiones)
|
|
71
|
-
const takenEmojis = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
|
|
72
|
-
const available = userEmojis.filter(e => !takenEmojis.has(e));
|
|
73
|
-
|
|
74
|
-
// Si por algun motivo la sala es masiva (> 26 usuarios), reciclamos de todos
|
|
75
|
-
const pool = available.length > 0 ? available : userEmojis;
|
|
76
|
-
const assignedEmoji = pool[Math.floor(Math.random() * pool.length)];
|
|
77
|
-
|
|
78
|
-
socketData.name = desiredName;
|
|
79
|
-
socketData.emoji = assignedEmoji;
|
|
80
|
-
|
|
81
|
-
server.send(`✅ ¡Listo ${desiredName}! Estás conectado con el avatar ${assignedEmoji}. Escribe /help para comandos.`);
|
|
82
|
-
broadcast(`\n[${getTime()}] 🟢 [\x1b[32m${assignedEmoji} ${desiredName}\x1b[0m] se ha unido al servidor.\n`, server);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const myName = socketData.name;
|
|
88
|
-
const myEmoji = socketData.emoji;
|
|
89
|
-
|
|
90
|
-
if (text === "/help") {
|
|
91
|
-
server.send(`\n──────────── 💡 COMANDOS ────────────
|
|
92
|
-
👥 /users → Ver conectados
|
|
93
|
-
🪙 /flip → Lanzar moneda
|
|
94
|
-
🎮 /rps <usr> <j> → Piedra-Papel-Tijera
|
|
95
|
-
🌟 /blink <msj> → Mensaje parpadeante
|
|
96
|
-
🔒 /w <usr> <msj> → Mensaje privado
|
|
97
|
-
🏪 /tiendita → Invitación a la Tiendita
|
|
98
|
-
ℹ️ /help → Esta ayuda
|
|
99
|
-
─────────────────────────────────────────────\n`);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (text === "/users") {
|
|
104
|
-
let userList = [];
|
|
105
|
-
for (const d of sessions.values()) if (d.name) userList.push(`${d.emoji} ${d.name}`);
|
|
106
|
-
server.send(`\n👥 \x1b[36mCONECTADOS AHORA (${userList.length}):\x1b[0m ` + userList.join(", ") + "\n");
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
35
|
+
// State para Tiping
|
|
36
|
+
let typingMap = new Map();
|
|
37
|
+
function broadcastTyping() {
|
|
38
|
+
const list = Array.from(typingMap.keys());
|
|
39
|
+
broadcast({ type: "typing_event", users: list });
|
|
40
|
+
}
|
|
109
41
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
42
|
+
wss.on("connection", (ws) => {
|
|
43
|
+
sessions.set(ws, { name: null, emoji: null, privateTarget: null });
|
|
44
|
+
|
|
45
|
+
ws.on("message", (raw) => {
|
|
46
|
+
let req;
|
|
47
|
+
try { req = JSON.parse(raw); } catch(e) { return; } // Ignorar todo lo que no sea JSON moderno
|
|
48
|
+
|
|
49
|
+
const user = sessions.get(ws);
|
|
50
|
+
|
|
51
|
+
// 1. REGISTRO
|
|
52
|
+
if (req.type === "register") {
|
|
53
|
+
const desiredName = req.name.trim();
|
|
54
|
+
if (targetExists(desiredName)) {
|
|
55
|
+
ws.send(JSON.stringify({ type: "error", msg: "Nombre en uso. Elige otro." }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const taken = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
|
|
59
|
+
const available = userEmojis.filter(e => !taken.has(e));
|
|
60
|
+
const emoji = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : "⭐";
|
|
61
|
+
|
|
62
|
+
user.name = desiredName;
|
|
63
|
+
user.emoji = emoji;
|
|
64
|
+
|
|
65
|
+
ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
|
|
66
|
+
broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Seguridad: Debe estar registrado para ejecutar comandos
|
|
71
|
+
if (!user.name) return;
|
|
72
|
+
|
|
73
|
+
const myName = user.name;
|
|
74
|
+
|
|
75
|
+
// Limpiar typing automático si envía un mensaje real
|
|
76
|
+
if (req.type === "chat" || req.type === "command") {
|
|
77
|
+
if (typingMap.has(myName)) {
|
|
78
|
+
clearTimeout(typingMap.get(myName));
|
|
79
|
+
typingMap.delete(myName);
|
|
80
|
+
broadcastTyping();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (req.type) {
|
|
85
|
+
case "typing":
|
|
86
|
+
if (typingMap.has(myName)) clearTimeout(typingMap.get(myName));
|
|
87
|
+
typingMap.set(myName, setTimeout(() => {
|
|
88
|
+
typingMap.delete(myName);
|
|
89
|
+
broadcastTyping();
|
|
90
|
+
}, 5000));
|
|
91
|
+
broadcastTyping();
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case "typing_stop":
|
|
95
|
+
if (typingMap.has(myName)) {
|
|
96
|
+
clearTimeout(typingMap.get(myName));
|
|
97
|
+
typingMap.delete(myName);
|
|
98
|
+
broadcastTyping();
|
|
115
99
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const targetUser = parts[1];
|
|
131
|
-
|
|
132
|
-
if (!targetExists(targetUser)) { server.send("❌ Usuario no encontrado."); return; }
|
|
133
|
-
if (targetUser === myName) { server.send("❌ ¿Hablando contigo mismo?"); return; }
|
|
134
|
-
|
|
135
|
-
// Si solo envían /w Mario (Se quedan en modo privado)
|
|
136
|
-
if (parts.length === 2) {
|
|
137
|
-
socketData.privateTarget = targetUser;
|
|
138
|
-
server.send(`\n[Sistema] 🔒 \x1b[35mModo Susurro activado con ${targetUser}.\x1b[0m Todo lo que escribas solo lo leerá él. Escribe /all para salir.\n`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Si lo envían todo de golpe /w Mario hola
|
|
143
|
-
const secretMsg = parts.slice(2).join(" ");
|
|
144
|
-
server.send(`\n[${getTime()}] 🔒 [Private → ${targetUser}]: \x1b[35m${secretMsg}\x1b[0m`);
|
|
145
|
-
sendToTarget(targetUser, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${secretMsg}\x1b[0m`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Moneda Animada ASCII Rotativa in-place
|
|
150
|
-
if (text === "/flip") {
|
|
151
|
-
const prep = `\n[${getTime()}] 🪙 [${myEmoji} ${myName}] lanza una moneda al aire...\n`;
|
|
152
|
-
server.send(prep);
|
|
153
|
-
broadcast(prep, server);
|
|
154
|
-
|
|
155
|
-
const asciiCoins = [
|
|
156
|
-
"\x1b[33m ( o ) \x1b[0m",
|
|
157
|
-
"\x1b[33m ( | ) \x1b[0m",
|
|
158
|
-
"\x1b[38;5;220m ( 0 ) \x1b[0m",
|
|
159
|
-
"\x1b[33m ( | ) \x1b[0m"
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
let i = 0;
|
|
163
|
-
const timer = setInterval(() => {
|
|
164
|
-
if (i < 15) { // 3 segundos a 200ms
|
|
165
|
-
const frame = `\r\x1b[A\x1b[K ${asciiCoins[i % asciiCoins.length]} zumbando...`;
|
|
166
|
-
server.send(frame);
|
|
167
|
-
broadcast(frame, server);
|
|
168
|
-
i++;
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case "chat":
|
|
103
|
+
const isPrivate = req.target || user.privateTarget;
|
|
104
|
+
const target = req.target || user.privateTarget;
|
|
105
|
+
|
|
106
|
+
if (isPrivate) {
|
|
107
|
+
if (!targetExists(target)) {
|
|
108
|
+
ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: target };
|
|
112
|
+
ws.send(JSON.stringify(pMsg));
|
|
113
|
+
sendToTarget(target, pMsg);
|
|
169
114
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
115
|
+
const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
|
|
116
|
+
ws.send(JSON.stringify(globalMsg));
|
|
117
|
+
broadcast(globalMsg, ws);
|
|
118
|
+
|
|
119
|
+
// Menciones (ding!) enviamos comando JSON puro de ping a clientes
|
|
120
|
+
const mentions = [...req.msg.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
|
|
121
|
+
for (const m of mentions) {
|
|
122
|
+
if (targetExists(m) && m !== myName) {
|
|
123
|
+
sendToTarget(m, { type: "ding" });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
175
126
|
}
|
|
176
|
-
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Comando Animado ASCII de Cambios de Colores
|
|
181
|
-
if (text === "/tiendita") {
|
|
182
|
-
const introMsg = `\n[${getTime()}] 🏪 \x1b[1m${myName}\x1b[0m convoca a todos a la tiendita...\n`;
|
|
183
|
-
server.send(introMsg);
|
|
184
|
-
broadcast(introMsg, server);
|
|
185
|
-
|
|
186
|
-
const asciiArt = `_____ _ _ _ _ _ _ _
|
|
187
|
-
|_ _(_) ___ _ __ __| (_) |_ __ _| | | |
|
|
188
|
-
| | | |/ _ \ '_ \ / _ | | __/ _ | | | |
|
|
189
|
-
| | | | __/ | | | (_ | | || (_ |_|_|_|
|
|
190
|
-
|_| |_|\___|_| |_|\__,_|_|\__\__,_(_|_|_)
|
|
191
|
-
`;
|
|
192
|
-
|
|
193
|
-
const baseColors = [
|
|
194
|
-
"\x1b[31m", // Rojo
|
|
195
|
-
"\x1b[33m", // Amarillo
|
|
196
|
-
"\x1b[32m", // Verde
|
|
197
|
-
"\x1b[36m", // Cyan
|
|
198
|
-
"\x1b[1m\x1b[35m" // Magenta Brillante
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
// Primer frame puro sin código de arrastre hacia arriba
|
|
202
|
-
const f0 = `${baseColors[0]}${asciiArt}\x1b[0m`;
|
|
203
|
-
server.send(f0);
|
|
204
|
-
broadcast(f0, server);
|
|
205
|
-
|
|
206
|
-
let i = 1;
|
|
207
|
-
globalTienditaTimer = setInterval(() => {
|
|
208
|
-
const color = baseColors[i % baseColors.length];
|
|
209
|
-
const frame = `\r\x1b[6A\x1b[0J< ${color}${asciiArt}\x1b[0m`;
|
|
210
|
-
server.send(frame);
|
|
211
|
-
broadcast(frame, server);
|
|
212
|
-
i++;
|
|
213
|
-
}, 600); // Bucle Infinito a 600ms
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (text.startsWith("/blink ")) {
|
|
218
|
-
const blnkText = text.replace("/blink ", "").trim();
|
|
219
|
-
const msg = `\n[${getTime()}] 🌟 [${myEmoji} ${myName}]: \x1b[5m${blnkText}\x1b[0m\n`;
|
|
220
|
-
server.send(msg);
|
|
221
|
-
broadcast(msg, server);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (text.startsWith("/rps ")) {
|
|
226
|
-
const parts = text.split(" ");
|
|
227
|
-
if (parts.length < 3) { server.send("❌ Error: /rps <usuario> <piedra|papel|tijera>"); return; }
|
|
228
|
-
const target = parts[1];
|
|
229
|
-
const choice = parts[2].toLowerCase();
|
|
230
|
-
|
|
231
|
-
if (!["piedra", "papel", "tijera"].includes(choice)) { server.send("❌ Elige piedra, papel o tijera."); return; }
|
|
232
|
-
if (target === myName) { server.send("❌ No contra ti mismo."); return; }
|
|
233
|
-
if (!targetExists(target)) { server.send(`❌ El usuario ${target} no existe.`); return; }
|
|
234
|
-
|
|
235
|
-
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
236
|
-
const retoAnterior = pendingRPS.get(myName);
|
|
237
|
-
pendingRPS.delete(myName);
|
|
238
|
-
|
|
239
|
-
const miJugada = choice;
|
|
240
|
-
const suJugada = retoAnterior.miJugada;
|
|
241
|
-
|
|
242
|
-
let winStr;
|
|
243
|
-
if (miJugada === suJugada) { winStr = "😐 Empate"; }
|
|
244
|
-
else if ((miJugada === "piedra" && suJugada === "tijera") || (miJugada === "papel" && suJugada === "piedra") || (miJugada === "tijera" && suJugada === "papel")) { winStr = `🎉 \x1b[32m${myName}!\x1b[0m`; }
|
|
245
|
-
else { winStr = `🎉 \x1b[32m${target}!\x1b[0m`; }
|
|
246
|
-
|
|
247
|
-
const outMsg = `\n[${getTime()}] 🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
|
|
248
|
-
server.send(outMsg);
|
|
249
|
-
broadcast(outMsg, server);
|
|
250
|
-
} else {
|
|
251
|
-
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
252
|
-
server.send(`\n[${getTime()}] 🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m`);
|
|
253
|
-
sendToTarget(target, `\n[${getTime()}] \x1b[5m\x1b[35m🚨 ¡${myName} te reta a muerte en RPS!\x1b[0m\n👉 Responde tu golpe con: /rps ${myName} <piedra|papel|tijera>\n`);
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
127
|
+
break;
|
|
257
128
|
|
|
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
|
-
|
|
129
|
+
case "command":
|
|
130
|
+
if (req.cmd === "users") {
|
|
131
|
+
const list = [];
|
|
132
|
+
for(const d of sessions.values()) if(d.name) list.push(`${d.emoji} ${d.name}`);
|
|
133
|
+
ws.send(JSON.stringify({ type: "users_list", users: list }));
|
|
134
|
+
}
|
|
135
|
+
else if (req.cmd === "flip") {
|
|
136
|
+
const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
|
|
137
|
+
// Delega el procesado nativo intensivo al cliente! Envía la órden e historial en 1 Ms.
|
|
138
|
+
const animData = { type: "animation", name: "flip", result: result, user: myName, emoji: user.emoji };
|
|
139
|
+
ws.send(JSON.stringify(animData));
|
|
140
|
+
broadcast(animData, ws);
|
|
141
|
+
}
|
|
142
|
+
else if (req.cmd === "tiendita") {
|
|
143
|
+
const animData = { type: "animation", name: "tiendita", user: myName, emoji: user.emoji };
|
|
144
|
+
ws.send(JSON.stringify(animData));
|
|
145
|
+
broadcast(animData, ws);
|
|
146
|
+
}
|
|
147
|
+
else if (req.cmd === "rps") {
|
|
148
|
+
const target = req.target;
|
|
149
|
+
const choice = req.choice;
|
|
150
|
+
if (!targetExists(target)) { ws.send(JSON.stringify({ type: "error", msg: "Usuario no existe" })); return; }
|
|
151
|
+
if (target === myName) { ws.send(JSON.stringify({ type: "error", msg: "No contigo mismo" })); return; }
|
|
152
|
+
|
|
153
|
+
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
154
|
+
const reto = pendingRPS.get(myName);
|
|
155
|
+
pendingRPS.delete(myName);
|
|
156
|
+
let win = "😐 Empate";
|
|
157
|
+
if (choice !== reto.miJugada) {
|
|
158
|
+
if ((choice === "piedra" && reto.miJugada === "tijera") || (choice === "papel" && reto.miJugada === "piedra") || (choice === "tijera" && reto.miJugada === "papel")) win = `🎉 Ganador: \x1b[32m${myName}\x1b[0m`;
|
|
159
|
+
else win = `🎉 Ganador: \x1b[32m${target}\x1b[0m`;
|
|
160
|
+
}
|
|
161
|
+
const resMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${reto.miJugada}) VS \x1b[36m${myName}\x1b[0m (${choice})\n🏆 ${win}\n`;
|
|
162
|
+
const resPayload = { type: "system", msg: resMsg };
|
|
163
|
+
ws.send(JSON.stringify(resPayload));
|
|
164
|
+
broadcast(resPayload, ws);
|
|
165
|
+
} else {
|
|
166
|
+
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
167
|
+
ws.send(JSON.stringify({ type: "system", msg: `🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m` }));
|
|
168
|
+
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>` });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (req.cmd === "mode_whisper") {
|
|
172
|
+
user.privateTarget = req.target;
|
|
173
|
+
ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${req.target}.\x1b[0m Usa /all para salir.` }));
|
|
174
|
+
}
|
|
175
|
+
else if (req.cmd === "mode_all") {
|
|
176
|
+
user.privateTarget = null;
|
|
177
|
+
ws.send(JSON.stringify({ type: "system", msg: `🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m` }));
|
|
178
|
+
}
|
|
179
|
+
else if (req.cmd === "blink") {
|
|
180
|
+
const bMsg = { type: "system", msg: `🌟 [${user.emoji} ${myName}]: \x1b[5m${req.msg}\x1b[0m` };
|
|
181
|
+
ws.send(JSON.stringify(bMsg));
|
|
182
|
+
broadcast(bMsg, ws);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
297
185
|
}
|
|
298
186
|
});
|
|
299
187
|
|
|
300
|
-
|
|
301
|
-
|
|
188
|
+
ws.on("close", () => {
|
|
189
|
+
const u = sessions.get(ws);
|
|
190
|
+
if (u && u.name) {
|
|
191
|
+
broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
|
|
192
|
+
if (typingMap.has(u.name)) {
|
|
193
|
+
clearTimeout(typingMap.get(u.name));
|
|
194
|
+
typingMap.delete(u.name);
|
|
195
|
+
broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
sessions.delete(ws);
|
|
199
|
+
pendingRPS.delete(u ? u.name : "");
|
|
200
|
+
for(const [t, data] of pendingRPS.entries()) {
|
|
201
|
+
if(data.from === u?.name) pendingRPS.delete(t);
|
|
202
|
+
}
|
|
302
203
|
});
|
|
303
204
|
});
|
|
304
205
|
|
|
305
|
-
console.log(`📡 Servidor
|
|
206
|
+
console.log(`📡 Servidor JSON-Core Central corriendo en puerto ${PORT}`);
|