|
|
|
|
|
let scene, camera, renderer; |
|
|
let player, playerModel; |
|
|
let moveSpeed = 0.1; |
|
|
let runSpeed = 0.2; |
|
|
let jumpHeight = 0.3; |
|
|
let isJumping = false; |
|
|
let velocity = { x: 0, y: 0, z: 0 }; |
|
|
let keys = {}; |
|
|
let mouseX = 0, mouseY = 0; |
|
|
let isPaused = false; |
|
|
let gameStartTime = Date.now(); |
|
|
let buildings = []; |
|
|
let ground; |
|
|
let clock = new THREE.Clock(); |
|
|
|
|
|
|
|
|
function init() { |
|
|
|
|
|
scene = new THREE.Scene(); |
|
|
scene.fog = new THREE.Fog(0x87CEEB, 10, 100); |
|
|
scene.background = new THREE.Color(0x87CEEB); |
|
|
|
|
|
|
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
|
camera.position.set(0, 5, 10); |
|
|
|
|
|
|
|
|
renderer = new THREE.WebGLRenderer({ |
|
|
canvas: document.getElementById('gameCanvas'), |
|
|
antialias: true |
|
|
}); |
|
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
|
renderer.shadowMap.enabled = true; |
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
|
|
|
|
|
|
|
|
setupLighting(); |
|
|
|
|
|
|
|
|
createGround(); |
|
|
createBuildings(); |
|
|
createPlayer(); |
|
|
|
|
|
|
|
|
setupControls(); |
|
|
|
|
|
|
|
|
animate(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
document.getElementById('loadingScreen').style.display = 'none'; |
|
|
}, 2000); |
|
|
} |
|
|
|
|
|
function setupLighting() { |
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.6); |
|
|
scene.add(ambientLight); |
|
|
|
|
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); |
|
|
directionalLight.position.set(50, 100, 50); |
|
|
directionalLight.castShadow = true; |
|
|
directionalLight.shadow.camera.left = -50; |
|
|
directionalLight.shadow.camera.right = 50; |
|
|
directionalLight.shadow.camera.top = 50; |
|
|
directionalLight.shadow.camera.bottom = -50; |
|
|
directionalLight.shadow.camera.near = 0.1; |
|
|
directionalLight.shadow.camera.far = 200; |
|
|
directionalLight.shadow.mapSize.width = 2048; |
|
|
directionalLight.shadow.mapSize.height = 2048; |
|
|
scene.add(directionalLight); |
|
|
} |
|
|
|
|
|
function createGround() { |
|
|
const groundGeometry = new THREE.PlaneGeometry(200, 200); |
|
|
const groundMaterial = new THREE.MeshLambertMaterial({ |
|
|
color: 0x3a5f3a, |
|
|
side: THREE.DoubleSide |
|
|
}); |
|
|
ground = new THREE.Mesh(groundGeometry, groundMaterial); |
|
|
ground.rotation.x = -Math.PI / 2; |
|
|
ground.receiveShadow = true; |
|
|
scene.add(ground); |
|
|
|
|
|
|
|
|
const roadGeometry = new THREE.PlaneGeometry(10, 200); |
|
|
const roadMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); |
|
|
const road = new THREE.Mesh(roadGeometry, roadMaterial); |
|
|
road.rotation.x = -Math.PI / 2; |
|
|
road.position.y = 0.01; |
|
|
road.receiveShadow = true; |
|
|
scene.add(road); |
|
|
} |
|
|
|
|
|
function createBuildings() { |
|
|
const buildingPositions = [ |
|
|
{ x: -20, z: -20, height: 30, width: 10, depth: 10 }, |
|
|
{ x: 20, z: -20, height: 25, width: 12, depth: 8 }, |
|
|
{ x: -20, z: 20, height: 35, width: 8, depth: 12 }, |
|
|
{ x: 20, z: 20, height: 40, width: 15, depth: 10 }, |
|
|
{ x: -40, z: 0, height: 20, width: 10, depth: 10 }, |
|
|
{ x: 40, z: 0, height: 28, width: 10, depth: 10 }, |
|
|
{ x: 0, z: -40, height: 32, width: 12, depth: 12 }, |
|
|
{ x: 0, z: 40, height: 24, width: 10, depth: 10 } |
|
|
]; |
|
|
|
|
|
buildingPositions.forEach(pos => { |
|
|
const buildingGeometry = new THREE.BoxGeometry(pos.width, pos.height, pos.depth); |
|
|
const buildingMaterial = new THREE.MeshLambertMaterial({ |
|
|
color: new THREE.Color().setHSL(Math.random() * 0.1 + 0.5, 0.5, 0.6) |
|
|
}); |
|
|
const building = new THREE.Mesh(buildingGeometry, buildingMaterial); |
|
|
building.position.set(pos.x, pos.height / 2, pos.z); |
|
|
building.castShadow = true; |
|
|
building.receiveShadow = true; |
|
|
buildings.push(building); |
|
|
scene.add(building); |
|
|
}); |
|
|
} |
|
|
|
|
|
function createPlayer() { |
|
|
|
|
|
player = new THREE.Group(); |
|
|
|
|
|
|
|
|
const bodyGeometry = new THREE.CapsuleGeometry(0.5, 1.5, 4, 8); |
|
|
const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x4169e1 }); |
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); |
|
|
body.position.y = 1.5; |
|
|
body.castShadow = true; |
|
|
player.add(body); |
|
|
|
|
|
|
|
|
const headGeometry = new THREE.SphereGeometry(0.3, 16, 16); |
|
|
const headMaterial = new THREE.MeshLambertMaterial({ color: 0xffdbac }); |
|
|
const head = new THREE.Mesh(headGeometry, headMaterial); |
|
|
head.position.y = 2.7; |
|
|
head.castShadow = true; |
|
|
player.add(head); |
|
|
|
|
|
player.position.set(0, 0, 0); |
|
|
playerModel = player; |
|
|
scene.add(player); |
|
|
} |
|
|
|
|
|
function setupControls() { |
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
|
keys[event.code] = true; |
|
|
|
|
|
if (event.code === 'Space' && !isJumping) { |
|
|
velocity.y = jumpHeight; |
|
|
isJumping = true; |
|
|
} |
|
|
}); |
|
|
|
|
|
document.addEventListener('keyup', (event) => { |
|
|
keys[event.code] = false; |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', (event) => { |
|
|
mouseX = (event.clientX / window.innerWidth) * 2 - 1; |
|
|
mouseY = -(event.clientY / window.innerHeight) * 2 + 1; |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => { |
|
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
|
camera.updateProjectionMatrix(); |
|
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('startButton').addEventListener('click', () => { |
|
|
document.getElementById('startScreen').style.display = 'none'; |
|
|
document.getElementById('hud').classList.remove('hidden'); |
|
|
document.body.requestPointerLock(); |
|
|
}); |
|
|
|
|
|
document.getElementById('togglePause').addEventListener('click', () => { |
|
|
isPaused = !isPaused; |
|
|
const btn = document.getElementById('togglePause'); |
|
|
btn.innerHTML = isPaused ? |
|
|
'<i data-feather="play" class="w-4 h-4"></i><span>Resume</span>' : |
|
|
'<i data-feather="pause" class="w-4 h-4"></i><span>Pause</span>'; |
|
|
feather.replace(); |
|
|
}); |
|
|
} |
|
|
|
|
|
function updatePlayer() { |
|
|
if (isPaused) return; |
|
|
|
|
|
const speed = keys['ShiftLeft'] ? runSpeed : moveSpeed; |
|
|
const moveVector = new THREE.Vector3(); |
|
|
|
|
|
|
|
|
if (keys['KeyW']) moveVector.z -= speed; |
|
|
if (keys['KeyS']) moveVector.z += speed; |
|
|
if (keys['KeyA']) moveVector.x -= speed; |
|
|
if (keys['KeyD']) moveVector.x += speed; |
|
|
|
|
|
|
|
|
const angle = camera.rotation.y; |
|
|
const rotatedVector = moveVector.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle); |
|
|
|
|
|
player.position.x += rotatedVector.x; |
|
|
player.position.z += rotatedVector.z; |
|
|
|
|
|
|
|
|
velocity.y -= 0.015; |
|
|
player.position.y += velocity.y; |
|
|
|
|
|
|
|
|
if (player.position.y <= 0) { |
|
|
player.position.y = 0; |
|
|
velocity.y = 0; |
|
|
isJumping = false; |
|
|
} |
|
|
|
|
|
|
|
|
buildings.forEach(building => { |
|
|
const distance = player.position.distanceTo(building.position); |
|
|
const minDistance = 5; |
|
|
if (distance < minDistance) { |
|
|
const direction = new THREE.Vector3() |
|
|
.subVectors(player.position, building.position) |
|
|
.normalize(); |
|
|
player.position.copy(building.position).add(direction.multiplyScalar(minDistance)); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const cameraOffset = new THREE.Vector3(0, 5, 10); |
|
|
cameraOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle); |
|
|
camera.position.copy(player.position).add(cameraOffset); |
|
|
camera.lookAt(player.position); |
|
|
|
|
|
|
|
|
camera.rotation.y = mouseX * Math.PI; |
|
|
camera.rotation.x = mouseY * Math.PI / 4; |
|
|
|
|
|
|
|
|
updateHUD(); |
|
|
} |
|
|
|
|
|
function updateHUD() { |
|
|
|
|
|
document.getElementById('position').textContent = |
|
|
`${Math.round(player.position.x)}, ${Math.round(player.position.z)}`; |
|
|
|
|
|
|
|
|
const currentSpeed = Math.sqrt(velocity.x ** 2 + velocity.z ** 2) * 100; |
|
|
document.getElementById('speed').textContent = Math.round(currentSpeed); |
|
|
|
|
|
|
|
|
const elapsed = Date.now() - gameStartTime; |
|
|
const minutes = Math.floor(elapsed / 60000); |
|
|
const seconds = Math.floor((elapsed % 60000) / 1000); |
|
|
document.getElementById('gameTime').textContent = |
|
|
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
|
|
|
|
|
|
|
|
updateMiniMap(); |
|
|
} |
|
|
|
|
|
function updateMiniMap() { |
|
|
const canvas = document.getElementById('miniMap'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
canvas.width = 192; |
|
|
canvas.height = 192; |
|
|
|
|
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; |
|
|
ctx.fillRect(0, 0, 192, 192); |
|
|
|
|
|
|
|
|
const mapX = (player.position.x + 100) * 0.96; |
|
|
const mapZ = (player.position.z + 100) * 0.96; |
|
|
|
|
|
ctx.fillStyle = '#4ade80'; |
|
|
ctx.beginPath(); |
|
|
ctx.arc(mapX, mapZ, 3, 0, Math.PI * 2); |
|
|
ctx.fill(); |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#6b7280'; |
|
|
buildings.forEach(building => { |
|
|
const bX = (building.position.x + 100) * 0.96; |
|
|
const bZ = (building.position.z + 100) * 0.96; |
|
|
ctx.fillRect(bX - 5, bZ - 5, 10, 10); |
|
|
}); |
|
|
} |
|
|
|
|
|
function animate() { |
|
|
requestAnimationFrame(animate); |
|
|
|
|
|
if (!isPaused) { |
|
|
updatePlayer(); |
|
|
|
|
|
|
|
|
if (keys['KeyW'] || keys['KeyS'] || keys['KeyA'] || keys['KeyD']) { |
|
|
player.rotation.y = camera.rotation.y; |
|
|
player.position.y = Math.sin(Date.now() * 0.01) * 0.05; |
|
|
} |
|
|
} |
|
|
|
|
|
renderer.render(scene, camera); |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('load', init); |