Category: three.js
-
world – car.glb
I have a
car.glbmodel. When the player character moves near the car and the user presses Enter, the character should hide, and the controls should switch to the car. The user can then drive the car using the same movement keys (WASD/Arrows), except the car cannot jump using the Spacebar. When the user presses Enter again, the character should exit the car, reappear next to it, and regain normal walking controls to continue the game. -
World – 7th MAZE & PERSISTENCE – The Labyrinth Update

Maze


Following the chaos of Thunder Mode, our 7th update focused on structure, persistence, and UX refinement. We introduced a Procedural Maze System, extended our Ghost Persistence to 24 hours, and performed “UX Subtraction” to ensure mobile controls don’t get in the way of gameplay.
1. Procedural Architecture: The Recursive Labyrinth
Instead of static maps, we implemented a Recursive Backtracking Algorithm to generate a unique 25×25 grid every time a session begins. To make it a true challenge, we punched only two exits (North and South), forcing players to navigate deep into the core before finding a way out.
The “No-Jump” Constraint:
To prevent players from simply hopping over the challenge, we dynamically disabled the Jump logic whenever Maze Mode is active. This forces a grounded, tactical experience where movement speed and memory are the only tools for survival.2. UX Subtraction: Fixing Mobile Friction
One of the biggest issues in Round 6 was “Control Interference.” On mobile, players frequently swiped down while trying to turn, accidentally triggering the Camera Flip, which inverted their controls and caused them to run directly into lightning bolts.
The Solution:
- Removed Swipe-Down Flip: We completely stripped the camera-flip gesture from mobile and the ‘V’ key from the maze. In a high-stakes maze or storm, the player needs a consistent perspective.
- The 45px Centering: We refined the landing page UI by calculating a precise dual-toggle layout. By offsetting the Thunder and Maze buttons by 45px from the center, we created a balanced, professional “Game Mode” selector that disappears once the player enters the world.
3. Camera Engineering: Preventing “Wall Cheating”
In early testing, the camera was too far back, allowing players to see over the walls and easily locate the exit. We re-engineered the camera for a “Close-In” feel.
code JavaScript
// Optimized Maze Perspective const mazeOn = maze && maze.enabled; const h = mazeOn ? 4 : 6; // Lower height to stay inside the hallways const d = mazeOn ? 6 : 12; // Shorter distance to block the "long view" const camOffset = new THREE.Vector3(0, h, -d).applyQuaternion(localPlayer.quaternion); camera.position.lerp(localPlayer.position.clone().add(camOffset), 0.1);To balance this difficulty, we added the ‘M’ Map Cheat. Pressing ‘M’ flies the camera 180 units into the sky. To prevent a “white-out” from the scene fog, we implemented a Dynamic Fog Override:
- Normal: Fog end = 150 (Atmospheric).
- Map View: Fog end = 2000 (Clear overview).
4. Persistence: The 24-Hour Ghost System
A world feels empty if you only see who is currently online. We updated the Node.js server to transform every disconnected player into a Persistent Ghost.
These ghosts stay at their last known coordinate for 24 hours. They are rendered with 50% opacity and marked as “(OFFLINE),” serving as a historical record of who has visited the world. If a player rejoins with the same name, the server automatically “cleans” their old ghost to prevent duplicates.
5. The Victory Loop: High-Precision Timers
Competitive play requires data. We integrated a millisecond-accurate timer that triggers the moment “START” is clicked.
code JavaScript
// Victory Logic in maze.js const timeTaken = ((Date.now() - this.startTime) / 1000).toFixed(2); const name = player.userData.name || "Explorer"; this.ui.msg.innerHTML = ` <div style="font-size:24px;">🏆 ${name} ESCAPED!</div> <div style="font-size:18px;">TIME: ${timeTaken}s</div> `;When a player crosses the maze boundary, the system automatically clears the geometry, displays a victory card, and logs the time, creating a “speed-run” meta-game within the social space.
The Result
Round 7 turned our world from a simple “look and see” experience into a “do and survive” platform. By tightening the camera, removing confusing mobile gestures, and adding persistent history through ghosts, we’ve created a world that feels lived-in and challenging.
The walls are up, and the clock is ticking—can you find the exit in under 60 seconds?
-
World – 6th THUNDER MODE – Chaos from the Sky – Building a Modular Lightning System
https://world.officialstupid.me



Our social 3D world was peaceful—too peaceful. To add some excitement, we decided to implement Thunder Mode, a survival challenge where players dodge procedurally generated lightning strikes. However, adding a complex “game mode” to a social engine often leads to “spaghetti code” and performance death.
Here is how we built a high-performance, modular weather system that creates zero load on the server and stays silky smooth on mobile.
1. The Strategy: Complete Modularity
The biggest mistake in web game dev is cramming every new feature into the main animate() loop. Instead, we built thunder.js as a Self-Contained Module.
When you load the script, it automatically injects its own CSS, creates its own HTML overlays (Health bars, toggle buttons), and manages its own internal state. If we remove the script, the game still runs perfectly.
The “Zero-Server-Load” Logic:
We don’t sync lightning positions through the server. Instead, each client calculates its own “personal” lightning. This means the server only handles player positions, while the heavy math happens locally on the user’s device.
2. Visual Optimization: The “Double-Layer” Bolt
Initially, we used full-screen blue flashes and camera shakes to simulate thunder. Result: FPS dropped to 10 on mobile. CSS background changes trigger “layout reflows,” which are expensive.
We pivoted to a purely 3D visual approach using a Two-Layer Bolt system:
- The Core: A thin, bright white TubeGeometry.
- The Glow: A thicker blue tube using AdditiveBlending.
code JavaScript
// High-performance bolt geometry logic createBolt(x, z) { const pts = generateZigZagPoints(x, z); const curve = new THREE.CatmullRomCurve3(pts); // Core (White) - 4 radial segments (square tube) const core = new THREE.Mesh(new THREE.TubeGeometry(curve, 16, 0.04, 4), coreMat); // Glow (Blue + Additive Blending) - 6 radial segments (hexagonal tube) const glow = new THREE.Mesh(new THREE.TubeGeometry(curve, 16, 0.2, 6), glowMat); boltGroup.add(glow, core); return boltGroup; }
3. Progressive Difficulty
A static game is a boring game. We implemented a difficulty scalar that increases every second you stay alive. This variable controls four things simultaneously:
- Spawn Rate: From one bolt every 2 seconds down to four bolts per second.
- Warning Time: The red “danger zone” circle stays on the ground for less time.
- Max Bolts: Increases the cap on how many bolts can be on screen at once (up to 15).
- Spawn Radius: As you survive, lightning fills a wider area around you.
4. Integrating GLB Death Animations
Manual character rotation looked “fake.” We wanted to use the professional animations built into our GLB models.
The Challenge: If you simply play the death animation in a loop, the character keeps dying and standing up repeatedly. To fix this, we implemented a State-Change Check.
code JavaScript
// Inside the main animation loop const isHit = thunder.isDead || thunder.isKnockedDown; if (isHit) { // Only trigger the animation ONCE when the state changes if (lastAnim !== 'death') { deathAction.reset().setLoop(THREE.LoopOnce).play(); deathAction.clampWhenFinished = true; // Stay on the last frame } anim = 'death'; } else { anim = 'idle'; } lastAnim = anim;
5. Framing the Chaos: UI Positioning and Depth
To improve the user experience, we made two major aesthetic adjustments:
- The 40% Rule: We adjusted the camera’s look at target to Y=3.5. This tilts the view slightly upward, pushing the character selection slider to the bottom 40% of the screen. This keeps the characters visible but leaves the top of the screen clear for the environment.
- Atmospheric Clarity: We thinned the fog by changing the far distance from 75 to 150. This makes the world feel larger and gives players more time to see the environment while they are dodging strikes.
- Expanded Roster: We expanded the character pool, supporting a full lineup of models from 01.glb to 09.glb, giving players more ways to express themselves before the storm hits.
6. The “Wasted” Loop: Delayed Restart
Survival games need a moment of impact. When a player finally loses all health, we don’t show the menu immediately. We added a 2-second delay where the camera lingers on the fallen character before the “START AGAIN” button fades in. This makes death feel meaningful and gives the player a moment to breathe before jumping back in.
The Result
By keeping the code modular, we managed to:
- Maintain 60 FPS: By avoiding CSS-based screen effects and using low-poly 3-sided tubes for bolts.
- Improve Visuals: The additive blue glow looks far more “electrifying” than a simple white line.
- Keep the Engine Clean: The main index.html only needs one or two lines to “talk” to the Thunder System.
Next time you see a red ring on the floor, you have exactly 0.8 seconds to move—Good Luck!
https://aistudio.google.comMORE NEW GLB – https://pixabay.com/3d-models/search/glb/
-
world – 5th – The “Invisible” Mobile Controller – Gestures for 3D Worlds
Moving a 3D character on a smartphone usually means cluttering the screen with ugly joysticks and transparent buttons. For our multiplayer project, we wanted something better. We built an Invisible Gesture Controller that turns the entire screen into a touch-sensitive pad.
The Strategy: Mapping Keys to Gestures
Our game engine already listens for specific keys like W (Walk), Shift (Run), and Space (Jump). Instead of rewriting the engine, we built a “Touch Wrapper” that translates finger movements into these virtual key presses.
1. Browser Normalization (CSS)
To make a web game feel like a native app, you have to kill the browser’s default behaviors. Without this, the phone will try to zoom or “select text” while you are trying to play.
code CSS
/* Add this to stop mobile browser interference */ body { touch-action: none; /* Disable pinch-to-zoom and scroll */ user-select: none; /* Disable text highlighting */ -webkit-touch-callout: none; /* Disable long-press menus */ -webkit-tap-highlight-color: transparent; /* Remove blue tap boxes */ }2. The Touch Logic (JavaScript)
This is the heart of the mobile support. It handles rotation, toggle-walking, sprinting, and vertical swipes for actions.
code JavaScript
let touchStartX = 0, touchStartY = 0, lastTapTime = 0; function setupMobileControls() { // CAPTURE START POSITION window.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }, { passive: false }); // ROTATION: Horizontal slide window.addEventListener('touchmove', (e) => { if (!isStarted || !localPlayer) return; let dx = e.touches[0].clientX - touchStartX; if (Math.abs(dx) > 5) { localPlayer.rotation.y -= dx * 0.01; // Rotate character touchStartX = e.touches[0].clientX; // Reset baseline } e.preventDefault(); // Stop page "bounce" }, { passive: false }); // TAPS & SWIPES: Logic for Walk, Run, Jump, and Camera window.addEventListener('touchend', (e) => { let dx = e.changedTouches[0].clientX - touchStartX; let dy = e.changedTouches[0].clientY - touchStartY; let now = Date.now(); let isDoubleTap = (now - lastTapTime) < 300; if (isStarted) { // SWIPE UP -> JUMP (Space) if (dy < -50 && Math.abs(dx) < 50) { keys[' '] = true; setTimeout(() => keys[' '] = false, 100); } // SWIPE DOWN -> CAMERA VIEW (V) else if (dy > 50 && Math.abs(dx) < 50) { toggleCameraView(); } // TAP LOGIC else if (Math.abs(dx) < 10 && Math.abs(dy) < 10) { if (isDoubleTap) { // Double Tap: Toggle between Running and Walking if (keys['shift']) { keys['shift'] = false; keys['w'] = true; } else { keys['w'] = true; keys['shift'] = true; } } else { // Single Tap: Start/Stop Walking if (keys['w'] || keys['shift']) { keys['w'] = false; keys['shift'] = false; // Stop } else { keys['w'] = true; // Start Walk } } lastTapTime = now; } } }, { passive: false }); }How the Gestures Work:
- Rotation: Sliding your finger left or right updates the localPlayer.rotation.y. It’s sensitive and smooth, allowing for quick 360-degree turns.
- The “Tap to Move” Toggle: On a keyboard, you hold ‘W’. On mobile, holding your finger down is tiring. We changed it to a Toggle: Tap once to start walking, tap again to stop.
- Double-Tap to Sprint: Just like Minecraft or other mobile titles, a quick double-tap switches the state from Walking to Running (Shift).
- Swipe Down for Selfie: We mapped the Camera Flip (V) to a downward swipe. It’s an intuitive gesture for switching perspectives.
- Swipe Up to Jump: A vertical flick triggers the jump physics instantly.
The Result
By using gestures instead of buttons, we kept the screen 100% clean. This maximizes the immersion of the 3D world and makes the game feel like a premium mobile app rather than a website.
Next time you log in from your phone, try the swipe-down to see your character’s face!
-
world – 4th round -Persistence in a 3D World – The “12-Hour Ghost” System
In most multiplayer games, when a player disconnects, their character simply vanishes. This makes the world feel empty and temporary. We decided to change that by building a Persistent Ghost System.
Now, when you leave our world, your avatar stays exactly where you left it for 12 hours, serving as a digital footprint of your visit.
The Problem: The “Empty Room” Syndrome
3D social spaces feel lonely if you aren’t there at the exact same time as someone else. We wanted a way for players to “leave their mark,” allowing the world to look populated even during off-peak hours.
The Solution: Ghost Conversion
We implemented a server-to-client bridge that handles “ghosting” in three tiny but powerful steps.
1. Server-Side: The 12-Hour Timer
Instead of deleting the player data on a disconnect event, the server rebrands the player. We change their ID to a ghost_ prefix and start a long-running timer.
code JavaScript
// On Disconnect: Convert to Ghost const ghostId = "ghost_" + socket.id; players[ghostId] = players[socket.id]; // Keep the data delete players[socket.id]; // Remove the active socket // Set the 12-hour expiration setTimeout(() => { delete players[ghostId]; io.emit('player_left', ghostId); }, 12 * 60 * 60 * 1000);2. Client-Side: Visual “Offline” Cues
To make sure active players can distinguish between live users and ghosts, we added visual shaders. When the client sees an ID starting with ghost_, it automatically:
- Sets the model to 50% opacity.
- Appends (OFFLINE) to the name tag.
- Forces the model into an Idle state.
code JavaScript
if (id.startsWith('ghost_')) { child.material.transparent = true; child.material.opacity = 0.5; // Visual hint that the player is gone }3. Physical Presence (No Ghosting)
Even though these characters are “offline,” they aren’t just holograms. We kept the Collision Detection active against them. You cannot walk through a ghost. This creates a sense of solid, physical history in the world—if a group of friends stood in a circle before logging off, that circle remains as a physical barrier for 12 hours.
Why it works
- Anti-Cloning: If a player logs back in, the server automatically finds and “kills” their old ghost before spawning their new live character. This prevents the world from becoming cluttered with clones of the same person.
- Memory Efficiency: By using a simple Map and setTimeout, the server handles the persistence without needing a heavy database.
- Social Proof: New players entering the world see a crowd of characters, making the project feel popular and alive.
Conclusion
Persistence doesn’t always require a massive database or a 24/7 server farm. With just a few lines of logic and some clever ID prefixing, we turned a temporary room into a persistent world where your presence actually matters.
Next time you log in, say hi to the ghosts of the players who came before you!
-
world – 3rd drama – GLB
Building a Smooth Multiplayer 3D Character Selection World
In our latest development sprint, we transformed a basic 3D multiplayer space into a polished, interactive character hub. We focused on three main pillars: User Experience, Animation Flexibility, and Physical Presence.
Here is a breakdown of what we’ve built.
1. The “Carousel” Selection System
Instead of a traditional 2D menu, we moved character selection into the 3D world.
- Mouse-Scroll Interaction: Users can cycle through characters by simply scrolling their mouse wheel.
- The Centering Effect: We used linear interpolation (Lerp) to smoothly slide the selected character to the center of the screen while rotating it, giving the player a 360-degree preview of their avatar.
2. Dynamic Animation Mapping (Fuzzy Search)
Different GLB models often come with wildly different animation naming conventions. A Robot might have RobotArmature|Walk, while a human might just have Walk.
- Keyword Matching: We implemented a “Fuzzy Search” logic that looks for keywords like Idle, Walk, Run, or Jump.
- The Skeleton Fallback: Some models, like our Skeleton, are missing specific animations (like a Walk). We built a fallback system that automatically detects a missing Walk and replaces it with the Run animation, ensuring the character never “glides” in a T-pose.
3. Social Interaction: Proximity Waving
To make the world feel alive, we added a social layer.
- Proximity Detection: When a player stands still near one or more other players, the system triggers a “Wave” or “Thumbs Up” animation automatically.
- Social Immersion: This small detail transforms the space from a lonely void into a social gathering hub.
4. No More “Ghosting”: Collision Detection
In early versions, players could walk through each other like ghosts. We solved this by implementing a Circle-Circle Collision system:
- Physical Boundaries: Every player now has a collision radius.
- The “Push-Back” Logic: If two players get too close, the math calculates the overlap and gently pushes the local player back, giving everyone a solid physical presence in the world.
5. Enhanced Camera & Physics
We kept the core gameplay tight with:
- Jump Physics: Gravity-based jumping with force and landing detection.
- View Toggling: Players can press ‘V’ or Right-Click to flip the camera between a standard third-person follow and a front-facing “Selfie” view.
Technical Highlights:
- Engine: Three.js (WebGL)
- Networking: Socket.io for real-time position and animation syncing.
- Assets: Optimized CC0 GLTF models.
What’s Next?
We are looking into adding customizable textures and a chat system to further enhance the multiplayer social experience!
Looking for the logic?
The secret to the animation flexibility was this helper function we developed:
code JavaScript
function findAction(mixer, clips, keywords) { const clip = clips.find(c => keywords.some(k => c.name.toLowerCase().includes(k.toLowerCase()))); return clip ? mixer.clipAction(clip) : null; }This allows us to drop in almost any GLB model, and the game “figures out” which animation is which!
https://chat.deepseek.com , https://aistudio.google.com , https://claude.ai (free)
-
world – start!
The backbone of our 3D world is built upon a highly organized file architecture and the use of the industry-standard GLB (GL Transmission Format Binary). Because the game needs to load massive amounts of data—meshes, textures, and complex animations—simultaneously for up to 100 players, the way these files are structured is critical to performance.
Here is a breakdown of how we managed the GLB assets and the project’s file hierarchy.
1. The GLB: The “All-in-One” 3D Container
We chose GLB over traditional formats (like OBJ or FBX) because it is the “JPEG of 3D.” In our game, each character (01, 02, and 03) is a single binary file that contains:
- The Mesh: The actual 3D body of the character.
- The Textures: The colors, skins, and materials embedded directly into the file so they don’t get lost.
- The Skeleton (Rig): The internal bone structure that allows the character to move.
- The Animation Library: This is the most important part. Each GLB carries its own “clip” library. When the game starts, our script scans the GLB for specific names like “Walk” and “Wave”. By packaging everything in one file, we ensure that when a player joins, the character doesn’t appear “naked” or “frozen”—everything loads in one single network request.
2. The Project Architecture
To manage the relationship between the 3D assets and the game logic, we implemented a strict “Root-Public” structure. This separation ensures that the 3D models stay secure and the game runs smoothly.
The Root Level:
- server.js: This is the “brain” of the world. It doesn’t handle graphics; it only handles “coordinates.” It tracks where Player 01 is and tells the other 99 players.
- package.json: The manifest that ensures the environment has the correct tools to sync the players.
The Public Folder (The Game’s Front Door):
- index.html: The “Canvas.” This file contains the Three.js engine that renders the world. It is the only file the user actually “sees.”
- player/ (The Asset Vault): We created a dedicated sub-folder specifically for our 3D models. Inside this folder live 01.glb, 02.glb, and 03.glb.
3. Dynamic Path Management
A key feature of our management system is how we handle file paths. Instead of hard-coding every character, the game uses a dynamic loading system.
- When a user clicks on a 3D character during selection, the game stores that character’s filename (e.g., “02.glb”).
- The script then looks into the public/player/ folder and pulls that specific file.
- This structure allows us to add 10 or 100 new characters just by dropping a new GLB into the folder and adding its name to a list, without changing a single line of the game’s core code.
4. Technical Normalization
Inside the file structure, we managed a major challenge: Scale. 3D artists often export models at different sizes. One GLB might be 100 times larger than another.
To manage this, we built a Normalization Layer into the loading process. Every time a file is pulled from the player/ folder, the game “measures” the model’s dimensions and automatically scales it to a standard height. This ensures that in our file structure, every GLB is treated as a uniform “Player” regardless of its original size.Summary of the File Map
code Text
/Project_Root │ ├── server.js (Multiplayer Logic) ├── package.json (Server Config) └── /public (Visible Game Files) ├── index.html (Three.js Engine & UI) └── /player (The 3D Asset Folder) ├── 01.glb (Character 1 + Animations) ├── 02.glb (Character 2 + Animations) └── 03.glb (Character 3 + Animations)By using this clean, modular structure, we created a game that is easy to update and incredibly fast to load, ensuring that the 3D “High-Key” studio experience is seamless for every player who enters the world.
-
three.js “blob” memory required for 3D graphics – .htaccess
What I changed:
- img-src: Added blob: (This allows Three.js to show textures).
- connect-src: Added blob: (This allows the game to load the 3D data).
- script-src: Added blob: (Backup for workers).
- worker-src: Added worker-src ‘self’ blob:; (This allows the Draco decoder to decompress your models).
How to apply:
- Copy the new line above.
- Paste it into your .htaccess file, replacing the old Header set Content-Security-Policy line.
- Save the file.
- Clear your browser cache (or open the site in an Incognito/Private window).
The game should now load perfectly because the browser finally has permission to use the “blob” memory required for 3D graphics.
Header set Content-Security-Policy "default-src 'self' https: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: blob:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https: blob:; font-src 'self' https: data:; media-src 'self' https:; frame-src 'self' https:; connect-src 'self' https: blob:; worker-src 'self' blob:;" -
😀 let’s build ‘Super Bear Adventure’. jatiiiiiiii sakincha!!! #start
#Trampolines
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Super Bear Adventure</title> <style> body { margin: 0; overflow: hidden; background: #87ceeb; font-family: 'Segoe UI', sans-serif; } #ui { position: absolute; top: 20px; left: 20px; color: white; background: rgba(0,0,0,0.6); padding: 15px; border-radius: 10px; pointer-events: none; border: 2px solid rgba(255,255,255,0.1); } .key { background: #333; padding: 2px 6px; border-radius: 4px; font-weight: bold; } </style> </head> <body> <div id="ui"> <b>SUPER BEAR PRO</b><br><br> <span class="key">W A S D</span> : Move & Rotate<br> <span class="key">SHIFT</span> : Run Faster<br> <span class="key">SPACE</span> : Jump<br> <br> <i>Status: <span id="status">Grounded</span></i> </div> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script> <script type="module"> import * as THREE from 'three'; // --- Constants --- const WALK_SPEED = 0.12; const RUN_SPEED = 0.22; const JUMP_FORCE = 0.26; const GRAVITY = 0.012; // --- Variables --- let scene, camera, renderer, clock; let bear, armL, armR, legL, legR; let platforms = []; let velY = 0; let keys = {}; // States: 'idle', 'moving', 'jumping', 'hanging', 'climbing' let state = 'idle'; init(); function init() { scene = new THREE.Scene(); scene.background = new THREE.Color(0x87ceeb); scene.fog = new THREE.Fog(0x87ceeb, 20, 100); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 500); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); document.body.appendChild(renderer.domElement); const sun = new THREE.DirectionalLight(0xffffff, 1.2); sun.position.set(10, 20, 10); scene.add(sun, new THREE.AmbientLight(0xffffff, 0.6)); createBear(); createLevel(); window.addEventListener('keydown', e => keys[e.code] = true); window.addEventListener('keyup', e => keys[e.code] = false); clock = new THREE.Clock(); loop(); } function createBear() { bear = new THREE.Group(); const brown = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); const tan = new THREE.MeshLambertMaterial({ color: 0xD2B48C }); // Body const body = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.9, 0.5), brown); // Head & Ears const head = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.45), brown); head.position.y = 0.7; const earGeo = new THREE.BoxGeometry(0.15, 0.15, 0.1); const earL = new THREE.Mesh(earGeo, brown); earL.position.set(0.2, 0.3, 0); const earR = new THREE.Mesh(earGeo, brown); earR.position.set(-0.2, 0.3, 0); head.add(earL, earR); // Arms (Pivoted at shoulders) const armGeo = new THREE.BoxGeometry(0.2, 0.6, 0.2); armL = new THREE.Group(); armL.position.set(0.45, 0.3, 0); const amL = new THREE.Mesh(armGeo, brown); amL.position.y = -0.25; armL.add(amL); armR = new THREE.Group(); armR.position.set(-0.45, 0.3, 0); const amR = new THREE.Mesh(armGeo, brown); amR.position.y = -0.25; armR.add(amR); // Legs const legGeo = new THREE.BoxGeometry(0.22, 0.5, 0.22); legL = new THREE.Mesh(legGeo, brown); legL.position.set(0.2, -0.6, 0); legR = new THREE.Mesh(legGeo, brown); legR.position.set(-0.2, -0.6, 0); bear.add(body, head, armL, armR, legL, legR); scene.add(bear); bear.position.y = 2; } function createLevel() { const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), new THREE.MeshLambertMaterial({ color: 0x567d46 })); ground.rotation.x = -Math.PI/2; scene.add(ground); const platMat = new THREE.MeshLambertMaterial({ color: 0x7a5c37 }); const data = [ {x: 0, y: 1, z: -5, w: 5, d: 5}, {x: 6, y: 3, z: -10, w: 4, d: 4}, {x: 2, y: 5, z: -16, w: 5, d: 3}, {x: -4, y: 7.5, z: -20, w: 4, d: 4} ]; data.forEach(d => { const p = new THREE.Mesh(new THREE.BoxGeometry(d.w, 1.5, d.d), platMat); p.position.set(d.x, d.y, d.z); scene.add(p); platforms.push({ mesh: p, w: d.w/2, d: d.d/2, h: 0.75 }); }); } function handlePhysics() { if (state === 'hanging' || state === 'climbing') return; velY -= GRAVITY; bear.position.y += velY; let grounded = false; if (bear.position.y < 0.75) { bear.position.y = 0.75; velY = 0; grounded = true; } platforms.forEach(p => { const b = bear.position; const pm = p.mesh.position; const dx = b.x - pm.x; const dz = b.z - pm.z; const distW = p.w + 0.4; const distD = p.d + 0.4; if (Math.abs(dx) < distW && Math.abs(dz) < distD) { const top = pm.y + p.h + 0.75; const bot = pm.y - p.h - 0.75; if (b.y >= top - 0.5 && velY <= 0) { b.y = top; velY = 0; grounded = true; } else if (b.y < top - 0.2 && b.y > bot) { // Push out of wall const ox = distW - Math.abs(dx); const oz = distD - Math.abs(dz); if (ox < oz) b.x += dx > 0 ? ox : -ox; else b.z += dz > 0 ? oz : -oz; // Ledge Grab check if (velY < 0 && b.y > top - 1.2 && b.y < top - 0.5) { startHang(top); } } } }); if (grounded) { if (state === 'jumping') state = 'idle'; if (keys['Space']) { velY = JUMP_FORCE; state = 'jumping'; } } else { state = 'jumping'; } } function startHang(topY) { state = 'hanging'; velY = 0; bear.position.y = topY - 0.9; document.getElementById('status').innerText = "Hanging..."; // Hands on ledge pose armL.rotation.x = armR.rotation.x = -2.8; legL.rotation.x = legR.rotation.x = 0.3; // Wait 1.5 seconds then climb setTimeout(() => { if (state === 'hanging') startClimb(topY); }, 1500); } function startClimb(targetY) { state = 'climbing'; document.getElementById('status').innerText = "Climbing Up..."; const startY = bear.position.y; const startZ = bear.position.z; const startX = bear.position.x; const forward = new THREE.Vector3(0, 0, -1.2).applyQuaternion(bear.quaternion); let p = 0; const interval = setInterval(() => { p += 0.02; // Slow climb speed // Lift up if (p <= 0.7) { bear.position.y = THREE.MathUtils.lerp(startY, targetY + 0.2, p / 0.7); } // Move forward else { const fp = (p - 0.7) / 0.3; bear.position.x = THREE.MathUtils.lerp(startX, startX + forward.x, fp); bear.position.z = THREE.MathUtils.lerp(startZ, startZ + forward.z, fp); bear.position.y = targetY; } if (p >= 1) { clearInterval(interval); state = 'idle'; document.getElementById('status').innerText = "Grounded"; } }, 20); } function update() { if (state === 'hanging' || state === 'climbing') return; // Movement logic const isRunning = keys['ShiftLeft'] || keys['ShiftRight']; const speed = isRunning ? RUN_SPEED : WALK_SPEED; if (keys['ArrowLeft'] || keys['KeyA']) bear.rotation.y += 0.06; if (keys['ArrowRight'] || keys['KeyD']) bear.rotation.y -= 0.06; const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(bear.quaternion); let moved = false; if (keys['ArrowUp'] || keys['KeyW']) { bear.position.add(dir.multiplyScalar(speed)); moved = true; } if (keys['ArrowDown'] || keys['KeyS']) { bear.position.add(dir.multiplyScalar(-speed * 0.5)); moved = true; } if (state !== 'jumping') { state = moved ? 'moving' : 'idle'; document.getElementById('status').innerText = isRunning && moved ? "Running" : (moved ? "Walking" : "Idle"); } handlePhysics(); // Animations const t = Date.now() * (isRunning ? 0.015 : 0.01); if (state === 'moving') { const swing = isRunning ? 1.2 : 0.7; armL.rotation.x = Math.sin(t) * swing; armR.rotation.x = -Math.sin(t) * swing; legL.rotation.x = -Math.sin(t) * swing; legR.rotation.x = Math.sin(t) * swing; } else if (state === 'jumping') { armL.rotation.x = THREE.MathUtils.lerp(armL.rotation.x, -2.5, 0.1); armR.rotation.x = THREE.MathUtils.lerp(armR.rotation.x, -2.5, 0.1); legL.rotation.x = 0.4; legR.rotation.x = -0.4; } else { armL.rotation.x = THREE.MathUtils.lerp(armL.rotation.x, 0, 0.1); armR.rotation.x = THREE.MathUtils.lerp(armR.rotation.x, 0, 0.1); legL.rotation.x = legR.rotation.x = 0; } // Camera follow const camOffset = new THREE.Vector3(0, 4, 8).applyQuaternion(bear.quaternion); const targetCam = bear.position.clone().add(camOffset); camera.position.lerp(targetCam, 0.1); camera.lookAt(bear.position.x, bear.position.y + 1, bear.position.z); } function loop() { update(); renderer.render(scene, camera); requestAnimationFrame(loop); } window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); </script> </body> </html>