Desktop: Click and drag to rotate, scroll to zoom
Mobile: Touch and drag to rotate, pinch to zoom
} setRandomPosition() { const angle = Math.random() * Math.PI * 2; const radius = Math.random() * (CONFIG.FLOOR_RADIUS * 0.7); this.mesh.position.set( Math.cos(angle) * radius, CONFIG.AVATAR_HEIGHT / 2, Math.sin(angle) * radius ); } startIdleAnimation() { // Subtle bobbing animation gsap.to(this.mesh.position, { y: "+=" + (Math.random() * 0.2 + 0.1), duration: 1.5 + Math.random(), yoyo: true, repeat: -1, ease: 'sine.inOut', delay: Math.random() * 2 }); // Random head turns this.mesh.rotation.y = (Math.random() - 0.5) * 0.5; gsap.to(this.mesh.rotation, { y: this.mesh.rotation.y + (Math.random() - 0.5) * 0.5, duration: 3 + Math.random() * 4, yoyo: true, repeat: -1, ease: 'sine.inOut', delay: Math.random() * 2 }); } getRandomInterests() { const count = Math.floor(Math.random() * 3) + 1; const shuffled = [...CONFIG.INTERESTS].sort(() => 0.5 - Math.random()); return shuffled.slice(0, count).join(', '); } update(delta) { // Update interaction cooldown if (this.interactionCooldown > 0) { this.interactionCooldown -= delta; } // Simple wandering behavior if (!this.isInteracting && Math.random() < 0.01) { this.wander(); } // Move toward target position if set if (this.targetPosition) { const direction = new THREE.Vector3().subVectors(this.targetPosition, this.mesh.position); const distance = direction.length(); if (distance > 0.1) { direction.normalize(); this.mesh.position.add(direction.multiplyScalar(delta * 2)); this.mesh.lookAt(this.targetPosition); this.mesh.rotation.x = 0; this.mesh.rotation.z = 0; } else { this.targetPosition = null; // Check for nearby avatars to interact with if (!this.isInteracting) { this.findNearbyAvatar(); } } } } wander() { if (this.targetPosition || this.isInteracting) return; const angle = Math.random() * Math.PI * 2; const distance = 2 + Math.random() * 5; this.targetPosition = new THREE.Vector3( Math.cos(angle) * distance + this.mesh.position.x, this.mesh.position.y, Math.sin(angle) * distance + this.mesh.position.z ); // Ensure position is within bounds const distanceFromCenter = new THREE.Vector2(this.targetPosition.x, this.targetPosition.z).length(); if (distanceFromCenter > CONFIG.FLOOR_RADIUS * 0.8) { this.targetPosition.x *= (CONFIG.FLOOR_RADIUS * 0.8) / distanceFromCenter; this.targetPosition.z *= (CONFIG.FLOOR_RADIUS * 0.8) / distanceFromCenter; } } findNearbyAvatar() { if (this.interactionCooldown > 0) return; const nearbyAvatars = []; const position = this.mesh.position.clone(); avatarGroup.children.forEach(child => { if (child !== this.mesh && child.userData.avatar) { const distance = child.position.distanceTo(position); if (distance < CONFIG.INTERACTION_DISTANCE) { nearbyAvatars.push({ avatar: child.userData.avatar, distance: distance }); } } }); if (nearbyAvatars.length > 0) { // Find the closest avatar nearbyAvatars.sort((a, b) => a.distance - b.distance); const targetAvatar = nearbyAvatars[0].avatar; // Only interact if the other avatar is also not interacting if (!targetAvatar.isInteracting) { this.interactWith(targetAvatar); targetAvatar.interactWith(this); } } } interactWith(avatar) { this.isInteracting = true; this.interactionTarget = avatar; this.targetPosition = null; // Face the other avatar const direction = new THREE.Vector3().subVectors( avatar.mesh.position, this.mesh.position ); this.mesh.lookAt(avatar.mesh.position); this.mesh.rotation.x = 0; this.mesh.rotation.z = 0; // Interaction animation const originalY = this.mesh.position.y; const jumpHeight = 0.5; // Bounce animation gsap.to(this.mesh.position, { y: originalY + jumpHeight, duration: 0.3, yoyo: true, repeat: 1, onComplete: () => { this.isInteracting = false; this.interactionTarget = null; this.interactionCooldown = CONFIG.INTERACTION_COOLDOWN; } }); // Show interaction effect this.showInteractionEffect(); } showInteractionEffect() { const effectTypes = ['wave', 'heart', 'star', 'idea']; const effectType = effectTypes[Math.floor(Math.random() * effectTypes.length)]; let emoji = '👋'; switch(effectType) { case 'heart': emoji = '❤️'; break; case 'star': emoji = '⭐'; break; case 'idea': emoji = '💡'; break; } const effect = document.createElement('div'); effect.className = 'interaction-effect'; effect.textContent = emoji; effect.style.position = 'absolute'; effect.style.pointerEvents = 'none'; effect.style.fontSize = '24px'; effect.style.transform = 'translate(-50%, -50%)'; effect.style.opacity = '0'; effect.style.transition = 'all 1s ease-out'; document.body.appendChild(effect); // Position above avatar const position = this.mesh.position.clone(); position.y += CONFIG.AVATAR_HEIGHT + 1; const vector = position.clone().project(camera); const x = (vector.x * 0.5 + 0.5) * window.innerWidth; const y = (-(vector.y * 0.5) + 0.5) * window.innerHeight; effect.style.left = x + 'px'; effect.style.top = y + 'px'; effect.style.opacity = '1'; // Animate setTimeout(() => { effect.style.transform = 'translate(-50%, -150%)'; effect.style.opacity = '0'; // Remove after animation setTimeout(() => { if (effect.parentNode) { effect.parentNode.removeChild(effect); } }, 1000); }, 100); } showInfo() { const infoPanel = document.getElementById('avatarInfo'); const nameElement = document.getElementById('avatarName'); const tierElement = document.getElementById('avatarTier'); const memberSinceElement = document.getElementById('memberSince'); const interestsElement = document.getElementById('interests'); const iconElement = document.getElementById('avatarIcon'); // Set info nameElement.textContent = this.name; tierElement.textContent = this.tier.charAt(0).toUpperCase() + this.tier.slice(1); tierElement.className = 'avatar-tier tier-' + this.tier; memberSinceElement.textContent = this.memberSince; interestsElement.textContent = this.interests; // Set icon based on tier const icons = { 'free': '👤', 'pro': '🌟', 'hustler': '🔥', 'conqueror': '👑' }; iconElement.textContent = icons[this.tier] || '👤'; // Show panel infoPanel.classList.add('visible'); } hideInfo() { document.getElementById('avatarInfo').classList.remove('visible'); } } // Main application class class LBEPlaza { constructor() { this.avatars = []; this.clock = new THREE.Clock(); this.init(); } init() { try { console.log('Initializing LBE Plaza...'); this.createScene(); console.log('Scene created'); this.createLights(); console.log('Lights created'); this.createFloor(); console.log('Floor created'); this.createAvatars(); console.log('Avatars created'); this.setupEventListeners(); console.log('Event listeners set up'); this.animate(); console.log('Animation started'); // Hide loading screen after everything is set up setTimeout(() => { const loadingEl = document.getElementById('loading'); if (loadingEl) { loadingEl.classList.add('hidden'); console.log('Loading screen hidden'); } else { console.error('Loading element not found'); } }, 1500); } catch (error) { console.error('Error initializing LBE Plaza:', error); // Show error to user const errorDiv = document.createElement('div'); errorDiv.style.position = 'fixed'; errorDiv.style.top = '10px'; errorDiv.style.left = '10px'; errorDiv.style.color = 'red'; errorDiv.style.backgroundColor = 'rgba(0,0,0,0.7)'; errorDiv.style.padding = '10px'; errorDiv.style.zIndex = '10000'; errorDiv.innerHTML = 'Error initializing 3D environment: ' + error.message; document.body.appendChild(errorDiv); } } createScene() { // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a1a); scene.fog = new THREE.FogExp2(0x0a0a1a, 0.01); // Camera camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set( Math.cos(CONFIG.CAMERA_ANGLE) * CONFIG.CAMERA_DISTANCE, CONFIG.CAMERA_HEIGHT, Math.sin(CONFIG.CAMERA_ANGLE) * CONFIG.CAMERA_DISTANCE ); camera.lookAt(0, 0, 0); // Renderer renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.setPixelRatio(window.devicePixelRatio); document.getElementById('gameContainer').appendChild(renderer.domElement); // Controls controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.minDistance = 10; controls.maxDistance = 50; controls.maxPolarAngle = Math.PI / 2; // Group for avatars avatarGroup = new THREE.Group(); scene.add(avatarGroup); // Raycaster for object picking this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); // Clock for animations this.clock = new THREE.Clock(); // Handle window resize window.addEventListener('resize', this.onWindowResize.bind(this), false); } createLights() { // Ambient light const ambientLight = new THREE.AmbientLight(0x404040, 0.5); scene.add(ambientLight); // Directional light (sun) const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 10); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; directionalLight.shadow.camera.left = -20; directionalLight.shadow.camera.right = 20; directionalLight.shadow.camera.top = 20; directionalLight.shadow.camera.bottom = -20; scene.add(directionalLight); // Hemisphere light for more natural lighting const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6); hemisphereLight.position.set(0, 20, 0); scene.add(hemisphereLight); } createFloor() { // Main floor const floorGeometry = new THREE.CircleGeometry(CONFIG.FLOOR_RADIUS, CONFIG.FLOOR_SEGMENTS); const floorMaterial = new THREE.MeshStandardMaterial({ color: CONFIG.COLORS.FLOOR, roughness: 0.8, metalness: 0.2 }); const floor = new THREE.Mesh(floorGeometry, floorMaterial); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor); // Grid helper const gridHelper = new THREE.GridHelper( CONFIG.FLOOR_RADIUS * 2, 20, CONFIG.COLORS.GRID, CONFIG.COLORS.GRID ); gridHelper.position.y = 0.01; // Slightly above floor to prevent z-fighting scene.add(gridHelper); // Edge glow const edgeGeometry = new THREE.RingGeometry( CONFIG.FLOOR_RADIUS * 0.95, CONFIG.FLOOR_RADIUS, 64 ); const edgeMaterial = new THREE.MeshBasicMaterial({ color: 0x4cc9f0, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); const edge = new THREE.Mesh(edgeGeometry, edgeMaterial); edge.rotation.x = -Math.PI / 2; edge.position.y = 0.02; scene.add(edge); } createAvatars() { // Create initial avatars const tiers = ['free', 'pro', 'hustler', 'conqueror']; const tierWeights = [0.5, 0.3, 0.15, 0.05]; // Weighted distribution for (let i = 0; i < CONFIG.AVATAR_COUNT; i++) { // Weighted random tier selection const rand = Math.random(); let tierIndex = 0; let cumulativeWeight = 0; for (let j = 0; j < tierWeights.length; j++) { cumulativeWeight += tierWeights[j]; if (rand <= cumulativeWeight) { tierIndex = j; break; } } const tier = tiers[tierIndex]; const name = CONFIG.NAMES[Math.floor(Math.random() * CONFIG.NAMES.length)]; // Create avatar with random position const angle = Math.random() * Math.PI * 2; const radius = Math.random() * (CONFIG.FLOOR_RADIUS * 0.7); const position = new THREE.Vector3( Math.cos(angle) * radius, CONFIG.AVATAR_HEIGHT / 2, Math.sin(angle) * radius ); const avatar = new Avatar(tier, name, position); this.avatars.push(avatar); } } setupEventListeners() { const container = document.getElementById('gameContainer'); // Spawn buttons document.getElementById('spawnFreeBtn').addEventListener('click', () => this.spawnAvatar('free')); document.getElementById('spawnProBtn').addEventListener('click', () => this.spawnAvatar('pro')); document.getElementById('spawnHustlerBtn').addEventListener('click', () => this.spawnAvatar('hustler')); document.getElementById('spawnConquerorBtn').addEventListener('click', () => this.spawnAvatar('conqueror')); // Connect button document.getElementById('connectBtn').addEventListener('click', () => { alert('Connect feature coming soon!'); }); // Challenge button document.getElementById('challengeBtn').addEventListener('click', () => { alert('Challenge feature coming soon!'); }); // Click to select avatar container.addEventListener('click', (event) => this.onDocumentClick(event), false); // Hide info panel when clicking outside document.addEventListener('click', (event) => { const infoPanel = document.getElementById('avatarInfo'); if (!event.target.closest('#avatarInfo') && !event.target.closest('.avatar')) { infoPanel.classList.remove('visible'); } }); } onDocumentClick(event) { event.preventDefault(); // Calculate mouse position in normalized device coordinates this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; this.mouse.y = - (event.clientY / window.innerHeight) * 2 + 1; // Update the picking ray with the camera and mouse position this.raycaster.setFromCamera(this.mouse, camera); // Calculate objects intersecting the picking ray const intersects = this.raycaster.intersectObjects(avatarGroup.children, true); if (intersects.length > 0) { // Find the avatar in the object's userData or parent's userData let avatar = null; let obj = intersects[0].object; while (obj && !avatar) { if (obj.userData.avatar) { avatar = obj.userData.avatar; break; } obj = obj.parent; } if (avatar) { avatar.showInfo(); return; } } // Hide info panel if clicking on empty space document.getElementById('avatarInfo').classList.remove('visible'); } spawnAvatar(tier) { const name = CONFIG.NAMES[Math.floor(Math.random() * CONFIG.NAMES.length)]; // Position new avatar in front of the camera const direction = new THREE.Vector3(0, 0, -1); direction.applyQuaternion(camera.quaternion); const distance = 10; // Distance from camera const position = new THREE.Vector3(); position.copy(camera.position).add(direction.multiplyScalar(distance)); position.y = CONFIG.AVATAR_HEIGHT / 2; // Place on ground const avatar = new Avatar(tier, name, position); this.avatars.push(avatar); // Show info for the new avatar setTimeout(() => { avatar.showInfo(); }, 500); } onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } animate() { requestAnimationFrame(() => this.animate()); const delta = this.clock.getDelta(); // Update controls if (controls) controls.update(); // Update avatars this.avatars.forEach(avatar => avatar.update(delta)); // Render scene renderer.render(scene, camera); } } // Global variables let scene, camera, renderer, controls, clock, mixer, avatars = []; // LBEPlaza - Main application class class LBEPlaza { constructor() { // Initialize properties initGround() { // Ground plane const groundGeometry = new THREE.PlaneGeometry(100, 100); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333344, roughness: 0.8, metalness: 0.2 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); // Grid helper const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222); scene.add(gridHelper); } initControls() { // Orbit controls controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.maxPolarAngle = Math.PI / 2.2; controls.minDistance = 5; controls.maxDistance = 50; } initUI() { // Add event listeners for UI controls document.getElementById('spawn-free').addEventListener('click', () => this.spawnAvatar('free')); document.getElementById('spawn-pro').addEventListener('click', () => this.spawnAvatar('pro')); document.getElementById('spawn-hustler').addEventListener('click', () => this.spawnAvatar('hustler')); document.getElementById('spawn-conqueror').addEventListener('click', () => this.spawnAvatar('conqueror')); } initEventListeners() { // Mouse events for interaction window.addEventListener('mousemove', (event) => this.onMouseMove(event), false); window.addEventListener('mousedown', (event) => this.onMouseDown(event), false); window.addEventListener('mouseup', () => this.onMouseUp(), false); // Touch events for mobile window.addEventListener('touchstart', (event) => this.onTouchStart(event), false); window.addEventListener('touchmove', (event) => this.onTouchMove(event), false); window.addEventListener('touchend', () => this.onTouchEnd(), false); } // Animation loop animate() { requestAnimationFrame(() => this.animate()); const delta = clock.getDelta(); // Update controls controls.update(); // Update animations if (mixer) { mixer.update(delta); } // Update avatars this.updateAvatars(delta); // Render scene renderer.render(scene, camera); } // Update all avatars updateAvatars(delta) { this.avatars.forEach(avatar => { if (avatar.update) { avatar.update(delta); } }); } // Spawn a new avatar spawnAvatar(tier) { var logMessage = 'Spawning ' + tier + ' avatar'; console.log(logMessage); // Create a simple avatar (will be replaced with proper model) const geometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8); let material; // Set material based on tier switch(tier) { case 'pro': material = new THREE.MeshPhongMaterial({ color: 0x3498db }); break; case 'hustler': material = new THREE.MeshPhongMaterial({ color: 0xe74c3c }); break; case 'conqueror': material = new THREE.MeshPhongMaterial({ color: 0xf1c40f }); break; default: // free material = new THREE.MeshPhongMaterial({ color: 0x95a5a6 }); } const avatar = new THREE.Mesh(geometry, material); avatar.castShadow = true; avatar.receiveShadow = true; // Position the avatar in front of the camera const direction = new THREE.Vector3(); camera.getWorldDirection(direction); direction.y = 0; // Keep on the ground direction.normalize(); avatar.position.copy(camera.position).add(direction.multiplyScalar(5)); avatar.position.y = 1; // Place on ground // Add to scene and avatars array scene.add(avatar); this.avatars.push(avatar); // Add spawn animation const startY = avatar.position.y; const endY = 0.5; const duration = 1; let time = 0; const spawnTween = (t) => { time += 0.016; // Approximate 60fps const progress = Math.min(time / duration, 1); // Bounce effect const bounce = Math.sin(progress * Math.PI) * 2; avatar.position.y = startY + (endY - startY) * progress + bounce * 0.5; // Scale up const scale = 0.5 + progress * 0.5; avatar.scale.set(scale, scale, scale); if (progress < 1) { requestAnimationFrame(spawnTween); } }; spawnTween(0); } // Event handlers onMouseMove(event) { // Update mouse position this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; this.mouse.y = - (event.clientY / window.innerHeight) * 2 + 1; if (this.isDragging && this.draggedObject) { // Update dragged object position this.raycaster.setFromCamera(this.mouse, camera); const intersects = this.raycaster.intersectObject(scene.getObjectByName('ground')); if (intersects.length > 0) { this.draggedObject.position.copy(intersects[0].point.add(this.dragOffset)); } } else { // Highlight hovered objects this.raycaster.setFromCamera(this.mouse, camera); const intersects = this.raycaster.intersectObjects(this.avatars); if (intersects.length > 0) { const object = intersects[0].object; if (this.intersected !== object) { // Reset previous intersection if (this.intersected) { this.intersected.scale.set(1, 1, 1); } // Set new intersection this.intersected = object; object.scale.set(1.2, 1.2, 1.2); // Change cursor style document.body.style.cursor = 'pointer'; } } else { if (this.intersected) { this.intersected.scale.set(1, 1, 1); this.intersected = null; document.body.style.cursor = ''; } } } } onMouseDown(event) { if (event.button !== 0) return; // Only left click this.raycaster.setFromCamera(this.mouse, camera); const intersects = this.raycaster.intersectObjects(this.avatars); if (intersects.length > 0) { this.isDragging = true; this.draggedObject = intersects[0].object; // Calculate offset from object center to click point const point = intersects[0].point; this.dragOffset.copy(this.draggedObject.position).sub(point); // Disable orbit controls while dragging controls.enabled = false; } } onMouseUp() { if (this.isDragging) { this.isDragging = false; this.draggedObject = null; controls.enabled = true; } } // Touch event handlers onTouchStart(event) { if (event.touches.length === 1) { const touch = event.touches[0]; const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY }); this.onMouseDown(mouseEvent); } } onTouchMove(event) { if (event.touches.length === 1) { const touch = event.touches[0]; const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); this.onMouseMove(mouseEvent); } } onTouchEnd() { this.onMouseUp(); } } // Initialize the LBE Plaza when the page loads window.addEventListener('load', () => { // Check if WebGL is supported if (!Detector.webgl) { const warning = Detector.getWebGLErrorMessage(); document.getElementById('gameContainer').appendChild(warning); return; } // Initialize the LBE Plaza const plaza = new LBEPlaza(); // Handle window resize const onWindowResize = () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }; window.addEventListener('resize', onWindowResize, false); // Hide loading screen after a short delay to ensure everything is ready setTimeout(() => { document.getElementById('loading').classList.add('hidden'); // Focus the game container for keyboard controls document.getElementById('gameContainer').focus(); }, 500); });
Little Big Empire https://littlebigempire.com