Guest User

Mercury 2 - Open-world action-adventure

a guest
Jul 16th, 2026
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 14.18 KB | Gaming | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Mini GTA‑3 Demo – Walk + Jump</title>
  6.  
  7. <!-- Import‑map for the bare specifier "three" -->
  8. <script type="importmap">
  9. {
  10.   "imports": {
  11.     "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js"
  12.   }
  13. }
  14. </script>
  15.  
  16. <script type="module">
  17.   import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
  18.   import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js';
  19.   import { PointerLockControls } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/PointerLockControls.js';
  20.  
  21.   // ---------- GLOBALS ----------
  22.   let scene, camera, renderer, controls;
  23.   let clock = new THREE.Clock();
  24.   const mixers = [];               // AnimationMixers for animated models
  25.   let isPaused = false;
  26.   const keys = {};                 // pressed keys / touch‑button states
  27.   let assetsToLoad = 2;            // robot + car
  28.  
  29.   // Player character
  30.   const player = new THREE.Group();   // will contain the robot model
  31.   const cameraOffset = new THREE.Vector3(0, 2, -5); // behind & slightly above player
  32.  
  33.  // Jump / gravity
  34.  const GRAVITY = -30;   // units / second²
  35.   const JUMP_SPEED = 12; // initial upward velocity
  36.   let verticalVelocity = 0;
  37.   let isGrounded = true;
  38.  
  39.   // Drag‑to‑look fallback (when pointer‑lock is blocked)
  40.   let isDragging = false;
  41.   let prevMouse = { x: 0, y: 0 };
  42.   const dragSens = 0.002;
  43.  
  44.   // ---------- UI ----------
  45.   const ui = document.createElement('div');
  46.   ui.innerHTML = `
  47.     <button id="startBtn" class="uiBtn">Start</button>
  48.     <button id="pauseBtn" class="uiBtn">Pause</button>
  49.     <div id="info" class="info">WASD / Arrow keys – move | Space – jump | Drag / mouse to turn</div>
  50.     <div id="loadingOverlay" class="loadingOverlay">Loading…</div>
  51.     <div id="clickOverlay" class="clickOverlay">Click “Start” to begin</div>
  52.  
  53.     <!-- On‑screen movement arrows -->
  54.     <div class="touchControls">
  55.       <button id="upBtn"   class="touchBtn"></button>
  56.       <button id="leftBtn" class="touchBtn"></button>
  57.       <button id="rightBtn" class="touchBtn"></button>
  58.       <button id="downBtn" class="touchBtn"></button>
  59.     </div>
  60.   `;
  61.   document.body.appendChild(ui);
  62.   document.getElementById('startBtn').onclick = () => requestPointerLock();
  63.   document.getElementById('pauseBtn').onclick = () => { isPaused = !isPaused; };
  64.  
  65.   // ---------- STYLES ----------
  66.   const style = document.createElement('style');
  67.   style.textContent = `
  68.     body { margin:0; overflow:hidden; font-family:Arial,Helvetica,sans-serif; background:#111; }
  69.  
  70.     .uiBtn {
  71.       position:fixed; top:10px; left:10px; margin-right:8px;
  72.       padding:8px 12px; background:#222; color:#fff; border:none;
  73.       border-radius:4px; cursor:pointer; font-size:14px;
  74.       z-index:2000; pointer-events:auto;
  75.     }
  76.     .uiBtn:hover { background:#444; }
  77.  
  78.     .info {
  79.       position:fixed; bottom:10px; left:10px; color:#ddd;
  80.       background:rgba(0,0,0,0.5); padding:6px 10px; border-radius:4px;
  81.       font-size:13px; z-index:2000;
  82.     }
  83.  
  84.     .loadingOverlay, .clickOverlay {
  85.       position:fixed; inset:0; background:rgba(0,0,0,0.8);
  86.       color:#fff; display:flex; align-items:center; justify-content:center;
  87.       font-size:24px; z-index:500; pointer-events:none;
  88.     }
  89.     .clickOverlay { display:none; }
  90.  
  91.     .touchControls {
  92.       position:fixed; bottom:80px; left:50%; transform:translateX(-50%);
  93.       display:grid; grid-template-columns:repeat(3, 60px); grid-gap:8px;
  94.       z-index:2000;
  95.     }
  96.     .touchBtn {
  97.       width:60px; height:60px; font-size:24px; background:#222; color:#fff;
  98.       border:none; border-radius:8px; cursor:pointer; opacity:0.8;
  99.     }
  100.     .touchBtn:hover { opacity:1; background:#444; }
  101.     .touchBtn:active { background:#666; }
  102.   `;
  103.   document.head.appendChild(style);
  104.  
  105.   // ---------- INITIALISE ----------
  106.   init();
  107.   animate();
  108.  
  109.   function init() {
  110.     // Scene & Camera
  111.    scene = new THREE.Scene();
  112.     scene.background = new THREE.Color(0x87ceeb); // sky blue
  113.  
  114.     camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
  115.     camera.position.set(0, 2, 5); // will be overwritten each frame
  116.  
  117.     // Renderer (focusable canvas)
  118.     renderer = new THREE.WebGLRenderer({ antialias:true });
  119.     renderer.setSize(window.innerWidth, window.innerHeight);
  120.     renderer.domElement.tabIndex = 0;          // allow focus
  121.     renderer.domElement.style.outline = 'none';
  122.     document.body.appendChild(renderer.domElement);
  123.  
  124.     // Lights
  125.     const ambient = new THREE.AmbientLight(0xffffff, 0.6);
  126.     scene.add(ambient);
  127.     const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
  128.     dirLight.position.set(10,20,10);
  129.     scene.add(dirLight);
  130.  
  131.     // Controls (only for mouse‑move capture)
  132.     controls = new PointerLockControls(camera, renderer.domElement);
  133.     scene.add(controls.getObject());
  134.  
  135.     // Add player group to the scene
  136.     scene.add(player);
  137.  
  138.     // Ground (road)
  139.     const roadTex = new THREE.TextureLoader().load('https://threejs.org/examples/textures/road.jpg');
  140.     roadTex.wrapS = roadTex.wrapT = THREE.RepeatWrapping;
  141.     roadTex.repeat.set(20,20);
  142.     const roadMat = new THREE.MeshStandardMaterial({ map:roadTex });
  143.     const roadGeo = new THREE.PlaneGeometry(200,200);
  144.     const road = new THREE.Mesh(roadGeo, roadMat);
  145.     road.rotation.x = -Math.PI/2;
  146.     scene.add(road);
  147.  
  148.     // Buildings (brick boxes)
  149.     const brickTex = new THREE.TextureLoader().load('https://threejs.org/examples/textures/brick_diffuse.jpg');
  150.     brickTex.wrapS = brickTex.wrapT = THREE.RepeatWrapping;
  151.     brickTex.repeat.set(1,1);
  152.     const brickMat = new THREE.MeshStandardMaterial({ map:brickTex });
  153.  
  154.     const makeBuilding = (x,z,w,h,d) => {
  155.       const geo = new THREE.BoxGeometry(w,h,d);
  156.       const mesh = new THREE.Mesh(geo, brickMat);
  157.       mesh.position.set(x, h/2, z);
  158.       scene.add(mesh);
  159.     };
  160.     makeBuilding(-20, -30, 8, 20, 8);
  161.     makeBuilding(15, -25, 12, 30, 12);
  162.     makeBuilding(-35, 10, 10, 25, 10);
  163.     makeBuilding(30, 20, 14, 35, 14);
  164.  
  165.     // ---------- LOADERS ----------
  166.     const loader = new GLTFLoader();
  167.  
  168.     // Robot – the player character
  169.     loader.load(
  170.       'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb',
  171.       (gltf) => {
  172.         const model = gltf.scene;
  173.         model.scale.set(0.3,0.3,0.3);          // make the character smaller
  174.         model.position.set(0,0,0);
  175.         player.add(model);               // attach to player group
  176.  
  177.         // Prepare animation actions (idle + walk)
  178.         const mixer = new THREE.AnimationMixer(model);
  179.         mixers.push(mixer);
  180.         const idleClip = gltf.animations.find(c => c.name.toLowerCase().includes('idle')) || gltf.animations[0];
  181.         const walkClip = gltf.animations.find(c => c.name.toLowerCase().includes('walk')) || gltf.animations[1];
  182.         const idleAction = mixer.clipAction(idleClip);
  183.         const walkAction = mixer.clipAction(walkClip);
  184.         idleAction.play();                // start with idle
  185.         // Store actions for later use
  186.         player.userData.idleAction = idleAction;
  187.         player.userData.walkAction = walkAction;
  188.         assetLoaded();
  189.       },
  190.       undefined,
  191.       (err) => console.error('Robot load error:', err)
  192.     );
  193.  
  194.     // Car model (static scenery)
  195.     loader.load(
  196.       'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/VC/glTF/VC.gltf',
  197.       (gltf) => {
  198.         const car = gltf.scene;
  199.         car.scale.set(0.5,0.5,0.5);
  200.         car.position.set(5,0,5);
  201.         scene.add(car);
  202.         if (gltf.animations.length) {
  203.           const mixer = new THREE.AnimationMixer(car);
  204.           mixers.push(mixer);
  205.           const clip = gltf.animations[0];
  206.           const action = mixer.clipAction(clip);
  207.           action.play();
  208.         }
  209.         assetLoaded();
  210.       },
  211.       undefined,
  212.       (err) => console.error('Car load error:', err)
  213.     );
  214.  
  215.     // Resize handling
  216.     window.addEventListener('resize', onWindowResize);
  217.     // Keyboard handling
  218.     document.addEventListener('keydown', (e) => keys[e.code] = true);
  219.     document.addEventListener('keyup',   (e) => keys[e.code] = false);
  220.  
  221.     // Pointer‑lock events (optional)
  222.     document.addEventListener('pointerlockchange', onPointerLockChange);
  223.     document.addEventListener('pointerlockerror', () => {
  224.       console.warn('Pointer lock failed – falling back to mouse‑drag control');
  225.     });
  226.  
  227.     // ---- On‑screen movement arrows ----
  228.     const touchMap = {
  229.       up:    document.getElementById('upBtn'),
  230.       down:  document.getElementById('downBtn'),
  231.       left:  document.getElementById('leftBtn'),
  232.       right: document.getElementById('rightBtn')
  233.     };
  234.     Object.entries(touchMap).forEach(([dir, btn]) => {
  235.       const press = () => {
  236.         keys[dir] = true;
  237.         requestPointerLock(); // try to lock pointer again
  238.       };
  239.       const release = () => keys[dir] = false;
  240.  
  241.       btn.addEventListener('mousedown',   press);
  242.       btn.addEventListener('touchstart', (e) => { e.preventDefault(); press(); });
  243.       btn.addEventListener('mouseup',     release);
  244.       btn.addEventListener('mouseleave', release);
  245.       btn.addEventListener('touchend',    (e) => { e.preventDefault(); release(); });
  246.       btn.addEventListener('touchcancel', (e) => { e.preventDefault(); release(); });
  247.     });
  248.  
  249.     // ---- Mouse‑drag fallback (when pointer‑lock is blocked) ----
  250.     renderer.domElement.addEventListener('mousedown', (e) => {
  251.       if (e.button === 0 && document.pointerLockElement !== renderer.domElement) {
  252.        isDragging = true;
  253.         prevMouse.x = e.clientX;
  254.         prevMouse.y = e.clientY;
  255.       }
  256.     });
  257.     window.addEventListener('mousemove', (e) => {
  258.       if (isDragging) {
  259.         const dx = e.clientX - prevMouse.x;
  260.         prevMouse.x = e.clientX;
  261.         prevMouse.y = e.clientY;
  262.         player.rotation.y -= dx * dragSens;
  263.       }
  264.     });
  265.     window.addEventListener('mouseup', () => { isDragging = false; });
  266.     window.addEventListener('mouseleave', () => { isDragging = false; });
  267.   }
  268.  
  269.   function requestPointerLock() {
  270.     renderer.domElement.requestPointerLock();
  271.   }
  272.  
  273.   function onPointerLockChange() {
  274.     const overlay = document.getElementById('clickOverlay');
  275.     const startBtn = document.getElementById('startBtn');
  276.     if (document.pointerLockElement === renderer.domElement) {
  277.       startBtn.style.display = 'none';
  278.       overlay.style.display = 'none';
  279.     } else {
  280.       startBtn.style.display = 'inline-block';
  281.       overlay.style.display = 'flex';
  282.     }
  283.   }
  284.  
  285.   function assetLoaded() {
  286.     assetsToLoad--;
  287.     if (assetsToLoad === 0) {
  288.       const overlay = document.getElementById('loadingOverlay');
  289.       if (overlay) overlay.style.display = 'none';
  290.     }
  291.   }
  292.  
  293.   function onWindowResize() {
  294.     camera.aspect = window.innerWidth / window.innerHeight;
  295.     camera.updateProjectionMatrix();
  296.     renderer.setSize(window.innerWidth, window.innerHeight);
  297.   }
  298.  
  299.   // ---------- ANIMATION LOOP ----------
  300.   function animate() {
  301.     requestAnimationFrame(animate);
  302.     const delta = clock.getDelta();
  303.  
  304.     // Update all mixers (robot idle/walk, car wheels)
  305.     mixers.forEach(m => m.update(delta));
  306.  
  307.     // -----------------
  308.     // INPUT & MOVEMENT
  309.    // -----------------
  310.    if (!isPaused) {
  311.      const speed = 5 * delta;
  312.       const move = new THREE.Vector3();
  313.  
  314.       // Gather movement input
  315.       if (keys['KeyW'] || keys['ArrowUp']   || keys['up'])    move.z -= 1;
  316.       if (keys['KeyS'] || keys['ArrowDown'] || keys['down'])  move.z += 1;
  317.       if (keys['KeyA'] || keys['ArrowLeft'] || keys['left'])  move.x -= 1;
  318.       if (keys['KeyD'] || keys['ArrowRight']|| keys['right']) move.x += 1;
  319.  
  320.       // ----- JUMP -----
  321.       if (keys['Space'] && isGrounded) {
  322.        verticalVelocity = JUMP_SPEED;
  323.         isGrounded = false;
  324.       }
  325.  
  326.       // Apply gravity
  327.       verticalVelocity += GRAVITY * delta;
  328.       player.position.y += verticalVelocity * delta;
  329.  
  330.       // Simple ground collision
  331.       if (player.position.y <= 0) {
  332.        player.position.y = 0;
  333.        verticalVelocity = 0;
  334.        isGrounded = true;
  335.      }
  336.  
  337.      // ----- MOVEMENT -----
  338.      if (move.lengthSq() > 0) {
  339.         move.normalize();
  340.  
  341.         // Convert local movement to world space using player's yaw
  342.         const yaw = player.rotation.y;
  343.         const forward = new THREE.Vector3(
  344.           Math.sin(yaw),
  345.           0,
  346.           Math.cos(yaw)
  347.         );
  348.         const right = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0,1,0));
  349.  
  350.         const worldMove = new THREE.Vector3();
  351.         worldMove.addScaledVector(forward, -move.z); // forward is -Z in Three.js
  352.         worldMove.addScaledVector(right,   move.x);
  353.         worldMove.multiplyScalar(speed);
  354.  
  355.         player.position.add(worldMove);
  356.  
  357.         // ---- WALK ANIMATION ----
  358.         if (player.userData.walkAction && player.userData.idleAction) {
  359.          player.userData.idleAction.stop();
  360.           player.userData.walkAction.play();
  361.         }
  362.       } else {
  363.         // ---- IDLE ANIMATION ----
  364.         if (player.userData.walkAction && player.userData.idleAction) {
  365.          player.userData.walkAction.stop();
  366.           player.userData.idleAction.play();
  367.         }
  368.       }
  369.     }
  370.  
  371.     // -----------------
  372.     // CAMERA FOLLOW
  373.     // -----------------
  374.     const offsetWorld = cameraOffset.clone().applyAxisAngle(new THREE.Vector3(0,1,0), player.rotation.y);
  375.     camera.position.copy(player.position).add(offsetWorld);
  376.     camera.lookAt(player.position);
  377.  
  378.     // -----------------
  379.     // ROTATION FROM POINTER LOCK
  380.     // -----------------
  381.     if (document.pointerLockElement === renderer.domElement) {
  382.       const onMouseMove = (e) => {
  383.         player.rotation.y -= e.movementX * 0.002;
  384.       };
  385.       // Attach once per frame (the listener will be removed automatically when lock ends)
  386.       document.addEventListener('mousemove', onMouseMove);
  387.     }
  388.  
  389.     renderer.render(scene, camera);
  390.   }
  391. </script>
  392. </head>
  393. <body>
  394. <!-- UI elements are injected by the script above -->
  395. </body>
  396. </html>
Advertisement
Add Comment
Please, Sign In to add comment