Spaces:
Running
Running
Update index.html
Browse files- index.html +458 -367
index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
<html lang="es">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width,
|
| 6 |
<title>Navegador Neuronal Semántico 3D de Twitter</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
|
@@ -97,7 +97,10 @@
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
@@ -113,15 +116,32 @@
|
|
| 113 |
</div>
|
| 114 |
</details>
|
| 115 |
|
| 116 |
-
<div class="flex gap-2 mb-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
@@ -168,7 +188,7 @@
|
|
| 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,
|
| 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}
|
|
@@ -181,365 +201,445 @@
|
|
| 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 |
-
|
| 187 |
-
const
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(()=>{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
}catch(e){
|
| 235 |
-
document.getElementById("saveUsernameButton").innerText="ERROR DE CONEXIÓN"
|
| 236 |
-
document.getElementById("saveUsernameButton").disabled=false
|
| 237 |
}
|
| 238 |
})
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
}else{
|
| 246 |
-
overlay.style.display="flex"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 274 |
-
scene
|
| 275 |
-
|
|
|
|
|
|
|
| 276 |
camera.position.set(0,5,30)
|
| 277 |
-
|
|
|
|
| 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 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
controls
|
| 287 |
-
controls.
|
| 288 |
-
controls.
|
| 289 |
-
controls.
|
|
|
|
|
|
|
| 290 |
controls.target.set(0,0,0)
|
| 291 |
-
|
| 292 |
-
const
|
| 293 |
-
bloomPass
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
scene.add(hashtagGroup)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
createBackground()
|
| 302 |
initComet()
|
| 303 |
-
solar=createNode("SOLAR",new THREE.Vector3(0,30,0))
|
| 304 |
-
solar.material.transparent=true
|
|
|
|
|
|
|
| 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 |
-
|
| 311 |
-
|
|
|
|
|
|
|
| 312 |
document.getElementById("zoomInButton").addEventListener("click",()=>{minimapScale*=1.5;drawMinimap()})
|
| 313 |
document.getElementById("zoomOutButton").addEventListener("click",()=>{minimapScale/=1.5;drawMinimap()})
|
| 314 |
-
|
|
|
|
|
|
|
| 315 |
setInterval(()=>{if(Math.random()<0.05)spawnStorm()},25000)
|
|
|
|
| 316 |
animate()
|
| 317 |
}
|
| 318 |
|
| 319 |
function createBackground(){
|
| 320 |
-
const count=
|
| 321 |
-
const
|
|
|
|
|
|
|
| 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 |
-
|
| 345 |
-
const
|
| 346 |
-
const
|
| 347 |
-
const
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 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 |
-
|
| 394 |
-
const
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 421 |
-
const
|
| 422 |
-
const
|
| 423 |
-
const
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 438 |
-
if(gameState.mode==="
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 457 |
-
const
|
|
|
|
| 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 |
-
|
| 471 |
-
const
|
| 472 |
-
const
|
| 473 |
-
const
|
| 474 |
-
const
|
|
|
|
|
|
|
| 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 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
phi=(
|
| 484 |
-
|
| 485 |
-
const
|
| 486 |
-
const
|
| 487 |
-
const
|
| 488 |
-
const
|
| 489 |
-
const
|
| 490 |
-
|
|
|
|
|
|
|
| 491 |
hashtagGroup.add(line)
|
| 492 |
-
|
|
|
|
| 493 |
sphere.position.copy(pos)
|
| 494 |
-
sphere.userData={label:tag,isGolden:isGold,level}
|
| 495 |
hashtagGroup.add(sphere)
|
| 496 |
-
|
| 497 |
-
const
|
| 498 |
-
const
|
|
|
|
| 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)
|
|
@@ -551,70 +651,70 @@
|
|
| 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)"
|
|
@@ -627,143 +727,122 @@
|
|
| 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
|
| 634 |
minimapDotCoords.forEach(d=>{
|
| 635 |
-
const
|
| 636 |
-
const dist=Math.hypot(
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 670 |
-
|
| 671 |
-
if(obj.userData.powerType==="
|
| 672 |
-
if(obj.userData.powerType==="
|
|
|
|
| 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
|
| 714 |
-
if(
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 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){} //
|
| 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 |
})
|
|
@@ -771,39 +850,51 @@
|
|
| 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 |
-
|
| 778 |
-
|
| 779 |
-
|
| 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
|
| 794 |
-
if(
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
const
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
}
|
|
|
|
|
|
|
|
|
|
| 807 |
}
|
| 808 |
|
| 809 |
initFirebase()
|
|
|
|
| 2 |
<html lang="es">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 6 |
<title>Navegador Neuronal Semántico 3D de Twitter</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
|
|
|
| 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">
|
| 101 |
+
<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>
|
| 102 |
+
<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...">
|
| 103 |
+
</div>
|
| 104 |
|
| 105 |
<div class="space-y-5 mb-6 px-1">
|
| 106 |
<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>
|
|
|
|
| 116 |
</div>
|
| 117 |
</details>
|
| 118 |
|
| 119 |
+
<div class="flex gap-2 mb-2">
|
| 120 |
+
<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)]">
|
| 121 |
+
<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>
|
| 122 |
+
<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>
|
| 123 |
+
<span id="actionBtnText" class="uppercase tracking-[0.2em] text-xs">Ejecutar Análisis</span>
|
| 124 |
+
</button>
|
| 125 |
+
</div>
|
| 126 |
|
| 127 |
<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>
|
| 128 |
|
| 129 |
+
<div id="userListContainer" class="flex-1 flex flex-col min-h-0 bg-black/40 rounded border border-white/5 relative overflow-hidden group">
|
| 130 |
+
<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>
|
| 131 |
+
<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>
|
| 132 |
+
<div id="userList" class="overflow-y-auto pr-1 space-y-1 custom-scrollbar text-xs p-2"></div>
|
| 133 |
+
<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>
|
| 134 |
+
</div>
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<div id="minimapContainer" class="fixed bottom-4 right-4 z-[200] glass-panel p-2 rounded-lg group w-[320px] transition-opacity duration-300">
|
| 138 |
+
<div class="absolute top-2 right-2 flex gap-1 z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
| 139 |
+
<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>
|
| 140 |
+
<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>
|
| 141 |
+
</div>
|
| 142 |
+
<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>
|
| 143 |
+
<div class="text-[9px] text-center text-cyan-500/40 mt-1 uppercase tracking-[0.3em]">Radar Galáctico</div>
|
| 144 |
+
</div>
|
| 145 |
|
| 146 |
<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>
|
| 147 |
|
|
|
|
| 188 |
let scene,camera,renderer,composer,controls,raycaster,mouse,clock,font
|
| 189 |
let hashtagGroup,tooltip,minimapCtx,minimapDotCoords=[],minimapScale=0.025
|
| 190 |
let cometGroup,cometHead,cometLight,cometParticlesMesh,cometParticlesData,cometAngle=0
|
| 191 |
+
let solar,trashGroup
|
| 192 |
let userId=null,userProfile=null,isAuthReady=false,isFontReady=false
|
| 193 |
let userMaps={},userProfileCache={}
|
| 194 |
let gameState={mode:"explorer",energy:100,rank:"OBSERVADOR",vault:[],score:0,shield:false,speedMultiplier:1,cometActive:false,stormActive:false}
|
|
|
|
| 201 |
const normalizeString = s => s ? s.normalize("NFD").replace(/[\u0300-\u036f]/g,"") : ""
|
| 202 |
|
| 203 |
const loader = new FontLoader()
|
| 204 |
+
loader.load("https://cdn.jsdelivr.net/npm/three@0.128.0/examples/fonts/helvetiker_regular.typeface.json", f => { font = f; isFontReady = true; checkAppReady() })
|
| 205 |
+
|
| 206 |
+
/* ------------ UI - Gemini Key ------------ */
|
| 207 |
+
const gkInput = document.getElementById("geminiKeyInput")
|
| 208 |
+
const gkBtn = document.getElementById("saveGeminiKeyBtn")
|
| 209 |
+
if (gkInput && gkBtn){
|
| 210 |
+
const stored = getLocalGeminiKey()
|
| 211 |
+
if (stored && stored !== DEFAULT_GEMINI_KEY) gkInput.value = stored
|
| 212 |
+
gkBtn.addEventListener("click", e => {
|
| 213 |
+
e.preventDefault()
|
| 214 |
+
setLocalGeminiKey(gkInput.value.trim())
|
| 215 |
+
document.getElementById("geminiKeyStatus").textContent = "GUARDADO"
|
| 216 |
+
setTimeout(() => document.getElementById("geminiKeyStatus").textContent = "", 2000)
|
| 217 |
+
})
|
| 218 |
}
|
| 219 |
|
| 220 |
+
/* ------------ AUTH ------------ */
|
| 221 |
async function initFirebase(){
|
| 222 |
+
const loginBtn = document.getElementById("loginButton")
|
| 223 |
+
const registerBtn= document.getElementById("registerButton")
|
| 224 |
+
const anonBtn = document.getElementById("btnGoToAnon")
|
| 225 |
+
const saveUserBtn= document.getElementById("saveUsernameButton")
|
| 226 |
+
const backBtn = document.getElementById("backToStep1")
|
| 227 |
+
const logoutBtn = document.getElementById("mainLogoutButton")
|
| 228 |
+
const msg = document.getElementById("loginMessage")
|
| 229 |
+
const email = document.getElementById("loginEmail")
|
| 230 |
+
const pass = document.getElementById("loginPassword")
|
| 231 |
+
const step1 = document.getElementById("authStep1")
|
| 232 |
+
const step2 = document.getElementById("authStep2")
|
| 233 |
+
const overlay = document.getElementById("loginOverlay")
|
| 234 |
+
const ui = document.getElementById("ui")
|
| 235 |
+
|
| 236 |
+
registerBtn.addEventListener("click", async () => {
|
| 237 |
+
if (email.value.length < 6){ msg.innerText="Email/Pass muy corto"; return }
|
| 238 |
try{
|
| 239 |
await setPersistence(auth,browserLocalPersistence)
|
| 240 |
await createUserWithEmailAndPassword(auth,email.value,pass.value)
|
| 241 |
+
}catch(e){ msg.innerText = "Error: " + e.message }
|
| 242 |
})
|
| 243 |
+
|
| 244 |
+
loginBtn.addEventListener("click", async () => {
|
| 245 |
try{
|
| 246 |
await setPersistence(auth,browserLocalPersistence)
|
| 247 |
await signInWithEmailAndPassword(auth,email.value,pass.value)
|
| 248 |
+
}catch(e){ msg.innerText = "Error: " + e.message }
|
| 249 |
})
|
| 250 |
+
|
| 251 |
+
anonBtn.addEventListener("click", () => {
|
| 252 |
+
step1.classList.add("hidden")
|
| 253 |
+
step2.classList.remove("hidden")
|
| 254 |
+
})
|
| 255 |
+
|
| 256 |
+
backBtn.addEventListener("click", () => {
|
| 257 |
+
step2.classList.add("hidden")
|
| 258 |
+
step1.classList.remove("hidden")
|
| 259 |
+
msg.innerText = ""
|
| 260 |
+
})
|
| 261 |
+
|
| 262 |
+
saveUserBtn.addEventListener("click", async () => {
|
| 263 |
+
const name = normalizeString(document.getElementById("usernameInput").value.trim())
|
| 264 |
+
if (name.length < 3){ document.getElementById("usernameInput").classList.add("border-red-500"); return }
|
| 265 |
+
document.getElementById("saveUsernameButton").innerText = "ESTABLECIENDO ENLACE..."
|
| 266 |
+
document.getElementById("saveUsernameButton").disabled = true
|
| 267 |
try{
|
| 268 |
+
if (!auth.currentUser) await signInAnonymously(auth)
|
| 269 |
await setDoc(doc(db,"artifacts","neuronal-1f3b9","users",auth.currentUser.uid,"user_data","profile"),{username:name,energy:100,rank:"OBSERVADOR",vault:[]})
|
| 270 |
+
overlay.style.opacity = "0"
|
| 271 |
+
setTimeout(() => {
|
| 272 |
+
overlay.style.display = "none"
|
| 273 |
+
ui.classList.remove("hidden")
|
| 274 |
+
ui.style.transform = "translateX(0)"
|
| 275 |
+
ui.style.opacity = "1"
|
| 276 |
+
document.getElementById("authStatus").textContent = name
|
| 277 |
+
document.getElementById("gameHUD").classList.remove("hidden")
|
| 278 |
+
setTimeout(() => document.getElementById("gameHUD").classList.remove("opacity-0"),500)
|
| 279 |
+
initScene()
|
| 280 |
+
loadAllMaps()
|
| 281 |
+
},700)
|
| 282 |
}catch(e){
|
| 283 |
+
document.getElementById("saveUsernameButton").innerText = "ERROR DE CONEXIÓN"
|
| 284 |
+
document.getElementById("saveUsernameButton").disabled = false
|
| 285 |
}
|
| 286 |
})
|
| 287 |
+
|
| 288 |
+
logoutBtn.addEventListener("click", async () => { await signOut(auth); location.reload() })
|
| 289 |
+
|
| 290 |
+
onAuthStateChanged(auth, async user => {
|
| 291 |
+
if (user){
|
| 292 |
+
userId = user.uid
|
| 293 |
+
isAuthReady = true
|
| 294 |
+
await fetchUserProfile(userId)
|
| 295 |
+
if (userProfile){
|
| 296 |
+
overlay.style.opacity = "0"
|
| 297 |
+
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)
|
| 298 |
+
}else{
|
| 299 |
+
step1.classList.add("hidden")
|
| 300 |
+
step2.classList.remove("hidden")
|
| 301 |
+
}
|
| 302 |
}else{
|
| 303 |
+
overlay.style.display = "flex"
|
| 304 |
+
overlay.style.opacity = "1"
|
| 305 |
+
step1.classList.remove("hidden")
|
| 306 |
+
step2.classList.add("hidden")
|
| 307 |
+
ui.classList.add("hidden")
|
| 308 |
}
|
| 309 |
})
|
| 310 |
}
|
| 311 |
|
| 312 |
async function fetchUserProfile(uid){
|
| 313 |
+
const snap = await getDoc(doc(db,"artifacts","neuronal-1f3b9","users",uid,"user_data","profile"))
|
| 314 |
+
if (snap.exists()){
|
| 315 |
+
userProfile = snap.data()
|
| 316 |
+
userProfileCache[uid] = userProfile
|
| 317 |
+
gameState.energy = userProfile.energy || 100
|
| 318 |
+
gameState.rank = userProfile.rank || "OBSERVADOR"
|
| 319 |
+
gameState.vault = userProfile.vault || []
|
| 320 |
updateHUD()
|
| 321 |
}
|
| 322 |
}
|
| 323 |
|
| 324 |
function updateHUD(){
|
| 325 |
+
document.getElementById("energyBar").style.width = gameState.energy + "%"
|
| 326 |
+
document.getElementById("rankDisplay").innerText = gameState.rank
|
| 327 |
+
document.getElementById("vaultCount").innerText = gameState.vault.length
|
| 328 |
}
|
| 329 |
|
| 330 |
+
function checkAppReady(){ if (isAuthReady && isFontReady && userProfile) { initScene(); loadAllMaps() } }
|
| 331 |
|
| 332 |
+
/* ------------ SCENE ------------ */
|
| 333 |
function initScene(){
|
| 334 |
+
if (scene) return
|
| 335 |
+
|
| 336 |
+
scene = new THREE.Scene()
|
| 337 |
+
scene.fog = new THREE.FogExp2(0x020205,0.005)
|
| 338 |
+
|
| 339 |
+
camera = new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.1,2500)
|
| 340 |
camera.position.set(0,5,30)
|
| 341 |
+
|
| 342 |
+
renderer = new THREE.WebGLRenderer({antialias:false,powerPreference:"high-performance"})
|
| 343 |
renderer.setSize(innerWidth,innerHeight)
|
| 344 |
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5))
|
| 345 |
+
renderer.toneMapping = THREE.ReinhardToneMapping
|
| 346 |
+
renderer.toneMappingExposure = 1.2
|
| 347 |
document.getElementById("container").appendChild(renderer.domElement)
|
| 348 |
+
|
| 349 |
+
tooltip = document.getElementById("tooltip")
|
| 350 |
+
|
| 351 |
+
controls = new OrbitControls(camera,renderer.domElement)
|
| 352 |
+
controls.enableDamping = true
|
| 353 |
+
controls.dampingFactor = 0.04
|
| 354 |
+
controls.rotateSpeed = 0.5
|
| 355 |
+
controls.zoomSpeed = 0.7
|
| 356 |
+
controls.maxDistance = 600
|
| 357 |
controls.target.set(0,0,0)
|
| 358 |
+
|
| 359 |
+
const renderPass = new RenderPass(scene,camera)
|
| 360 |
+
const bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight),1.5,0.4,0.85)
|
| 361 |
+
bloomPass.threshold = 0.15
|
| 362 |
+
bloomPass.strength = 1.4
|
| 363 |
+
bloomPass.radius = 0.6
|
| 364 |
+
composer = new EffectComposer(renderer)
|
| 365 |
+
composer.addPass(renderPass)
|
| 366 |
+
composer.addPass(bloomPass)
|
| 367 |
+
|
| 368 |
+
raycaster = new THREE.Raycaster()
|
| 369 |
+
mouse = new THREE.Vector2()
|
| 370 |
+
clock = new THREE.Clock()
|
| 371 |
+
|
| 372 |
+
hashtagGroup = new THREE.Group()
|
| 373 |
scene.add(hashtagGroup)
|
| 374 |
+
|
| 375 |
+
scene.add(new THREE.AmbientLight(0x404040,1))
|
| 376 |
+
const dir = new THREE.DirectionalLight(0xaaccff,1.2)
|
| 377 |
+
dir.position.set(50,80,50)
|
| 378 |
+
scene.add(dir)
|
| 379 |
+
|
| 380 |
createBackground()
|
| 381 |
initComet()
|
| 382 |
+
solar = createNode("SOLAR",new THREE.Vector3(0,30,0))
|
| 383 |
+
solar.material.transparent = true
|
| 384 |
+
solar.material.opacity = 0.6
|
| 385 |
+
|
| 386 |
document.getElementById("visualizeButton").addEventListener("click",handleAnalysisAndVisualization)
|
| 387 |
+
|
| 388 |
;["level1","level2","level3"].forEach(l=>{document.getElementById(`${l}Slider`).addEventListener("input",e=>document.getElementById(`${l}Value`).innerText=e.target.value)})
|
| 389 |
+
|
| 390 |
window.addEventListener("resize",onWindowResize)
|
| 391 |
window.addEventListener("mousemove",onPointerMove)
|
| 392 |
window.addEventListener("click",onMouseClick)
|
| 393 |
+
|
| 394 |
+
const mm = document.getElementById("minimap")
|
| 395 |
+
if (mm){ minimapCtx = mm.getContext("2d"); mm.addEventListener("click",onMinimapClick) }
|
| 396 |
+
|
| 397 |
document.getElementById("zoomInButton").addEventListener("click",()=>{minimapScale*=1.5;drawMinimap()})
|
| 398 |
document.getElementById("zoomOutButton").addEventListener("click",()=>{minimapScale/=1.5;drawMinimap()})
|
| 399 |
+
|
| 400 |
+
setInterval(()=>{if(Math.random()<0.25)spawnNode()},2000)
|
| 401 |
+
setInterval(()=>{if(Math.random()<0.1)spawnPowerUp()},8000)
|
| 402 |
setInterval(()=>{if(Math.random()<0.05)spawnStorm()},25000)
|
| 403 |
+
|
| 404 |
animate()
|
| 405 |
}
|
| 406 |
|
| 407 |
function createBackground(){
|
| 408 |
+
const count = 5000
|
| 409 |
+
const geo = new THREE.BufferGeometry()
|
| 410 |
+
const pos = new Float32Array(count*3)
|
| 411 |
+
const sizes = new Float32Array(count)
|
| 412 |
for(let i=0;i<count;i++){
|
| 413 |
+
pos[i*3] = (Math.random()-0.5)*1500
|
| 414 |
+
pos[i*3+1] = (Math.random()-0.5)*1500
|
| 415 |
+
pos[i*3+2] = (Math.random()-0.5)*1500
|
| 416 |
+
sizes[i] = Math.random()
|
| 417 |
}
|
| 418 |
geo.setAttribute("position",new THREE.BufferAttribute(pos,3))
|
| 419 |
geo.setAttribute("size",new THREE.BufferAttribute(sizes,1))
|
| 420 |
+
const mat = new THREE.PointsMaterial({color:0x88ccff,size:1,transparent:true,opacity:0.6,sizeAttenuation:true})
|
| 421 |
+
const particles = new THREE.Points(geo,mat)
|
| 422 |
scene.add(particles)
|
| 423 |
}
|
| 424 |
|
| 425 |
function initComet(){
|
| 426 |
+
cometGroup = new THREE.Group()
|
| 427 |
+
cometHead = new THREE.Mesh(new THREE.SphereGeometry(0.5,32,32),new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 428 |
+
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}))
|
| 429 |
cometHead.add(halo)
|
| 430 |
cometGroup.add(cometHead)
|
| 431 |
+
cometLight = new THREE.PointLight(0x00ffff,2.5,60)
|
| 432 |
cometGroup.add(cometLight)
|
| 433 |
scene.add(cometGroup)
|
| 434 |
+
|
| 435 |
+
const pGeo = new THREE.BufferGeometry()
|
| 436 |
+
const positions = new Float32Array(400*3)
|
| 437 |
+
const colors = new Float32Array(400*3)
|
| 438 |
+
const sizes = new Float32Array(400)
|
| 439 |
pGeo.setAttribute("position",new THREE.BufferAttribute(positions,3))
|
| 440 |
pGeo.setAttribute("color",new THREE.BufferAttribute(colors,3))
|
| 441 |
pGeo.setAttribute("size",new THREE.BufferAttribute(sizes,1))
|
| 442 |
+
const pMat = new THREE.PointsMaterial({vertexColors:true,size:1,transparent:true,opacity:0.9,blending:THREE.AdditiveBlending,depthWrite:false,sizeAttenuation:true})
|
| 443 |
+
cometParticlesMesh = new THREE.Points(pGeo,pMat)
|
| 444 |
scene.add(cometParticlesMesh)
|
| 445 |
+
|
| 446 |
+
cometParticlesData = []
|
| 447 |
for(let i=0;i<400;i++){
|
| 448 |
cometParticlesData.push({life:-1,velocity:new THREE.Vector3()})
|
| 449 |
+
positions[i*3] = 99999
|
| 450 |
}
|
| 451 |
}
|
| 452 |
|
| 453 |
+
function randomPos(){ const r=70; const a=Math.random()*Math.PI*2; return new THREE.Vector3(Math.cos(a)*r,Math.random()*15,Math.sin(a)*r) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
function createNode(label,pos){
|
| 456 |
+
const col = label==="GOLD"?0xffd700:0x22d3ee
|
| 457 |
+
const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.4,12,12),new THREE.MeshStandardMaterial({color:col}))
|
| 458 |
sphere.position.copy(pos)
|
| 459 |
+
sphere.userData.label = label
|
| 460 |
hashtagGroup.add(sphere)
|
| 461 |
+
|
| 462 |
+
const txtGeo = new TextGeometry(label,{font,size:0.25,height:0.01})
|
| 463 |
+
const txtMesh = new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 464 |
txtMesh.position.copy(pos).add(new THREE.Vector3(0,0.6,0))
|
| 465 |
+
txtMesh.userData.isText = true
|
| 466 |
hashtagGroup.add(txtMesh)
|
| 467 |
return sphere
|
| 468 |
}
|
| 469 |
|
| 470 |
+
function spawnNode(){ const pos = randomPos(); const isGold = Math.random()<0.15; const node = createNode(isGold?"GOLD":"REG",pos); node.userData.isGolden = isGold }
|
| 471 |
+
|
| 472 |
+
function spawnPowerUp(){
|
| 473 |
+
const types = ["DOUBLE","SHIELD","SPEED"]
|
| 474 |
+
const type = types[Math.floor(Math.random()*types.length)]
|
| 475 |
+
const pu = createNode(type,randomPos())
|
| 476 |
+
pu.userData.powerType = type.toLowerCase()
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
function spawnStorm(){
|
| 480 |
+
gameState.stormActive = true
|
| 481 |
+
renderer.setClearColor(0x000010)
|
| 482 |
+
setTimeout(()=>{gameState.stormActive = false; renderer.setClearColor(0x020205)},30000)
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
async function callGemini(topic,mc,vc,svc){
|
| 486 |
+
const key = getLocalGeminiKey()
|
| 487 |
+
const prompt = gameState.mode==="bridge"
|
| 488 |
?`Puente entre ${topic} y ${document.getElementById("bridgeTargetInput").value}. Genera 3 opciones.`
|
| 489 |
:`Tema: ${topic}. ${mc} palabras clave, ${vc} variantes, ${svc} sub‑variantes. JSON puro.`
|
| 490 |
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${key}`
|
| 491 |
+
const resp = await fetch(url,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contents:[{parts:[{text:prompt}]}]})})
|
| 492 |
+
if (!resp.ok) throw new Error(await resp.text())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
return await resp.json()
|
| 494 |
}
|
| 495 |
|
| 496 |
async function handleAnalysisAndVisualization(){
|
| 497 |
+
const topic = normalizeString(document.getElementById("topicInput").value)
|
| 498 |
+
if (!topic) return
|
| 499 |
+
if (gameState.mode==="miner" && gameState.energy<10){ alert("Energía insuficiente"); return }
|
| 500 |
+
|
| 501 |
+
const btn = document.getElementById("visualizeButton")
|
| 502 |
+
const pb = document.getElementById("progressBar")
|
| 503 |
+
const pbc = document.getElementById("progressBarContainer")
|
| 504 |
+
const original = btn.innerHTML
|
| 505 |
+
|
| 506 |
+
btn.disabled = true
|
| 507 |
+
btn.innerHTML = '<span class="animate-pulse">PROCESANDO</span>'
|
| 508 |
+
pbc.classList.remove("hidden")
|
| 509 |
+
pb.style.width = "90%"
|
| 510 |
+
pb.style.transition = "width 15s ease-out"
|
| 511 |
+
|
| 512 |
try{
|
| 513 |
+
const mc = document.getElementById("level1Slider").value
|
| 514 |
+
const vc = document.getElementById("level2Slider").value
|
| 515 |
+
const svc = document.getElementById("level3Slider").value
|
| 516 |
+
const origin = randomPos()
|
| 517 |
+
const res = await callGemini(topic,mc,vc,svc)
|
| 518 |
+
const txt = res.candidates?.[0]?.content?.parts?.[0]?.text
|
| 519 |
+
if (!txt) throw new Error("Respuesta inválida")
|
| 520 |
+
const data = JSON.parse(txt)
|
| 521 |
+
|
| 522 |
visualizeRoot(topic,origin)
|
| 523 |
visualizeHashtags(data.lista_palabras,origin,1)
|
| 524 |
+
|
| 525 |
+
if (gameState.mode==="miner"){ gameState.energy = Math.max(0,gameState.energy-10); updateHUD() }
|
| 526 |
+
if (gameState.mode==="bridge"){
|
| 527 |
gameState.bridgeHops++
|
| 528 |
+
document.getElementById("bridgeHops").innerText = gameState.bridgeHops
|
| 529 |
+
document.getElementById("bridgeOriginDisplay").innerText = topic
|
| 530 |
}
|
| 531 |
+
}catch(e){ console.error(e); alert("Error: "+e.message) }
|
| 532 |
finally{
|
| 533 |
+
btn.disabled = false
|
| 534 |
+
btn.innerHTML = original
|
| 535 |
+
pb.style.width = "100%"
|
| 536 |
+
setTimeout(()=>{pbc.classList.add("hidden"); pb.style.width="0%"},500)
|
| 537 |
}
|
| 538 |
}
|
| 539 |
|
| 540 |
function visualizeRoot(topic,origin){
|
| 541 |
+
const hue = Math.abs(topic.split("").reduce((a,b)=>a+b.charCodeAt(0),0)%360)
|
| 542 |
+
const col = `hsl(${hue},75%,60%)`
|
| 543 |
+
const mat = new THREE.MeshStandardMaterial({color:new THREE.Color(col),emissive:new THREE.Color(col),emissiveIntensity:2.2,roughness:0.4})
|
| 544 |
+
const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.7,32,32),mat)
|
| 545 |
sphere.position.copy(origin)
|
| 546 |
hashtagGroup.add(sphere)
|
| 547 |
+
|
| 548 |
+
const txtGeo = new TextGeometry(topic.toUpperCase(),{font,size:0.6,height:0.05})
|
| 549 |
+
const txtMesh = new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 550 |
txtMesh.position.copy(origin).add(new THREE.Vector3(0,1,0))
|
| 551 |
hashtagGroup.add(txtMesh)
|
| 552 |
}
|
| 553 |
|
| 554 |
function visualizeHashtags(list,origin,level,parentCol=null){
|
| 555 |
+
if (!list?.length) return
|
| 556 |
list.forEach(item=>{
|
| 557 |
let tag,variants
|
| 558 |
+
if (level===1){ tag=normalizeString(item.palabra_principal); variants=item.variantes||[] }
|
| 559 |
+
else if (level===2){ tag=normalizeString(item.palabra_variante); variants=item.sub_variantes||[] }
|
| 560 |
+
else{ tag=normalizeString(item); variants=[] }
|
| 561 |
+
if (!tag) return
|
| 562 |
+
|
| 563 |
+
const hue = Math.abs(tag.split("").reduce((a,b)=>a+b.charCodeAt(0),0)%360)
|
| 564 |
+
const col = `hsl(${hue},75%,60%)`
|
| 565 |
+
const nodeCol = level===1 ? col : parentCol
|
| 566 |
+
const isGold = gameState.mode==="miner" && level>=2 && Math.random()<0.15
|
| 567 |
+
|
| 568 |
+
const mat = new THREE.MeshPhysicalMaterial({
|
| 569 |
color:new THREE.Color(isGold?"#ffd700":nodeCol),
|
| 570 |
emissive:new THREE.Color(isGold?"#ffd700":nodeCol),
|
| 571 |
emissiveIntensity:isGold?2:level===1?0.8:0.4,
|
| 572 |
roughness:0.2,metalness:0.1,transmission:0.1,transparent:true,opacity:0.95
|
| 573 |
})
|
| 574 |
+
|
| 575 |
+
const theta = (hue/360)*Math.PI*2
|
| 576 |
+
let phi = 0
|
| 577 |
+
for(let i=0;i<tag.length;i++) phi = (phi + tag.charCodeAt(i)*13)%180
|
| 578 |
+
phi = ((phi/180)*90+45)*(Math.PI/180)
|
| 579 |
+
const radius = 12/(level*level)
|
| 580 |
+
const cx = radius*Math.sin(phi)*Math.cos(theta)
|
| 581 |
+
const cy = radius*Math.cos(phi)
|
| 582 |
+
const cz = radius*Math.sin(phi)*Math.sin(theta)
|
| 583 |
+
const pos = new THREE.Vector3(cx,cy,cz).add(origin)
|
| 584 |
+
|
| 585 |
+
const lineMat = new THREE.LineBasicMaterial({color:new THREE.Color(nodeCol),transparent:true,opacity:0.25})
|
| 586 |
+
const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints([origin,pos]),lineMat)
|
| 587 |
hashtagGroup.add(line)
|
| 588 |
+
|
| 589 |
+
const sphere = new THREE.Mesh(new THREE.SphereGeometry(level===1?0.35:level===2?0.18:0.1,16,16),mat)
|
| 590 |
sphere.position.copy(pos)
|
| 591 |
+
sphere.userData = {label:tag,isGolden:isGold,level}
|
| 592 |
hashtagGroup.add(sphere)
|
| 593 |
+
|
| 594 |
+
const txtSize = level===1?0.35:level===2?0.18:0.12
|
| 595 |
+
const txtGeo = new TextGeometry(tag.toUpperCase(),{font,size:txtSize,height:0.01})
|
| 596 |
+
const txtMesh = new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:new THREE.Color(nodeCol)}))
|
| 597 |
txtMesh.position.copy(pos).add(new THREE.Vector3(0,level===1?0.6:0.4,0))
|
| 598 |
+
txtMesh.userData.isText = true
|
| 599 |
hashtagGroup.add(txtMesh)
|
| 600 |
+
|
| 601 |
visualizeHashtags(variants,pos,level+1,isGold?"#ffd700":nodeCol)
|
| 602 |
})
|
| 603 |
}
|
| 604 |
|
| 605 |
+
/* ------------ MAPS & USERS ------------ */
|
| 606 |
function loadAllMaps(){
|
| 607 |
+
if (!db || !font){ setTimeout(loadAllMaps,500); return }
|
| 608 |
+
const q = query(collection(db,"artifacts","neuronal-1f3b9","public","data","maps"))
|
| 609 |
onSnapshot(q,snap=>{
|
| 610 |
+
while (hashtagGroup.children.length){
|
| 611 |
+
const obj = hashtagGroup.children[0]
|
| 612 |
hashtagGroup.remove(obj)
|
| 613 |
+
if (obj.geometry) obj.geometry.dispose()
|
| 614 |
+
if (obj.material){
|
| 615 |
+
if (Array.isArray(obj.material)) obj.material.forEach(m=>m.dispose())
|
| 616 |
else obj.material.dispose()
|
| 617 |
}
|
| 618 |
}
|
| 619 |
+
userMaps = {}
|
| 620 |
snap.docs.forEach(d=>{
|
| 621 |
+
const m = d.data()
|
| 622 |
+
if (!m.origin) return
|
| 623 |
+
const o = new THREE.Vector3(m.origin.x,m.origin.y,m.origin.z)
|
| 624 |
+
if (!userMaps[m.userId]) userMaps[m.userId] = []
|
| 625 |
userMaps[m.userId].push(o)
|
| 626 |
try{
|
| 627 |
+
const data = JSON.parse(m.data)
|
| 628 |
visualizeRoot(m.topic,o)
|
| 629 |
visualizeHashtags(data.lista_palabras,o,1)
|
| 630 |
}catch{}
|
| 631 |
})
|
| 632 |
+
const list = document.getElementById("userList")
|
| 633 |
+
if (list) list.innerHTML = ""
|
| 634 |
Object.keys(userMaps).forEach(uid=>{
|
| 635 |
+
const prof = userProfileCache[uid]
|
| 636 |
+
const name = prof ? prof.username : "ANON "+uid.slice(0,4)
|
| 637 |
+
const item = document.createElement("div")
|
| 638 |
+
item.className = "text-cyan-400 hover:text-white cursor-pointer hover:bg-white/5 p-1 rounded transition-colors text-[10px] tracking-widest"
|
| 639 |
+
item.innerHTML = `> <span class="font-bold">${name}</span>`
|
| 640 |
+
item.onclick = () => teleportToUser(uid)
|
| 641 |
list.appendChild(item)
|
| 642 |
+
const cent = new THREE.Vector3()
|
| 643 |
userMaps[uid].forEach(p=>cent.add(p))
|
| 644 |
cent.divideScalar(userMaps[uid].length)
|
| 645 |
createUserSun(cent,name,uid===userId)
|
|
|
|
| 651 |
}
|
| 652 |
|
| 653 |
function createUserSun(pos,name,isMe){
|
| 654 |
+
const col = isMe ? 0xffaa00 : 0x00ff88
|
| 655 |
+
const mat = new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:1.8,roughness:0.2})
|
| 656 |
+
const mesh = new THREE.Mesh(new THREE.SphereGeometry(3,32,32),mat)
|
| 657 |
mesh.position.copy(pos)
|
| 658 |
hashtagGroup.add(mesh)
|
| 659 |
+
const txtGeo = new TextGeometry(name.toUpperCase(),{font,size:1.2})
|
| 660 |
+
const txtMesh = new THREE.Mesh(txtGeo,new THREE.MeshBasicMaterial({color:0xffffff}))
|
| 661 |
txtMesh.position.copy(pos).add(new THREE.Vector3(0,4,0))
|
| 662 |
hashtagGroup.add(txtMesh)
|
| 663 |
}
|
| 664 |
|
| 665 |
function focusOnUserMaps(){
|
| 666 |
+
if (!controls || !userId || !userMaps[userId]) return
|
| 667 |
+
const c = getCurrentUserCentroid()
|
| 668 |
controls.target.copy(c)
|
| 669 |
camera.position.copy(c).add(new THREE.Vector3(0,10,40))
|
| 670 |
}
|
| 671 |
|
| 672 |
function getCurrentUserCentroid(){
|
| 673 |
+
if (!userId || !userMaps[userId] || userMaps[userId].length===0) return new THREE.Vector3(0,0,0)
|
| 674 |
+
const cen = new THREE.Vector3()
|
| 675 |
userMaps[userId].forEach(p=>cen.add(p))
|
| 676 |
cen.divideScalar(userMaps[userId].length)
|
| 677 |
return cen
|
| 678 |
}
|
| 679 |
|
| 680 |
function teleportToUser(uid){
|
| 681 |
+
if (!userMaps[uid]) return
|
| 682 |
+
const cen = new THREE.Vector3()
|
| 683 |
userMaps[uid].forEach(p=>cen.add(p))
|
| 684 |
cen.divideScalar(userMaps[uid].length)
|
| 685 |
controls.target.copy(cen)
|
| 686 |
camera.position.copy(cen).add(new THREE.Vector3(0,10,40))
|
| 687 |
}
|
| 688 |
|
| 689 |
+
/* ------------ MINIMAP ------------ */
|
| 690 |
function drawMinimap(){
|
| 691 |
+
if (!minimapCtx) return
|
| 692 |
+
const w = minimapCtx.canvas.width, h = minimapCtx.canvas.height
|
|
|
|
| 693 |
minimapCtx.clearRect(0,0,w,h)
|
| 694 |
+
minimapDotCoords = []
|
| 695 |
+
const myC = getCurrentUserCentroid()
|
| 696 |
Object.keys(userMaps).forEach(uid=>{
|
| 697 |
+
const cen = new THREE.Vector3()
|
| 698 |
userMaps[uid].forEach(p=>cen.add(p))
|
| 699 |
cen.divideScalar(userMaps[uid].length)
|
| 700 |
+
const x = w/2 + (cen.x - myC.x)*minimapScale
|
| 701 |
+
const y = h/2 + (cen.z - myC.z)*minimapScale
|
| 702 |
+
const isMe = uid===userId
|
| 703 |
minimapDotCoords.push({x,y,uid})
|
| 704 |
+
minimapCtx.fillStyle = isMe?"#22d3ee":"#64748b"
|
| 705 |
minimapCtx.beginPath()
|
| 706 |
minimapCtx.arc(x,y,isMe?4:2.5,0,Math.PI*2)
|
| 707 |
minimapCtx.fill()
|
| 708 |
+
if (isMe){
|
| 709 |
minimapCtx.strokeStyle="rgba(34,211,238,0.3)"
|
| 710 |
minimapCtx.beginPath()
|
| 711 |
minimapCtx.arc(x,y,8,0,Math.PI*2)
|
| 712 |
minimapCtx.stroke()
|
| 713 |
}
|
| 714 |
})
|
| 715 |
+
if (cometGroup){
|
| 716 |
+
const cx = w/2 + (cometGroup.position.x - myC.x)*minimapScale
|
| 717 |
+
const cy = h/2 + (cometGroup.position.z - myC.z)*minimapScale
|
| 718 |
minimapCtx.beginPath()
|
| 719 |
minimapCtx.arc(cx,cy,3,0,Math.PI*2)
|
| 720 |
minimapCtx.fillStyle="rgba(0,255,255,0.4)"
|
|
|
|
| 727 |
}
|
| 728 |
|
| 729 |
function onMinimapClick(e){
|
| 730 |
+
const rect = minimapCtx.canvas.getBoundingClientRect()
|
| 731 |
+
const x = e.clientX - rect.left
|
| 732 |
+
const y = e.clientY - rect.top
|
| 733 |
+
let closest=null, minDist=20
|
| 734 |
minimapDotCoords.forEach(d=>{
|
| 735 |
+
const dx = d.x - x, dy = d.y - y
|
| 736 |
+
const dist = Math.hypot(dx,dy)
|
| 737 |
+
if (dist < minDist){ minDist = dist; closest = d.uid }
|
| 738 |
})
|
| 739 |
+
if (closest) teleportToUser(closest)
|
| 740 |
}
|
| 741 |
|
| 742 |
+
/* ------------ EVENTS ------------ */
|
| 743 |
function onWindowResize(){
|
| 744 |
+
camera.aspect = innerWidth / innerHeight
|
| 745 |
camera.updateProjectionMatrix()
|
| 746 |
renderer.setSize(innerWidth,innerHeight)
|
| 747 |
composer.setSize(innerWidth,innerHeight)
|
| 748 |
}
|
| 749 |
|
| 750 |
function onPointerMove(e){
|
| 751 |
+
mouse.x = (e.clientX / innerWidth) * 2 - 1
|
| 752 |
+
mouse.y = -(e.clientY / innerHeight) * 2 + 1
|
| 753 |
+
tooltip.style.left = e.clientX + 20 + "px"
|
| 754 |
+
tooltip.style.top = e.clientY + "px"
|
| 755 |
}
|
| 756 |
|
| 757 |
function onMouseClick(){
|
| 758 |
raycaster.setFromCamera(mouse,camera)
|
| 759 |
+
const objs = hashtagGroup.children.filter(o=>!o.userData.isText)
|
| 760 |
+
const hits = raycaster.intersectObjects(objs,false)
|
| 761 |
+
if (!hits.length) return
|
| 762 |
+
const obj = hits[0].object
|
| 763 |
+
|
| 764 |
+
if (obj.userData.isGolden){
|
| 765 |
gameState.vault.push(obj.userData.label)
|
| 766 |
scene.add(createExplosion(obj.position))
|
| 767 |
hashtagGroup.remove(obj)
|
| 768 |
+
gameState.energy = Math.min(100,gameState.energy+12)
|
| 769 |
updateHUD()
|
| 770 |
}
|
| 771 |
+
|
| 772 |
+
if (obj.userData.powerType){
|
| 773 |
+
if (obj.userData.powerType==="shield"){ gameState.shield=true; setTimeout(()=>gameState.shield=false,5000) }
|
| 774 |
+
if (obj.userData.powerType==="speed"){ controls.rotateSpeed*=2; setTimeout(()=>controls.rotateSpeed/=2,5000) }
|
| 775 |
+
if (obj.userData.powerType==="double"){ gameState.score*=2 }
|
| 776 |
scene.remove(obj)
|
| 777 |
}
|
| 778 |
}
|
| 779 |
|
| 780 |
function updateRaycaster(){
|
| 781 |
raycaster.setFromCamera(mouse,camera)
|
| 782 |
+
const targets = [...hashtagGroup.children]
|
| 783 |
+
const hits = raycaster.intersectObjects(targets,false)
|
| 784 |
+
if (hits.length){
|
| 785 |
+
const o = hits[0].object
|
| 786 |
+
if (o.userData.label){
|
| 787 |
tooltip.classList.remove("hidden")
|
| 788 |
+
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>`
|
| 789 |
+
document.body.style.cursor = "pointer"
|
| 790 |
}else{
|
| 791 |
tooltip.classList.add("hidden")
|
| 792 |
+
document.body.style.cursor = "default"
|
| 793 |
}
|
| 794 |
}else{
|
| 795 |
tooltip.classList.add("hidden")
|
| 796 |
+
document.body.style.cursor = "default"
|
| 797 |
}
|
| 798 |
}
|
| 799 |
|
| 800 |
function createExplosion(pos,color=0xff0000){
|
| 801 |
+
const geo = new THREE.BufferGeometry()
|
| 802 |
+
const cnt = 30
|
| 803 |
+
const arr = new Float32Array(cnt*3)
|
| 804 |
+
for(let i=0;i<cnt*3;i++) arr[i] = (Math.random()-0.5)*2
|
| 805 |
geo.setAttribute("position",new THREE.BufferAttribute(arr,3))
|
| 806 |
+
const mat = new THREE.PointsMaterial({color, size:0.5, transparent:true})
|
| 807 |
+
const pts = new THREE.Points(geo,mat)
|
| 808 |
pts.position.copy(pos)
|
| 809 |
let life=1
|
| 810 |
+
function anim(){ life-=0.05; pts.scale.multiplyScalar(1.1); mat.opacity=life; if(life>0) requestAnimationFrame(anim); else scene.remove(pts) }
|
| 811 |
anim()
|
| 812 |
scene.add(pts)
|
| 813 |
return pts
|
| 814 |
}
|
| 815 |
|
| 816 |
+
function spawnTrashNodes(){
|
| 817 |
+
if (trashGroup) scene.remove(trashGroup)
|
| 818 |
+
trashGroup = new THREE.Group()
|
| 819 |
+
scene.add(trashGroup)
|
| 820 |
+
const words = ["VIRAL","FAKE","GLITCH","ERROR","NOISE","SPAM","BOT"]
|
| 821 |
+
for(let i=0;i<20;i++){
|
| 822 |
+
const geo = new THREE.DodecahedronGeometry(0.8,0)
|
| 823 |
+
const mat = new THREE.MeshBasicMaterial({color:0xff0000,wireframe:true})
|
| 824 |
+
const mesh = new THREE.Mesh(geo,mat)
|
| 825 |
+
mesh.position.copy(randomPos())
|
| 826 |
+
mesh.userData.isTrash = true
|
| 827 |
+
mesh.userData.word = words[Math.floor(Math.random()*words.length)]
|
| 828 |
+
trashGroup.add(mesh)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 829 |
}
|
|
|
|
|
|
|
|
|
|
| 830 |
}
|
| 831 |
|
| 832 |
function animate(){
|
| 833 |
requestAnimationFrame(animate)
|
| 834 |
+
const dt = clock.getDelta()
|
| 835 |
controls.update()
|
| 836 |
+
if (gameState.cometActive) updateComet(dt)
|
| 837 |
+
if (gameState.stormActive) {} // color already changed in spawnStorm()
|
| 838 |
updateRaycaster()
|
| 839 |
drawMinimap()
|
| 840 |
hashtagGroup.children.forEach(o=>{
|
| 841 |
+
if (o.userData.isText){
|
| 842 |
o.lookAt(camera.position)
|
| 843 |
+
const d = o.position.distanceTo(camera.position)
|
| 844 |
+
let s = (1/d)*12
|
| 845 |
+
s = Math.max(0.6,Math.min(5,s))
|
| 846 |
o.scale.set(s,s,s)
|
| 847 |
}
|
| 848 |
})
|
|
|
|
| 850 |
}
|
| 851 |
|
| 852 |
function setGameMode(mode){
|
| 853 |
+
gameState.mode = mode
|
| 854 |
+
document.getElementById("activeModeText").innerText = mode==="bridge"?"PUENTE NEURAL":(mode==="miner"?"MINERO DE DATOS":(mode==="comet"?"DEFENSA COMETA":"EXPLORACIÓN LIBRE"))
|
| 855 |
+
if (mode==="bridge"){ document.getElementById("bridgeControls").classList.remove("hidden"); document.getElementById("actionBtnText").innerText = "CONSTRUIR NODO" }
|
| 856 |
+
else{ document.getElementById("bridgeControls").classList.add("hidden"); document.getElementById("actionBtnText").innerText = "EJECUTAR ANÁLISIS" }
|
| 857 |
+
if (mode==="comet"){ gameState.cometActive = true; spawnTrashNodes() }
|
| 858 |
+
else{ gameState.cometActive = false; if(trashGroup){ scene.remove(trashGroup); trashGroup=null } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 859 |
}
|
| 860 |
+
window.setGameMode = setGameMode
|
| 861 |
|
| 862 |
+
function updateComet(dt){
|
| 863 |
+
if (!cometGroup) return
|
| 864 |
+
let speed = gameState.cometActive ? 0.8 : 0.35
|
| 865 |
+
cometAngle += dt * speed
|
| 866 |
+
const rX = 80, rZ = 60
|
| 867 |
+
const x = cometGroup.position.x + Math.cos(cometAngle)*rX
|
| 868 |
+
const z = cometGroup.position.z + Math.sin(cometAngle)*rZ
|
| 869 |
+
const y = cometGroup.position.y + Math.sin(cometAngle*2)*20
|
| 870 |
+
cometGroup.position.set(x,y,z)
|
| 871 |
+
|
| 872 |
+
const pos = cometParticlesMesh.geometry.attributes.position.array
|
| 873 |
+
const col = cometParticlesMesh.geometry.attributes.color.array
|
| 874 |
+
const sz = cometParticlesMesh.geometry.attributes.size.array
|
| 875 |
+
let spawn = gameState.cometActive ? 10 : 5
|
| 876 |
+
|
| 877 |
+
for(let i=0;i<400;i++){
|
| 878 |
+
if (spawn>0 && cometParticlesData[i].life<0){
|
| 879 |
+
cometParticlesData[i].life = 1
|
| 880 |
+
pos[i*3] = cometGroup.position.x + (Math.random()-0.5)
|
| 881 |
+
pos[i*3+1] = cometGroup.position.y + (Math.random()-0.5)
|
| 882 |
+
pos[i*3+2] = cometGroup.position.z + (Math.random()-0.5)
|
| 883 |
+
if (gameState.cometActive){ col[i*3]=1; col[i*3+1]=0.2; col[i*3+2]=0 }
|
| 884 |
+
else{ col[i*3]=0.2; col[i*3+1]=1; col[i*3+2]=1 }
|
| 885 |
+
sz[i] = 1.2
|
| 886 |
+
spawn--
|
| 887 |
+
}
|
| 888 |
+
}
|
| 889 |
+
for(let i=0;i<400;i++){
|
| 890 |
+
if (cometParticlesData[i].life>0){
|
| 891 |
+
cometParticlesData[i].life -= dt*0.7
|
| 892 |
+
sz[i] = cometParticlesData[i].life*1.8
|
| 893 |
+
}else pos[i*3]=99999
|
| 894 |
}
|
| 895 |
+
cometParticlesMesh.geometry.attributes.position.needsUpdate = true
|
| 896 |
+
cometParticlesMesh.geometry.attributes.color.needsUpdate = true
|
| 897 |
+
cometParticlesMesh.geometry.attributes.size.needsUpdate = true
|
| 898 |
}
|
| 899 |
|
| 900 |
initFirebase()
|