salomonsky commited on
Commit
9b73acc
·
verified ·
1 Parent(s): b665289

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +717 -1112
index.html CHANGED
@@ -1,78 +1,88 @@
1
  <!DOCTYPE html>
2
  <html lang="es">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>NEXUS: Ludificación Semántica</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
9
  <style>
10
- body {
11
- scrollbar-width: none;
12
- -ms-overflow-style: none;
13
- background-color: #020205;
14
- user-select: none;
 
 
 
15
  }
16
- body::-webkit-scrollbar { display: none; }
17
-
18
  .glass-panel {
19
- background: rgba(10, 15, 30, 0.75);
20
- backdrop-filter: blur(16px);
21
- -webkit-backdrop-filter: blur(16px);
22
- border: 1px solid rgba(255, 255, 255, 0.08);
23
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
24
  }
25
-
26
  @keyframes shimmer {
27
- 0% { transform: translateX(-100%); }
28
- 100% { transform: translateX(100%); }
29
  }
30
  .animate-shimmer {
31
- animation: shimmer 2s infinite;
32
  }
33
-
34
  .custom-scrollbar::-webkit-scrollbar { width: 4px; }
35
  .custom-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); }
36
  .custom-scrollbar::-webkit-scrollbar-thumb { background: #22d3ee; border-radius: 4px; }
37
-
38
  @keyframes glitch-anim {
39
- 0% { transform: translate(0); }
40
- 20% { transform: translate(-2px, 2px); }
41
- 40% { transform: translate(-2px, -2px); }
42
- 60% { transform: translate(2px, 2px); }
43
- 80% { transform: translate(2px, -2px); }
44
- 100% { transform: translate(0); }
45
  }
46
  .glitch-active {
47
- animation: glitch-anim 0.2s cubic-bezier(.25, .46, .45, .94) both infinite;
48
- filter: hue-rotate(90deg) contrast(1.5);
49
  }
50
  .golden-node {
51
- box-shadow: 0 0 15px #ffd700;
52
- border: 1px solid #ffd700;
 
 
 
 
 
 
 
53
  }
54
  </style>
55
  </head>
56
-
57
  <body class="m-0 overflow-hidden font-[Orbitron] text-slate-200">
58
 
59
  <div id="glitchOverlay" class="fixed inset-0 pointer-events-none z-[99999] hidden mix-blend-overlay bg-red-900/20"></div>
60
 
61
  <div id="loginOverlay" class="fixed inset-0 bg-black/95 z-[9999] flex items-center justify-center transition-opacity duration-700 opacity-100">
62
  <div id="loginModal" class="glass-panel p-8 rounded-2xl w-full max-w-md text-white relative overflow-hidden border-t border-cyan-500/30">
63
- <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-cyan-500 to-transparent opacity-70"></div>
 
64
  <div id="authStep1">
65
  <h2 class="text-4xl font-bold text-center mb-2 text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 drop-shadow-[0_0_15px_rgba(34,211,238,0.5)]">NEXUS</h2>
66
  <p class="text-cyan-500/60 text-center mb-8 text-[10px] tracking-[0.3em] uppercase">Visualizador Semántico Neural v2.5</p>
 
67
  <button id="btnGoToAnon" class="w-full bg-white/5 hover:bg-white/10 text-cyan-300 border border-cyan-500/20 font-bold py-4 px-4 rounded-lg transition-all flex items-center justify-center gap-3 mb-6 group hover:shadow-[0_0_15px_rgba(34,211,238,0.2)]">
68
- <svg class="w-5 h-5 group-hover:scale-110 transition-transform text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
 
 
69
  Entrar como Anónimo
70
  </button>
 
71
  <div class="relative flex py-2 items-center mb-4">
72
  <div class="flex-grow border-t border-white/10"></div>
73
  <span class="flex-shrink-0 mx-4 text-gray-500 text-[10px] uppercase tracking-wider">Credenciales de Acceso</span>
74
  <div class="flex-grow border-t border-white/10"></div>
75
  </div>
 
76
  <form id="emailAuthForm" class="space-y-4">
77
  <div class="relative group">
78
  <input type="email" id="loginEmail" class="w-full p-3 pl-4 rounded-lg bg-black/50 border border-white/10 text-white placeholder-gray-600 focus:border-cyan-500 focus:outline-none text-sm transition-all focus:bg-black/70" placeholder="ID de Usuario (Email)">
@@ -80,15 +90,17 @@
80
  </div>
81
  <div class="relative group">
82
  <input type="password" id="loginPassword" class="w-full p-3 pl-4 rounded-lg bg-black/50 border border-white/10 text-white placeholder-gray-600 focus:border-cyan-500 focus:outline-none text-sm transition-all focus:bg-black/70" placeholder="Clave de Acceso">
83
- <div class="absolute inset-0 border border-cyan-500/0 rounded-lg group-hover:border-cyan-500/20 pointer-events-none transition-colors"></div>
84
  </div>
85
  <div class="flex gap-3 pt-2">
86
- <button type="button" id="loginButton" class="flex-1 bg-blue-600/10 hover:bg-blue-600/30 text-blue-400 py-2 rounded border border-blue-500/30 text-xs font-bold transition-all uppercase tracking-wider hover:shadow-[0_0_10px_rgba(59,130,246,0.3)]">Entrar</button>
87
- <button type="button" id="registerButton" class="flex-1 bg-green-600/10 hover:bg-green-600/30 text-green-400 py-2 rounded border border-green-500/30 text-xs font-bold transition-all uppercase tracking-wider hover:shadow-[0_0_10px_rgba(74,222,128,0.3)]">Registrar</button>
88
  </div>
89
  </form>
 
90
  <p id="loginMessage" class="text-center text-xs mt-4 min-h-[1.5rem] text-red-400 font-bold tracking-wide"></p>
91
  </div>
 
92
  <div id="authStep2" class="hidden">
93
  <h2 class="text-2xl font-bold text-white mb-2 text-center">Identidad Digital</h2>
94
  <p class="text-gray-400 text-xs mb-8 text-center">Asigna un nombre clave a tu constelación.</p>
@@ -102,35 +114,35 @@
102
  </div>
103
 
104
  <div id="container" class="w-screen h-screen fixed top-0 left-0 bg-[#020205]"></div>
105
-
106
  <div id="gameHUD" class="fixed top-0 w-full z-[90] pointer-events-none flex justify-between p-4 hidden opacity-0 transition-opacity duration-1000">
107
  <div class="glass-panel px-4 py-2 rounded-lg flex items-center gap-4 border-t border-yellow-500/30 pointer-events-auto">
108
- <div>
109
- <div class="text-[9px] text-yellow-500 uppercase tracking-widest">Energía Neural</div>
110
- <div class="w-32 h-2 bg-gray-800 rounded-full mt-1 overflow-hidden border border-white/10">
111
- <div id="energyBar" class="h-full bg-yellow-400 w-full transition-all duration-500 shadow-[0_0_10px_#fbbf24]"></div>
112
- </div>
113
- </div>
114
- <div class="text-right border-l border-white/10 pl-4">
115
- <div class="text-[9px] text-cyan-500 uppercase tracking-widest">Rango</div>
116
- <div id="rankDisplay" class="text-sm font-bold text-white tracking-widest">OBSERVADOR</div>
117
- </div>
118
- <div class="text-right pl-4 border-l border-white/10">
119
- <div class="text-[9px] text-purple-500 uppercase tracking-widest">Bóveda</div>
120
- <div id="vaultCount" class="text-sm font-bold text-white font-mono">0</div>
121
  </div>
 
 
 
 
 
 
 
 
 
122
  </div>
123
-
124
  <div id="missionDisplay" class="glass-panel px-6 py-2 rounded-lg text-center pointer-events-auto">
125
- <div class="text-[9px] text-green-400 uppercase tracking-widest mb-1">Modo Activo</div>
126
- <div id="activeModeText" class="text-xs font-bold text-white tracking-[0.2em]">EXPLORACIÓN LIBRE</div>
127
  </div>
128
  </div>
129
 
130
  <div id="tooltip" class="absolute hidden bg-black/80 text-white px-4 py-3 rounded-none border-l-2 border-cyan-500 z-[101] pointer-events-none text-sm backdrop-blur-md shadow-[0_0_15px_rgba(6,182,212,0.2)] max-w-xs"></div>
131
 
132
- <div id="ui" class="fixed top-16 left-5 z-[100] glass-panel p-6 rounded-xl max-w-[380px] text-white h-[calc(100vh-80px)] flex flex-col transition-all hidden transform duration-700 translate-x-[-20px] opacity-0 overflow-y-auto custom-scrollbar">
133
-
134
  <div class="flex items-center justify-between mb-4 border-b border-white/10 pb-4">
135
  <h1 class="text-lg font-bold text-white flex items-center space-x-3">
136
  <div class="w-2 h-2 bg-cyan-400 rounded-full animate-pulse shadow-[0_0_10px_#22d3ee]"></div>
@@ -143,22 +155,23 @@
143
  </div>
144
 
145
  <div class="grid grid-cols-3 gap-2 mb-4">
146
- <button onclick="setGameMode('explorer')" id="btnModeExplorer" class="bg-cyan-900/30 border border-cyan-500/50 text-cyan-300 text-[9px] py-2 rounded uppercase tracking-wider hover:bg-cyan-800/50 transition-all font-bold">Explorar</button>
147
- <button onclick="setGameMode('miner')" id="btnModeMiner" class="bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:border-yellow-500/50 hover:text-yellow-400 transition-all font-bold">Minero</button>
148
- <button onclick="setGameMode('bridge')" id="btnModeBridge" class="bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:border-purple-500/50 hover:text-purple-400 transition-all font-bold">Puente</button>
149
  </div>
 
150
  <button onclick="setGameMode('comet')" id="btnModeComet" class="w-full mb-4 bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:border-red-500/50 hover:text-red-400 transition-all font-bold">Defensa Cometa</button>
151
 
152
  <div id="bridgeControls" class="hidden space-y-3 mb-4 p-3 bg-purple-900/10 rounded border border-purple-500/30">
153
- <div class="text-[10px] text-purple-300 uppercase tracking-widest mb-1">Objetivo del Puente</div>
154
- <input type="text" id="bridgeTargetInput" class="w-full p-2 rounded bg-black/50 border border-purple-500/30 text-white text-xs mb-2" placeholder="Destino (Punto B)">
155
- <div class="text-[9px] text-gray-400">Origen: <span id="bridgeOriginDisplay" class="text-white font-bold">Sin definir</span></div>
156
- <div class="text-[9px] text-gray-400">Saltos: <span id="bridgeHops" class="text-white font-bold">0</span></div>
157
  </div>
158
 
159
  <div class="relative mb-5 group">
160
- <div class="absolute -inset-0.5 bg-gradient-to-r from-cyan-500 to-purple-600 rounded-lg blur opacity-20 group-hover:opacity-60 transition duration-500"></div>
161
- <input type="text" id="topicInput" class="relative w-full p-4 rounded-lg bg-black/80 text-white border border-white/10 focus:outline-none focus:border-cyan-500/50 text-sm placeholder-gray-500 font-mono" placeholder="Ingresar semilla semántica...">
162
  </div>
163
 
164
  <div class="space-y-5 mb-6 px-1">
@@ -197,7 +210,7 @@
197
  <button id="visualizeButton" class="relative w-full overflow-hidden bg-cyan-900/20 hover:bg-cyan-800/40 text-cyan-300 font-bold py-4 px-4 rounded-lg border border-cyan-500/30 transition-all flex items-center justify-center space-x-3 group hover:shadow-[0_0_15px_rgba(34,211,238,0.2)]">
198
  <div class="absolute inset-0 w-full h-full bg-gradient-to-r from-transparent via-cyan-500/10 to-transparent -translate-x-full group-hover:animate-shimmer"></div>
199
  <svg class="w-4 h-4 text-cyan-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
200
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
201
  </svg>
202
  <span id="actionBtnText" class="uppercase tracking-[0.2em] text-xs">Ejecutar Análisis</span>
203
  </button>
@@ -207,28 +220,32 @@
207
  <div id="progressBar" class="bg-gradient-to-r from-cyan-500 via-blue-500 to-purple-600 h-1 w-0 shadow-[0_0_10px_#22d3ee]"></div>
208
  </div>
209
 
210
- <div id="userListContainer" class="flex-1 flex flex-col min-h-0 bg-black/40 rounded border border-white/5 mb-4 relative overflow-hidden group">
211
  <div class="absolute top-0 left-0 w-full h-4 bg-gradient-to-b from-black/80 to-transparent pointer-events-none z-10"></div>
212
  <h2 class="font-bold text-[9px] text-gray-500 mb-2 uppercase tracking-[0.2em] p-3 border-b border-white/5 sticky top-0 bg-black/40 backdrop-blur-sm">Exploradores Activos</h2>
213
  <div id="userList" class="overflow-y-auto pr-1 space-y-1 custom-scrollbar text-xs p-2"></div>
214
  <div class="absolute bottom-0 left-0 w-full h-4 bg-gradient-to-t from-black/80 to-transparent pointer-events-none z-10"></div>
215
  </div>
 
216
 
217
- <div id="minimapContainer" class="shrink-0 relative group border-t border-white/5 pt-4">
218
- <div class="flex justify-between items-center mb-1 absolute top-6 right-2 z-10 gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
219
- <button id="zoomOutButton" class="w-6 h-6 bg-black/80 hover:bg-cyan-900/50 rounded border border-white/20 text-white flex items-center justify-center text-xs backdrop-blur-sm transition-colors">-</button>
220
- <button id="zoomInButton" class="w-6 h-6 bg-black/80 hover:bg-cyan-900/50 rounded border border-white/20 text-white flex items-center justify-center text-xs backdrop-blur-sm transition-colors">+</button>
221
- </div>
222
- <canvas id="minimap" width="340" height="130" class="w-full h-[130px] bg-black/60 rounded border border-white/10 cursor-crosshair shadow-inner"></canvas>
223
- <div class="text-[9px] text-center text-cyan-500/40 mt-2 uppercase tracking-[0.3em]">Radar Galáctico</div>
224
  </div>
 
 
 
 
 
 
 
225
  </div>
226
 
227
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
228
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
229
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/geometries/TextGeometry.js"></script>
230
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/FontLoader.js"></script>
231
-
232
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
233
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
234
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
@@ -237,10 +254,9 @@
237
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
238
 
239
  <script type="module">
240
- import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
241
- import { getAnalytics } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-analytics.js";
242
- import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signInAnonymously, signOut, setPersistence, browserLocalPersistence } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
243
- import { getFirestore, doc, addDoc, onSnapshot, collection, setDoc, getDoc, query, setLogLevel, updateDoc, arrayUnion } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
244
 
245
  const firebaseConfig = {
246
  apiKey: "AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",
@@ -250,1084 +266,673 @@
250
  messagingSenderId: "208887839866",
251
  appId: "1:208887839866:web:adbb697dd0b63195b10fc3",
252
  measurementId: "G-102SEBLQFJ"
253
- };
254
-
255
- const DEFAULT_GEMINI_KEY = 'AIzaSyDmoQNpzgzW21f_WFCU9YbaAeI1fdOJMlo';
256
- function getLocalGeminiKey() { try { return localStorage.getItem('GEMINI_API_KEY') || DEFAULT_GEMINI_KEY; } catch { return DEFAULT_GEMINI_KEY; } }
257
- function setLocalGeminiKey(k) { try { localStorage.setItem('GEMINI_API_KEY', k || ''); } catch {} }
258
-
259
- const THREE = window.THREE;
260
- const OrbitControls = THREE.OrbitControls;
261
- const TextGeometry = THREE.TextGeometry;
262
- const FontLoader = THREE.FontLoader;
263
-
264
- let scene, camera, renderer, controls, raycaster, mouse;
265
- let composer;
266
- let hashtagGroup, tooltip, trashGroup;
267
- let font;
268
- let clock = new THREE.Clock();
269
-
270
- let cometGroup, cometHead, cometLight, cometText;
271
- let cometParticlesMesh, cometParticlesData = [];
272
- const COMET_PARTICLE_COUNT = 400;
273
- let cometAngle = 0;
274
- let userCentroidForComet = new THREE.Vector3(0,0,0);
275
- let bgParticles;
276
-
277
- let db, auth, analytics, userId = null, appId = "neuronal-1f3b9", userProfile = null;
278
- let userMaps = {}, userProfileCache = {};
279
- let isAuthReady = false, isFontReady = false;
280
- let minimapCtx, minimapDotCoords = [], minimapScale = 0.025;
281
-
282
- let gameState = {
283
- mode: 'explorer',
284
- energy: 100,
285
- rank: 'OBSERVADOR',
286
- vault: [],
287
- bridgeOrigin: null,
288
- bridgeTarget: null,
289
- bridgeHops: 0,
290
- cometActive: false,
291
- score: 0
292
- };
293
-
294
- const isHFStatic = /\.hf\.space$/.test(location.hostname);
295
- let authMode = null;
296
- const normalizeString = (str) => { if (!str) return ""; return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); };
297
-
298
- window.setGameMode = function(mode) {
299
- gameState.mode = mode;
300
- const els = ['btnModeExplorer', 'btnModeMiner', 'btnModeBridge', 'btnModeComet'];
301
- els.forEach(id => {
302
- const btn = document.getElementById(id);
303
- if(id.toLowerCase().includes(mode)) {
304
- btn.classList.remove('bg-black/30', 'text-gray-500', 'bg-cyan-900/30', 'text-cyan-300');
305
- if(mode==='miner') btn.classList.add('bg-yellow-900/30', 'text-yellow-400', 'border-yellow-500');
306
- else if(mode==='bridge') btn.classList.add('bg-purple-900/30', 'text-purple-400', 'border-purple-500');
307
- else if(mode==='comet') btn.classList.add('bg-red-900/30', 'text-red-400', 'border-red-500');
308
- else btn.classList.add('bg-cyan-900/30', 'text-cyan-300', 'border-cyan-500');
309
- } else {
310
- btn.className = "bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:bg-white/5 transition-all font-bold";
311
- if(id === 'btnModeComet') btn.classList.add('w-full', 'mb-4');
312
- }
313
- });
314
-
315
- document.getElementById('activeModeText').innerText = mode === 'bridge' ? 'PUENTE NEURAL' : (mode === 'miner' ? 'MINERO DE DATOS' : (mode === 'comet' ? 'DEFENSA DEL COMETA' : 'EXPLORACIÓN LIBRE'));
316
-
317
- const bridgeCtrl = document.getElementById('bridgeControls');
318
- const actBtn = document.getElementById('actionBtnText');
319
-
320
- if(mode === 'bridge') {
321
- bridgeCtrl.classList.remove('hidden');
322
- actBtn.innerText = "CONSTRUIR NODO";
323
- } else {
324
- bridgeCtrl.classList.add('hidden');
325
- actBtn.innerText = "EJECUTAR ANÁLISIS";
326
- }
327
-
328
- if(mode === 'comet') {
329
- gameState.cometActive = true;
330
- spawnTrashNodes();
331
- } else {
332
- gameState.cometActive = false;
333
- if(trashGroup) { scene.remove(trashGroup); trashGroup = null; }
334
- }
335
- }
336
-
337
- const loader = new FontLoader();
338
- loader.load('https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_regular.typeface.json', (loadedFont) => {
339
- font = loadedFont;
340
- isFontReady = true;
341
- checkAppReady();
342
- });
343
-
344
- initFirebase();
345
-
346
- const gkInput = document.getElementById('geminiKeyInput');
347
- const gkBtn = document.getElementById('saveGeminiKeyBtn');
348
- if (gkInput && gkBtn) {
349
- const existing = getLocalGeminiKey();
350
- if(existing && existing !== DEFAULT_GEMINI_KEY) gkInput.value = existing;
351
- gkBtn.addEventListener('click', (e) => {
352
- e.preventDefault();
353
- setLocalGeminiKey(gkInput.value.trim());
354
- document.getElementById('geminiKeyStatus').textContent = 'GUARDADO';
355
- setTimeout(() => document.getElementById('geminiKeyStatus').textContent = '', 2000);
356
- });
357
  }
358
 
359
- async function initFirebase() {
360
- try {
361
- const app = initializeApp(firebaseConfig);
362
- db = getFirestore(app);
363
- auth = getAuth(app);
364
- analytics = getAnalytics(app);
365
- setLogLevel('Silent');
366
-
367
- const els = {
368
- msg: document.getElementById('loginMessage'),
369
- overlay: document.getElementById('loginOverlay'),
370
- step1: document.getElementById('authStep1'),
371
- step2: document.getElementById('authStep2'),
372
- email: document.getElementById('loginEmail'),
373
- pass: document.getElementById('loginPassword'),
374
- loginBtn: document.getElementById('loginButton'),
375
- regBtn: document.getElementById('registerButton'),
376
- anonBtn: document.getElementById('btnGoToAnon'),
377
- saveUserBtn: document.getElementById('saveUsernameButton'),
378
- userInput: document.getElementById('usernameInput'),
379
- backBtn: document.getElementById('backToStep1'),
380
- logoutBtn: document.getElementById('mainLogoutButton'),
381
- ui: document.getElementById('ui')
382
- };
383
-
384
- els.regBtn.addEventListener('click', async () => {
385
- if(els.email.value.length < 6) { els.msg.innerText = "Email/Pass muy corto"; return; }
386
- els.msg.innerText = "Procesando registro...";
387
- authMode = 'email';
388
- try {
389
- await setPersistence(auth, browserLocalPersistence);
390
- await createUserWithEmailAndPassword(auth, els.email.value, els.pass.value);
391
- } catch(e) { els.msg.innerText = "Error: " + e.message; }
392
- });
393
-
394
- els.loginBtn.addEventListener('click', async () => {
395
- els.msg.innerText = "Autenticando...";
396
- authMode = 'email';
397
- try {
398
- await setPersistence(auth, browserLocalPersistence);
399
- await signInWithEmailAndPassword(auth, els.email.value, els.pass.value);
400
- } catch(e) { els.msg.innerText = "Error: " + e.message; }
401
- });
402
-
403
- els.anonBtn.addEventListener('click', () => {
404
- authMode = 'anonymous';
405
- els.step1.classList.add('hidden');
406
- els.step2.classList.remove('hidden');
407
- });
408
-
409
- els.backBtn.addEventListener('click', () => {
410
- els.step2.classList.add('hidden');
411
- els.step1.classList.remove('hidden');
412
- els.msg.innerText = "";
413
- });
414
-
415
- els.saveUserBtn.addEventListener('click', async () => {
416
- const name = normalizeString(els.userInput.value.trim());
417
- if(name.length < 3) { els.userInput.classList.add('border-red-500'); return; }
418
- els.userInput.classList.remove('border-red-500');
419
- els.saveUserBtn.innerText = "ESTABLECIENDO ENLACE...";
420
- els.saveUserBtn.disabled = true;
421
- try {
422
- if(authMode === 'anonymous') await signInAnonymously(auth);
423
- if(auth.currentUser) {
424
- await saveUserProfile(auth.currentUser.uid, name);
425
- els.overlay.style.opacity = '0';
426
- setTimeout(() => els.overlay.style.display = 'none', 700);
427
- initScene();
428
- loadAllMaps();
429
- els.ui.classList.remove('hidden');
430
- setTimeout(() => { els.ui.style.transform = 'translateX(0)'; els.ui.style.opacity = '1'; }, 100);
431
- document.getElementById('gameHUD').classList.remove('hidden');
432
- setTimeout(() => document.getElementById('gameHUD').classList.remove('opacity-0'), 500);
433
- }
434
- } catch(e) {
435
- els.saveUserBtn.innerText = "ERROR DE CONEXIÓN";
436
- els.saveUserBtn.disabled = false;
437
- }
438
- });
439
-
440
- els.logoutBtn.addEventListener('click', async () => { await signOut(auth); location.reload(); });
441
-
442
- onAuthStateChanged(auth, async (user) => {
443
- if(user) {
444
- userId = user.uid;
445
- isAuthReady = true;
446
- await fetchUserProfile(userId);
447
- if(userProfile) {
448
- els.overlay.style.opacity = '0';
449
- setTimeout(() => els.overlay.style.display = 'none', 700);
450
- els.ui.classList.remove('hidden');
451
- setTimeout(() => { els.ui.style.transform = 'translateX(0)'; els.ui.style.opacity = '1'; }, 100);
452
- document.getElementById('authStatus').textContent = userProfile.username;
453
- document.getElementById('gameHUD').classList.remove('hidden');
454
- setTimeout(() => document.getElementById('gameHUD').classList.remove('opacity-0'), 500);
455
- updateHUD();
456
- if(!scene) { initScene(); loadAllMaps(); }
457
- } else {
458
- els.step1.classList.add('hidden');
459
- els.step2.classList.remove('hidden');
460
- if(authMode === 'email') els.userInput.focus();
461
- }
462
- } else {
463
- els.overlay.style.display = 'flex';
464
- els.overlay.style.opacity = '1';
465
- els.step1.classList.remove('hidden');
466
- els.step2.classList.add('hidden');
467
- els.ui.classList.add('hidden');
468
- }
469
- });
470
-
471
- } catch (e) { console.error("Firebase Error", e); }
472
- }
473
-
474
- async function fetchUserProfile(uid) {
475
- if(!db) return;
476
- try {
477
- const snap = await getDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'));
478
- if(snap.exists()) {
479
- userProfile = snap.data();
480
- userProfileCache[uid] = userProfile;
481
- gameState.energy = userProfile.energy || 100;
482
- gameState.rank = userProfile.rank || 'OBSERVADOR';
483
- gameState.vault = userProfile.vault || [];
484
- }
485
- } catch {}
486
- }
487
- async function saveUserProfile(uid, name) {
488
- if(!db) return;
489
- await setDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'), { username: name, energy: 100, rank: 'OBSERVADOR', vault: [] });
490
- userProfile = { username: name };
491
- userProfileCache[uid] = userProfile;
492
- document.getElementById('authStatus').textContent = name;
493
- }
494
-
495
- async function updateGameProfile() {
496
- if(!db || !userId) return;
497
- try {
498
- await updateDoc(doc(db, 'artifacts', appId, 'users', userId, 'user_data', 'profile'), {
499
- energy: gameState.energy,
500
- rank: gameState.rank
501
- });
502
- } catch {}
503
- }
504
 
505
- async function addToVault(word) {
506
- if(gameState.vault.includes(word)) return;
507
- gameState.vault.push(word);
508
- if(!db || !userId) return;
509
- await updateDoc(doc(db, 'artifacts', appId, 'users', userId, 'user_data', 'profile'), {
510
- vault: arrayUnion(word)
511
- });
512
- updateHUD();
513
  }
 
514
 
515
- function checkAppReady() { if(isAuthReady && isFontReady && userProfile) { initScene(); loadAllMaps(); } }
516
-
517
- function initScene() {
518
- if(scene) return;
519
- scene = new THREE.Scene();
520
- scene.fog = new THREE.FogExp2(0x020205, 0.005);
521
-
522
- camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 2500);
523
- camera.position.set(0, 5, 30);
524
-
525
- renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
526
- renderer.setSize(window.innerWidth, window.innerHeight);
527
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
528
- renderer.toneMapping = THREE.ReinhardToneMapping;
529
- renderer.toneMappingExposure = 1.2;
530
- document.getElementById('container').appendChild(renderer.domElement);
531
-
532
- tooltip = document.getElementById('tooltip');
533
-
534
- controls = new OrbitControls(camera, renderer.domElement);
535
- controls.enableDamping = true;
536
- controls.dampingFactor = 0.04;
537
- controls.rotateSpeed = 0.5;
538
- controls.zoomSpeed = 0.7;
539
- controls.maxDistance = 600;
540
- controls.target.set(0,0,0);
541
-
542
- scene.add(new THREE.AmbientLight(0x404040, 1.0));
543
- const dirLight = new THREE.DirectionalLight(0xaaccff, 1.2);
544
- dirLight.position.set(50, 80, 50);
545
- scene.add(dirLight);
546
-
547
- const renderScene = new THREE.RenderPass(scene, camera);
548
- const bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
549
- bloomPass.threshold = 0.15;
550
- bloomPass.strength = 1.4;
551
- bloomPass.radius = 0.6;
552
-
553
- composer = new THREE.EffectComposer(renderer);
554
- composer.addPass(renderScene);
555
- composer.addPass(bloomPass);
556
-
557
- raycaster = new THREE.Raycaster();
558
- mouse = new THREE.Vector2();
559
-
560
- hashtagGroup = new THREE.Group();
561
- scene.add(hashtagGroup);
562
-
563
- createAdvancedBackground();
564
- initAdvancedComet();
565
-
566
- const visBtn = document.getElementById('visualizeButton');
567
- if(!visBtn.dataset.bound) {
568
- visBtn.addEventListener('click', handleAnalysisAndVisualization);
569
- visBtn.dataset.bound = '1';
570
- }
571
 
572
- ['level1', 'level2', 'level3'].forEach(l => {
573
- document.getElementById(`${l}Slider`).addEventListener('input', e => document.getElementById(`${l}Value`).innerText = e.target.value);
574
- });
 
 
 
 
 
 
 
575
 
576
- window.addEventListener('resize', onWindowResize);
577
- window.addEventListener('mousemove', onPointerMove);
578
- window.addEventListener('click', onMouseClick);
579
 
580
- const mmCanvas = document.getElementById('minimap');
581
- if(mmCanvas) {
582
- minimapCtx = mmCanvas.getContext('2d');
583
- mmCanvas.addEventListener('click', onMinimapClick);
584
- }
585
- document.getElementById('zoomInButton').addEventListener('click', () => { minimapScale *= 1.5; drawMinimap(); });
586
- document.getElementById('zoomOutButton').addEventListener('click', () => { minimapScale /= 1.5; drawMinimap(); });
587
 
588
- animate();
589
- }
590
 
591
- function updateHUD() {
592
- document.getElementById('energyBar').style.width = gameState.energy + '%';
593
- document.getElementById('rankDisplay').innerText = gameState.rank;
594
- document.getElementById('vaultCount').innerText = gameState.vault.length;
595
-
596
- if(gameState.vault.length > 5) gameState.rank = "ARQUITECTO";
597
- if(gameState.vault.length > 15) gameState.rank = "ORÁCULO";
598
- }
599
-
600
- function createAdvancedBackground() {
601
- const pGeo = new THREE.BufferGeometry();
602
- const count = 5000;
603
- const pos = new Float32Array(count * 3);
604
- const sizes = new Float32Array(count);
605
-
606
- for(let i=0; i<count; i++) {
607
- pos[i*3] = (Math.random()-0.5) * 1500;
608
- pos[i*3+1] = (Math.random()-0.5) * 1500;
609
- pos[i*3+2] = (Math.random()-0.5) * 1500;
610
- sizes[i] = Math.random();
611
- }
612
- pGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
613
- pGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
614
-
615
- const pMat = new THREE.PointsMaterial({
616
- color: 0x88ccff,
617
- size: 1.0,
618
- transparent: true,
619
- opacity: 0.6,
620
- sizeAttenuation: true
621
- });
622
-
623
- bgParticles = new THREE.Points(pGeo, pMat);
624
- scene.add(bgParticles);
625
- }
626
 
627
- function initAdvancedComet() {
628
- if(cometGroup) { scene.remove(cometGroup); }
629
- if(cometParticlesMesh) { scene.remove(cometParticlesMesh); }
630
-
631
- if(!font || !userId) return;
632
-
633
- userCentroidForComet.copy(getCurrentUserCentroid());
634
- cometGroup = new THREE.Group();
635
-
636
- const coreGeo = new THREE.SphereGeometry(0.5, 32, 32);
637
- const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
638
- cometHead = new THREE.Mesh(coreGeo, coreMat);
639
-
640
- const haloGeo = new THREE.SphereGeometry(0.9, 32, 32);
641
- const haloMat = new THREE.MeshBasicMaterial({
642
- color: 0x00ffff,
643
- transparent: true,
644
- opacity: 0.25,
645
- blending: THREE.AdditiveBlending
646
- });
647
- const halo = new THREE.Mesh(haloGeo, haloMat);
648
- cometHead.add(halo);
649
- cometGroup.add(cometHead);
650
-
651
- cometLight = new THREE.PointLight(0x00ffff, 2.5, 60);
652
- cometGroup.add(cometLight);
653
-
654
- scene.add(cometGroup);
655
-
656
- const pGeo = new THREE.BufferGeometry();
657
- const positions = new Float32Array(COMET_PARTICLE_COUNT * 3);
658
- const colors = new Float32Array(COMET_PARTICLE_COUNT * 3);
659
- const sizes = new Float32Array(COMET_PARTICLE_COUNT);
660
-
661
- pGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
662
- pGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
663
- pGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
664
-
665
- const pMat = new THREE.PointsMaterial({
666
- vertexColors: true,
667
- size: 1.0,
668
- transparent: true,
669
- opacity: 0.9,
670
- blending: THREE.AdditiveBlending,
671
- depthWrite: false,
672
- sizeAttenuation: true
673
- });
674
-
675
- cometParticlesMesh = new THREE.Points(pGeo, pMat);
676
- scene.add(cometParticlesMesh);
677
-
678
- cometParticlesData = [];
679
- for(let i=0; i<COMET_PARTICLE_COUNT; i++) {
680
- cometParticlesData.push({ life: -1, velocity: new THREE.Vector3() });
681
- positions[i*3] = 99999;
682
  }
683
- }
684
-
685
- function spawnTrashNodes() {
686
- if(trashGroup) scene.remove(trashGroup);
687
- trashGroup = new THREE.Group();
688
- scene.add(trashGroup);
689
-
690
- const trashWords = ["VIRAL", "FAKE", "GLITCH", "ERROR", "NOISE", "SPAM", "BOT"];
691
-
692
- for(let i=0; i<20; i++) {
693
- const geo = new THREE.DodecahedronGeometry(0.8, 0);
694
- const mat = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true });
695
- const mesh = new THREE.Mesh(geo, mat);
696
-
697
- const rX = 80; const rZ = 60;
698
- const ang = Math.random() * Math.PI * 2;
699
- const x = userCentroidForComet.x + Math.cos(ang) * rX;
700
- const z = userCentroidForComet.z + Math.sin(ang) * rZ;
701
- const y = userCentroidForComet.y + Math.sin(ang * 2.0) * 20;
702
-
703
- mesh.position.set(x,y,z);
704
- mesh.userData = { isTrash: true, word: trashWords[Math.floor(Math.random()*trashWords.length)] };
705
- trashGroup.add(mesh);
 
 
 
 
 
 
706
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
707
  }
708
 
709
- function updateAdvancedComet(delta, time) {
710
- if(!cometGroup || !cometParticlesMesh) return;
711
-
712
- let speed = 0.35;
713
- if(gameState.cometActive) speed = 0.8;
714
-
715
- cometAngle += delta * speed;
716
- const rX = 80; const rZ = 60;
717
-
718
- const x = userCentroidForComet.x + Math.cos(cometAngle) * rX;
719
- const z = userCentroidForComet.z + Math.sin(cometAngle) * rZ;
720
- const y = userCentroidForComet.y + Math.sin(cometAngle * 2.0) * 20;
721
-
722
- cometGroup.position.set(x, y, z);
723
-
724
- if(gameState.cometActive && trashGroup) {
725
- trashGroup.children.forEach(trash => {
726
- if(trash.position.distanceTo(cometGroup.position) < 3) {
727
- document.getElementById('glitchOverlay').classList.remove('hidden');
728
- document.getElementById('glitchOverlay').classList.add('glitch-active');
729
- setTimeout(() => {
730
- document.getElementById('glitchOverlay').classList.add('hidden');
731
- document.getElementById('glitchOverlay').classList.remove('glitch-active');
732
- }, 500);
733
- trash.position.y += 1000;
734
- }
735
- });
736
- }
737
-
738
- const positions = cometParticlesMesh.geometry.attributes.position.array;
739
- const colors = cometParticlesMesh.geometry.attributes.color.array;
740
- const sizes = cometParticlesMesh.geometry.attributes.size.array;
741
-
742
- let spawnCount = gameState.cometActive ? 10 : 5;
743
- for(let i=0; i<COMET_PARTICLE_COUNT; i++) {
744
- if(spawnCount > 0 && cometParticlesData[i].life < 0) {
745
- cometParticlesData[i].life = 1.0;
746
- positions[i*3] = cometGroup.position.x + (Math.random()-0.5);
747
- positions[i*3+1] = cometGroup.position.y + (Math.random()-0.5);
748
- positions[i*3+2] = cometGroup.position.z + (Math.random()-0.5);
749
-
750
- if(gameState.cometActive) {
751
- colors[i*3] = 1.0; colors[i*3+1] = 0.2; colors[i*3+2] = 0.0;
752
- } else {
753
- colors[i*3] = 0.2; colors[i*3+1] = 1.0; colors[i*3+2] = 1.0;
754
- }
755
- sizes[i] = 1.2;
756
- spawnCount--;
757
- }
758
- }
759
-
760
- for(let i=0; i<COMET_PARTICLE_COUNT; i++) {
761
- if(cometParticlesData[i].life > 0) {
762
- const d = cometParticlesData[i];
763
- d.life -= delta * 0.7;
764
- sizes[i] = d.life * 1.8;
765
- } else {
766
- positions[i*3] = 99999;
767
- }
768
- }
769
-
770
- cometParticlesMesh.geometry.attributes.position.needsUpdate = true;
771
- cometParticlesMesh.geometry.attributes.color.needsUpdate = true;
772
- cometParticlesMesh.geometry.attributes.size.needsUpdate = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  }
774
 
775
- function animate() {
776
- requestAnimationFrame(animate);
777
- const delta = clock.getDelta();
778
- const time = clock.getElapsedTime();
779
-
780
- controls.update();
781
-
782
- if(bgParticles) {
783
- bgParticles.rotation.y = time * 0.015;
784
- bgParticles.rotation.z = time * 0.005;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  }
786
-
787
- updateAdvancedComet(delta, time);
788
- updateRaycaster();
789
- drawMinimap();
790
-
791
- hashtagGroup.children.forEach(obj => {
792
- if (obj.userData.isText) {
793
- obj.lookAt(camera.position);
794
- const dist = obj.position.distanceTo(camera.position);
795
- let scale = (1/dist) * 12;
796
- scale = Math.max(0.6, Math.min(5.0, scale));
797
- obj.scale.set(scale, scale, scale);
798
- }
799
- });
800
-
801
- if(trashGroup) {
802
- trashGroup.rotation.y += 0.01;
803
- trashGroup.children.forEach(t => t.rotation.x += 0.02);
804
  }
805
-
806
- composer.render();
 
 
 
 
 
 
 
807
  }
808
 
809
- function getCurrentUserCentroid() {
810
- if (!userId || !userMaps[userId] || userMaps[userId].length === 0) return new THREE.Vector3(0,0,0);
811
- const centroid = new THREE.Vector3(0,0,0);
812
- userMaps[userId].forEach(o => centroid.add(o));
813
- centroid.divideScalar(userMaps[userId].length);
814
- return centroid;
 
 
 
 
 
815
  }
816
 
817
- function visualizeHashtags(dataList, origin, level, parentColor = null) {
818
- if (!dataList || dataList.length === 0) return;
819
- for (const item of dataList) {
820
- let currentTag, variantsList;
821
  if (level === 1) {
822
- currentTag = normalizeString(item.palabra_principal);
823
- variantsList = item.variantes || [];
824
  } else if (level === 2) {
825
- currentTag = normalizeString(item.palabra_variante);
826
- variantsList = item.sub_variantes || [];
827
  } else {
828
- currentTag = normalizeString(item);
829
- variantsList = [];
830
  }
831
- if (!currentTag) continue;
832
-
833
- const { color, h } = stringToHslColor(currentTag);
834
- let nodeColor = (level === 1) ? color : parentColor;
835
-
836
- let isGolden = false;
837
- if(gameState.mode === 'miner' && level >= 2) {
838
- if(Math.random() < 0.15) {
839
- isGolden = true;
840
- nodeColor = '#fbbf24';
841
- }
842
- }
843
-
844
- const nodeMaterial = new THREE.MeshPhysicalMaterial({
845
- color: new THREE.Color(nodeColor),
846
- emissive: new THREE.Color(nodeColor),
847
- emissiveIntensity: isGolden ? 2.0 : (level === 1 ? 0.8 : 0.4),
848
  roughness: 0.2,
849
  metalness: 0.1,
850
- transmission: 0.1,
851
  transparent: true,
852
  opacity: 0.95
853
- });
854
-
855
- const theta = (h / 360) * Math.PI * 2;
856
- let phiHash = 0;
857
- for (let i = 0; i < currentTag.length; i++) phiHash = (phiHash + currentTag.charCodeAt(i) * 13) % 180;
858
- const phi = ((phiHash / 180) * 90 + 45) * (Math.PI / 180);
859
-
860
- const baseRadius = 12 / (level * level);
861
- const cx = baseRadius * Math.sin(phi) * Math.cos(theta);
862
- const cy = baseRadius * Math.cos(phi);
863
- const cz = baseRadius * Math.sin(phi) * Math.sin(theta);
864
- const clusterCenter = new THREE.Vector3(cx, cy, cz).add(origin);
865
-
866
- const lineMat = new THREE.LineBasicMaterial({
867
- color: new THREE.Color(nodeColor),
868
- transparent: true,
869
- opacity: 0.25
870
- });
871
- const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints([origin, clusterCenter]), lineMat);
872
- hashtagGroup.add(line);
873
-
874
- let sRad = (level === 1) ? 0.35 : (level === 2 ? 0.18 : 0.1);
875
- if(isGolden) sRad *= 1.5;
876
-
877
- const sphere = new THREE.Mesh(new THREE.SphereGeometry(sRad, 16, 16), nodeMaterial);
878
- sphere.position.copy(clusterCenter);
879
- sphere.userData.hashtag = currentTag;
880
- sphere.userData.level = level;
881
- sphere.userData.isGolden = isGolden;
882
- hashtagGroup.add(sphere);
883
-
884
- let tSize = (level === 1) ? 0.35 : (level === 2 ? 0.18 : 0.12);
885
- const tGeo = new TextGeometry(currentTag.toUpperCase(), { font: font, size: tSize, height: 0.01, bevelEnabled: false });
886
- tGeo.computeBoundingBox();
887
- const tMesh = new THREE.Mesh(tGeo, new THREE.MeshBasicMaterial({ color: nodeColor }));
888
- tMesh.position.copy(clusterCenter);
889
- tMesh.position.y += sRad + 0.15;
890
- tMesh.position.x -= (tGeo.boundingBox.max.x - tGeo.boundingBox.min.x)/2;
891
- tMesh.userData.isText = true;
892
- hashtagGroup.add(tMesh);
893
-
894
- visualizeHashtags(variantsList, clusterCenter, level + 1, nodeColor);
895
- }
896
- }
897
-
898
- function visualizeRoot(topic, origin) {
899
- const { color } = stringToHslColor(topic);
900
- const mat = new THREE.MeshStandardMaterial({
901
- color: new THREE.Color(color),
902
- emissive: new THREE.Color(color),
903
- emissiveIntensity: 2.2,
904
- roughness: 0.4
905
- });
906
- const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.7, 32, 32), mat);
907
- sphere.position.copy(origin);
908
- sphere.userData.hashtag = topic;
909
- sphere.userData.level = 0;
910
- hashtagGroup.add(sphere);
911
-
912
- const tGeo = new TextGeometry(topic.toUpperCase(), { font: font, size: 0.6, height: 0.05, bevelEnabled: false });
913
- tGeo.computeBoundingBox();
914
- const tMesh = new THREE.Mesh(tGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }));
915
- tMesh.position.copy(origin);
916
- tMesh.position.y += 1.0;
917
- tMesh.position.x -= (tGeo.boundingBox.max.x - tGeo.boundingBox.min.x) / 2;
918
- tMesh.userData.isText = true;
919
- hashtagGroup.add(tMesh);
920
- }
921
-
922
- function createUserSun(pos, name, isMe) {
923
- const col = isMe ? 0xffaa00 : 0x00ff88;
924
- const mat = new THREE.MeshStandardMaterial({
925
- color: col, emissive: col, emissiveIntensity: 1.8, roughness: 0.2
926
- });
927
- const mesh = new THREE.Mesh(new THREE.SphereGeometry(3, 32, 32), mat);
928
- mesh.position.copy(pos);
929
- mesh.userData.hashtag = `Usuario: ${name}`;
930
- mesh.userData.isPlaceholder = true;
931
- hashtagGroup.add(mesh);
932
-
933
- const tGeo = new TextGeometry(name.toUpperCase(), { font: font, size: 1.2, height: 0.1 });
934
- tGeo.computeBoundingBox();
935
- const tMesh = new THREE.Mesh(tGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }));
936
- tMesh.position.copy(pos);
937
- tMesh.position.y += 4;
938
- tMesh.position.x -= (tGeo.boundingBox.max.x - tGeo.boundingBox.min.x)/2;
939
- tMesh.userData.isText = true;
940
- hashtagGroup.add(tMesh);
941
- }
942
-
943
- async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
944
- const controller = new AbortController();
945
- const id = setTimeout(() => controller.abort(), timeoutMs);
946
- try {
947
- return await fetch(url, { ...options, signal: controller.signal });
948
- } finally { clearTimeout(id); }
949
- }
950
-
951
- async function fetchWithBackoff(url, options, retries = 2, delay = 1000) {
952
- try {
953
- return await fetchWithTimeout(url, options);
954
- } catch (err) {
955
- if (retries > 0) {
956
- await new Promise(r => setTimeout(r, delay));
957
- return fetchWithBackoff(url, options, retries - 1, delay * 2);
958
- } else { throw err; }
959
- }
960
- }
961
-
962
- async function callGemini(topic, mc, vc, svc) {
963
- const k = getLocalGeminiKey();
964
- if(!k || k.length<10) throw new Error("Falta API Key");
965
-
966
- let prompt = "";
967
- if(gameState.mode === 'bridge') {
968
- const target = document.getElementById('bridgeTargetInput').value;
969
- prompt = `Juego "Puente Semántico". Punto A: ${topic}. Punto B: ${target}. Genera palabras que sirvan de puente lógico. Genera 3 opciones principales.`;
970
- } else {
971
- prompt = `Tema: ${topic}. 1. Genera ${mc} palabras clave (Nivel 1). 2. Para cada una, ${vc} variantes (Nivel 2). 3. Para cada variante, ${svc} sub-variantes (Nivel 3). JSON Puro sin markdown.`;
972
  }
973
-
974
- const schema = {
975
- type: "OBJECT",
976
- properties: {
977
- analisis: { type: "STRING" },
978
- lista_palabras: { type: "ARRAY", items: { type: "OBJECT", properties: { palabra_principal: {type:"STRING"}, variantes: {type:"ARRAY", items: {type:"OBJECT", properties: {palabra_variante: {type:"STRING"}, sub_variantes: {type:"ARRAY", items:{type:"STRING"}}}}} } } }
979
- }
980
- };
981
-
982
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${k}`;
983
-
984
- if (!isHFStatic) {
985
- try {
986
- const proxyResp = await fetchWithBackoff('/.netlify/functions/gemini-proxy', {
987
- method: 'POST', headers: { 'Content-Type': 'application/json' },
988
- body: JSON.stringify({ model: 'gemini-2.0-flash-exp', payload: { contents: [{parts:[{text: prompt}]}] } })
989
- }, 1, 1000);
990
- if (proxyResp.ok) return await proxyResp.json();
991
- } catch (e) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
992
  }
993
-
994
- const resp = await fetchWithBackoff(url, {
995
- method: 'POST', headers: {'Content-Type': 'application/json'},
996
- body: JSON.stringify({ contents: [{parts:[{text: prompt}]}], generationConfig: { responseMimeType: "application/json", responseSchema: schema } })
997
- });
998
- if(!resp.ok) throw new Error(await resp.text());
999
- return await resp.json();
 
 
 
 
 
 
1000
  }
1001
 
1002
- async function handleAnalysisAndVisualization() {
1003
- if(!font) return;
1004
- const topic = normalizeString(document.getElementById('topicInput').value);
1005
- if(!topic) return;
1006
-
1007
- if(gameState.mode === 'miner' && gameState.energy < 10) {
1008
- alert("Energía Insuficiente. Explora nodos existentes para recargar.");
1009
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  }
1011
-
1012
- const btn = document.getElementById('visualizeButton');
1013
- const pb = document.getElementById('progressBar');
1014
- const pbc = document.getElementById('progressBarContainer');
1015
- const originalBtnHtml = btn.innerHTML;
1016
-
1017
- btn.disabled = true; btn.innerHTML = '<span class="animate-pulse">PROCESANDO RED...</span>';
1018
- pbc.classList.remove('hidden'); pb.style.width = "90%"; pb.style.transition = "width 15s ease-out";
1019
-
1020
- try {
1021
- const mc = document.getElementById('level1Slider').value;
1022
- const vc = document.getElementById('level2Slider').value;
1023
- const svc = document.getElementById('level3Slider').value;
1024
-
1025
- let origin = new THREE.Vector3(0,0,0);
1026
- const myMaps = userMaps[userId] || [];
1027
- if(myMaps.length === 0) {
1028
- const globalIdx = Object.keys(userMaps).length;
1029
- origin.set(globalIdx*800*Math.cos(globalIdx), 0, globalIdx*800*Math.sin(globalIdx));
1030
- } else {
1031
- let center = new THREE.Vector3(); myMaps.forEach(m=>center.add(m)); center.divideScalar(myMaps.length);
1032
- const r = Math.sqrt(myMaps.length)*40; const a = myMaps.length;
1033
- origin.set(center.x + r*Math.cos(a), 0, center.z + r*Math.sin(a));
1034
- }
1035
-
1036
- const res = await callGemini(topic, mc, vc, svc);
1037
- const txt = res.candidates?.[0]?.content?.parts?.[0]?.text;
1038
- if(!txt) throw new Error("La IA no generó datos válidos.");
1039
- const json = JSON.parse(txt);
1040
-
1041
- visualizeRoot(topic, origin);
1042
- visualizeHashtags(json.lista_palabras, origin, 1);
1043
-
1044
- if(gameState.mode === 'miner') {
1045
- gameState.energy -= 10;
1046
- updateGameProfile();
1047
- updateHUD();
1048
- }
1049
-
1050
- if(gameState.mode === 'bridge') {
1051
- gameState.bridgeHops++;
1052
- document.getElementById('bridgeHops').innerText = gameState.bridgeHops;
1053
- document.getElementById('bridgeOriginDisplay').innerText = topic;
1054
- document.getElementById('topicInput').value = "";
1055
- }
1056
-
1057
- if(db && userId) await addDoc(collection(db,'artifacts',appId,'public','data','maps'), {
1058
- topic, depth: "3", origin: {x:origin.x, y:origin.y, z:origin.z}, data: JSON.stringify(json), createdAt: new Date(), userId
1059
- });
1060
-
1061
- } catch(e) {
1062
- console.error(e);
1063
- alert("Error en análisis: " + e.message);
1064
- } finally {
1065
- btn.disabled = false; btn.innerHTML = originalBtnHtml;
1066
- pb.style.width = "100%"; setTimeout(()=>{pbc.classList.add('hidden'); pb.style.width="0%";}, 500);
1067
  }
1068
- }
1069
-
1070
- function loadAllMaps() {
1071
- if(!db || !font) { setTimeout(loadAllMaps,500); return; }
1072
- const q = query(collection(db,'artifacts',appId,'public','data','maps'));
1073
-
1074
- onSnapshot(q, async (snap) => {
1075
- while(hashtagGroup.children.length > 0){
1076
- let obj = hashtagGroup.children[0];
1077
- hashtagGroup.remove(obj);
1078
- if(obj.geometry) obj.geometry.dispose();
1079
- if(obj.material) {
1080
- if(Array.isArray(obj.material)) obj.material.forEach(m=>m.dispose());
1081
- else obj.material.dispose();
1082
- }
1083
- }
1084
-
1085
- userMaps = {};
1086
-
1087
- snap.docs.forEach(d => {
1088
- const m = d.data();
1089
- if(!m.origin) return;
1090
- const o = new THREE.Vector3(m.origin.x, m.origin.y, m.origin.z);
1091
- if(!userMaps[m.userId]) userMaps[m.userId] = [];
1092
- userMaps[m.userId].push(o);
1093
-
1094
- try {
1095
- const data = JSON.parse(m.data);
1096
- visualizeRoot(m.topic, o);
1097
- visualizeHashtags(data.lista_palabras, o, 1);
1098
- } catch {}
1099
- });
1100
-
1101
- const uids = Object.keys(userMaps);
1102
- const list = document.getElementById('userList');
1103
- if(list) list.innerHTML = '';
1104
-
1105
- for(const uid of uids) {
1106
- const prof = await getProfile(uid);
1107
- const name = prof ? prof.username : "ANON " + uid.substring(0,4);
1108
-
1109
- if(list) {
1110
- const item = document.createElement('div');
1111
- item.className = 'text-cyan-400 hover:text-white cursor-pointer hover:bg-white/5 p-1 rounded transition-colors text-[10px] tracking-wide';
1112
- item.innerHTML = `> <span class="font-bold">${name}</span>`;
1113
- item.onclick = () => teleportToUser(uid);
1114
- list.appendChild(item);
1115
- }
1116
-
1117
- const origins = userMaps[uid];
1118
- if(origins.length > 0) {
1119
- const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
1120
- createUserSun(c, name, uid === userId);
1121
- }
1122
- }
1123
-
1124
- initAdvancedComet();
1125
- drawMinimap();
1126
- focusOnUserMaps();
1127
- });
1128
- }
1129
-
1130
- async function getProfile(uid) {
1131
- if(userProfileCache[uid]) return userProfileCache[uid];
1132
- try {
1133
- const snap = await getDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'));
1134
- if(snap.exists()) { userProfileCache[uid] = snap.data(); return snap.data(); }
1135
- } catch {}
1136
- return null;
1137
- }
1138
-
1139
- function focusOnUserMaps() {
1140
- if(!controls || !userId || !userMaps[userId]) return;
1141
- const c = getCurrentUserCentroid();
1142
- controls.target.copy(c);
1143
- camera.position.copy(c).add(new THREE.Vector3(0,10,40));
1144
- }
1145
-
1146
- function teleportToUser(uid) {
1147
- if(!userMaps[uid]) return;
1148
- const origins = userMaps[uid];
1149
- const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
1150
- controls.target.copy(c);
1151
- camera.position.copy(c).add(new THREE.Vector3(0,10,40));
1152
- }
1153
-
1154
- function drawMinimap() {
1155
- if(!minimapCtx) return;
1156
- const w = minimapCtx.canvas.width; const h = minimapCtx.canvas.height;
1157
- minimapCtx.clearRect(0,0,w,h);
1158
- minimapDotCoords = [];
1159
-
1160
- const myC = getCurrentUserCentroid();
1161
-
1162
- Object.keys(userMaps).forEach(uid => {
1163
- const origins = userMaps[uid];
1164
- const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
1165
-
1166
- const x = w/2 + (c.x - myC.x)*minimapScale;
1167
- const y = h/2 + (c.z - myC.z)*minimapScale;
1168
-
1169
- const isMe = (uid === userId);
1170
- const mainColor = isMe ? '#22d3ee' : '#64748b';
1171
-
1172
- minimapDotCoords.push({x, y, uid});
1173
-
1174
- if(origins.length > 0) {
1175
- const satColor = isMe ? 'rgba(34, 211, 238, 0.4)' : 'rgba(100, 116, 139, 0.4)';
1176
- origins.forEach(o => {
1177
- const sx = w/2 + (o.x - myC.x)*minimapScale;
1178
- const sy = h/2 + (o.z - myC.z)*minimapScale;
1179
- minimapCtx.beginPath();
1180
- minimapCtx.arc(sx, sy, 1, 0, Math.PI*2);
1181
- minimapCtx.fillStyle = satColor;
1182
- minimapCtx.fill();
1183
- });
1184
- }
1185
-
1186
- minimapCtx.fillStyle = mainColor;
1187
- minimapCtx.beginPath();
1188
- minimapCtx.arc(x,y, isMe?4:2.5, 0, Math.PI*2);
1189
- minimapCtx.fill();
1190
-
1191
- if(isMe) {
1192
- minimapCtx.strokeStyle = 'rgba(34, 211, 238, 0.3)';
1193
- minimapCtx.beginPath(); minimapCtx.arc(x,y, 8, 0, Math.PI*2); minimapCtx.stroke();
1194
- }
1195
- });
1196
-
1197
- if (cometGroup) {
1198
- const cometX = w/2 + (cometGroup.position.x - myC.x) * minimapScale;
1199
- const cometY = h/2 + (cometGroup.position.z - myC.z) * minimapScale;
1200
- minimapCtx.beginPath(); minimapCtx.arc(cometX, cometY, 3, 0, Math.PI * 2);
1201
- minimapCtx.fillStyle = 'rgba(0, 255, 255, 0.4)'; minimapCtx.fill();
1202
- minimapCtx.beginPath(); minimapCtx.arc(cometX, cometY, 1.5, 0, Math.PI * 2);
1203
- minimapCtx.fillStyle = '#ffffff'; minimapCtx.fill();
1204
  }
 
 
1205
  }
1206
 
1207
- function onMinimapClick(e) {
1208
- if(!minimapCtx) return;
1209
- const rect = minimapCtx.canvas.getBoundingClientRect();
1210
- const x = e.clientX - rect.left;
1211
- const y = e.clientY - rect.top;
1212
-
1213
- let closest = null; let minD = 20;
1214
-
1215
- minimapDotCoords.forEach(dot => {
1216
- const d = Math.sqrt((x-dot.x)**2 + (y-dot.y)**2);
1217
- if(d < minD) { minD = d; closest = dot.uid; }
1218
- });
1219
- if(closest) teleportToUser(closest);
1220
- }
1221
-
1222
- function onWindowResize() {
1223
- camera.aspect = window.innerWidth/window.innerHeight;
1224
- camera.updateProjectionMatrix();
1225
- renderer.setSize(window.innerWidth, window.innerHeight);
1226
- composer.setSize(window.innerWidth, window.innerHeight);
1227
- }
1228
-
1229
- function onPointerMove(e) {
1230
- mouse.x = (e.clientX/window.innerWidth)*2-1;
1231
- mouse.y = -(e.clientY/window.innerHeight)*2+1;
1232
- tooltip.style.left = e.clientX+20+'px';
1233
- tooltip.style.top = e.clientY+'px';
1234
- }
1235
-
1236
- function onMouseClick(e) {
1237
- if(!raycaster) return;
1238
- raycaster.setFromCamera(mouse, camera);
1239
-
1240
- if(gameState.cometActive && trashGroup) {
1241
- const intersects = raycaster.intersectObjects(trashGroup.children);
1242
- if(intersects.length > 0) {
1243
- const obj = intersects[0].object;
1244
- scene.add(createExplosion(obj.position));
1245
- trashGroup.remove(obj);
1246
- gameState.score += 100;
1247
- document.getElementById('missionText').innerText = `PUNTOS: ${gameState.score}`;
1248
- return;
1249
- }
1250
- }
1251
-
1252
- if(gameState.mode === 'miner') {
1253
- let targets = [...hashtagGroup.children];
1254
- const intersects = raycaster.intersectObjects(targets, false);
1255
- if(intersects.length > 0) {
1256
- const obj = intersects[0].object;
1257
- if(obj.userData.isGolden) {
1258
- addToVault(obj.userData.hashtag);
1259
- scene.add(createExplosion(obj.position, 0xffd700));
1260
- hashtagGroup.remove(obj);
1261
- gameState.energy += 15;
1262
- if(gameState.energy > 100) gameState.energy = 100;
1263
- updateHUD();
1264
- }
1265
- }
1266
  }
 
 
 
 
1267
  }
1268
 
1269
- function createExplosion(pos, color = 0xff0000) {
1270
- const geo = new THREE.BufferGeometry();
1271
- const count = 30;
1272
- const positions = new Float32Array(count * 3);
1273
- for(let i=0;i<count*3;i++) positions[i] = (Math.random()-0.5)*2;
1274
- geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
1275
- const mat = new THREE.PointsMaterial({color: color, size: 0.5, transparent:true});
1276
- const pts = new THREE.Points(geo, mat);
1277
- pts.position.copy(pos);
1278
-
1279
- let life = 1.0;
1280
- function animExplosion() {
1281
- life -= 0.05;
1282
- pts.scale.multiplyScalar(1.1);
1283
- mat.opacity = life;
1284
- if(life > 0) requestAnimationFrame(animExplosion);
1285
- else scene.remove(pts);
1286
- }
1287
- animExplosion();
1288
- return pts;
1289
- }
1290
-
1291
- function updateRaycaster() {
1292
- raycaster.setFromCamera(mouse, camera);
1293
- let targets = [...hashtagGroup.children];
1294
- if(cometHead) targets.push(cometHead);
1295
-
1296
- const intersects = raycaster.intersectObjects(targets, false);
1297
-
1298
- if(intersects.length > 0) {
1299
- let o = intersects[0].object;
1300
- if(o.parent === cometHead || o.parent === cometGroup) o = cometHead;
1301
-
1302
- if(o === cometHead) {
1303
- tooltip.classList.remove('hidden');
1304
- tooltip.innerHTML = `<div class="text-cyan-300 font-bold text-xs">COMETA NEURAL</div>`;
1305
- document.body.style.cursor = 'pointer';
1306
- return;
1307
- }
1308
-
1309
- const d = o.userData;
1310
- if(d.hashtag) {
1311
- tooltip.classList.remove('hidden');
1312
- let typeColor = d.isGolden ? "text-yellow-400" : (d.isPlaceholder ? "text-green-400" : "text-cyan-300");
1313
- let typeText = d.isGolden ? "NODO DORADO (CLICK PARA RECOGER)" : (d.isPlaceholder ? "NODO USUARIO" : `NIVEL ${d.level}`);
1314
-
1315
- tooltip.innerHTML = `
1316
- <div class="${typeColor} font-bold tracking-widest text-sm mb-1">${d.hashtag}</div>
1317
- <div class="text-gray-400 text-[10px] uppercase">${typeText}</div>
1318
- `;
1319
- document.body.style.cursor = 'pointer';
1320
- }
1321
- } else {
1322
- tooltip.classList.add('hidden');
1323
- document.body.style.cursor = 'default';
1324
  }
 
 
1325
  }
1326
 
1327
- function stringToHslColor(str) {
1328
- let hash = 0; for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
1329
- return { color: `hsl(${Math.abs(hash % 360)}, 75%, 60%)`, h: Math.abs(hash % 360) };
1330
- }
1331
  </script>
1332
  </body>
1333
- </html>
 
1
  <!DOCTYPE html>
2
  <html lang="es">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Navegador Neuronal Semántico 3D de Twitter</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
9
  <style>
10
+ body {
11
+ scrollbar-width: none;
12
+ -ms-overflow-style: none;
13
+ background-color: #020205;
14
+ user-select: none;
15
+ }
16
+ body::-webkit-scrollbar {
17
+ display: none;
18
  }
 
 
19
  .glass-panel {
20
+ background: rgba(10, 15, 30, 0.75);
21
+ backdrop-filter: blur(16px);
22
+ -webkit-backdrop-filter: blur(16px);
23
+ border: 1px solid rgba(255, 255, 255, 0.08);
24
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
25
  }
 
26
  @keyframes shimmer {
27
+ 0% { transform: translateX(-100%); }
28
+ 100% { transform: translateX(100%); }
29
  }
30
  .animate-shimmer {
31
+ animation: shimmer 2s infinite;
32
  }
 
33
  .custom-scrollbar::-webkit-scrollbar { width: 4px; }
34
  .custom-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); }
35
  .custom-scrollbar::-webkit-scrollbar-thumb { background: #22d3ee; border-radius: 4px; }
 
36
  @keyframes glitch-anim {
37
+ 0% { transform: translate(0); }
38
+ 20% { transform: translate(-2px, 2px); }
39
+ 40% { transform: translate(-2px, -2px); }
40
+ 60% { transform: translate(2px, 2px); }
41
+ 80% { transform: translate(2px, -2px); }
42
+ 100% { transform: translate(0); }
43
  }
44
  .glitch-active {
45
+ animation: glitch-anim 0.2s cubic-bezier(.25, .46, .45, .94) both infinite;
46
+ filter: hue-rotate(90deg) contrast(1.5);
47
  }
48
  .golden-node {
49
+ box-shadow: 0 0 15px #ffd700;
50
+ border: 1px solid #ffd700;
51
+ }
52
+ #missionPanel {
53
+ right: 1rem;
54
+ top: 6rem;
55
+ position: fixed;
56
+ width: 200px;
57
+ z-index: 95;
58
  }
59
  </style>
60
  </head>
 
61
  <body class="m-0 overflow-hidden font-[Orbitron] text-slate-200">
62
 
63
  <div id="glitchOverlay" class="fixed inset-0 pointer-events-none z-[99999] hidden mix-blend-overlay bg-red-900/20"></div>
64
 
65
  <div id="loginOverlay" class="fixed inset-0 bg-black/95 z-[9999] flex items-center justify-center transition-opacity duration-700 opacity-100">
66
  <div id="loginModal" class="glass-panel p-8 rounded-2xl w-full max-w-md text-white relative overflow-hidden border-t border-cyan-500/30">
67
+ <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-cyan-500 to-transparent opacity-70"></div>
68
+
69
  <div id="authStep1">
70
  <h2 class="text-4xl font-bold text-center mb-2 text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 drop-shadow-[0_0_15px_rgba(34,211,238,0.5)]">NEXUS</h2>
71
  <p class="text-cyan-500/60 text-center mb-8 text-[10px] tracking-[0.3em] uppercase">Visualizador Semántico Neural v2.5</p>
72
+
73
  <button id="btnGoToAnon" class="w-full bg-white/5 hover:bg-white/10 text-cyan-300 border border-cyan-500/20 font-bold py-4 px-4 rounded-lg transition-all flex items-center justify-center gap-3 mb-6 group hover:shadow-[0_0_15px_rgba(34,211,238,0.2)]">
74
+ <svg class="w-5 h-5 group-hover:scale-110 transition-transform text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
76
+ </svg>
77
  Entrar como Anónimo
78
  </button>
79
+
80
  <div class="relative flex py-2 items-center mb-4">
81
  <div class="flex-grow border-t border-white/10"></div>
82
  <span class="flex-shrink-0 mx-4 text-gray-500 text-[10px] uppercase tracking-wider">Credenciales de Acceso</span>
83
  <div class="flex-grow border-t border-white/10"></div>
84
  </div>
85
+
86
  <form id="emailAuthForm" class="space-y-4">
87
  <div class="relative group">
88
  <input type="email" id="loginEmail" class="w-full p-3 pl-4 rounded-lg bg-black/50 border border-white/10 text-white placeholder-gray-600 focus:border-cyan-500 focus:outline-none text-sm transition-all focus:bg-black/70" placeholder="ID de Usuario (Email)">
 
90
  </div>
91
  <div class="relative group">
92
  <input type="password" id="loginPassword" class="w-full p-3 pl-4 rounded-lg bg-black/50 border border-white/10 text-white placeholder-gray-600 focus:border-cyan-500 focus:outline-none text-sm transition-all focus:bg-black/70" placeholder="Clave de Acceso">
93
+ <div class="absolute inset-0 border border-cyan-500/0 rounded-lg group-hover:border-cyan-500/20 pointer-events-none transition-colors"></div>
94
  </div>
95
  <div class="flex gap-3 pt-2">
96
+ <button type="button" id="loginButton" class="flex-1 bg-blue-600/10 hover:bg-blue-600/30 text-blue-400 py-2 rounded border border-blue-500/30 text-xs font-bold uppercase tracking-wider hover:shadow-[0_0_10px_rgba(59,130,246,0.3)]">Entrar</button>
97
+ <button type="button" id="registerButton" class="flex-1 bg-green-600/10 hover:bg-green-600/30 text-green-400 py-2 rounded border border-green-500/30 text-xs font-bold uppercase tracking-wider hover:shadow-[0_0_10px_rgba(74,222,128,0.3)]">Registrar</button>
98
  </div>
99
  </form>
100
+
101
  <p id="loginMessage" class="text-center text-xs mt-4 min-h-[1.5rem] text-red-400 font-bold tracking-wide"></p>
102
  </div>
103
+
104
  <div id="authStep2" class="hidden">
105
  <h2 class="text-2xl font-bold text-white mb-2 text-center">Identidad Digital</h2>
106
  <p class="text-gray-400 text-xs mb-8 text-center">Asigna un nombre clave a tu constelación.</p>
 
114
  </div>
115
 
116
  <div id="container" class="w-screen h-screen fixed top-0 left-0 bg-[#020205]"></div>
117
+
118
  <div id="gameHUD" class="fixed top-0 w-full z-[90] pointer-events-none flex justify-between p-4 hidden opacity-0 transition-opacity duration-1000">
119
  <div class="glass-panel px-4 py-2 rounded-lg flex items-center gap-4 border-t border-yellow-500/30 pointer-events-auto">
120
+ <div>
121
+ <div class="text-[9px] text-yellow-500 uppercase tracking-widest">Energía Neural</div>
122
+ <div class="w-32 h-2 bg-gray-800 rounded-full mt-1 overflow-hidden border border-white/10">
123
+ <div id="energyBar" class="h-full bg-yellow-400 w-full transition-all duration-500 shadow-[0_0_10px_#fbbf24]"></div>
 
 
 
 
 
 
 
 
 
124
  </div>
125
+ </div>
126
+ <div class="text-right border-l border-white/10 pl-4">
127
+ <div class="text-[9px] text-cyan-500 uppercase tracking-widest">Rango</div>
128
+ <div id="rankDisplay" class="text-sm font-bold text-white tracking-widest">OBSERVADOR</div>
129
+ </div>
130
+ <div class="text-right pl-4 border-l border-white/10">
131
+ <div class="text-[9px] text-purple-500 uppercase tracking-widest">Bóveda</div>
132
+ <div id="vaultCount" class="text-sm font-bold text-white font-mono">0</div>
133
+ </div>
134
  </div>
135
+
136
  <div id="missionDisplay" class="glass-panel px-6 py-2 rounded-lg text-center pointer-events-auto">
137
+ <div class="text-[9px] text-green-400 uppercase tracking-widest mb-1">Modo Activo</div>
138
+ <div id="activeModeText" class="text-xs font-bold text-white tracking-[0.2em]">EXPLORACIÓN LIBRE</div>
139
  </div>
140
  </div>
141
 
142
  <div id="tooltip" class="absolute hidden bg-black/80 text-white px-4 py-3 rounded-none border-l-2 border-cyan-500 z-[101] pointer-events-none text-sm backdrop-blur-md shadow-[0_0_15px_rgba(6,182,212,0.2)] max-w-xs"></div>
143
 
144
+ <div id="ui" class="fixed top-16 left-5 z-[100] glass-panel p-6 rounded-xl max-w-[380px] text-white h-[calc(100vh-80px)] flex flex-col transition-all hidden transform duration-700 -translate-x-2 opacity-0 overflow-y-auto custom-scrollbar">
145
+
146
  <div class="flex items-center justify-between mb-4 border-b border-white/10 pb-4">
147
  <h1 class="text-lg font-bold text-white flex items-center space-x-3">
148
  <div class="w-2 h-2 bg-cyan-400 rounded-full animate-pulse shadow-[0_0_10px_#22d3ee]"></div>
 
155
  </div>
156
 
157
  <div class="grid grid-cols-3 gap-2 mb-4">
158
+ <button onclick="setGameMode('explorer')" id="btnModeExplorer" class="bg-cyan-900/30 border border-cyan-500/50 text-cyan-300 text-[9px] py-2 rounded uppercase tracking-wider hover:bg-cyan-800/50 transition-all font-bold">Explorar</button>
159
+ <button onclick="setGameMode('miner')" id="btnModeMiner" class="bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:border-yellow-500/50 hover:text-yellow-400 transition-all font-bold">Minero</button>
160
+ <button onclick="setGameMode('bridge')" id="btnModeBridge" class="bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:border-purple-500/50 hover:text-purple-400 transition-all font-bold">Puente</button>
161
  </div>
162
+
163
  <button onclick="setGameMode('comet')" id="btnModeComet" class="w-full mb-4 bg-black/30 border border-white/10 text-gray-500 text-[9px] py-2 rounded uppercase tracking-wider hover:border-red-500/50 hover:text-red-400 transition-all font-bold">Defensa Cometa</button>
164
 
165
  <div id="bridgeControls" class="hidden space-y-3 mb-4 p-3 bg-purple-900/10 rounded border border-purple-500/30">
166
+ <div class="text-[10px] text-purple-300 uppercase tracking-widest mb-1">Objetivo del Puente</div>
167
+ <input type="text" id="bridgeTargetInput" class="w-full p-2 rounded bg-black/50 border border-purple-500/30 text-white text-xs mb-2" placeholder="Destino (Punto B)">
168
+ <div class="text-[9px] text-gray-400">Origen: <span id="bridgeOriginDisplay" class="text-white font-bold">Sin definir</span></div>
169
+ <div class="text-[9px] text-gray-400">Saltos: <span id="bridgeHops" class="text-white font-bold">0</span></div>
170
  </div>
171
 
172
  <div class="relative mb-5 group">
173
+ <div class="absolute -inset-0.5 bg-gradient-to-r from-cyan-500 to-purple-600 rounded-lg blur opacity-20 group-hover:opacity-60 transition duration-500"></div>
174
+ <input type="text" id="topicInput" class="relative w-full p-4 rounded-lg bg-black/80 text-white border border-white/10 focus:outline-none focus:border-cyan-500/50 text-sm placeholder-gray-500 font-mono" placeholder="Ingresar semilla semántica...">
175
  </div>
176
 
177
  <div class="space-y-5 mb-6 px-1">
 
210
  <button id="visualizeButton" class="relative w-full overflow-hidden bg-cyan-900/20 hover:bg-cyan-800/40 text-cyan-300 font-bold py-4 px-4 rounded-lg border border-cyan-500/30 transition-all flex items-center justify-center space-x-3 group hover:shadow-[0_0_15px_rgba(34,211,238,0.2)]">
211
  <div class="absolute inset-0 w-full h-full bg-gradient-to-r from-transparent via-cyan-500/10 to-transparent -translate-x-full group-hover:animate-shimmer"></div>
212
  <svg class="w-4 h-4 text-cyan-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
213
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"></path>
214
  </svg>
215
  <span id="actionBtnText" class="uppercase tracking-[0.2em] text-xs">Ejecutar Análisis</span>
216
  </button>
 
220
  <div id="progressBar" class="bg-gradient-to-r from-cyan-500 via-blue-500 to-purple-600 h-1 w-0 shadow-[0_0_10px_#22d3ee]"></div>
221
  </div>
222
 
223
+ <div id="userListContainer" class="flex-1 flex flex-col min-h-0 bg-black/40 rounded border border-white/5 relative overflow-hidden group">
224
  <div class="absolute top-0 left-0 w-full h-4 bg-gradient-to-b from-black/80 to-transparent pointer-events-none z-10"></div>
225
  <h2 class="font-bold text-[9px] text-gray-500 mb-2 uppercase tracking-[0.2em] p-3 border-b border-white/5 sticky top-0 bg-black/40 backdrop-blur-sm">Exploradores Activos</h2>
226
  <div id="userList" class="overflow-y-auto pr-1 space-y-1 custom-scrollbar text-xs p-2"></div>
227
  <div class="absolute bottom-0 left-0 w-full h-4 bg-gradient-to-t from-black/80 to-transparent pointer-events-none z-10"></div>
228
  </div>
229
+ </div>
230
 
231
+ <div id="minimapContainer" class="fixed bottom-4 right-4 z-[200] glass-panel p-2 rounded-lg group w-[320px] transition-opacity duration-300">
232
+ <div class="absolute top-2 right-2 flex gap-1 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
233
+ <button id="zoomOutButton" class="w-6 h-6 bg-black/80 hover:bg-cyan-900/50 rounded border border-white/20 text-white flex items-center justify-center text-xs backdrop-blur-sm transition-colors">-</button>
234
+ <button id="zoomInButton" class="w-6 h-6 bg-black/80 hover:bg-cyan-900/50 rounded border border-white/20 text-white flex items-center justify-center text-xs backdrop-blur-sm transition-colors">+</button>
 
 
 
235
  </div>
236
+ <canvas id="minimap" width="320" height="130" class="w-full h-[130px] bg-black/60 rounded border border-white/10 cursor-crosshair shadow-inner"></canvas>
237
+ <div class="text-[9px] text-center text-cyan-500/40 mt-1 uppercase tracking-[0.3em]">Radar Galáctico</div>
238
+ </div>
239
+
240
+ <div id="missionPanel" class="glass-panel p-3">
241
+ <h3 class="text-sm font-bold mb-2">MISIÓN</h3>
242
+ <ul id="missionList" class="text-xs space-y-1"></ul>
243
  </div>
244
 
245
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
246
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
247
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/geometries/TextGeometry.js"></script>
248
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/FontLoader.js"></script>
 
249
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
250
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
251
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
 
254
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
255
 
256
  <script type="module">
257
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"
258
+ import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signInAnonymously, signOut, setPersistence, browserLocalPersistence } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"
259
+ import { getFirestore, doc, getDoc, setDoc, updateDoc, arrayUnion, onSnapshot, collection, query } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"
 
260
 
261
  const firebaseConfig = {
262
  apiKey: "AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",
 
266
  messagingSenderId: "208887839866",
267
  appId: "1:208887839866:web:adbb697dd0b63195b10fc3",
268
  measurementId: "G-102SEBLQFJ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  }
270
 
271
+ const app = initializeApp(firebaseConfig)
272
+ const auth = getAuth(app)
273
+ const db = getFirestore(app)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ const DEFAULT_GEMINI_KEY = "AIzaSyDmoQNpzgzW21f_WFCU9YbaAeI1fdOJMlo"
276
+ const getLocalGeminiKey = () => {
277
+ try { return localStorage.getItem("GEMINI_API_KEY") || DEFAULT_GEMINI_KEY } catch { return DEFAULT_GEMINI_KEY }
 
 
 
 
 
278
  }
279
+ const setLocalGeminiKey = (k) => { try { localStorage.setItem("GEMINI_API_KEY", k || "") } catch {} }
280
 
281
+ const THREE = window.THREE
282
+ const OrbitControls = THREE.OrbitControls
283
+ const FontLoader = THREE.FontLoader
284
+ const TextGeometry = THREE.TextGeometry
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ let scene, camera, renderer, composer, controls, raycaster, mouse, clock, font
287
+ let hashtagGroup, tooltip, minimapCtx, minimapDotCoords = [], minimapScale = 0.025
288
+ let userId = null, userProfile = null, isAuthReady = false, isFontReady = false
289
+ let userMaps = {}, userProfileCache = {}
290
+ let gameState = { mode: "explorer", energy: 100, rank: "OBSERVADOR", vault: [], score: 0, shield: false, speedMultiplier: 1, cometActive: false, stormActive: false }
291
+ const missions = [
292
+ { type: "gold", target: 5, progress: 0 },
293
+ { type: "visit", target: 3, progress: 0 },
294
+ { type: "bridge", target: 2, progress: 0 }
295
+ ]
296
 
297
+ const normalizeString = (s) => s ? s.normalize("NFD").replace(/[\u0300-\u036f]/g, "") : ""
 
 
298
 
299
+ const loader = new FontLoader()
300
+ loader.load("https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_regular.typeface.json", f => { font = f; isFontReady = true; checkAppReady() })
 
 
 
 
 
301
 
302
+ initFirebase()
 
303
 
304
+ const gkInput = document.getElementById("geminiKeyInput")
305
+ const gkBtn = document.getElementById("saveGeminiKeyBtn")
306
+ if (gkInput && gkBtn) {
307
+ const existing = getLocalGeminiKey()
308
+ if (existing && existing !== DEFAULT_GEMINI_KEY) gkInput.value = existing
309
+ gkBtn.addEventListener("click", e => {
310
+ e.preventDefault()
311
+ setLocalGeminiKey(gkInput.value.trim())
312
+ document.getElementById("geminiKeyStatus").textContent = "GUARDADO"
313
+ setTimeout(() => document.getElementById("geminiKeyStatus").textContent = "", 2000)
314
+ })
315
+ }
316
+
317
+ async function initFirebase () {
318
+ const loginBtn = document.getElementById("loginButton")
319
+ const registerBtn = document.getElementById("registerButton")
320
+ const anonBtn = document.getElementById("btnGoToAnon")
321
+ const saveUserBtn = document.getElementById("saveUsernameButton")
322
+ const backBtn = document.getElementById("backToStep1")
323
+ const logoutBtn = document.getElementById("mainLogoutButton")
324
+ const msg = document.getElementById("loginMessage")
325
+ const email = document.getElementById("loginEmail")
326
+ const pass = document.getElementById("loginPassword")
327
+ const step1 = document.getElementById("authStep1")
328
+ const step2 = document.getElementById("authStep2")
329
+ const overlay = document.getElementById("loginOverlay")
330
+ const ui = document.getElementById("ui")
331
+
332
+ registerBtn.addEventListener("click", async () => {
333
+ if (email.value.length < 6) { msg.innerText = "Email/Pass muy corto"; return }
334
+ try {
335
+ await setPersistence(auth, browserLocalPersistence)
336
+ await createUserWithEmailAndPassword(auth, email.value, pass.value)
337
+ } catch (e) { msg.innerText = "Error: " + e.message }
338
+ })
339
 
340
+ loginBtn.addEventListener("click", async () => {
341
+ try {
342
+ await setPersistence(auth, browserLocalPersistence)
343
+ await signInWithEmailAndPassword(auth, email.value, pass.value)
344
+ } catch (e) { msg.innerText = "Error: " + e.message }
345
+ })
346
+
347
+ anonBtn.addEventListener("click", () => {
348
+ step1.classList.add("hidden")
349
+ step2.classList.remove("hidden")
350
+ })
351
+
352
+ backBtn.addEventListener("click", () => {
353
+ step2.classList.add("hidden")
354
+ step1.classList.remove("hidden")
355
+ msg.innerText = ""
356
+ })
357
+
358
+ saveUserBtn.addEventListener("click", async () => {
359
+ const name = normalizeString(document.getElementById("usernameInput").value.trim())
360
+ if (name.length < 3) { document.getElementById("usernameInput").classList.add("border-red-500"); return }
361
+ document.getElementById("saveUsernameButton").innerText = "ESTABLECIENDO ENLACE..."
362
+ document.getElementById("saveUsernameButton").disabled = true
363
+ try {
364
+ if (!auth.currentUser) await signInAnonymously(auth)
365
+ await setDoc(doc(db, "artifacts", "neuronal-1f3b9", "users", auth.currentUser.uid, "user_data", "profile"), { username: name, energy: 100, rank: "OBSERVADOR", vault: [] })
366
+ overlay.style.opacity = "0"
367
+ setTimeout(() => overlay.style.display = "none", 700)
368
+ ui.classList.remove("hidden")
369
+ setTimeout(() => { ui.style.transform = "translateX(0)"; ui.style.opacity = "1" }, 100)
370
+ document.getElementById("gameHUD").classList.remove("hidden")
371
+ setTimeout(() => document.getElementById("gameHUD").classList.remove("opacity-0"), 500)
372
+ initScene()
373
+ loadAllMaps()
374
+ } catch (e) {
375
+ document.getElementById("saveUsernameButton").innerText = "ERROR DE CONEXIÓN"
376
+ document.getElementById("saveUsernameButton").disabled = false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  }
378
+ })
379
+
380
+ logoutBtn.addEventListener("click", async () => { await signOut(auth); location.reload() })
381
+
382
+ onAuthStateChanged(auth, async user => {
383
+ if (user) {
384
+ userId = user.uid
385
+ isAuthReady = true
386
+ await fetchUserProfile(userId)
387
+ if (userProfile) {
388
+ overlay.style.opacity = "0"
389
+ setTimeout(() => overlay.style.display = "none", 700)
390
+ ui.classList.remove("hidden")
391
+ setTimeout(() => { ui.style.transform = "translateX(0)"; ui.style.opacity = "1" }, 100)
392
+ document.getElementById("authStatus").textContent = userProfile.username
393
+ document.getElementById("gameHUD").classList.remove("hidden")
394
+ setTimeout(() => document.getElementById("gameHUD").classList.remove("opacity-0"), 500)
395
+ updateHUD()
396
+ if (!scene) { initScene(); loadAllMaps() }
397
+ } else {
398
+ step1.classList.add("hidden")
399
+ step2.classList.remove("hidden")
400
+ }
401
+ } else {
402
+ overlay.style.display = "flex"
403
+ overlay.style.opacity = "1"
404
+ step1.classList.remove("hidden")
405
+ step2.classList.add("hidden")
406
+ ui.classList.add("hidden")
407
  }
408
+ })
409
+ }
410
+
411
+ async function fetchUserProfile (uid) {
412
+ const snap = await getDoc(doc(db, "artifacts", "neuronal-1f3b9", "users", uid, "user_data", "profile"))
413
+ if (snap.exists()) {
414
+ userProfile = snap.data()
415
+ userProfileCache[uid] = userProfile
416
+ gameState.energy = userProfile.energy || 100
417
+ gameState.rank = userProfile.rank || "OBSERVADOR"
418
+ gameState.vault = userProfile.vault || []
419
+ updateHUD()
420
+ }
421
  }
422
 
423
+ function updateHUD () {
424
+ document.getElementById("energyBar").style.width = gameState.energy + "%"
425
+ document.getElementById("rankDisplay").innerText = gameState.rank
426
+ document.getElementById("vaultCount").innerText = gameState.vault.length
427
+ }
428
+
429
+ function checkAppReady () { if (isAuthReady && isFontReady && userProfile) { initScene(); loadAllMaps() } }
430
+
431
+ function initScene () {
432
+ if (scene) return
433
+ scene = new THREE.Scene()
434
+ scene.fog = new THREE.FogExp2(0x020205, 0.005)
435
+ camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 2500)
436
+ camera.position.set(0, 5, 30)
437
+ renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" })
438
+ renderer.setSize(innerWidth, innerHeight)
439
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5))
440
+ renderer.toneMapping = THREE.ReinhardToneMapping
441
+ renderer.toneMappingExposure = 1.2
442
+ document.getElementById("container").appendChild(renderer.domElement)
443
+ tooltip = document.getElementById("tooltip")
444
+ controls = new OrbitControls(camera, renderer.domElement)
445
+ controls.enableDamping = true
446
+ controls.dampingFactor = 0.04
447
+ controls.rotateSpeed = 0.5
448
+ controls.zoomSpeed = 0.7
449
+ controls.maxDistance = 600
450
+ controls.target.set(0, 0, 0)
451
+ const renderPass = new RenderPass(scene, camera)
452
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 1.5, 0.4, 0.85)
453
+ bloomPass.threshold = 0.15
454
+ bloomPass.strength = 1.4
455
+ bloomPass.radius = 0.6
456
+ composer = new EffectComposer(renderer)
457
+ composer.addPass(renderPass)
458
+ composer.addPass(bloomPass)
459
+ raycaster = new THREE.Raycaster()
460
+ mouse = new THREE.Vector2()
461
+ hashtagGroup = new THREE.Group()
462
+ scene.add(hashtagGroup)
463
+ createBackground()
464
+ initComet()
465
+ document.getElementById("visualizeButton").addEventListener("click", handleAnalysisAndVisualization)
466
+ ;["level1", "level2", "level3"].forEach(l => {
467
+ document.getElementById(`${l}Slider`).addEventListener("input", e => document.getElementById(`${l}Value`).innerText = e.target.value)
468
+ })
469
+ window.addEventListener("resize", onWindowResize)
470
+ window.addEventListener("mousemove", onPointerMove)
471
+ window.addEventListener("click", onMouseClick)
472
+ const mmCanvas = document.getElementById("minimap")
473
+ if (mmCanvas) {
474
+ minimapCtx = mmCanvas.getContext("2d")
475
+ mmCanvas.addEventListener("click", onMinimapClick)
476
+ }
477
+ document.getElementById("zoomInButton").addEventListener("click", () => { minimapScale *= 1.5; drawMinimap() })
478
+ document.getElementById("zoomOutButton").addEventListener("click", () => { minimapScale /= 1.5; drawMinimap() })
479
+ animate()
480
+ }
481
+
482
+ function createBackground () {
483
+ const count = 5000
484
+ const geo = new THREE.BufferGeometry()
485
+ const pos = new Float32Array(count * 3)
486
+ const sizes = new Float32Array(count)
487
+ for (let i = 0; i < count; i++) {
488
+ pos[i * 3] = (Math.random() - 0.5) * 1500
489
+ pos[i * 3 + 1] = (Math.random() - 0.5) * 1500
490
+ pos[i * 3 + 2] = (Math.random() - 0.5) * 1500
491
+ sizes[i] = Math.random()
492
+ }
493
+ geo.setAttribute("position", new THREE.BufferAttribute(pos, 3))
494
+ geo.setAttribute("size", new THREE.BufferAttribute(sizes, 1))
495
+ const mat = new THREE.PointsMaterial({ color: 0x88ccff, size: 1, transparent: true, opacity: 0.6, sizeAttenuation: true })
496
+ const particles = new THREE.Points(geo, mat)
497
+ scene.add(particles)
498
+ }
499
+
500
+ function initComet () {
501
+ cometGroup = new THREE.Group()
502
+ cometHead = new THREE.Mesh(new THREE.SphereGeometry(0.5, 32, 32), new THREE.MeshBasicMaterial({ color: 0xffffff }))
503
+ const halo = new THREE.Mesh(new THREE.SphereGeometry(0.9, 32, 32), new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.25, blending: THREE.AdditiveBlending }))
504
+ cometHead.add(halo)
505
+ cometGroup.add(cometHead)
506
+ cometLight = new THREE.PointLight(0x00ffff, 2.5, 60)
507
+ cometGroup.add(cometLight)
508
+ scene.add(cometGroup)
509
+ const pGeo = new THREE.BufferGeometry()
510
+ const positions = new Float32Array(400 * 3)
511
+ const colors = new Float32Array(400 * 3)
512
+ const sizes = new Float32Array(400)
513
+ pGeo.setAttribute("position", new THREE.BufferAttribute(positions, 3))
514
+ pGeo.setAttribute("color", new THREE.BufferAttribute(colors, 3))
515
+ pGeo.setAttribute("size", new THREE.BufferAttribute(sizes, 1))
516
+ const pMat = new THREE.PointsMaterial({ vertexColors: true, size: 1, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true })
517
+ cometParticlesMesh = new THREE.Points(pGeo, pMat)
518
+ scene.add(cometParticlesMesh)
519
+ cometParticlesData = []
520
+ for (let i = 0; i < 400; i++) {
521
+ cometParticlesData.push({ life: -1, velocity: new THREE.Vector3() })
522
+ positions[i * 3] = 99999
523
+ }
524
  }
525
 
526
+ function spawnPowerUp () {
527
+ const types = ["double", "shield", "speed"]
528
+ const type = types[Math.floor(Math.random() * types.length)]
529
+ const pu = createNode(type.toUpperCase(), randomPos())
530
+ pu.userData.powerType = type
531
+ }
532
+
533
+ function spawnStorm () {
534
+ gameState.stormActive = true
535
+ renderer.setClearColor(0x000010)
536
+ setTimeout(() => {
537
+ gameState.stormActive = false
538
+ renderer.setClearColor(0x020205)
539
+ }, 30000)
540
+ }
541
+
542
+ function randomPos () {
543
+ const r = 70
544
+ const a = Math.random() * Math.PI * 2
545
+ return new THREE.Vector3(Math.cos(a) * r, Math.random() * 15, Math.sin(a) * r)
546
+ }
547
+
548
+ function createNode (label, pos) {
549
+ const col = label === "GOLD" ? 0xffd700 : 0x22d3ee
550
+ const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.4, 12, 12), new THREE.MeshStandardMaterial({ color: col }))
551
+ sphere.position.copy(pos)
552
+ sphere.userData.label = label
553
+ hashtagGroup.add(sphere)
554
+ const txtGeo = new TextGeometry(label, { font, size: 0.25, height: 0.01 })
555
+ const txtMesh = new THREE.Mesh(txtGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }))
556
+ txtMesh.position.copy(pos).add(new THREE.Vector3(0, 0.6, 0))
557
+ txtMesh.userData.isText = true
558
+ hashtagGroup.add(txtMesh)
559
+ return sphere
560
+ }
561
+
562
+ async function callGemini (topic, mc, vc, svc) {
563
+ const key = getLocalGeminiKey()
564
+ const prompt = gameState.mode === "bridge"
565
+ ? `Puente entre ${topic} y ${document.getElementById("bridgeTargetInput").value}. Genera 3 opciones.`
566
+ : `Tema: ${topic}. ${mc} palabras clave, ${vc} variantes, ${svc} sub‑variantes. JSON puro.`
567
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${key}`
568
+ const resp = await fetch(url, {
569
+ method: "POST",
570
+ headers: { "Content-Type": "application/json" },
571
+ body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
572
+ })
573
+ if (!resp.ok) throw new Error(await resp.text())
574
+ return await resp.json()
575
+ }
576
+
577
+ async function handleAnalysisAndVisualization () {
578
+ const topic = normalizeString(document.getElementById("topicInput").value)
579
+ if (!topic) return
580
+ if (gameState.mode === "miner" && gameState.energy < 10) {
581
+ alert("Energía insuficiente")
582
+ return
583
+ }
584
+ const btn = document.getElementById("visualizeButton")
585
+ const pb = document.getElementById("progressBar")
586
+ const pbc = document.getElementById("progressBarContainer")
587
+ const original = btn.innerHTML
588
+ btn.disabled = true
589
+ btn.innerHTML = '<span class="animate-pulse">PROCESANDO</span>'
590
+ pbc.classList.remove("hidden")
591
+ pb.style.width = "90%"
592
+ pb.style.transition = "width 15s ease-out"
593
+ try {
594
+ const mc = document.getElementById("level1Slider").value
595
+ const vc = document.getElementById("level2Slider").value
596
+ const svc = document.getElementById("level3Slider").value
597
+ const origin = randomPos()
598
+ const res = await callGemini(topic, mc, vc, svc)
599
+ const txt = res.candidates?.[0]?.content?.parts?.[0]?.text
600
+ if (!txt) throw new Error("Respuesta inválida")
601
+ const data = JSON.parse(txt)
602
+ visualizeRoot(topic, origin)
603
+ visualizeHashtags(data.lista_palabras, origin, 1)
604
+ if (gameState.mode === "miner") {
605
+ gameState.energy = Math.max(0, gameState.energy - 10)
606
+ updateHUD()
607
  }
608
+ if (gameState.mode === "bridge") {
609
+ gameState.bridgeHops++
610
+ document.getElementById("bridgeHops").innerText = gameState.bridgeHops
611
+ document.getElementById("bridgeOriginDisplay").innerText = topic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  }
613
+ } catch (e) {
614
+ console.error(e)
615
+ alert("Error: " + e.message)
616
+ } finally {
617
+ btn.disabled = false
618
+ btn.innerHTML = original
619
+ pb.style.width = "100%"
620
+ setTimeout(() => { pbc.classList.add("hidden"); pb.style.width = "0%" }, 500)
621
+ }
622
  }
623
 
624
+ function visualizeRoot (topic, origin) {
625
+ const hue = Math.abs(topic.split("").reduce((a, b) => a + b.charCodeAt(0), 0) % 360)
626
+ const col = `hsl(${hue},75%,60%)`
627
+ const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color(col), emissive: new THREE.Color(col), emissiveIntensity: 2.2, roughness: 0.4 })
628
+ const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.7, 32, 32), mat)
629
+ sphere.position.copy(origin)
630
+ hashtagGroup.add(sphere)
631
+ const txtGeo = new TextGeometry(topic.toUpperCase(), { font, size: 0.6, height: 0.05 })
632
+ const txtMesh = new THREE.Mesh(txtGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }))
633
+ txtMesh.position.copy(origin).add(new THREE.Vector3(0, 1, 0))
634
+ hashtagGroup.add(txtMesh)
635
  }
636
 
637
+ function visualizeHashtags (list, origin, level, parentColor = null) {
638
+ if (!list?.length) return
639
+ list.forEach(item => {
640
+ let tag, variants
641
  if (level === 1) {
642
+ tag = normalizeString(item.palabra_principal)
643
+ variants = item.variantes || []
644
  } else if (level === 2) {
645
+ tag = normalizeString(item.palabra_variante)
646
+ variants = item.sub_variantes || []
647
  } else {
648
+ tag = normalizeString(item)
649
+ variants = []
650
  }
651
+ if (!tag) return
652
+ const hue = Math.abs(tag.split("").reduce((a, b) => a + b.charCodeAt(0), 0) % 360)
653
+ const col = `hsl(${hue},75%,60%)`
654
+ const nodeCol = level === 1 ? col : parentColor
655
+ const isGold = gameState.mode === "miner" && level >= 2 && Math.random() < 0.15
656
+ const mat = new THREE.MeshPhysicalMaterial({
657
+ color: new THREE.Color(isGold ? "#ffd700" : nodeCol),
658
+ emissive: new THREE.Color(isGold ? "#ffd700" : nodeCol),
659
+ emissiveIntensity: isGold ? 2 : level === 1 ? 0.8 : 0.4,
 
 
 
 
 
 
 
 
660
  roughness: 0.2,
661
  metalness: 0.1,
662
+ transmission: 0.1,
663
  transparent: true,
664
  opacity: 0.95
665
+ })
666
+ const theta = (hue / 360) * Math.PI * 2
667
+ let phi = 0
668
+ for (let i = 0; i < tag.length; i++) phi = (phi + tag.charCodeAt(i) * 13) % 180
669
+ phi = ((phi / 180) * 90 + 45) * (Math.PI / 180)
670
+ const radius = 12 / (level * level)
671
+ const cx = radius * Math.sin(phi) * Math.cos(theta)
672
+ const cy = radius * Math.cos(phi)
673
+ const cz = radius * Math.sin(phi) * Math.sin(theta)
674
+ const pos = new THREE.Vector3(cx, cy, cz).add(origin)
675
+ const lineMat = new THREE.LineBasicMaterial({ color: new THREE.Color(nodeCol), transparent: true, opacity: 0.25 })
676
+ const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints([origin, pos]), lineMat)
677
+ hashtagGroup.add(line)
678
+ const sphere = new THREE.Mesh(new THREE.SphereGeometry(level === 1 ? 0.35 : level === 2 ? 0.18 : 0.1, 16, 16), mat)
679
+ sphere.position.copy(pos)
680
+ sphere.userData = { label: tag, isGolden: isGold, level }
681
+ hashtagGroup.add(sphere)
682
+ const txtSize = level === 1 ? 0.35 : level === 2 ? 0.18 : 0.12
683
+ const txtGeo = new TextGeometry(tag.toUpperCase(), { font, size: txtSize, height: 0.01 })
684
+ const txtMesh = new THREE.Mesh(txtGeo, new THREE.MeshBasicMaterial({ color: new THREE.Color(nodeCol) }))
685
+ txtMesh.position.copy(pos).add(new THREE.Vector3(0, (level === 1 ? 0.6 : 0.4), 0))
686
+ txtMesh.userData.isText = true
687
+ hashtagGroup.add(txtMesh)
688
+ visualizeHashtags(variants, pos, level + 1, isGold ? "#ffd700" : nodeCol)
689
+ })
690
+ }
691
+
692
+ function loadAllMaps () {
693
+ if (!db || !font) { setTimeout(loadAllMaps, 500); return }
694
+ const q = query(collection(db, "artifacts", "neuronal-1f3b9", "public", "data", "maps"))
695
+ onSnapshot(q, snap => {
696
+ while (hashtagGroup.children.length) {
697
+ const obj = hashtagGroup.children[0]
698
+ hashtagGroup.remove(obj)
699
+ if (obj.geometry) obj.geometry.dispose()
700
+ if (obj.material) {
701
+ if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose())
702
+ else obj.material.dispose()
703
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  }
705
+ userMaps = {}
706
+ snap.docs.forEach(d => {
707
+ const m = d.data()
708
+ if (!m.origin) return
709
+ const o = new THREE.Vector3(m.origin.x, m.origin.y, m.origin.z)
710
+ if (!userMaps[m.userId]) userMaps[m.userId] = []
711
+ userMaps[m.userId].push(o)
712
+ try {
713
+ const data = JSON.parse(m.data)
714
+ visualizeRoot(m.topic, o)
715
+ visualizeHashtags(data.lista_palabras, o, 1)
716
+ } catch {}
717
+ })
718
+ const list = document.getElementById("userList")
719
+ if (list) list.innerHTML = ""
720
+ Object.keys(userMaps).forEach(uid => {
721
+ const prof = userProfileCache[uid]
722
+ const name = prof ? prof.username : "ANON " + uid.slice(0, 4)
723
+ const item = document.createElement("div")
724
+ item.className = "text-cyan-400 hover:text-white cursor-pointer hover:bg-white/5 p-1 rounded transition-colors text-[10px] tracking-widest"
725
+ item.innerHTML = `> <span class="font-bold">${name}</span>`
726
+ item.onclick = () => teleportToUser(uid)
727
+ list.appendChild(item)
728
+ const cent = new THREE.Vector3()
729
+ userMaps[uid].forEach(p => cent.add(p))
730
+ cent.divideScalar(userMaps[uid].length)
731
+ createUserSun(cent, name, uid === userId)
732
+ })
733
+ initComet()
734
+ drawMinimap()
735
+ focusOnUserMaps()
736
+ })
737
+ }
738
+
739
+ function createUserSun (pos, name, isMe) {
740
+ const col = isMe ? 0xffaa00 : 0x00ff88
741
+ const mat = new THREE.MeshStandardMaterial({ color: col, emissive: col, emissiveIntensity: 1.8, roughness: 0.2 })
742
+ const mesh = new THREE.Mesh(new THREE.SphereGeometry(3, 32, 32), mat)
743
+ mesh.position.copy(pos)
744
+ hashtagGroup.add(mesh)
745
+ const txtGeo = new TextGeometry(name.toUpperCase(), { font, size: 1.2 })
746
+ const txtMesh = new THREE.Mesh(txtGeo, new THREE.MeshBasicMaterial({ color: 0xffffff }))
747
+ txtMesh.position.copy(pos).add(new THREE.Vector3(0, 4, 0))
748
+ hashtagGroup.add(txtMesh)
749
+ }
750
+
751
+ function focusOnUserMaps () {
752
+ if (!controls || !userId || !userMaps[userId]) return
753
+ const c = getCurrentUserCentroid()
754
+ controls.target.copy(c)
755
+ camera.position.copy(c).add(new THREE.Vector3(0, 10, 40))
756
+ }
757
+
758
+ function getCurrentUserCentroid () {
759
+ if (!userId || !userMaps[userId] || userMaps[userId].length === 0) return new THREE.Vector3(0, 0, 0)
760
+ const cen = new THREE.Vector3()
761
+ userMaps[userId].forEach(p => cen.add(p))
762
+ cen.divideScalar(userMaps[userId].length)
763
+ return cen
764
+ }
765
+
766
+ function teleportToUser (uid) {
767
+ if (!userMaps[uid]) return
768
+ const cen = new THREE.Vector3()
769
+ userMaps[uid].forEach(p => cen.add(p))
770
+ cen.divideScalar(userMaps[uid].length)
771
+ controls.target.copy(cen)
772
+ camera.position.copy(cen).add(new THREE.Vector3(0, 10, 40))
773
+ }
774
+
775
+ function drawMinimap () {
776
+ if (!minimapCtx) return
777
+ const w = minimapCtx.canvas.width
778
+ const h = minimapCtx.canvas.height
779
+ minimapCtx.clearRect(0, 0, w, h)
780
+ minimapDotCoords = []
781
+ const myC = getCurrentUserCentroid()
782
+ Object.keys(userMaps).forEach(uid => {
783
+ const cen = new THREE.Vector3()
784
+ userMaps[uid].forEach(p => cen.add(p))
785
+ cen.divideScalar(userMaps[uid].length)
786
+ const x = w / 2 + (cen.x - myC.x) * minimapScale
787
+ const y = h / 2 + (cen.z - myC.z) * minimapScale
788
+ const isMe = uid === userId
789
+ minimapDotCoords.push({ x, y, uid })
790
+ minimapCtx.fillStyle = isMe ? "#22d3ee" : "#64748b"
791
+ minimapCtx.beginPath()
792
+ minimapCtx.arc(x, y, isMe ? 4 : 2.5, 0, Math.PI * 2)
793
+ minimapCtx.fill()
794
+ if (isMe) {
795
+ minimapCtx.strokeStyle = "rgba(34,211,238,0.3)"
796
+ minimapCtx.beginPath()
797
+ minimapCtx.arc(x, y, 8, 0, Math.PI * 2)
798
+ minimapCtx.stroke()
799
  }
800
+ })
801
+ if (cometGroup) {
802
+ const cx = w / 2 + (cometGroup.position.x - myC.x) * minimapScale
803
+ const cy = h / 2 + (cometGroup.position.z - myC.z) * minimapScale
804
+ minimapCtx.beginPath()
805
+ minimapCtx.arc(cx, cy, 3, 0, Math.PI * 2)
806
+ minimapCtx.fillStyle = "rgba(0,255,255,0.4)"
807
+ minimapCtx.fill()
808
+ minimapCtx.beginPath()
809
+ minimapCtx.arc(cx, cy, 1.5, 0, Math.PI * 2)
810
+ minimapCtx.fillStyle = "#ffffff"
811
+ minimapCtx.fill()
812
+ }
813
  }
814
 
815
+ function onMinimapClick (e) {
816
+ const rect = minimapCtx.canvas.getBoundingClientRect()
817
+ const x = e.clientX - rect.left
818
+ const y = e.clientY - rect.top
819
+ let closest = null
820
+ let minDist = 20
821
+ minimapDotCoords.forEach(d => {
822
+ const dX = d.x - x
823
+ const dY = d.y - y
824
+ const dist = Math.sqrt(dX * dX + dY * dY)
825
+ if (dist < minDist) { minDist = dist; closest = d.uid }
826
+ })
827
+ if (closest) teleportToUser(closest)
828
+ }
829
+
830
+ function onWindowResize () {
831
+ camera.aspect = innerWidth / innerHeight
832
+ camera.updateProjectionMatrix()
833
+ renderer.setSize(innerWidth, innerHeight)
834
+ composer.setSize(innerWidth, innerHeight)
835
+ }
836
+
837
+ function onPointerMove (e) {
838
+ mouse.x = (e.clientX / innerWidth) * 2 - 1
839
+ mouse.y = -(e.clientY / innerHeight) * 2 + 1
840
+ tooltip.style.left = e.clientX + 20 + "px"
841
+ tooltip.style.top = e.clientY + "px"
842
+ }
843
+
844
+ function onMouseClick () {
845
+ raycaster.setFromCamera(mouse, camera)
846
+ const objs = hashtagGroup.children.filter(o => !o.userData.isText)
847
+ const hits = raycaster.intersectObjects(objs, false)
848
+ if (!hits.length) return
849
+ const obj = hits[0].object
850
+ if (obj.userData.isGolden) {
851
+ gameState.vault.push(obj.userData.label)
852
+ scene.add(createExplosion(obj.position))
853
+ hashtagGroup.remove(obj)
854
+ gameState.energy = Math.min(100, gameState.energy + 12)
855
+ updateHUD()
856
+ }
857
+ if (obj.userData.powerType) {
858
+ if (obj.userData.powerType === "shield") {
859
+ gameState.shield = true
860
+ setTimeout(() => gameState.shield = false, 5000)
861
  }
862
+ if (obj.userData.powerType === "speed") {
863
+ controls.rotateSpeed *= 2
864
+ setTimeout(() => controls.rotateSpeed /= 2, 5000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  }
866
+ if (obj.userData.powerType === "double") {
867
+ gameState.score *= 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  }
869
+ scene.remove(obj)
870
+ }
871
  }
872
 
873
+ function updateRaycaster () {
874
+ raycaster.setFromCamera(mouse, camera)
875
+ const targets = [...hashtagGroup.children]
876
+ const hits = raycaster.intersectObjects(targets, false)
877
+ if (hits.length) {
878
+ const o = hits[0].object
879
+ if (o.userData.label) {
880
+ tooltip.classList.remove("hidden")
881
+ tooltip.innerHTML = `<div class="text-cyan-300 font-bold text-sm">${o.userData.label}</div><div class="text-gray-400 text-[10px] uppercase">Nivel ${o.userData.level}</div>`
882
+ document.body.style.cursor = "pointer"
883
+ } else {
884
+ tooltip.classList.add("hidden")
885
+ document.body.style.cursor = "default"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
886
  }
887
+ } else {
888
+ tooltip.classList.add("hidden")
889
+ document.body.style.cursor = "default"
890
+ }
891
  }
892
 
893
+ function createExplosion (pos, color = 0xff0000) {
894
+ const geo = new THREE.BufferGeometry()
895
+ const cnt = 30
896
+ const positions = new Float32Array(cnt * 3)
897
+ for (let i = 0; i < cnt * 3; i++) positions[i] = (Math.random() - 0.5) * 2
898
+ geo.setAttribute("position", new THREE.BufferAttribute(positions, 3))
899
+ const mat = new THREE.PointsMaterial({ color, size: 0.5, transparent: true })
900
+ const pts = new THREE.Points(geo, mat)
901
+ pts.position.copy(pos)
902
+ let life = 1
903
+ function anim () {
904
+ life -= 0.05
905
+ pts.scale.multiplyScalar(1.1)
906
+ mat.opacity = life
907
+ if (life > 0) requestAnimationFrame(anim)
908
+ else scene.remove(pts)
909
+ }
910
+ anim()
911
+ return pts
912
+ }
913
+
914
+ function animate () {
915
+ requestAnimationFrame(animate)
916
+ const dt = clock.getDelta()
917
+ controls.update()
918
+ if (gameState.cometActive) updateComet(dt)
919
+ if (gameState.stormActive) updateStorm(dt)
920
+ updateRaycaster()
921
+ drawMinimap()
922
+ hashtagGroup.children.forEach(o => {
923
+ if (o.userData.isText) {
924
+ o.lookAt(camera.position)
925
+ const d = o.position.distanceTo(camera.position)
926
+ let s = (1 / d) * 12
927
+ s = Math.max(0.6, Math.min(5, s))
928
+ o.scale.set(s, s, s)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
929
  }
930
+ })
931
+ composer.render()
932
  }
933
 
934
+ window.setGameMode = setGameMode
935
+ initScene()
 
 
936
  </script>
937
  </body>
938
+ </html>