Spaces:
Running
Running
Update index.html
Browse files- index.html +755 -881
index.html
CHANGED
|
@@ -7,932 +7,806 @@
|
|
| 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 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
}
|
| 19 |
-
.
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 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)">
|
| 89 |
-
<div class="absolute inset-0 border border-cyan-500/0 rounded-lg group-hover:border-cyan-500/20 pointer-events-none transition-colors"></div>
|
| 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>
|
| 107 |
-
<input type="text" id="usernameInput" class="w-full p-4 rounded-lg bg-black/50 border border-white/10 text-white placeholder-gray-600 focus:outline-none focus:border-purple-500 transition-all text-lg mb-8 text-center tracking-[0.2em] font-bold" placeholder="NICKNAME">
|
| 108 |
-
<button id="saveUsernameButton" class="w-full bg-gradient-to-r from-cyan-600 to-blue-700 hover:from-cyan-500 hover:to-blue-600 text-white font-bold py-4 px-4 rounded-lg shadow-[0_0_20px_rgba(6,182,212,0.4)] transition-all uppercase text-sm tracking-widest mb-4">
|
| 109 |
-
Establecer Enlace Neural
|
| 110 |
-
</button>
|
| 111 |
-
<button id="backToStep1" class="w-full mt-2 text-gray-500 text-[10px] hover:text-white transition-colors uppercase tracking-widest">Abortar Secuencia</button>
|
| 112 |
-
</div>
|
| 113 |
-
</div>
|
| 114 |
-
</div>
|
| 115 |
|
| 116 |
-
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
<div class="
|
| 122 |
-
|
| 123 |
-
<
|
| 124 |
</div>
|
| 125 |
-
</
|
| 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 |
-
|
| 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>
|
| 149 |
-
<span class="text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 to-blue-500 tracking-[0.2em]">NEXUS</span>
|
| 150 |
-
</h1>
|
| 151 |
-
<div class="flex items-center gap-3">
|
| 152 |
-
<span id="authStatus" class="text-[9px] text-cyan-500/80 uppercase tracking-widest border border-cyan-500/20 px-2 py-1 rounded bg-cyan-900/10"></span>
|
| 153 |
-
<button id="mainLogoutButton" class="text-red-400/70 hover:text-red-300 text-[10px] uppercase transition-colors font-bold tracking-wider">Salir</button>
|
| 154 |
-
</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">
|
| 178 |
-
<div>
|
| 179 |
-
<label class="text-[10px] text-gray-400 flex justify-between uppercase tracking-wider mb-2">Densidad Nivel 1 <span id="level1Value" class="text-cyan-400 font-mono bg-cyan-900/20 px-1 rounded">10</span></label>
|
| 180 |
-
<input type="range" id="level1Slider" min="1" max="15" value="10" class="w-full h-1 bg-gray-800 rounded-lg appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400">
|
| 181 |
-
</div>
|
| 182 |
-
<div>
|
| 183 |
-
<label class="text-[10px] text-gray-400 flex justify-between uppercase tracking-wider mb-2">Densidad Nivel 2 <span id="level2Value" class="text-cyan-400 font-mono bg-cyan-900/20 px-1 rounded">5</span></label>
|
| 184 |
-
<input type="range" id="level2Slider" min="5" max="8" value="5" class="w-full h-1 bg-gray-800 rounded-lg appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400">
|
| 185 |
-
</div>
|
| 186 |
-
<div>
|
| 187 |
-
<label class="text-[10px] text-gray-400 flex justify-between uppercase tracking-wider mb-2">Densidad Nivel 3 <span id="level3Value" class="text-cyan-400 font-mono bg-cyan-900/20 px-1 rounded">3</span></label>
|
| 188 |
-
<input type="range" id="level3Slider" min="1" max="3" value="3" class="w-full h-1 bg-gray-800 rounded-lg appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400">
|
| 189 |
-
</div>
|
| 190 |
-
</div>
|
| 191 |
-
|
| 192 |
-
<details class="mb-5 bg-black/40 rounded border border-white/5 overflow-hidden group">
|
| 193 |
-
<summary class="cursor-pointer text-[10px] text-blue-300/80 font-bold p-3 uppercase tracking-wide hover:bg-white/5 hover:text-cyan-300 transition-colors list-none flex justify-between items-center">
|
| 194 |
-
<span>Configuración API</span>
|
| 195 |
-
<span class="text-xs group-open:rotate-180 transition-transform duration-300 text-cyan-500">▼</span>
|
| 196 |
-
</summary>
|
| 197 |
-
<div class="p-4 space-y-3 border-t border-white/5 bg-black/60">
|
| 198 |
-
<div>
|
| 199 |
-
<label class="block text-[9px] text-gray-500 mb-1 uppercase tracking-widest">Gemini API Key</label>
|
| 200 |
-
<input type="password" id="geminiKeyInput" class="w-full p-2 rounded bg-black/50 text-white border border-white/10 text-xs focus:border-cyan-500/50 focus:outline-none font-mono tracking-tighter" placeholder="Pegar Key aquí...">
|
| 201 |
-
</div>
|
| 202 |
-
<div class="flex items-center gap-3 justify-end">
|
| 203 |
-
<span id="geminiKeyStatus" class="text-[9px] text-green-400 italic font-mono"></span>
|
| 204 |
-
<button id="saveGeminiKeyBtn" class="bg-white/5 hover:bg-white/10 px-3 py-1.5 rounded text-gray-300 text-[10px] border border-white/10 transition-colors uppercase tracking-wider hover:border-cyan-500/30 hover:text-cyan-300">Guardar</button>
|
| 205 |
-
</div>
|
| 206 |
-
</div>
|
| 207 |
-
</details>
|
| 208 |
-
|
| 209 |
-
<div class="flex gap-2 mb-2">
|
| 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>
|
| 217 |
</div>
|
| 218 |
|
| 219 |
-
<div id="
|
| 220 |
-
<
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
<
|
| 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 |
-
|
| 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 |
-
|
| 241 |
-
|
| 242 |
-
<
|
|
|
|
|
|
|
| 243 |
</div>
|
|
|
|
|
|
|
| 244 |
|
| 245 |
-
|
| 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>
|
| 252 |
-
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
|
| 253 |
-
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.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",
|
| 263 |
-
authDomain: "neuronal-1f3b9.firebaseapp.com",
|
| 264 |
-
projectId: "neuronal-1f3b9",
|
| 265 |
-
storageBucket: "neuronal-1f3b9.firebasestorage.app",
|
| 266 |
-
messagingSenderId: "208887839866",
|
| 267 |
-
appId: "1:208887839866:web:adbb697dd0b63195b10fc3",
|
| 268 |
-
measurementId: "G-102SEBLQFJ"
|
| 269 |
-
}
|
| 270 |
|
| 271 |
-
|
| 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 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 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 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
|
| 358 |
-
|
| 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 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 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 |
-
|
| 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 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
}
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 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 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
}
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
sphere.position.copy(pos)
|
| 552 |
-
sphere.userData
|
| 553 |
hashtagGroup.add(sphere)
|
| 554 |
-
const
|
| 555 |
-
const
|
| 556 |
-
txtMesh
|
| 557 |
-
txtMesh.
|
|
|
|
| 558 |
hashtagGroup.add(txtMesh)
|
| 559 |
-
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 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 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 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 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 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 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
}
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 770 |
cen.divideScalar(userMaps[uid].length)
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 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(
|
| 810 |
-
minimapCtx.
|
| 811 |
-
minimapCtx.fill()
|
| 812 |
}
|
| 813 |
-
}
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
const
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
}
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
}
|
| 871 |
}
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 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 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
|
|
|
|
|
|
|
|
|
| 909 |
}
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
</body>
|
| 938 |
</html>
|
|
|
|
| 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{scrollbar-width:none;-ms-overflow-style:none;background:#020205;user-select:none}
|
| 11 |
+
body::-webkit-scrollbar{display:none}
|
| 12 |
+
.glass-panel{background:rgba(10,15,30,.75);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,.08);box-shadow:0 8px 32px rgba(0,0,0,.6);border-radius:8px;padding:1rem}
|
| 13 |
+
@keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
| 14 |
+
.animate-shimmer{animation:shimmer 2s infinite}
|
| 15 |
+
.custom-scrollbar::-webkit-scrollbar{width:4px}
|
| 16 |
+
.custom-scrollbar::-webkit-scrollbar-track{background:rgba(0,0,0,.3)}
|
| 17 |
+
.custom-scrollbar::-webkit-scrollbar-thumb{background:#22d3ee;border-radius:4px}
|
| 18 |
+
@keyframes glitch-anim{0%{transform:translate(0)}20%{transform:translate(-2px,2px)}40%{transform:translate(-2px,-2px)}60%{transform:translate(2px,2px)}80%{transform:translate(2px,-2px)}100%{transform:translate(0)}}
|
| 19 |
+
.glitch-active{animation:glitch-anim .2s cubic-bezier(.25,.46,.45,.94) both infinite;filter:hue-rotate(90deg) contrast(1.5)}
|
| 20 |
+
.golden-node{box-shadow:0 0 15px #ffd700;border:1px solid #ffd700}
|
| 21 |
+
#missionPanel{right:1rem;top:6rem;position:fixed;width:200px;z-index:95}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
</style>
|
| 23 |
</head>
|
| 24 |
<body class="m-0 overflow-hidden font-[Orbitron] text-slate-200">
|
| 25 |
|
| 26 |
+
<div id="glitchOverlay" class="fixed inset-0 pointer-events-none z-[99999] hidden mix-blend-overlay bg-red-900/20"></div>
|
| 27 |
|
| 28 |
+
<div id="loginOverlay" class="fixed inset-0 bg-black/95 z-[9999] flex items-center justify-center transition-opacity duration-700 opacity-100">
|
| 29 |
+
<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">
|
| 30 |
+
<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>
|
| 31 |
|
| 32 |
+
<div id="authStep1">
|
| 33 |
+
<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>
|
| 34 |
+
<p class="text-cyan-500/60 text-center mb-8 text-[10px] tracking-[0.3em] uppercase">Visualizador Semántico Neural v2.5</p>
|
| 35 |
|
| 36 |
+
<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)]">
|
| 37 |
+
<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"/></svg>
|
| 38 |
+
Entrar como Anónimo
|
| 39 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
<div class="relative flex py-2 items-center mb-4"><div class="flex-grow border-t border-white/10"></div><span class="flex-shrink-0 mx-4 text-gray-500 text-[10px] uppercase tracking-wider">Credenciales de Acceso</span><div class="flex-grow border-t border-white/10"></div></div>
|
| 42 |
|
| 43 |
+
<form id="emailAuthForm" class="space-y-4">
|
| 44 |
+
<div class="relative group"><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)"><div class="absolute inset-0 border border-cyan-500/0 rounded-lg group-hover:border-cyan-500/20 pointer-events-none transition-colors"></div></div>
|
| 45 |
+
<div class="relative group"><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"><div class="absolute inset-0 border border-cyan-500/0 rounded-lg group-hover:border-cyan-500/20 pointer-events-none transition-colors"></div></div>
|
| 46 |
+
<div class="flex gap-3 pt-2">
|
| 47 |
+
<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>
|
| 48 |
+
<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>
|
| 49 |
</div>
|
| 50 |
+
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
<p id="loginMessage" class="text-center text-xs mt-4 min-h-[1.5rem] text-red-400 font-bold tracking-wide"></p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</div>
|
| 54 |
|
| 55 |
+
<div id="authStep2" class="hidden">
|
| 56 |
+
<h2 class="text-2xl font-bold text-white mb-2 text-center">Identidad Digital</h2>
|
| 57 |
+
<p class="text-gray-400 text-xs mb-8 text-center">Asigna un nombre clave a tu constelación.</p>
|
| 58 |
+
<input type="text" id="usernameInput" class="w-full p-4 rounded-lg bg-black/50 border border-white/10 text-white placeholder-gray-600 focus:outline-none focus:border-purple-500 transition-all text-lg mb-8 text-center tracking-[0.2em] font-bold" placeholder="NICKNAME">
|
| 59 |
+
<button id="saveUsernameButton" class="w-full bg-gradient-to-r from-cyan-600 to-blue-700 hover:from-cyan-500 hover:to-blue-600 text-white font-bold py-4 px-4 rounded-lg shadow-[0_0_20px_rgba(6,182,212,0.4)] transition-all uppercase text-sm tracking-widest mb-4">Establecer Enlace Neural</button>
|
| 60 |
+
<button id="backToStep1" class="w-full mt-2 text-gray-500 text-[10px] hover:text-white transition-colors uppercase tracking-widest">Abortar Secuencia</button>
|
|
|
|
|
|
|
|
|
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
+
</div>
|
| 64 |
|
| 65 |
+
<div id="container" class="w-screen h-screen fixed top-0 left-0 bg-[#020205]"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
+
<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">
|
| 68 |
+
<div class="glass-panel px-4 py-2 rounded-lg flex items-center gap-4 border-t border-yellow-500/30 pointer-events-auto">
|
| 69 |
+
<div><div class="text-[9px] text-yellow-500 uppercase tracking-widest">Energía Neural</div><div class="w-32 h-2 bg-gray-800 rounded-full mt-1 overflow-hidden border border-white/10"><div id="energyBar" class="h-full bg-yellow-400 w-full transition-all duration-500 shadow-[0_0_10px_#fbbf24]"></div></div></div>
|
| 70 |
+
<div class="text-right border-l border-white/10 pl-4"><div class="text-[9px] text-cyan-500 uppercase tracking-widest">Rango</div><div id="rankDisplay" class="text-sm font-bold text-white tracking-widest">OBSERVADOR</div></div>
|
| 71 |
+
<div class="text-right pl-4 border-l border-white/10"><div class="text-[9px] text-purple-500 uppercase tracking-widest">Bóveda</div><div id="vaultCount" class="text-sm font-bold text-white font-mono">0</div></div>
|
| 72 |
</div>
|
| 73 |
+
<div id="missionDisplay" class="glass-panel px-6 py-2 rounded-lg text-center pointer-events-auto"><div class="text-[9px] text-green-400 uppercase tracking-widest mb-1">Modo Activo</div><div id="activeModeText" class="text-xs font-bold text-white tracking-[0.2em]">EXPLORACIÓN LIBRE</div></div>
|
| 74 |
+
</div>
|
| 75 |
|
| 76 |
+
<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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
<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">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
<div class="flex items-center justify-between mb-4 border-b border-white/10 pb-4">
|
| 81 |
+
<h1 class="text-lg font-bold text-white flex items-center space-x-3"><div class="w-2 h-2 bg-cyan-400 rounded-full animate-pulse shadow-[0_0_10px_#22d3ee]"></div><span class="text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 to-blue-500 tracking-[0.2em]">NEXUS</span></h1>
|
| 82 |
+
<div class="flex items-center gap-3"><span id="authStatus" class="text-[9px] text-cyan-500/80 uppercase tracking-widest border border-cyan-500/20 px-2 py-1 rounded bg-cyan-900/10"></span><button id="mainLogoutButton" class="text-red-400/70 hover:text-red-300 text-[10px] uppercase transition-colors font-bold tracking-wider">Salir</button></div>
|
| 83 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
<div class="grid grid-cols-3 gap-2 mb-4">
|
| 86 |
+
<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>
|
| 87 |
+
<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>
|
| 88 |
+
<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>
|
| 89 |
+
</div>
|
| 90 |
|
| 91 |
+
<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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
<div id="bridgeControls" class="hidden space-y-3 mb-4 p-3 bg-purple-900/10 rounded border border-purple-500/30">
|
| 94 |
+
<div class="text-[10px] text-purple-300 uppercase tracking-widest mb-1">Objetivo del Puente</div>
|
| 95 |
+
<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)">
|
| 96 |
+
<div class="text-[9px] text-gray-400">Origen: <span id="bridgeOriginDisplay" class="text-white font-bold">Sin definir</span></div>
|
| 97 |
+
<div class="text-[9px] text-gray-400">Saltos: <span id="bridgeHops" class="text-white font-bold">0</span></div>
|
| 98 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
<div class="relative mb-5 group"><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><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..."></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
<div class="space-y-5 mb-6 px-1">
|
| 103 |
+
<div><label class="text-[10px] text-gray-400 flex justify-between uppercase tracking-wider mb-2">Densidad Nivel 1 <span id="level1Value" class="text-cyan-400 font-mono bg-cyan-900/20 px-1 rounded">10</span></label><input type="range" id="level1Slider" min="1" max="15" value="10" class="w-full h-1 bg-gray-800 rounded-lg appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400"></div>
|
| 104 |
+
<div><label class="text-[10px] text-gray-400 flex justify-between uppercase tracking-wider mb-2">Densidad Nivel 2 <span id="level2Value" class="text-cyan-400 font-mono bg-cyan-900/20 px-1 rounded">5</span></label><input type="range" id="level2Slider" min="5" max="8" value="5" class="w-full h-1 bg-gray-800 rounded-lg appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400"></div>
|
| 105 |
+
<div><label class="text-[10px] text-gray-400 flex justify-between uppercase tracking-wider mb-2">Densidad Nivel 3 <span id="level3Value" class="text-cyan-400 font-mono bg-cyan-900/20 px-1 rounded">3</span></label><input type="range" id="level3Slider" min="1" max="3" value="3" class="w-full h-1 bg-gray-800 rounded-lg appearance-none cursor-pointer accent-cyan-500 hover:accent-cyan-400"></div>
|
| 106 |
+
</div>
|
| 107 |
|
| 108 |
+
<details class="mb-5 bg-black/40 rounded border border-white/5 overflow-hidden group">
|
| 109 |
+
<summary class="cursor-pointer text-[10px] text-blue-300/80 font-bold p-3 uppercase tracking-wide hover:bg-white/5 hover:text-cyan-300 transition-colors list-none flex justify-between items-center"><span>Configuración API</span><span class="text-xs group-open:rotate-180 transition-transform duration-300 text-cyan-500">▼</span></summary>
|
| 110 |
+
<div class="p-4 space-y-3 border-t border-white/5 bg-black/60">
|
| 111 |
+
<div><label class="block text-[9px] text-gray-500 mb-1 uppercase tracking-widest">Gemini API Key</label><input type="password" id="geminiKeyInput" class="w-full p-2 rounded bg-black/50 text-white border border-white/10 text-xs focus:border-cyan-500/50 focus:outline-none font-mono tracking-tighter" placeholder="Pegar Key aquí..."></div>
|
| 112 |
+
<div class="flex items-center gap-3 justify-end"><span id="geminiKeyStatus" class="text-[9px] text-green-400 italic font-mono"></span><button id="saveGeminiKeyBtn" class="bg-white/5 hover:bg-white/10 px-3 py-1.5 rounded text-gray-300 text-[10px] border border-white/10 transition-colors uppercase tracking-wider hover:border-cyan-500/30 hover:text-cyan-300">Guardar</button></div>
|
| 113 |
+
</div>
|
| 114 |
+
</details>
|
| 115 |
+
|
| 116 |
+
<div class="flex gap-2 mb-2"><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)]"><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><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"><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"/></svg><span id="actionBtnText" class="uppercase tracking-[0.2em] text-xs">Ejecutar Análisis</span></button></div>
|
| 117 |
+
|
| 118 |
+
<div id="progressBarContainer" class="w-full bg-gray-900 rounded-full h-1 mb-4 overflow-hidden hidden border border-white/10"><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></div>
|
| 119 |
+
|
| 120 |
+
<div id="userListContainer" class="flex-1 flex flex-col min-h-0 bg-black/40 rounded border border-white/5 relative overflow-hidden group"><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><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><div id="userList" class="overflow-y-auto pr-1 space-y-1 custom-scrollbar text-xs p-2"></div><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></div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<div id="minimapContainer" class="fixed bottom-4 right-4 z-[200] glass-panel p-2 rounded-lg group w-[320px] transition-opacity duration-300">
|
| 124 |
+
<div class="absolute top-2 right-2 flex gap-1 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"><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><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></div><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><div class="text-[9px] text-center text-cyan-500/40 mt-1 uppercase tracking-[0.3em]">Radar Galáctico</div></div>
|
| 125 |
+
|
| 126 |
+
<div id="missionPanel" class="glass-panel p-3"><h3 class="text-sm font-bold mb-2">MISIÓN</h3><ul id="missionList" class="text-xs space-y-1"></ul></div>
|
| 127 |
+
|
| 128 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
|
| 129 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
| 130 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/geometries/TextGeometry.js"></script>
|
| 131 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/FontLoader.js"></script>
|
| 132 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
|
| 133 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
|
| 134 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
|
| 135 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
|
| 136 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
|
| 137 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
|
| 138 |
+
|
| 139 |
+
<script type="module">
|
| 140 |
+
import {initializeApp} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js"
|
| 141 |
+
import {getAuth,onAuthStateChanged,createUserWithEmailAndPassword,signInWithEmailAndPassword,signInAnonymously,signOut,setPersistence,browserLocalPersistence} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js"
|
| 142 |
+
import {getFirestore,doc,getDoc,setDoc,updateDoc,arrayUnion,onSnapshot,collection,query,setLogLevel} from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js"
|
| 143 |
+
|
| 144 |
+
const firebaseConfig = {
|
| 145 |
+
apiKey:"AIzaSyB4EZKkAH3weFSZNBQA8Y63gt5XbaeZGsQ",
|
| 146 |
+
authDomain:"neuronal-1f3b9.firebaseapp.com",
|
| 147 |
+
projectId:"neuronal-1f3b9",
|
| 148 |
+
storageBucket:"neuronal-1f3b9.firebasestorage.app",
|
| 149 |
+
messagingSenderId:"208887839866",
|
| 150 |
+
appId:"1:208887839866:web:adbb697dd0b63195b10fc3",
|
| 151 |
+
measurementId:"G-102SEBLQFJ"
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
const app = initializeApp(firebaseConfig)
|
| 155 |
+
const auth = getAuth(app)
|
| 156 |
+
const db = getFirestore(app)
|
| 157 |
+
setLogLevel('Silent')
|
| 158 |
+
|
| 159 |
+
const DEFAULT_GEMINI_KEY = "AIzaSyDmoQNpzgzW21f_WFCU9YbaAeI1fdOJMlo"
|
| 160 |
+
const getLocalGeminiKey = () => { try { return localStorage.getItem("GEMINI_API_KEY") || DEFAULT_GEMINI_KEY } catch { return DEFAULT_GEMINI_KEY } }
|
| 161 |
+
const setLocalGeminiKey = k => { try { localStorage.setItem("GEMINI_API_KEY", k || "") } catch {} }
|
| 162 |
+
|
| 163 |
+
const THREE = window.THREE
|
| 164 |
+
const OrbitControls = THREE.OrbitControls
|
| 165 |
+
const FontLoader = THREE.FontLoader
|
| 166 |
+
const TextGeometry = THREE.TextGeometry
|
| 167 |
+
|
| 168 |
+
let scene,camera,renderer,composer,controls,raycaster,mouse,clock,font
|
| 169 |
+
let hashtagGroup,tooltip,minimapCtx,minimapDotCoords=[],minimapScale=0.025
|
| 170 |
+
let cometGroup,cometHead,cometLight,cometParticlesMesh,cometParticlesData,cometAngle=0
|
| 171 |
+
let solar,bridgeControls,missionList,missionPanel
|
| 172 |
+
let userId=null,userProfile=null,isAuthReady=false,isFontReady=false
|
| 173 |
+
let userMaps={},userProfileCache={}
|
| 174 |
+
let gameState={mode:"explorer",energy:100,rank:"OBSERVADOR",vault:[],score:0,shield:false,speedMultiplier:1,cometActive:false,stormActive:false}
|
| 175 |
+
const missions=[
|
| 176 |
+
{type:"gold",target:5,progress:0},
|
| 177 |
+
{type:"visit",target:3,progress:0},
|
| 178 |
+
{type:"bridge",target:2,progress:0}
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
const normalizeString = s => s ? s.normalize("NFD").replace(/[\u0300-\u036f]/g,"") : ""
|
| 182 |
+
|
| 183 |
+
const loader = new FontLoader()
|
| 184 |
+
loader.load("https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_regular.typeface.json", f=>{font=f;isFontReady=true;checkAppReady()})
|
| 185 |
+
|
| 186 |
+
const gkInput=document.getElementById("geminiKeyInput")
|
| 187 |
+
const gkBtn=document.getElementById("saveGeminiKeyBtn")
|
| 188 |
+
if(gkInput && gkBtn){
|
| 189 |
+
const existing=getLocalGeminiKey()
|
| 190 |
+
if(existing && existing!==DEFAULT_GEMINI_KEY) gkInput.value=existing
|
| 191 |
+
gkBtn.addEventListener("click",e=>{e.preventDefault();setLocalGeminiKey(gkInput.value.trim());document.getElementById("geminiKeyStatus").textContent="GUARDADO";setTimeout(()=>document.getElementById("geminiKeyStatus").textContent="",2000)})
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
async function initFirebase(){
|
| 195 |
+
const loginBtn=document.getElementById("loginButton")
|
| 196 |
+
const registerBtn=document.getElementById("registerButton")
|
| 197 |
+
const anonBtn=document.getElementById("btnGoToAnon")
|
| 198 |
+
const saveUserBtn=document.getElementById("saveUsernameButton")
|
| 199 |
+
const backBtn=document.getElementById("backToStep1")
|
| 200 |
+
const logoutBtn=document.getElementById("mainLogoutButton")
|
| 201 |
+
const msg=document.getElementById("loginMessage")
|
| 202 |
+
const email=document.getElementById("loginEmail")
|
| 203 |
+
const pass=document.getElementById("loginPassword")
|
| 204 |
+
const step1=document.getElementById("authStep1")
|
| 205 |
+
const step2=document.getElementById("authStep2")
|
| 206 |
+
const overlay=document.getElementById("loginOverlay")
|
| 207 |
+
const ui=document.getElementById("ui")
|
| 208 |
+
|
| 209 |
+
registerBtn.addEventListener("click",async()=>{
|
| 210 |
+
if(email.value.length<6){msg.innerText="Email/Pass muy corto";return}
|
| 211 |
+
try{
|
| 212 |
+
await setPersistence(auth,browserLocalPersistence)
|
| 213 |
+
await createUserWithEmailAndPassword(auth,email.value,pass.value)
|
| 214 |
+
}catch(e){msg.innerText="Error: "+e.message}
|
| 215 |
+
})
|
| 216 |
+
loginBtn.addEventListener("click",async()=>{
|
| 217 |
+
try{
|
| 218 |
+
await setPersistence(auth,browserLocalPersistence)
|
| 219 |
+
await signInWithEmailAndPassword(auth,email.value,pass.value)
|
| 220 |
+
}catch(e){msg.innerText="Error: "+e.message}
|
| 221 |
+
})
|
| 222 |
+
anonBtn.addEventListener("click",()=>{step1.classList.add("hidden");step2.classList.remove("hidden")})
|
| 223 |
+
backBtn.addEventListener("click",()=>{step2.classList.add("hidden");step1.classList.remove("hidden");msg.innerText=""})
|
| 224 |
+
saveUserBtn.addEventListener("click",async()=>{
|
| 225 |
+
const name=normalizeString(document.getElementById("usernameInput").value.trim()
|
| 226 |
+
if(name.length<3){document.getElementById("usernameInput").classList.add("border-red-500");return}
|
| 227 |
+
document.getElementById("saveUsernameButton").innerText="ESTABLECIENDO ENLACE..."
|
| 228 |
+
document.getElementById("saveUsernameButton").disabled=true
|
| 229 |
+
try{
|
| 230 |
+
if(!auth.currentUser) await signInAnonymously(auth)
|
| 231 |
+
await setDoc(doc(db,"artifacts","neuronal-1f3b9","users",auth.currentUser.uid,"user_data","profile"),{username:name,energy:100,rank:"OBSERVADOR",vault:[]})
|
| 232 |
+
overlay.style.opacity="0"
|
| 233 |
+
setTimeout(()=>{overlay.style.display="none";ui.classList.remove("hidden");ui.style.transform="translateX(0)";ui.style.opacity="1";document.getElementById("gameHUD").classList.remove("hidden");setTimeout(()=>document.getElementById("gameHUD").classList.remove("opacity-0"),500);initScene();loadAllMaps()},700)
|
| 234 |
+
}catch(e){
|
| 235 |
+
document.getElementById("saveUsernameButton").innerText="ERROR DE CONEXIÓN"
|
| 236 |
+
document.getElementById("saveUsernameButton").disabled=false
|
| 237 |
}
|
| 238 |
+
})
|
| 239 |
+
logoutBtn.addEventListener("click",async()=>{await signOut(auth);location.reload()})
|
| 240 |
+
onAuthStateChanged(auth,async user=>{
|
| 241 |
+
if(user){
|
| 242 |
+
userId=user.uid;isAuthReady=true;await fetchUserProfile(userId);if(userProfile){
|
| 243 |
+
overlay.style.opacity="0";setTimeout(()=>{overlay.style.display="none";ui.classList.remove("hidden");ui.style.transform="translateX(0)";ui.style.opacity="1";document.getElementById("authStatus").textContent=userProfile.username;document.getElementById("gameHUD").classList.remove("hidden");setTimeout(()=>document.getElementById("gameHUD").classList.remove("opacity-0"),500);updateHUD();if(!scene){initScene();loadAllMaps()}} ,700)
|
| 244 |
+
}else{step1.classList.add("hidden");step2.classList.remove("hidden")}
|
| 245 |
+
}else{
|
| 246 |
+
overlay.style.display="flex";overlay.style.opacity="1";step1.classList.remove("hidden");step2.classList.add("hidden");ui.classList.add("hidden")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
}
|
| 248 |
+
})
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
async function fetchUserProfile(uid){
|
| 252 |
+
const snap=await getDoc(doc(db,"artifacts","neuronal-1f3b9","users",uid,"user_data","profile"))
|
| 253 |
+
if(snap.exists()){
|
| 254 |
+
userProfile=snap.data()
|
| 255 |
+
userProfileCache[uid]=userProfile
|
| 256 |
+
gameState.energy=userProfile.energy||100
|
| 257 |
+
gameState.rank=userProfile.rank||"OBSERVADOR"
|
| 258 |
+
gameState.vault=userProfile.vault||[]
|
| 259 |
+
updateHUD()
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
function updateHUD(){
|
| 264 |
+
document.getElementById("energyBar").style.width=gameState.energy+"%"
|
| 265 |
+
document.getElementById("rankDisplay").innerText=gameState.rank
|
| 266 |
+
document.getElementById("vaultCount").innerText=gameState.vault.length
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
function checkAppReady(){if(isAuthReady&&isFontReady&&userProfile){initScene();loadAllMaps()}}
|
| 270 |
+
|
| 271 |
+
function initScene(){
|
| 272 |
+
if(scene)return
|
| 273 |
+
scene=new THREE.Scene()
|
| 274 |
+
scene.fog=new THREE.FogExp2(0x020205,.005)
|
| 275 |
+
camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,.1,2500)
|
| 276 |
+
camera.position.set(0,5,30)
|
| 277 |
+
renderer=new THREE.WebGLRenderer({antialias:false,powerPreference:"high-performance"})
|
| 278 |
+
renderer.setSize(innerWidth,innerHeight)
|
| 279 |
+
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5))
|
| 280 |
+
renderer.toneMapping=THREE.ReinhardToneMapping
|
| 281 |
+
renderer.toneMappingExposure=1.2
|
| 282 |
+
document.getElementById("container").appendChild(renderer.domElement)
|
| 283 |
+
tooltip=document.getElementById("tooltip")
|
| 284 |
+
controls=new OrbitControls(camera,renderer.domElement)
|
| 285 |
+
controls.enableDamping=true
|
| 286 |
+
controls.dampingFactor=0.04
|
| 287 |
+
controls.rotateSpeed=0.5
|
| 288 |
+
controls.zoomSpeed=0.7
|
| 289 |
+
controls.maxDistance=600
|
| 290 |
+
controls.target.set(0,0,0)
|
| 291 |
+
const renderPass=new RenderPass(scene,camera)
|
| 292 |
+
const bloomPass=new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight),1.5,0.4,0.85)
|
| 293 |
+
bloomPass.threshold=0.15;bloomPass.strength=1.4;bloomPass.radius=0.6
|
| 294 |
+
composer=new EffectComposer(renderer)
|
| 295 |
+
composer.addPass(renderPass);composer.addPass(bloomPass)
|
| 296 |
+
raycaster=new THREE.Raycaster()
|
| 297 |
+
mouse=new THREE.Vector2()
|
| 298 |
+
clock=new THREE.Clock()
|
| 299 |
+
hashtagGroup=new THREE.Group()
|
| 300 |
+
scene.add(hashtagGroup)
|
| 301 |
+
createBackground()
|
| 302 |
+
initComet()
|
| 303 |
+
solar=createNode("SOLAR",new THREE.Vector3(0,30,0))
|
| 304 |
+
solar.material.transparent=true;solar.material.opacity=0.6
|
| 305 |
+
document.getElementById("visualizeButton").addEventListener("click",handleAnalysisAndVisualization)
|
| 306 |
+
;["level1","level2","level3"].forEach(l=>{document.getElementById(`${l}Slider`).addEventListener("input",e=>document.getElementById(`${l}Value`).innerText=e.target.value)})
|
| 307 |
+
window.addEventListener("resize",onWindowResize)
|
| 308 |
+
window.addEventListener("mousemove",onPointerMove)
|
| 309 |
+
window.addEventListener("click",onMouseClick)
|
| 310 |
+
const mm=document.getElementById("minimap")
|
| 311 |
+
if(mm){minimapCtx=mm.getContext("2d");mm.addEventListener("click",onMinimapClick)}
|
| 312 |
+
document.getElementById("zoomInButton").addEventListener("click",()=>{minimapScale*=1.5;drawMinimap()})
|
| 313 |
+
document.getElementById("zoomOutButton").addEventListener("click",()=>{minimapScale/=1.5;drawMinimap()})
|
| 314 |
+
setInterval(()=>{if(Math.random()<0.25)spawnNode();if(Math.random()<0.1)spawnPowerUp()},2000)
|
| 315 |
+
setInterval(()=>{if(Math.random()<0.05)spawnStorm()},25000)
|
| 316 |
+
animate()
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
function createBackground(){
|
| 320 |
+
const count=5000,geo=new THREE.BufferGeometry()
|
| 321 |
+
const pos=new Float32Array(count*3),sizes=new Float32Array(count)
|
| 322 |
+
for(let i=0;i<count;i++){
|
| 323 |
+
pos[i*3]=(Math.random()-0.5)*1500
|
| 324 |
+
pos[i*3+1]=(Math.random()-0.5)*1500
|
| 325 |
+
pos[i*3+2]=(Math.random()-0.5)*1500
|
| 326 |
+
sizes[i]=Math.random()
|
| 327 |
+
}
|
| 328 |
+
geo.setAttribute("position",new THREE.BufferAttribute(pos,3))
|
| 329 |
+
geo.setAttribute("size",new THREE.BufferAttribute(sizes,1))
|
| 330 |
+
const mat=new THREE.PointsMaterial({color:0x88ccff,size:1,transparent:true,opacity:0.6,sizeAttenuation:true})
|
| 331 |
+
const particles=new THREE.Points(geo,mat)
|
| 332 |
+
scene.add(particles)
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
function initComet(){
|
| 336 |
+
cometGroup=new THREE.Group()
|
| 337 |
+
cometHead=new THREE.Mesh(new THREE.SphereGeometry(0.5,32,32),new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 338 |
+
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}))
|
| 339 |
+
cometHead.add(halo)
|
| 340 |
+
cometGroup.add(cometHead)
|
| 341 |
+
cometLight=new THREE.PointLight(0x00ffff,2.5,60)
|
| 342 |
+
cometGroup.add(cometLight)
|
| 343 |
+
scene.add(cometGroup)
|
| 344 |
+
const pGeo=new THREE.BufferGeometry()
|
| 345 |
+
const positions=new Float32Array(400*3)
|
| 346 |
+
const colors=new Float32Array(400*3)
|
| 347 |
+
const sizes=new Float32Array(400)
|
| 348 |
+
pGeo.setAttribute("position",new THREE.BufferAttribute(positions,3))
|
| 349 |
+
pGeo.setAttribute("color",new THREE.BufferAttribute(colors,3))
|
| 350 |
+
pGeo.setAttribute("size",new THREE.BufferAttribute(sizes,1))
|
| 351 |
+
const pMat=new THREE.PointsMaterial({vertexColors:true,size:1,transparent:true,opacity:0.9,blending:THREE.AdditiveBlending,depthWrite:false,sizeAttenuation:true})
|
| 352 |
+
cometParticlesMesh=new THREE.Points(pGeo,pMat)
|
| 353 |
+
scene.add(cometParticlesMesh)
|
| 354 |
+
cometParticlesData=[]
|
| 355 |
+
for(let i=0;i<400;i++){
|
| 356 |
+
cometParticlesData.push({life:-1,velocity:new THREE.Vector3()})
|
| 357 |
+
positions[i*3]=99999
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
function spawnNode(){
|
| 362 |
+
const pos=randomPos()
|
| 363 |
+
const isGold=Math.random()<0.15
|
| 364 |
+
const label=isGold?"GOLD":"REG"
|
| 365 |
+
const node=createNode(label,pos)
|
| 366 |
+
node.userData.isGolden=isGold
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
function spawnPowerUp(){
|
| 370 |
+
const types=["DOUBLE","SHIELD","SPEED"]
|
| 371 |
+
const type=types[Math.floor(Math.random()*types.length)]
|
| 372 |
+
const pu=createNode(type,randomPos())
|
| 373 |
+
pu.userData.powerType=type.toLowerCase()
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
function spawnStorm(){
|
| 377 |
+
gameState.stormActive=true
|
| 378 |
+
renderer.setClearColor(0x000010)
|
| 379 |
+
setTimeout(()=>{gameState.stormActive=false;renderer.setClearColor(0x020205)},30000)
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
function randomPos(){
|
| 383 |
+
const r=70;const a=Math.random()*Math.PI*2
|
| 384 |
+
return new THREE.Vector3(Math.cos(a)*r,Math.random()*15,Math.sin(a)*r)
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
function createNode(label,pos){
|
| 388 |
+
const col=label==="GOLD"?0xffd700:0x22d3ee
|
| 389 |
+
const sphere=new THREE.Mesh(new THREE.SphereGeometry(0.4,12,12),new THREE.MeshStandardMaterial({color:col}))
|
| 390 |
+
sphere.position.copy(pos)
|
| 391 |
+
sphere.userData.label=label
|
| 392 |
+
hashtagGroup.add(sphere)
|
| 393 |
+
const txtGeo=new TextGeometry(label,{font,size:0.25,height:0.01})
|
| 394 |
+
const txtMesh=new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 395 |
+
txtMesh.position.copy(pos).add(new THREE.Vector3(0,0.6,0))
|
| 396 |
+
txtMesh.userData.isText=true
|
| 397 |
+
hashtagGroup.add(txtMesh)
|
| 398 |
+
return sphere
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
async function callGemini(topic,mc,vc,svc){
|
| 402 |
+
const key=getLocalGeminiKey()
|
| 403 |
+
const prompt=gameState.mode==="bridge"
|
| 404 |
+
?`Puente entre ${topic} y ${document.getElementById("bridgeTargetInput").value}. Genera 3 opciones.`
|
| 405 |
+
:`Tema: ${topic}. ${mc} palabras clave, ${vc} variantes, ${svc} sub‑variantes. JSON puro.`
|
| 406 |
+
const url=`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${key}`
|
| 407 |
+
const resp=await fetch(url,{
|
| 408 |
+
method:"POST",
|
| 409 |
+
headers:{"Content-Type":"application/json"},
|
| 410 |
+
body:JSON.stringify({contents:[{parts:[{text:prompt}]}]})
|
| 411 |
+
})
|
| 412 |
+
if(!resp.ok)throw new Error(await resp.text())
|
| 413 |
+
return await resp.json()
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
async function handleAnalysisAndVisualization(){
|
| 417 |
+
const topic=normalizeString(document.getElementById("topicInput").value)
|
| 418 |
+
if(!topic)return
|
| 419 |
+
if(gameState.mode==="miner" && gameState.energy<10){alert("Energía insuficiente");return}
|
| 420 |
+
const btn=document.getElementById("visualizeButton")
|
| 421 |
+
const pb=document.getElementById("progressBar")
|
| 422 |
+
const pbc=document.getElementById("progressBarContainer")
|
| 423 |
+
const original=btn.innerHTML
|
| 424 |
+
btn.disabled=true;btn.innerHTML='<span class="animate-pulse">PROCESANDO</span>'
|
| 425 |
+
pbc.classList.remove("hidden");pb.style.width="90%";pb.style.transition="width 15s ease-out"
|
| 426 |
+
try{
|
| 427 |
+
const mc=document.getElementById("level1Slider").value
|
| 428 |
+
const vc=document.getElementById("level2Slider").value
|
| 429 |
+
const svc=document.getElementById("level3Slider").value
|
| 430 |
+
const origin=randomPos()
|
| 431 |
+
const res=await callGemini(topic,mc,vc,svc)
|
| 432 |
+
const txt=res.candidates?.[0]?.content?.parts?.[0]?.text
|
| 433 |
+
if(!txt)throw new Error("Respuesta inválida")
|
| 434 |
+
const data=JSON.parse(txt)
|
| 435 |
+
visualizeRoot(topic,origin)
|
| 436 |
+
visualizeHashtags(data.lista_palabras,origin,1)
|
| 437 |
+
if(gameState.mode==="miner"){gameState.energy=Math.max(0,gameState.energy-10);updateHUD()}
|
| 438 |
+
if(gameState.mode==="bridge"){
|
| 439 |
+
gameState.bridgeHops++
|
| 440 |
+
document.getElementById("bridgeHops").innerText=gameState.bridgeHops
|
| 441 |
+
document.getElementById("bridgeOriginDisplay").innerText=topic
|
| 442 |
}
|
| 443 |
+
}catch(e){console.error(e);alert("Error: "+e.message)}
|
| 444 |
+
finally{
|
| 445 |
+
btn.disabled=false;btn.innerHTML=original;pb.style.width="100%";setTimeout(()=>{pbc.classList.add("hidden");pb.style.width="0%"},500)
|
| 446 |
+
}
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function visualizeRoot(topic,origin){
|
| 450 |
+
const hue=Math.abs(topic.split("").reduce((a,b)=>a+b.charCodeAt(0),0)%360)
|
| 451 |
+
const col=`hsl(${hue},75%,60%)`
|
| 452 |
+
const mat=new THREE.MeshStandardMaterial({color:new THREE.Color(col),emissive:new THREE.Color(col),emissiveIntensity:2.2,roughness:0.4})
|
| 453 |
+
const sphere=new THREE.Mesh(new THREE.SphereGeometry(0.7,32,32),mat)
|
| 454 |
+
sphere.position.copy(origin)
|
| 455 |
+
hashtagGroup.add(sphere)
|
| 456 |
+
const txtGeo=new TextGeometry(topic.toUpperCase(),{font,size:0.6,height:0.05})
|
| 457 |
+
const txtMesh=new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 458 |
+
txtMesh.position.copy(origin).add(new THREE.Vector3(0,1,0))
|
| 459 |
+
hashtagGroup.add(txtMesh)
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
function visualizeHashtags(list,origin,level,parentCol=null){
|
| 463 |
+
if(!list?.length)return
|
| 464 |
+
list.forEach(item=>{
|
| 465 |
+
let tag,variants
|
| 466 |
+
if(level===1){tag=normalizeString(item.palabra_principal);variants=item.variantes||[]}
|
| 467 |
+
else if(level===2){tag=normalizeString(item.palabra_variante);variants=item.sub_variantes||[]}
|
| 468 |
+
else{tag=normalizeString(item);variants=[]}
|
| 469 |
+
if(!tag)return
|
| 470 |
+
const hue=Math.abs(tag.split("").reduce((a,b)=>a+b.charCodeAt(0),0)%360)
|
| 471 |
+
const col=`hsl(${hue},75%,60%)`
|
| 472 |
+
const nodeCol=level===1?col:parentCol
|
| 473 |
+
const isGold=gameState.mode==="miner"&&level>=2&&Math.random()<0.15
|
| 474 |
+
const mat=new THREE.MeshPhysicalMaterial({
|
| 475 |
+
color:new THREE.Color(isGold?"#ffd700":nodeCol),
|
| 476 |
+
emissive:new THREE.Color(isGold?"#ffd700":nodeCol),
|
| 477 |
+
emissiveIntensity:isGold?2:level===1?0.8:0.4,
|
| 478 |
+
roughness:0.2,metalness:0.1,transmission:0.1,transparent:true,opacity:0.95
|
| 479 |
+
})
|
| 480 |
+
const theta=(hue/360)*Math.PI*2
|
| 481 |
+
let phi=0
|
| 482 |
+
for(let i=0;i<tag.length;i++)phi=(phi+tag.charCodeAt(i)*13)%180
|
| 483 |
+
phi=((phi/180)*90+45)*(Math.PI/180)
|
| 484 |
+
const radius=12/(level*level)
|
| 485 |
+
const cx=radius*Math.sin(phi)*Math.cos(theta)
|
| 486 |
+
const cy=radius*Math.cos(phi)
|
| 487 |
+
const cz=radius*Math.sin(phi)*Math.sin(theta)
|
| 488 |
+
const pos=new THREE.Vector3(cx,cy,cz).add(origin)
|
| 489 |
+
const lineMat=new THREE.LineBasicMaterial({color:new THREE.Color(nodeCol),transparent:true,opacity:0.25})
|
| 490 |
+
const line=new THREE.Line(new THREE.BufferGeometry().setFromPoints([origin,pos]),lineMat)
|
| 491 |
+
hashtagGroup.add(line)
|
| 492 |
+
const sphere=new THREE.Mesh(new THREE.SphereGeometry(level===1?0.35:level===2?0.18:0.1,16,16),mat)
|
| 493 |
sphere.position.copy(pos)
|
| 494 |
+
sphere.userData={label:tag,isGolden:isGold,level}
|
| 495 |
hashtagGroup.add(sphere)
|
| 496 |
+
const txtSize=level===1?0.35:level===2?0.18:0.12
|
| 497 |
+
const txtGeo=new TextGeometry(tag.toUpperCase(),{font,size:txtSize,height:0.01})
|
| 498 |
+
const txtMesh=new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:new THREE.Color(nodeCol)}))
|
| 499 |
+
txtMesh.position.copy(pos).add(new THREE.Vector3(0,level===1?0.6:0.4,0))
|
| 500 |
+
txtMesh.userData.isText=true
|
| 501 |
hashtagGroup.add(txtMesh)
|
| 502 |
+
visualizeHashtags(variants,pos,level+1,isGold?"#ffd700":nodeCol)
|
| 503 |
+
})
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
function loadAllMaps(){
|
| 507 |
+
if(!db||!font){setTimeout(loadAllMaps,500);return}
|
| 508 |
+
const q=query(collection(db,"artifacts","neuronal-1f3b9","public","data","maps"))
|
| 509 |
+
onSnapshot(q,snap=>{
|
| 510 |
+
while(hashtagGroup.children.length){
|
| 511 |
+
const obj=hashtagGroup.children[0]
|
| 512 |
+
hashtagGroup.remove(obj)
|
| 513 |
+
if(obj.geometry)obj.geometry.dispose()
|
| 514 |
+
if(obj.material){
|
| 515 |
+
if(Array.isArray(obj.material))obj.material.forEach(m=>m.dispose())
|
| 516 |
+
else obj.material.dispose()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
}
|
| 519 |
+
userMaps={}
|
| 520 |
+
snap.docs.forEach(d=>{
|
| 521 |
+
const m=d.data()
|
| 522 |
+
if(!m.origin)return
|
| 523 |
+
const o=new THREE.Vector3(m.origin.x,m.origin.y,m.origin.z)
|
| 524 |
+
if(!userMaps[m.userId])userMaps[m.userId]=[]
|
| 525 |
+
userMaps[m.userId].push(o)
|
| 526 |
+
try{
|
| 527 |
+
const data=JSON.parse(m.data)
|
| 528 |
+
visualizeRoot(m.topic,o)
|
| 529 |
+
visualizeHashtags(data.lista_palabras,o,1)
|
| 530 |
+
}catch{}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
})
|
| 532 |
+
const list=document.getElementById("userList")
|
| 533 |
+
if(list)list.innerHTML=""
|
| 534 |
+
Object.keys(userMaps).forEach(uid=>{
|
| 535 |
+
const prof=userProfileCache[uid]
|
| 536 |
+
const name=prof?prof.username:"ANON "+uid.slice(0,4)
|
| 537 |
+
const item=document.createElement("div")
|
| 538 |
+
item.className="text-cyan-400 hover:text-white cursor-pointer hover:bg-white/5 p-1 rounded transition-colors text-[10px] tracking-widest"
|
| 539 |
+
item.innerHTML=`> <span class="font-bold">${name}</span>`
|
| 540 |
+
item.onclick=()=>teleportToUser(uid)
|
| 541 |
+
list.appendChild(item)
|
| 542 |
+
const cent=new THREE.Vector3()
|
| 543 |
+
userMaps[uid].forEach(p=>cent.add(p))
|
| 544 |
+
cent.divideScalar(userMaps[uid].length)
|
| 545 |
+
createUserSun(cent,name,uid===userId)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
})
|
| 547 |
+
initComet()
|
| 548 |
+
drawMinimap()
|
| 549 |
+
focusOnUserMaps()
|
| 550 |
+
})
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
function createUserSun(pos,name,isMe){
|
| 554 |
+
const col=isMe?0xffaa00:0x00ff88
|
| 555 |
+
const mat=new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:1.8,roughness:0.2})
|
| 556 |
+
const mesh=new THREE.Mesh(new THREE.SphereGeometry(3,32,32),mat)
|
| 557 |
+
mesh.position.copy(pos)
|
| 558 |
+
hashtagGroup.add(mesh)
|
| 559 |
+
const txtGeo=new TextGeometry(name.toUpperCase(),{font,size:1.2})
|
| 560 |
+
const txtMesh=new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 561 |
+
txtMesh.position.copy(pos).add(new THREE.Vector3(0,4,0))
|
| 562 |
+
hashtagGroup.add(txtMesh)
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
function focusOnUserMaps(){
|
| 566 |
+
if(!controls||!userId||!userMaps[userId])return
|
| 567 |
+
const c=getCurrentUserCentroid()
|
| 568 |
+
controls.target.copy(c)
|
| 569 |
+
camera.position.copy(c).add(new THREE.Vector3(0,10,40))
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
function getCurrentUserCentroid(){
|
| 573 |
+
if(!userId||!userMaps[userId]||userMaps[userId].length===0)return new THREE.Vector3(0,0,0)
|
| 574 |
+
const cen=new THREE.Vector3()
|
| 575 |
+
userMaps[userId].forEach(p=>cen.add(p))
|
| 576 |
+
cen.divideScalar(userMaps[userId].length)
|
| 577 |
+
return cen
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
function teleportToUser(uid){
|
| 581 |
+
if(!userMaps[uid])return
|
| 582 |
+
const cen=new THREE.Vector3()
|
| 583 |
+
userMaps[uid].forEach(p=>cen.add(p))
|
| 584 |
+
cen.divideScalar(userMaps[uid].length)
|
| 585 |
+
controls.target.copy(cen)
|
| 586 |
+
camera.position.copy(cen).add(new THREE.Vector3(0,10,40))
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
function drawMinimap(){
|
| 590 |
+
if(!minimapCtx)return
|
| 591 |
+
const w=minimapCtx.canvas.width
|
| 592 |
+
const h=minimapCtx.canvas.height
|
| 593 |
+
minimapCtx.clearRect(0,0,w,h)
|
| 594 |
+
minimapDotCoords=[]
|
| 595 |
+
const myC=getCurrentUserCentroid()
|
| 596 |
+
Object.keys(userMaps).forEach(uid=>{
|
| 597 |
+
const cen=new THREE.Vector3()
|
| 598 |
+
userMaps[uid].forEach(p=>cen.add(p))
|
| 599 |
cen.divideScalar(userMaps[uid].length)
|
| 600 |
+
const x=w/2+(cen.x-myC.x)*minimapScale
|
| 601 |
+
const y=h/2+(cen.z-myC.z)*minimapScale
|
| 602 |
+
const isMe=uid===userId
|
| 603 |
+
minimapDotCoords.push({x,y,uid})
|
| 604 |
+
minimapCtx.fillStyle=isMe?"#22d3ee":"#64748b"
|
| 605 |
+
minimapCtx.beginPath()
|
| 606 |
+
minimapCtx.arc(x,y,isMe?4:2.5,0,Math.PI*2)
|
| 607 |
+
minimapCtx.fill()
|
| 608 |
+
if(isMe){
|
| 609 |
+
minimapCtx.strokeStyle="rgba(34,211,238,0.3)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
minimapCtx.beginPath()
|
| 611 |
+
minimapCtx.arc(x,y,8,0,Math.PI*2)
|
| 612 |
+
minimapCtx.stroke()
|
|
|
|
| 613 |
}
|
| 614 |
+
})
|
| 615 |
+
if(cometGroup){
|
| 616 |
+
const cx=w/2+(cometGroup.position.x-myC.x)*minimapScale
|
| 617 |
+
const cy=h/2+(cometGroup.position.z-myC.z)*minimapScale
|
| 618 |
+
minimapCtx.beginPath()
|
| 619 |
+
minimapCtx.arc(cx,cy,3,0,Math.PI*2)
|
| 620 |
+
minimapCtx.fillStyle="rgba(0,255,255,0.4)"
|
| 621 |
+
minimapCtx.fill()
|
| 622 |
+
minimapCtx.beginPath()
|
| 623 |
+
minimapCtx.arc(cx,cy,1.5,0,Math.PI*2)
|
| 624 |
+
minimapCtx.fillStyle="#ffffff"
|
| 625 |
+
minimapCtx.fill()
|
| 626 |
+
}
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
function onMinimapClick(e){
|
| 630 |
+
const rect=minimapCtx.canvas.getBoundingClientRect()
|
| 631 |
+
const x=e.clientX-rect.left
|
| 632 |
+
const y=e.clientY-rect.top
|
| 633 |
+
let closest=null;let minDist=20
|
| 634 |
+
minimapDotCoords.forEach(d=>{
|
| 635 |
+
const dX=d.x-x;const dY=d.y-y
|
| 636 |
+
const dist=Math.hypot(dX,dY)
|
| 637 |
+
if(dist<minDist){minDist=dist;closest=d.uid}
|
| 638 |
+
})
|
| 639 |
+
if(closest)teleportToUser(closest)
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
function onWindowResize(){
|
| 643 |
+
camera.aspect=innerWidth/innerHeight
|
| 644 |
+
camera.updateProjectionMatrix()
|
| 645 |
+
renderer.setSize(innerWidth,innerHeight)
|
| 646 |
+
composer.setSize(innerWidth,innerHeight)
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
function onPointerMove(e){
|
| 650 |
+
mouse.x=(e.clientX/innerWidth)*2-1
|
| 651 |
+
mouse.y=-(e.clientY/innerHeight)*2+1
|
| 652 |
+
tooltip.style.left=e.clientX+20+"px"
|
| 653 |
+
tooltip.style.top=e.clientY+"px"
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
function onMouseClick(){
|
| 657 |
+
raycaster.setFromCamera(mouse,camera)
|
| 658 |
+
const objs=hashtagGroup.children.filter(o=>!o.userData.isText)
|
| 659 |
+
const hits=raycaster.intersectObjects(objs,false)
|
| 660 |
+
if(!hits.length)return
|
| 661 |
+
const obj=hits[0].object
|
| 662 |
+
if(obj.userData.isGolden){
|
| 663 |
+
gameState.vault.push(obj.userData.label)
|
| 664 |
+
scene.add(createExplosion(obj.position))
|
| 665 |
+
hashtagGroup.remove(obj)
|
| 666 |
+
gameState.energy=Math.min(100,gameState.energy+12)
|
| 667 |
+
updateHUD()
|
| 668 |
+
}
|
| 669 |
+
if(obj.userData.powerType){
|
| 670 |
+
if(obj.userData.powerType==="shield"){gameState.shield=true;setTimeout(()=>gameState.shield=false,5000)}
|
| 671 |
+
if(obj.userData.powerType==="speed"){controls.rotateSpeed*=2;setTimeout(()=>controls.rotateSpeed/=2,5000)}
|
| 672 |
+
if(obj.userData.powerType==="double"){gameState.score*=2}
|
| 673 |
+
scene.remove(obj)
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
function updateRaycaster(){
|
| 678 |
+
raycaster.setFromCamera(mouse,camera)
|
| 679 |
+
const targets=hashtagGroup.children
|
| 680 |
+
const hits=raycaster.intersectObjects(targets,false)
|
| 681 |
+
if(hits.length){
|
| 682 |
+
const o=hits[0].object
|
| 683 |
+
if(o.userData.label){
|
| 684 |
+
tooltip.classList.remove("hidden")
|
| 685 |
+
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>`
|
| 686 |
+
document.body.style.cursor="pointer"
|
| 687 |
+
}else{
|
| 688 |
+
tooltip.classList.add("hidden")
|
| 689 |
+
document.body.style.cursor="default"
|
| 690 |
}
|
| 691 |
+
}else{
|
| 692 |
+
tooltip.classList.add("hidden")
|
| 693 |
+
document.body.style.cursor="default"
|
| 694 |
+
}
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
function createExplosion(pos,color=0xff0000){
|
| 698 |
+
const geo=new THREE.BufferGeometry()
|
| 699 |
+
const cnt=30
|
| 700 |
+
const arr=new Float32Array(cnt*3)
|
| 701 |
+
for(let i=0;i<cnt*3;i++)arr[i]=(Math.random()-0.5)*2
|
| 702 |
+
geo.setAttribute("position",new THREE.BufferAttribute(arr,3))
|
| 703 |
+
const mat=new THREE.PointsMaterial({color, size:0.5, transparent:true})
|
| 704 |
+
const pts=new THREE.Points(geo,mat)
|
| 705 |
+
pts.position.copy(pos)
|
| 706 |
+
let life=1
|
| 707 |
+
function anim(){life-=0.05;pts.scale.multiplyScalar(1.1);mat.opacity=life;if(life>0)requestAnimationFrame(anim);else scene.remove(pts)}
|
| 708 |
+
anim()
|
| 709 |
+
scene.add(pts)
|
| 710 |
+
return pts
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
function updateComet(dt){
|
| 714 |
+
if(!cometGroup)return
|
| 715 |
+
let speed=0.35
|
| 716 |
+
if(gameState.cometActive)speed=0.8
|
| 717 |
+
cometAngle+=dt*speed
|
| 718 |
+
const rX=80,rZ=60
|
| 719 |
+
const x= cometGroup.position.x + Math.cos(cometAngle)*rX
|
| 720 |
+
const z= cometGroup.position.z + Math.sin(cometAngle)*rZ
|
| 721 |
+
const y= cometGroup.position.y + Math.sin(cometAngle*2)*20
|
| 722 |
+
cometGroup.position.set(x,y,z)
|
| 723 |
+
|
| 724 |
+
const positions=cometParticlesMesh.geometry.attributes.position.array
|
| 725 |
+
const colors=cometParticlesMesh.geometry.attributes.color.array
|
| 726 |
+
const sizes=cometParticlesMesh.geometry.attributes.size.array
|
| 727 |
+
let spawn=gameState.cometActive?10:5
|
| 728 |
+
for(let i=0;i<400;i++){
|
| 729 |
+
if(spawn>0 && cometParticlesData[i].life<0){
|
| 730 |
+
cometParticlesData[i].life=1
|
| 731 |
+
positions[i*3]=cometGroup.position.x+(Math.random()-0.5)
|
| 732 |
+
positions[i*3+1]=cometGroup.position.y+(Math.random()-0.5)
|
| 733 |
+
positions[i*3+2]=cometGroup.position.z+(Math.random()-0.5)
|
| 734 |
+
if(gameState.cometActive){colors[i*3]=1;colors[i*3+1]=0.2;colors[i*3+2]=0}
|
| 735 |
+
else{colors[i*3]=0.2;colors[i*3+1]=1;colors[i*3+2]=1}
|
| 736 |
+
sizes[i]=1.2
|
| 737 |
+
spawn--
|
| 738 |
}
|
| 739 |
}
|
| 740 |
+
for(let i=0;i<400;i++){
|
| 741 |
+
if(cometParticlesData[i].life>0){
|
| 742 |
+
cometParticlesData[i].life-=dt*0.7
|
| 743 |
+
sizes[i]=cometParticlesData[i].life*1.8
|
| 744 |
+
}else{
|
| 745 |
+
positions[i*3]=99999
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
}
|
| 747 |
}
|
| 748 |
+
cometParticlesMesh.geometry.attributes.position.needsUpdate=true
|
| 749 |
+
cometParticlesMesh.geometry.attributes.color.needsUpdate=true
|
| 750 |
+
cometParticlesMesh.geometry.attributes.size.needsUpdate=true
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
function animate(){
|
| 754 |
+
requestAnimationFrame(animate)
|
| 755 |
+
const dt=clock.getDelta()
|
| 756 |
+
controls.update()
|
| 757 |
+
if(gameState.cometActive)updateComet(dt)
|
| 758 |
+
if(gameState.stormActive){} // storm visual effect handled by background color change
|
| 759 |
+
updateRaycaster()
|
| 760 |
+
drawMinimap()
|
| 761 |
+
hashtagGroup.children.forEach(o=>{
|
| 762 |
+
if(o.userData.isText){
|
| 763 |
+
o.lookAt(camera.position)
|
| 764 |
+
const d=o.position.distanceTo(camera.position)
|
| 765 |
+
let s=(1/d)*12
|
| 766 |
+
s=Math.max(0.6,Math.min(5,s))
|
| 767 |
+
o.scale.set(s,s,s)
|
| 768 |
}
|
| 769 |
+
})
|
| 770 |
+
composer.render()
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
function setGameMode(mode){
|
| 774 |
+
gameState.mode=mode
|
| 775 |
+
document.getElementById("activeModeText").innerText=mode==="bridge"?"PUENTE NEURAL":(mode==="miner"?"MINERO DE DATOS":(mode==="comet"?"DEFENSA COMETA":"EXPLORACIÓN LIBRE"))
|
| 776 |
+
if(mode==="bridge"){
|
| 777 |
+
document.getElementById("bridgeControls").classList.remove("hidden")
|
| 778 |
+
document.getElementById("actionBtnText").innerText="CONSTRUIR NODO"
|
| 779 |
+
}else{
|
| 780 |
+
document.getElementById("bridgeControls").classList.add("hidden")
|
| 781 |
+
document.getElementById("actionBtnText").innerText="EJECUTAR ANÁLISIS"
|
| 782 |
+
}
|
| 783 |
+
if(mode==="comet"){
|
| 784 |
+
gameState.cometActive=true
|
| 785 |
+
spawnTrashNodes()
|
| 786 |
+
}else{
|
| 787 |
+
gameState.cometActive=false
|
| 788 |
+
if(trashGroup){scene.remove(trashGroup);trashGroup=null}
|
| 789 |
+
}
|
| 790 |
+
}
|
| 791 |
+
window.setGameMode=setGameMode
|
| 792 |
+
|
| 793 |
+
function spawnTrashNodes(){
|
| 794 |
+
if(trashGroup)scene.remove(trashGroup)
|
| 795 |
+
trashGroup=new THREE.Group()
|
| 796 |
+
scene.add(trashGroup)
|
| 797 |
+
const words=["VIRAL","FAKE","GLITCH","ERROR","NOISE","SPAM","BOT"]
|
| 798 |
+
for(let i=0;i<20;i++){
|
| 799 |
+
const geo=new THREE.DodecahedronGeometry(0.8,0)
|
| 800 |
+
const mat=new THREE.MeshBasicMaterial({color:0xff0000,wireframe:true})
|
| 801 |
+
const mesh=new THREE.Mesh(geo,mat)
|
| 802 |
+
mesh.position.copy(randomPos())
|
| 803 |
+
mesh.userData.isTrash=true
|
| 804 |
+
mesh.userData.word=words[Math.floor(Math.random()*words.length)]
|
| 805 |
+
trashGroup.add(mesh)
|
| 806 |
+
}
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
initFirebase()
|
| 810 |
+
</script>
|
| 811 |
</body>
|
| 812 |
</html>
|