| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>3D射击跟枪练习器</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.js"></script> |
| <style> |
| body { |
| margin: 0; |
| overflow: hidden; |
| font-family: 'Arial', sans-serif; |
| } |
| #container { |
| position: relative; |
| width: 100%; |
| height: 100vh; |
| } |
| #crosshair { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| width: 30px; |
| height: 30px; |
| pointer-events: none; |
| z-index: 100; |
| } |
| #ui { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| padding: 20px; |
| color: white; |
| background-color: rgba(0, 0, 0, 0.5); |
| z-index: 10; |
| } |
| #start-screen { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.8); |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| z-index: 200; |
| color: white; |
| } |
| #instructions { |
| position: absolute; |
| bottom: 20px; |
| left: 20px; |
| color: white; |
| background-color: rgba(0, 0, 0, 0.5); |
| padding: 10px; |
| border-radius: 5px; |
| z-index: 10; |
| } |
| .target-hit { |
| position: absolute; |
| color: red; |
| font-size: 24px; |
| font-weight: bold; |
| animation: fadeOut 1s forwards; |
| } |
| @keyframes fadeOut { |
| to { |
| opacity: 0; |
| transform: translateY(-50px); |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="container"> |
| <div id="crosshair"> |
| <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"> |
| <circle cx="50" cy="50" r="40" fill="none" stroke="white" stroke-width="2"/> |
| <line x1="50" y1="10" x2="50" y2="30" stroke="white" stroke-width="2"/> |
| <line x1="50" y1="70" x2="50" y2="90" stroke="white" stroke-width="2"/> |
| <line x1="10" y1="50" x2="30" y2="50" stroke="white" stroke-width="2"/> |
| <line x1="70" y1="50" x2="90" y2="50" stroke="white" stroke-width="2"/> |
| </svg> |
| </div> |
| |
| <div id="ui"> |
| <div class="flex justify-between"> |
| <div> |
| <h2 class="text-xl font-bold">3D射击跟枪练习器</h2> |
| <p>提高你的Roblox射击跟枪技巧</p> |
| </div> |
| <div class="text-right"> |
| <p class="text-lg">得分: <span id="score">0</span></p> |
| <p>命中率: <span id="accuracy">0</span>%</p> |
| <p>剩余时间: <span id="time">60</span>秒</p> |
| <p>剩余敌人: <span id="enemies">5</span></p> |
| </div> |
| </div> |
| </div> |
| |
| <div id="start-screen"> |
| <h1 class="text-4xl font-bold mb-8">3D射击跟枪练习器</h1> |
| <p class="text-xl mb-8">练习你的跟枪技巧,提高Roblox游戏中的表现</p> |
| <div class="mb-8"> |
| <label for="difficulty" class="block mb-2">选择难度:</label> |
| <select id="difficulty" class="bg-gray-700 text-white p-2 rounded"> |
| <option value="easy">简单</option> |
| <option value="medium" selected>中等</option> |
| <option value="hard">困难</option> |
| <option value="extreme">极限</option> |
| </select> |
| </div> |
| <button id="start-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-full text-xl transition duration-300"> |
| 开始练习 |
| </button> |
| </div> |
|
|
| <div id="instructions"> |
| <p>WASD: 移动</p> |
| <p>鼠标: 瞄准</p> |
| <p>左键: 射击</p> |
| <p>空格: 跳跃</p> |
| <p>Shift: 冲刺</p> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let scene, camera, renderer, controls; |
| let enemies = []; |
| let score = 0; |
| let shotsFired = 0; |
| let hits = 0; |
| let gameTime = 60; |
| let gameInterval; |
| let difficulty = 'medium'; |
| let gameStarted = false; |
| let clock = new THREE.Clock(); |
| let player, playerVelocity = new THREE.Vector3(); |
| let playerDirection = new THREE.Vector3(); |
| let moveForward = false; |
| let moveBackward = false; |
| let moveLeft = false; |
| let moveRight = false; |
| let canJump = true; |
| let isSprinting = false; |
| let pointerLockEnabled = false; |
| |
| |
| function init() { |
| |
| scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x88ccff); |
| scene.fog = new THREE.FogExp2(0x88ccff, 0.002); |
| |
| |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
| camera.position.y = 1.6; |
| |
| |
| renderer = new THREE.WebGLRenderer({ antialias: true }); |
| renderer.setPixelRatio(window.devicePixelRatio); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| document.getElementById('container').prepend(renderer.domElement); |
| |
| |
| const ambientLight = new THREE.AmbientLight(0x404040); |
| scene.add(ambientLight); |
| |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); |
| directionalLight.position.set(1, 1, 1); |
| scene.add(directionalLight); |
| |
| |
| const groundGeometry = new THREE.PlaneGeometry(100, 100); |
| const groundMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x3a5f0b, |
| 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); |
| |
| |
| addObstacles(); |
| |
| |
| createPlayer(); |
| |
| |
| controls = new THREE.PointerLockControls(camera, document.body); |
| |
| |
| window.addEventListener('resize', onWindowResize); |
| document.addEventListener('click', onMouseClick, false); |
| document.addEventListener('keydown', onKeyDown); |
| document.addEventListener('keyup', onKeyUp); |
| |
| |
| document.addEventListener('pointerlockchange', onPointerLockChange, false); |
| document.addEventListener('mozpointerlockchange', onPointerLockChange, false); |
| document.addEventListener('webkitpointerlockchange', onPointerLockChange, false); |
| |
| |
| document.getElementById('start-btn').addEventListener('click', function() { |
| |
| const element = document.body; |
| element.requestPointerLock = element.requestPointerLock || |
| element.mozRequestPointerLock || |
| element.webkitRequestPointerLock; |
| element.requestPointerLock(); |
| }); |
| |
| |
| document.getElementById('difficulty').addEventListener('change', function(e) { |
| difficulty = e.target.value; |
| }); |
| |
| |
| animate(); |
| } |
| |
| |
| function onPointerLockChange() { |
| pointerLockEnabled = (document.pointerLockElement === document.body || |
| document.mozPointerLockElement === document.body || |
| document.webkitPointerLockElement === document.body); |
| |
| if (pointerLockEnabled && !gameStarted) { |
| |
| startGame(); |
| } else if (!pointerLockEnabled && gameStarted) { |
| |
| pauseGame(); |
| } |
| } |
| |
| |
| function addObstacles() { |
| const boxGeometry = new THREE.BoxGeometry(5, 5, 5); |
| const boxMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x8b4513, |
| roughness: 0.7, |
| metalness: 0.3 |
| }); |
| |
| |
| for (let i = 0; i < 10; i++) { |
| const box = new THREE.Mesh(boxGeometry, boxMaterial); |
| box.position.x = Math.random() * 80 - 40; |
| box.position.z = Math.random() * 80 - 40; |
| box.position.y = 2.5; |
| box.castShadow = true; |
| box.receiveShadow = true; |
| scene.add(box); |
| } |
| |
| |
| const wallGeometry = new THREE.BoxGeometry(1, 3, 20); |
| const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x7f7f7f }); |
| |
| for (let i = 0; i < 5; i++) { |
| const wall = new THREE.Mesh(wallGeometry, wallMaterial); |
| wall.position.x = Math.random() * 60 - 30; |
| wall.position.z = Math.random() * 60 - 30; |
| wall.position.y = 1.5; |
| wall.castShadow = true; |
| wall.receiveShadow = true; |
| scene.add(wall); |
| } |
| } |
| |
| |
| function createPlayer() { |
| |
| const geometry = new THREE.BoxGeometry(0.5, 1.8, 0.5); |
| const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }); |
| player = new THREE.Mesh(geometry, material); |
| player.position.y = 1.8; |
| player.visible = false; |
| scene.add(player); |
| } |
| |
| |
| function onWindowResize() { |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| } |
| |
| |
| function onMouseClick(event) { |
| if (!gameStarted || !pointerLockEnabled) return; |
| |
| |
| if (event.button === 0) { |
| shotsFired++; |
| updateAccuracy(); |
| |
| |
| const raycaster = new THREE.Raycaster(); |
| raycaster.setFromCamera(new THREE.Vector2(0, 0), camera); |
| |
| |
| const intersects = raycaster.intersectObjects(enemies); |
| |
| if (intersects.length > 0) { |
| const enemy = intersects[0].object; |
| |
| |
| scene.remove(enemy); |
| enemies = enemies.filter(e => e !== enemy); |
| document.getElementById('enemies').textContent = enemies.length; |
| |
| |
| score += 10; |
| document.getElementById('score').textContent = score; |
| |
| |
| hits++; |
| updateAccuracy(); |
| |
| |
| showHitEffect(); |
| |
| |
| if (enemies.length === 0) { |
| endGame(true); |
| } |
| } |
| } |
| } |
| |
| |
| function showHitEffect() { |
| const hitEffect = document.createElement('div'); |
| hitEffect.className = 'target-hit'; |
| hitEffect.textContent = '+10'; |
| hitEffect.style.left = '50%'; |
| hitEffect.style.top = '50%'; |
| document.getElementById('container').appendChild(hitEffect); |
| |
| setTimeout(() => { |
| hitEffect.remove(); |
| }, 1000); |
| } |
| |
| |
| function updateAccuracy() { |
| const accuracy = shotsFired > 0 ? Math.round((hits / shotsFired) * 100) : 0; |
| document.getElementById('accuracy').textContent = accuracy; |
| } |
| |
| |
| function onKeyDown(event) { |
| if (!gameStarted || !pointerLockEnabled) return; |
| |
| switch(event.code) { |
| case 'KeyW': moveForward = true; break; |
| case 'KeyA': moveLeft = true; break; |
| case 'KeyS': moveBackward = true; break; |
| case 'KeyD': moveRight = true; break; |
| case 'Space': |
| if (canJump) { |
| playerVelocity.y += 15; |
| canJump = false; |
| } |
| break; |
| case 'ShiftLeft': |
| isSprinting = true; |
| break; |
| case 'Escape': |
| |
| document.exitPointerLock = document.exitPointerLock || |
| document.mozExitPointerLock || |
| document.webkitExitPointerLock; |
| document.exitPointerLock(); |
| break; |
| } |
| } |
| |
| |
| function onKeyUp(event) { |
| if (!gameStarted || !pointerLockEnabled) return; |
| |
| switch(event.code) { |
| case 'KeyW': moveForward = false; break; |
| case 'KeyA': moveLeft = false; break; |
| case 'KeyS': moveBackward = false; break; |
| case 'KeyD': moveRight = false; break; |
| case 'ShiftLeft': |
| isSprinting = false; |
| break; |
| } |
| } |
| |
| |
| function createEnemy() { |
| const enemyGeometry = new THREE.BoxGeometry(0.8, 1.8, 0.8); |
| const enemyMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xff0000, |
| roughness: 0.7, |
| metalness: 0.3 |
| }); |
| const enemy = new THREE.Mesh(enemyGeometry, enemyMaterial); |
| |
| |
| let x, z; |
| do { |
| x = (Math.random() * 60) - 30; |
| z = (Math.random() * 60) - 30; |
| } while (Math.sqrt(x*x + z*z) < 10); |
| |
| enemy.position.set(x, 0.9, z); |
| enemy.castShadow = true; |
| enemy.receiveShadow = true; |
| scene.add(enemy); |
| enemies.push(enemy); |
| |
| |
| animateEnemy(enemy); |
| } |
| |
| |
| function animateEnemy(enemy) { |
| let speed = 0.02; |
| let directionChangeTime = 0; |
| let currentDirection = new THREE.Vector3( |
| Math.random() * 2 - 1, |
| 0, |
| Math.random() * 2 - 1 |
| ).normalize(); |
| |
| |
| switch(difficulty) { |
| case 'easy': speed = 0.015; break; |
| case 'medium': speed = 0.025; break; |
| case 'hard': speed = 0.035; break; |
| case 'extreme': speed = 0.045; break; |
| } |
| |
| function move() { |
| if (!gameStarted) return; |
| |
| |
| directionChangeTime--; |
| if (directionChangeTime <= 0) { |
| |
| if (Math.random() > 0.7) { |
| currentDirection = new THREE.Vector3().subVectors( |
| player.position, |
| enemy.position |
| ).normalize(); |
| } else { |
| currentDirection = new THREE.Vector3( |
| Math.random() * 2 - 1, |
| 0, |
| Math.random() * 2 - 1 |
| ).normalize(); |
| } |
| directionChangeTime = 100 + Math.random() * 100; |
| } |
| |
| |
| enemy.position.x += currentDirection.x * speed; |
| enemy.position.z += currentDirection.z * speed; |
| |
| |
| if (enemy.position.x < -45) enemy.position.x = -45; |
| if (enemy.position.x > 45) enemy.position.x = 45; |
| if (enemy.position.z < -45) enemy.position.z = -45; |
| if (enemy.position.z > 45) enemy.position.z = 45; |
| |
| |
| enemy.lookAt(player.position); |
| |
| requestAnimationFrame(move); |
| } |
| |
| move(); |
| } |
| |
| |
| function startGame() { |
| |
| score = 0; |
| shotsFired = 0; |
| hits = 0; |
| gameTime = 60; |
| gameStarted = true; |
| |
| |
| document.getElementById('score').textContent = score; |
| document.getElementById('accuracy').textContent = '0'; |
| document.getElementById('time').textContent = gameTime; |
| |
| |
| document.getElementById('start-screen').style.display = 'none'; |
| |
| |
| enemies.forEach(enemy => scene.remove(enemy)); |
| enemies = []; |
| |
| |
| const enemyCount = difficulty === 'easy' ? 3 : |
| difficulty === 'medium' ? 5 : |
| difficulty === 'hard' ? 7 : 10; |
| |
| for (let i = 0; i < enemyCount; i++) { |
| createEnemy(); |
| } |
| |
| document.getElementById('enemies').textContent = enemyCount; |
| |
| |
| player.position.set(0, 1.8, 0); |
| controls.getObject().position.set(0, 1.6, 0); |
| controls.getObject().rotation.set(0, 0, 0); |
| |
| |
| gameInterval = setInterval(() => { |
| gameTime--; |
| document.getElementById('time').textContent = gameTime; |
| |
| if (gameTime <= 0) { |
| endGame(false); |
| } |
| }, 1000); |
| } |
| |
| |
| function pauseGame() { |
| |
| const startScreen = document.getElementById('start-screen'); |
| startScreen.style.display = 'flex'; |
| |
| |
| const accuracy = shotsFired > 0 ? Math.round((hits / shotsFired) * 100) : 0; |
| startScreen.innerHTML = ` |
| <h1 class="text-4xl font-bold mb-8">游戏暂停</h1> |
| <div class="text-xl mb-8"> |
| <p>当前得分: ${score}</p> |
| <p>命中率: ${accuracy}%</p> |
| <p>总射击次数: ${shotsFired}</p> |
| <p>命中次数: ${hits}</p> |
| <p>剩余敌人: ${enemies.length}</p> |
| </div> |
| <div class="mb-8"> |
| <label for="difficulty" class="block mb-2">选择难度:</label> |
| <select id="difficulty" class="bg-gray-700 text-white p-2 rounded"> |
| <option value="easy">简单</option> |
| <option value="medium" selected>中等</option> |
| <option value="hard">困难</option> |
| <option value="extreme">极限</option> |
| </select> |
| </div> |
| <button id="start-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-full text-xl transition duration-300"> |
| 继续游戏 |
| </button> |
| `; |
| |
| |
| document.getElementById('start-btn').addEventListener('click', function() { |
| |
| const element = document.body; |
| element.requestPointerLock = element.requestPointerLock || |
| element.mozRequestPointerLock || |
| element.webkitRequestPointerLock; |
| element.requestPointerLock(); |
| }); |
| |
| document.getElementById('difficulty').addEventListener('change', function(e) { |
| difficulty = e.target.value; |
| }); |
| } |
| |
| |
| function endGame(win) { |
| gameStarted = false; |
| clearInterval(gameInterval); |
| |
| |
| document.exitPointerLock = document.exitPointerLock || |
| document.mozExitPointerLock || |
| document.webkitExitPointerLock; |
| document.exitPointerLock(); |
| |
| |
| const startScreen = document.getElementById('start-screen'); |
| startScreen.style.display = 'flex'; |
| |
| |
| const accuracy = shotsFired > 0 ? Math.round((hits / shotsFired) * 100) : 0; |
| startScreen.innerHTML = ` |
| <h1 class="text-4xl font-bold mb-8">${win ? '胜利!' : '游戏结束'}</h1> |
| <div class="text-xl mb-8"> |
| <p>最终得分: ${score}</p> |
| <p>命中率: ${accuracy}%</p> |
| <p>总射击次数: ${shotsFired}</p> |
| <p>命中次数: ${hits}</p> |
| <p>剩余敌人: ${enemies.length}</p> |
| </div> |
| <div class="mb-8"> |
| <label for="difficulty" class="block mb-2">选择难度:</label> |
| <select id="difficulty" class="bg-gray-700 text-white p-2 rounded"> |
| <option value="easy">简单</option> |
| <option value="medium" selected>中等</option> |
| <option value="hard">困难</option> |
| <option value="extreme">极限</option> |
| </select> |
| </div> |
| <button id="start-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-full text-xl transition duration-300"> |
| 再玩一次 |
| </button> |
| `; |
| |
| |
| document.getElementById('start-btn').addEventListener('click', function() { |
| |
| const element = document.body; |
| element.requestPointerLock = element.requestPointerLock || |
| element.mozRequestPointerLock || |
| element.webkitRequestPointerLock; |
| element.requestPointerLock(); |
| }); |
| |
| document.getElementById('difficulty').addEventListener('change', function(e) { |
| difficulty = e.target.value; |
| }); |
| } |
| |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| |
| const delta = clock.getDelta(); |
| |
| if (controls.isLocked && gameStarted) { |
| |
| const speed = isSprinting ? 30 : 15; |
| |
| playerVelocity.x -= playerVelocity.x * 10.0 * delta; |
| playerVelocity.z -= playerVelocity.z * 10.0 * delta; |
| |
| playerDirection.z = Number(moveForward) - Number(moveBackward); |
| playerDirection.x = Number(moveRight) - Number(moveLeft); |
| playerDirection.normalize(); |
| |
| if (moveForward || moveBackward) playerVelocity.z -= playerDirection.z * speed * delta; |
| if (moveLeft || moveRight) playerVelocity.x -= playerDirection.x * speed * delta; |
| |
| |
| playerVelocity.y -= 9.8 * delta; |
| |
| |
| controls.moveRight(-playerVelocity.x * delta); |
| controls.moveForward(-playerVelocity.z * delta); |
| controls.getObject().position.y += playerVelocity.y * delta; |
| |
| |
| if (controls.getObject().position.y < 1.6) { |
| playerVelocity.y = 0; |
| controls.getObject().position.y = 1.6; |
| canJump = true; |
| } |
| |
| |
| const pos = controls.getObject().position; |
| if (pos.x < -45) pos.x = -45; |
| if (pos.x > 45) pos.x = 45; |
| if (pos.z < -45) pos.z = -45; |
| if (pos.z > 45) pos.z = 45; |
| |
| |
| player.position.copy(controls.getObject().position); |
| player.position.y += 0.2; |
| } |
| |
| renderer.render(scene, camera); |
| } |
| |
| |
| init(); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=Aprec/test" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |