@pdc-test/chat-io 1.1.0 → 1.1.1

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 CHANGED
@@ -33,16 +33,46 @@ function clearTopLine() {
33
33
  }
34
34
 
35
35
  // Interfaz del Teclado Local
36
- const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all', '/blink'];
36
+ const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all', '/blink', '/exit', '/clear'];
37
+ let knownUsersList = [];
38
+
37
39
  function completer(line) {
38
- const hits = commands.filter((c) => c.startsWith(line));
40
+ const tokens = line.split(" ");
41
+ let completions = [];
42
+
43
+ if (line.startsWith("@")) {
44
+ completions = knownUsersList.map(u => "@" + u);
45
+ } else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
46
+ completions = knownUsersList.map(u => tokens[0] + " " + u + " ");
47
+ } else if (line.toLowerCase().startsWith("/rps ")) {
48
+ if (tokens.length === 2) {
49
+ completions = knownUsersList.map(u => "/rps " + u + " ");
50
+ } else if (tokens.length === 3) {
51
+ const rpsOpts = ["piedra", "papel", "tijera"];
52
+ completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
53
+ }
54
+ } else if (line.includes("@")) {
55
+ const lastAt = line.lastIndexOf("@");
56
+ const prefix = line.substring(0, lastAt);
57
+ completions = knownUsersList.map(u => prefix + "@" + u + " ");
58
+ } else {
59
+ completions = [...commands, ...knownUsersList.map(u => "/w " + u)];
60
+ }
61
+
62
+ const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
39
63
  return [hits.length ? hits : [], line];
40
64
  }
41
65
 
42
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ' });
66
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ', removeHistoryDuplicates: true });
67
+
68
+ rl.on('SIGINT', () => {
69
+ printMessage("\x1b[33mSaliendo...\x1b[0m");
70
+ ws.close();
71
+ process.exit(0);
72
+ });
43
73
 
44
74
  process.stdin.on('keypress', (str, key) => {
45
- if (key && key.name !== 'return' && key.name !== 'enter' && myName) {
75
+ if (key && key.name !== 'return' && key.name !== 'enter' && key.name !== 'c' && myName) {
46
76
  if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
47
77
  currentIsTyping = true;
48
78
  ws.send(JSON.stringify({ type: "typing" }));
@@ -113,6 +143,10 @@ ws.on('message', (data) => {
113
143
  activeTypers = res.users.filter(u => u !== myName);
114
144
  break;
115
145
 
146
+ case "users_update":
147
+ if (res.plain) knownUsersList = res.plain.filter(u => u !== myName);
148
+ break;
149
+
116
150
  case "ding":
117
151
  process.stdout.write("\x07"); // ASCII Bell físico disparado remotamente
118
152
  break;
@@ -203,7 +237,13 @@ rl.on('line', (input) => {
203
237
  }
204
238
 
205
239
  // Local Helper Menú
206
- if (line === "/help") {
240
+ if (line.toLowerCase() === "/clear") { console.clear(); rl.setPrompt('> '); rl.prompt(true); return; }
241
+ if (line.toLowerCase() === "/exit") {
242
+ printMessage("\x1b[33mSaliendo...\x1b[0m");
243
+ ws.close();
244
+ process.exit(0);
245
+ }
246
+ if (line.toLowerCase() === "/help") {
207
247
  printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
208
248
  👥 /users → Ver conectados remotamente
209
249
  🪙 /flip → Lanzar moneda 60FPS
@@ -212,6 +252,8 @@ rl.on('line', (input) => {
212
252
  🔒 /w <usr> [msj] → Modo Susurro directo a una persona
213
253
  🌍 /all → Volver a la Sala Global Abierta
214
254
  🏪 /tiendita → Letrero de Neón de colores acelerado
255
+ 🧹 /clear → Limpia historial de consola
256
+ ❌ /exit → Salir del chat
215
257
  ℹ️ /help → Te ayuda desde la memoria Caché
216
258
  ─────────────────────────────────────────────\n`);
217
259
  return;
@@ -219,18 +261,27 @@ rl.on('line', (input) => {
219
261
 
220
262
  // Constructor Arquitectónico y Limpio de Comandos a Servidor
221
263
  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") {
264
+ const lowerLine = line.toLowerCase();
265
+ if (lowerLine === "/users" || lowerLine === "/flip" || lowerLine === "/tiendita") {
266
+ ws.send(JSON.stringify({ type: "command", cmd: lowerLine.substring(1) }));
267
+ } else if (lowerLine === "/all") {
225
268
  ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
226
- } else if (line.startsWith("/w ")) {
269
+ } else if (lowerLine.startsWith("/w ")) {
227
270
  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 ")) {
271
+ if (p.length === 2) {
272
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
273
+ } else if (p.length > 2) {
274
+ const inlineMsg = p.slice(2).join(" ").trim();
275
+ if (inlineMsg) {
276
+ ws.send(JSON.stringify({ type: "chat", target: p[1], msg: inlineMsg }));
277
+ } else {
278
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
279
+ }
280
+ }
281
+ } else if (lowerLine.startsWith("/rps ")) {
231
282
  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 ")) {
283
+ if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2].toLowerCase() }));
284
+ } else if (lowerLine.startsWith("/blink ")) {
234
285
  ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
235
286
  } else {
236
287
  printMessage(`❌ Comando desconocido. Intenta /help`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdc-test/chat-io",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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.0",
12
+ "@pdc-test/chat-io": "^1.1.1",
13
13
  "crypto-js": "^4.2.0",
14
14
  "ws": "^8.0.0"
15
15
  }
package/src/chat/index.js CHANGED
@@ -24,19 +24,39 @@ function startChat(onExit, mode = 'udp') {
24
24
  let animationInterval;
25
25
 
26
26
  function completer(line) {
27
- const completions = [
28
- ...Array.from(knownUsers).map(u => `/chat ${u}`),
29
- ...Array.from(knownUsers).map(u => `/rps ${u} `),
30
- "/all", "/clear", "/help", "/users", "/exit", "/flip", "/blink ", "/chat ", "/rps "
31
- ];
32
- const hits = completions.filter((c) => c.startsWith(line));
27
+ const tokens = line.split(" ");
28
+ let completions = [];
29
+ const cmds = ["/all", "/clear", "/help", "/users", "/exit", "/flip", "/blink", "/chat", "/rps", "/w"];
30
+ const users = Array.from(knownUsers);
31
+
32
+ if (line.startsWith("@")) {
33
+ completions = users.map(u => "@" + u);
34
+ } else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
35
+ completions = users.map(u => tokens[0] + " " + u + " ");
36
+ } else if (line.toLowerCase().startsWith("/rps ")) {
37
+ if (tokens.length === 2) {
38
+ completions = users.map(u => "/rps " + u + " ");
39
+ } else if (tokens.length === 3) {
40
+ const rpsOpts = ["piedra", "papel", "tijera"];
41
+ completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
42
+ }
43
+ } else if (line.includes("@")) {
44
+ const lastAt = line.lastIndexOf("@");
45
+ const prefix = line.substring(0, lastAt);
46
+ completions = users.map(u => prefix + "@" + u + " ");
47
+ } else {
48
+ completions = [...cmds, ...users.map(u => "/w " + u)];
49
+ }
50
+
51
+ const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
33
52
  return [hits.length ? hits : completions, line];
34
53
  }
35
54
 
36
55
  const rl = readline.createInterface({
37
56
  input: process.stdin,
38
57
  output: process.stdout,
39
- completer: completer
58
+ completer: completer,
59
+ removeHistoryDuplicates: true
40
60
  });
41
61
 
42
62
  animationInterval = setInterval(() => {
@@ -114,7 +134,10 @@ function startChat(onExit, mode = 'udp') {
114
134
  return;
115
135
  }
116
136
 
117
- if (data.type === "rps_challenge" && data.to === username) {
137
+ // Resolving case insensitivity for target
138
+ const isTargetMe = data.to && data.to.toLowerCase() === username.toLowerCase();
139
+
140
+ if (data.type === "rps_challenge" && isTargetMe) {
118
141
  incomingRPS.add(data.from);
119
142
  readline.clearLine(process.stdout, 0);
120
143
  readline.cursorTo(process.stdout, 0);
@@ -124,11 +147,13 @@ function startChat(onExit, mode = 'udp') {
124
147
  return;
125
148
  }
126
149
 
127
- if (data.type === "rps_response" && data.to === username) {
128
- if (pendingRPS.has(data.from)) {
129
- const miJugada = pendingRPS.get(data.from).miJugada;
150
+ if (data.type === "rps_response" && isTargetMe) {
151
+ // Allow case insensitive checking for pendingRPS
152
+ const pendingKey = Array.from(pendingRPS.keys()).find(k => k.toLowerCase() === data.from.toLowerCase());
153
+ if (pendingKey) {
154
+ const miJugada = pendingRPS.get(pendingKey).miJugada;
130
155
  const suJugada = data.choice;
131
- pendingRPS.delete(data.from);
156
+ pendingRPS.delete(pendingKey);
132
157
 
133
158
  let resultStr = "😐 Es un empate.";
134
159
  let win = false;
@@ -172,7 +197,7 @@ function startChat(onExit, mode = 'udp') {
172
197
  }
173
198
 
174
199
  if (!data.text) return;
175
- if (data.type === "message" && data.to && data.to !== username) return;
200
+ if (data.type === "message" && data.to && data.to.toLowerCase() !== username.toLowerCase() && data.from.toLowerCase() !== username.toLowerCase()) return;
176
201
 
177
202
  readline.clearLine(process.stdout, 0);
178
203
  readline.cursorTo(process.stdout, 0);
@@ -257,12 +282,19 @@ function startChat(onExit, mode = 'udp') {
257
282
  }, 200);
258
283
  }
259
284
 
285
+ rl.on('SIGINT', () => {
286
+ cleanupAndExit();
287
+ });
288
+
260
289
  rl.on("line", (line) => {
261
290
  if (!username) return;
262
291
  const input = line.trim();
292
+ if (!input) { updatePrompt(); return; }
293
+
294
+ const inputLower = input.toLowerCase();
263
295
 
264
- if (input === "/exit") { cleanupAndExit(); return; }
265
- if (input === "/users") {
296
+ if (inputLower === "/exit") { cleanupAndExit(); return; }
297
+ if (inputLower === "/users") {
266
298
  console.log("──────────── 👥 USUARIOS CONECTADOS ────────────");
267
299
  const usersArr = Array.from(knownUsers);
268
300
  if (usersArr.length === 0) console.log(" 👻 No hay otros usuarios todavía.");
@@ -271,22 +303,39 @@ function startChat(onExit, mode = 'udp') {
271
303
  updatePrompt();
272
304
  return;
273
305
  }
274
- if (input === "/clear") { console.clear(); updatePrompt(); return; }
275
- if (input === "/help") {
306
+ if (inputLower === "/clear") { console.clear(); updatePrompt(); return; }
307
+ if (inputLower === "/help") {
276
308
  console.log("──────────── 💡 COMANDOS DISPONIBLES ────────────");
277
309
  console.log(" 💬 /chat <usuario> → Iniciar chat privado");
310
+ console.log(" 💬 /w <usuario> → Susurro / chat privado");
278
311
  console.log(" 🌍 /all → Volver al chat público");
279
312
  console.log(" 🪙 /flip → Lanzar una moneda");
280
313
  console.log(" 🎮 /rps <usr> <j> → Retar a Piedra/Papel/Tijera");
281
314
  console.log(" 🌟 /blink <msj> → Enviar msj parpadeante");
315
+ console.log(" 🧹 /clear → Limpiar historial de consola");
282
316
  console.log(" ❌ /exit → Salir al menú");
283
317
  updatePrompt();
284
318
  return;
285
319
  }
286
- if (input === "/all") { currentChat = ""; console.log("🌍 Chat público"); updatePrompt(); return; }
287
- if (input.startsWith("/chat ")) { currentChat = input.replace("/chat ", "").trim(); console.log(`🔒 Chat privado con ${currentChat}`); updatePrompt(); return; }
320
+ if (inputLower === "/all") { currentChat = ""; console.log("🌍 Chat público"); updatePrompt(); return; }
321
+ if (inputLower.startsWith("/chat ") || inputLower.startsWith("/w ")) {
322
+ const parts = input.split(" ");
323
+ currentChat = parts[1] || "";
324
+ if (parts.length > 2) {
325
+ const inlineMsg = parts.slice(2).join(" ").trim();
326
+ if (inlineMsg) {
327
+ broadcastData(encrypt(JSON.stringify({ type: "message", from: username, to: currentChat, text: inlineMsg, blink: false })));
328
+ readline.moveCursor(process.stdout, 0, -1);
329
+ readline.clearLine(process.stdout, 0);
330
+ console.log(`🔒 [Tú ${getUserEmoji(username)} → ${getUserEmoji(currentChat)} ${currentChat}]: ${inlineMsg}`);
331
+ updatePrompt();
332
+ return;
333
+ }
334
+ }
335
+ console.log(`🔒 Chat privado con ${currentChat}`); updatePrompt(); return;
336
+ }
288
337
 
289
- if (input === "/flip") {
338
+ if (inputLower === "/flip") {
290
339
  const choices = ["CARA", "ESCUDO"];
291
340
  const result = choices[Math.floor(Math.random() * choices.length)];
292
341
  console.log(`🪙 Lanzaste la moneda y salió: \x1b[1m\x1b[33m${result}\x1b[0m`);
@@ -295,19 +344,20 @@ function startChat(onExit, mode = 'udp') {
295
344
  return;
296
345
  }
297
346
 
298
- if (input.startsWith("/rps ")) {
347
+ if (inputLower.startsWith("/rps ")) {
299
348
  const parts = input.split(" ");
300
349
  if (parts.length < 3) { console.log("❌ Usa: /rps <usuario> <piedra|papel|tijera>"); updatePrompt(); return; }
301
350
 
302
351
  const targetUser = parts[1];
303
352
  const jugada = parts[2].toLowerCase();
304
353
  if (!["piedra", "papel", "tijera"].includes(jugada)) { console.log("❌ piedra, papel o tijera."); updatePrompt(); return; }
305
- if (targetUser === username) { console.log("❌ No puedes jugar contigo mismo."); updatePrompt(); return; }
354
+ if (targetUser.toLowerCase() === username.toLowerCase()) { console.log("❌ No puedes jugar contigo mismo."); updatePrompt(); return; }
306
355
 
307
- if (incomingRPS.has(targetUser)) {
308
- incomingRPS.delete(targetUser);
356
+ const incomingKey = Array.from(incomingRPS).find(k => k.toLowerCase() === targetUser.toLowerCase());
357
+ if (incomingKey) {
358
+ incomingRPS.delete(incomingKey);
309
359
  console.log(`🎮 Respuesta enviada (${jugada})...`);
310
- broadcastData(encrypt(JSON.stringify({ type: "rps_response", from: username, to: targetUser, choice: jugada })));
360
+ broadcastData(encrypt(JSON.stringify({ type: "rps_response", from: username, to: incomingKey, choice: jugada })));
311
361
  } else {
312
362
  pendingRPS.set(targetUser, { miJugada: jugada, time: Date.now() });
313
363
  console.log(`🎮 Retaste a ${targetUser} con ${jugada}...`);
@@ -317,7 +367,7 @@ function startChat(onExit, mode = 'udp') {
317
367
  return;
318
368
  }
319
369
 
320
- if (input.startsWith("/") && !input.startsWith("/blink ")) {
370
+ if (inputLower.startsWith("/") && !inputLower.startsWith("/blink ")) {
321
371
  console.log("❌ Comando no reconocido.");
322
372
  updatePrompt();
323
373
  return;
@@ -325,9 +375,9 @@ function startChat(onExit, mode = 'udp') {
325
375
 
326
376
  let textToSend = input;
327
377
  let isBlink = false;
328
- if (input.startsWith("/blink ")) {
378
+ if (inputLower.startsWith("/blink ")) {
329
379
  isBlink = true;
330
- textToSend = input.replace("/blink ", "").trim();
380
+ textToSend = input.substring(7).trim();
331
381
  if (!textToSend) { updatePrompt(); return; }
332
382
  } else if (!input) { updatePrompt(); return; }
333
383
 
@@ -19,21 +19,42 @@ function broadcast(jsonObj, ignoreWs = null) {
19
19
  }
20
20
 
21
21
  function sendToTarget(targetName, jsonObj) {
22
+ if (!targetName) return;
22
23
  const payload = JSON.stringify(jsonObj);
23
24
  for (const [s, data] of sessions.entries()) {
24
- if (data.name === targetName && s.readyState === WebSocket.OPEN) {
25
+ if (data.name && data.name.toLowerCase() === targetName.toLowerCase() && s.readyState === WebSocket.OPEN) {
25
26
  try { s.send(payload); } catch(e) {}
26
27
  }
27
28
  }
28
29
  }
29
30
 
30
31
  function targetExists(name) {
31
- for (const d of sessions.values()) if (d.name === name) return true;
32
+ if (!name) return false;
33
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return true;
32
34
  return false;
33
35
  }
34
36
 
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;
41
+ }
42
+
35
43
  // State para Tiping
36
44
  let typingMap = new Map();
45
+
46
+ function broadcastUsers() {
47
+ const list = [];
48
+ const plainList = [];
49
+ for(const d of sessions.values()) {
50
+ if(d.name) {
51
+ list.push(`${d.emoji} ${d.name}`);
52
+ plainList.push(d.name);
53
+ }
54
+ }
55
+ broadcast({ type: "users_update", users: list, plain: plainList });
56
+ }
57
+
37
58
  function broadcastTyping() {
38
59
  const list = Array.from(typingMap.keys());
39
60
  broadcast({ type: "typing_event", users: list });
@@ -64,6 +85,7 @@ wss.on("connection", (ws) => {
64
85
 
65
86
  ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
66
87
  broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
88
+ broadcastUsers();
67
89
  return;
68
90
  }
69
91
 
@@ -108,9 +130,10 @@ wss.on("connection", (ws) => {
108
130
  ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
109
131
  return;
110
132
  }
111
- const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: target };
133
+ const exactTarget = getExactName(target);
134
+ const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
112
135
  ws.send(JSON.stringify(pMsg));
113
- sendToTarget(target, pMsg);
136
+ sendToTarget(exactTarget, pMsg);
114
137
  } else {
115
138
  const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
116
139
  ws.send(JSON.stringify(globalMsg));
@@ -169,8 +192,13 @@ wss.on("connection", (ws) => {
169
192
  }
170
193
  }
171
194
  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.` }));
195
+ const exactTarget = getExactName(req.target);
196
+ if (!targetExists(exactTarget)) {
197
+ ws.send(JSON.stringify({ type: "error", msg: `${req.target} no está conectado.` }));
198
+ return;
199
+ }
200
+ user.privateTarget = exactTarget;
201
+ ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${exactTarget}.\x1b[0m Usa /all para salir.` }));
174
202
  }
175
203
  else if (req.cmd === "mode_all") {
176
204
  user.privateTarget = null;
@@ -196,9 +224,12 @@ wss.on("connection", (ws) => {
196
224
  }
197
225
  }
198
226
  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);
227
+ if (u && u.name) {
228
+ broadcastUsers();
229
+ pendingRPS.delete(u.name);
230
+ for(const [t, data] of pendingRPS.entries()) {
231
+ if(data.from === u.name) pendingRPS.delete(t);
232
+ }
202
233
  }
203
234
  });
204
235
  });
@@ -50,10 +50,10 @@ export default {
50
50
  text = text.trim();
51
51
 
52
52
  if (!socketData.name) {
53
- if (text.startsWith("/name ")) {
54
- const desiredName = text.replace("/name ", "").trim();
53
+ if (text.toLowerCase().startsWith("/name ")) {
54
+ const desiredName = text.substring(6).trim();
55
55
  let alreadyExists = false;
56
- for (const d of sessions.values()) if (d.name === desiredName) alreadyExists = true;
56
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === desiredName.toLowerCase()) alreadyExists = true;
57
57
 
58
58
  if (alreadyExists) {
59
59
  server.send("❌ Ese nombre ya está en uso. Usa /name con otro.");
@@ -70,20 +70,21 @@ export default {
70
70
 
71
71
  const myName = socketData.name;
72
72
  const myEmoji = getUserEmoji(myName);
73
+ const textLower = text.toLowerCase();
73
74
 
74
- if (text === "/help") {
75
+ if (textLower === "/help") {
75
76
  server.send(`\n──────────── 💡 COMANDOS CENTRALIZADOS ────────────\n 👥 /users → Ver conectados\n 🪙 /flip → Lanzar moneda al aire\n 🎮 /rps <usr> <j> → Piedra-Papel-Tijera\n 🌟 /blink <msj> → Mensaje parpadeante\n ℹ️ /help → Esta ayuda\n───────────────────────────────────────────────────\n`);
76
77
  return;
77
78
  }
78
79
 
79
- if (text === "/users") {
80
+ if (textLower === "/users") {
80
81
  let userList = [];
81
82
  for (const d of sessions.values()) if (d.name) userList.push(`${getUserEmoji(d.name)} ${d.name}`);
82
83
  server.send("\n👥 \x1b[36mCONECTADOS AHORA:\x1b[0m " + userList.join(", ") + "\n");
83
84
  return;
84
85
  }
85
86
 
86
- if (text === "/flip") {
87
+ if (textLower === "/flip") {
87
88
  const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
88
89
  const msg = `\n🪙 [${myEmoji} ${myName}] ha lanzado la moneda y cayó en: \x1b[1m\x1b[33m${result}\x1b[0m!\n`;
89
90
  server.send(msg); // <--- ECHO LOCAL agregado
@@ -91,24 +92,32 @@ export default {
91
92
  return;
92
93
  }
93
94
 
94
- if (text.startsWith("/blink ")) {
95
- const blnkText = text.replace("/blink ", "").trim();
95
+ if (textLower.startsWith("/blink ")) {
96
+ const blnkText = text.substring(7).trim();
96
97
  const msg = `\n🌟 [${myEmoji} ${myName}]: \x1b[5m${blnkText}\x1b[0m\n`;
97
98
  server.send(msg); // <--- ECHO LOCAL agregado
98
99
  broadcast(msg, server);
99
100
  return;
100
101
  }
101
102
 
102
- if (text.startsWith("/rps ")) {
103
+ if (textLower.startsWith("/rps ")) {
103
104
  const parts = text.split(" ");
104
105
  if (parts.length < 3) { server.send("❌ Error: /rps <usuario> <piedra|papel|tijera>"); return; }
105
106
  const target = parts[1];
106
107
  const choice = parts[2].toLowerCase();
107
108
 
109
+ let exactTargetName = target;
110
+ for (const d of sessions.values()) {
111
+ if (d.name && d.name.toLowerCase() === target.toLowerCase()) {
112
+ exactTargetName = d.name;
113
+ break;
114
+ }
115
+ }
116
+
108
117
  if (!["piedra", "papel", "tijera"].includes(choice)) { server.send("❌ Elige piedra, papel o tijera."); return; }
109
- if (target === myName) { server.send("❌ No contra ti mismo."); return; }
118
+ if (exactTargetName.toLowerCase() === myName.toLowerCase()) { server.send("❌ No contra ti mismo."); return; }
110
119
 
111
- if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
120
+ if (pendingRPS.has(myName) && pendingRPS.get(myName).from.toLowerCase() === exactTargetName.toLowerCase()) {
112
121
  const retoAnterior = pendingRPS.get(myName);
113
122
  pendingRPS.delete(myName);
114
123
 
@@ -118,20 +127,20 @@ export default {
118
127
  let winStr;
119
128
  if (miJugada === suJugada) { winStr = "😐 Empate"; }
120
129
  else if ( (miJugada === "piedra" && suJugada === "tijera") || (miJugada === "papel" && suJugada === "piedra") || (miJugada === "tijera" && suJugada === "papel") ) { winStr = `🎉 \x1b[32m${myName}!\x1b[0m`; }
121
- else { winStr = `🎉 \x1b[32m${target}!\x1b[0m`; }
130
+ else { winStr = `🎉 \x1b[32m${exactTargetName}!\x1b[0m`; }
122
131
 
123
- const outMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
132
+ const outMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${exactTargetName}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
124
133
  server.send(outMsg); // ECHO LOCAL
125
134
  broadcast(outMsg, server);
126
135
  } else {
127
- pendingRPS.set(target, { from: myName, miJugada: choice });
128
- server.send(`\n🎮 \x1b[33mTu reto (${choice}) ha sido enviado a ${target}.\x1b[0m`);
129
- sendToTarget(target, `\n\x1b[5m\x1b[35m🚨 ¡${myName} te reta a RPS!\x1b[0m\n👉 Responde: /rps ${myName} <piedra|papel|tijera>\n`);
136
+ pendingRPS.set(exactTargetName, { from: myName, miJugada: choice });
137
+ server.send(`\n🎮 \x1b[33mTu reto (${choice}) ha sido enviado a ${exactTargetName}.\x1b[0m`);
138
+ sendToTarget(exactTargetName, `\n\x1b[5m\x1b[35m🚨 ¡${myName} te reta a RPS!\x1b[0m\n👉 Responde: /rps ${myName} <piedra|papel|tijera>\n`);
130
139
  }
131
140
  return;
132
141
  }
133
142
 
134
- if (text.startsWith("/")) {
143
+ if (textLower.startsWith("/")) {
135
144
  server.send("❌ Comando no reconocido. Usa /help");
136
145
  return;
137
146
  }