-
-
-
-
+
+
+
+
+
+
+
Radar Galáctico
-
Modo Activo
EXPLORACIÓN LIBRE
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+ async function getProfile(uid) {
+ if(userProfileCache[uid]) return userProfileCache[uid];
+ try {
+ const snap = await getDoc(doc(db, 'artifacts', appId, 'users', uid, 'user_data', 'profile'));
+ if(snap.exists()) { userProfileCache[uid] = snap.data(); return snap.data(); }
+ } catch {}
+ return null;
+ }
+
+ function focusOnUserMaps() {
+ if(!controls || !userId || !userMaps[userId]) return;
+ const c = getCurrentUserCentroid();
+ controls.target.copy(c);
+ camera.position.copy(c).add(new THREE.Vector3(0,10,40));
+ }
+
+ function teleportToUser(uid) {
+ if(!userMaps[uid]) return;
+ const origins = userMaps[uid];
+ const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
+ controls.target.copy(c);
+ camera.position.copy(c).add(new THREE.Vector3(0,10,40));
+ }
+
+ function drawMinimap() {
+ if(!minimapCtx) return;
+ const w = minimapCtx.canvas.width; const h = minimapCtx.canvas.height;
+ minimapCtx.clearRect(0,0,w,h);
+ minimapDotCoords = [];
+
+ const myC = getCurrentUserCentroid();
+
+ Object.keys(userMaps).forEach(uid => {
+ const origins = userMaps[uid];
+ const c = new THREE.Vector3(); origins.forEach(p=>c.add(p)); c.divideScalar(origins.length);
+
+ const x = w/2 + (c.x - myC.x)*minimapScale;
+ const y = h/2 + (c.z - myC.z)*minimapScale;
+
+ const isMe = (uid === userId);
+ const mainColor = isMe ? '#22d3ee' : '#64748b';
+
+ minimapDotCoords.push({x, y, uid});
+
+ if(origins.length > 0) {
+ const satColor = isMe ? 'rgba(34, 211, 238, 0.4)' : 'rgba(100, 116, 139, 0.4)';
+ origins.forEach(o => {
+ const sx = w/2 + (o.x - myC.x)*minimapScale;
+ const sy = h/2 + (o.z - myC.z)*minimapScale;
+ minimapCtx.beginPath();
+ minimapCtx.arc(sx, sy, 1, 0, Math.PI*2);
+ minimapCtx.fillStyle = satColor;
+ minimapCtx.fill();
+ });
+ }
+
+ minimapCtx.fillStyle = mainColor;
+ minimapCtx.beginPath();
+ minimapCtx.arc(x,y, isMe?4:2.5, 0, Math.PI*2);
+ minimapCtx.fill();
+
+ if(isMe) {
+ minimapCtx.strokeStyle = 'rgba(34, 211, 238, 0.3)';
+ minimapCtx.beginPath(); minimapCtx.arc(x,y, 8, 0, Math.PI*2); minimapCtx.stroke();
+ }
+ });
+
+ if (cometGroup) {
+ const cometX = w/2 + (cometGroup.position.x - myC.x) * minimapScale;
+ const cometY = h/2 + (cometGroup.position.z - myC.z) * minimapScale;
+ minimapCtx.beginPath(); minimapCtx.arc(cometX, cometY, 3, 0, Math.PI * 2);
+ minimapCtx.fillStyle = 'rgba(0, 255, 255, 0.4)'; minimapCtx.fill();
+ minimapCtx.beginPath(); minimapCtx.arc(cometX, cometY, 1.5, 0, Math.PI * 2);
+ minimapCtx.fillStyle = '#ffffff'; minimapCtx.fill();
+ }
+ }
+
+ function onMinimapClick(e) {
+ if(!minimapCtx) return;
+ const rect = minimapCtx.canvas.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ let closest = null; let minD = 20;
+
+ minimapDotCoords.forEach(dot => {
+ const d = Math.sqrt((x-dot.x)**2 + (y-dot.y)**2);
+ if(d < minD) { minD = d; closest = dot.uid; }
+ });
+ if(closest) teleportToUser(closest);
+ }
+
+ function onWindowResize() {
+ camera.aspect = window.innerWidth/window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ composer.setSize(window.innerWidth, window.innerHeight);
+ }
+
+ function onPointerMove(e) {
+ mouse.x = (e.clientX/window.innerWidth)*2-1;
+ mouse.y = -(e.clientY/window.innerHeight)*2+1;
+ tooltip.style.left = e.clientX+20+'px';
+ tooltip.style.top = e.clientY+'px';
+ }
+
+ function onMouseClick(e) {
+ if(!raycaster) return;
+ raycaster.setFromCamera(mouse, camera);
+
+ if(gameState.cometActive && trashGroup) {
+ const intersects = raycaster.intersectObjects(trashGroup.children);
+ if(intersects.length > 0) {
+ const obj = intersects[0].object;
+ scene.add(createExplosion(obj.position));
+ trashGroup.remove(obj);
+ gameState.score += 100;
+ document.getElementById('missionText').innerText = `PUNTOS: ${gameState.score}`;
+ return;
+ }
+ }
+
+ if(gameState.mode === 'miner') {
+ let targets = [...hashtagGroup.children];
+ const intersects = raycaster.intersectObjects(targets, false);
+ if(intersects.length > 0) {
+ const obj = intersects[0].object;
+ if(obj.userData.isGolden) {
+ addToVault(obj.userData.hashtag);
+ scene.add(createExplosion(obj.position, 0xffd700));
+ hashtagGroup.remove(obj);
+ gameState.energy += 15;
+ if(gameState.energy > 100) gameState.energy = 100;
+ updateHUD();
+ }
+ }
+ }
+ }
+
+ function createExplosion(pos, color = 0xff0000) {
+ const geo = new THREE.BufferGeometry();
+ const count = 30;
+ const positions = new Float32Array(count * 3);
+ for(let i=0;i
0) requestAnimationFrame(animExplosion);
+ else scene.remove(pts);
+ }
+ animExplosion();
+ return pts;
+ }
+
+ function updateRaycaster() {
+ raycaster.setFromCamera(mouse, camera);
+ let targets = [...hashtagGroup.children];
+ if(cometHead) targets.push(cometHead);
+
+ const intersects = raycaster.intersectObjects(targets, false);
+
+ if(intersects.length > 0) {
+ let o = intersects[0].object;
+ if(o.parent === cometHead || o.parent === cometGroup) o = cometHead;
+
+ if(o === cometHead) {
+ tooltip.classList.remove('hidden');
+ tooltip.innerHTML = `COMETA NEURAL
`;
+ document.body.style.cursor = 'pointer';
+ return;
+ }
+
+ const d = o.userData;
+ if(d.hashtag) {
+ tooltip.classList.remove('hidden');
+ let typeColor = d.isGolden ? "text-yellow-400" : (d.isPlaceholder ? "text-green-400" : "text-cyan-300");
+ let typeText = d.isGolden ? "NODO DORADO (CLICK PARA RECOGER)" : (d.isPlaceholder ? "NODO USUARIO" : `NIVEL ${d.level}`);
+
+ tooltip.innerHTML = `
+ ${d.hashtag}
+ ${typeText}
+ `;
+ document.body.style.cursor = 'pointer';
+ }
+ } else {
+ tooltip.classList.add('hidden');
+ document.body.style.cursor = 'default';
+ }
+ }
+
+ function stringToHslColor(str) {
+ let hash = 0; for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ return { color: `hsl(${Math.abs(hash % 360)}, 75%, 60%)`, h: Math.abs(hash % 360) };
+ }
+
-
+