289 lines
11 KiB
HTML
289 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Scrap-Garden Prototype: Phase 1</title>
|
|
<style>
|
|
body { margin: 0; overflow: hidden; background-color: #000; font-family: monospace; }
|
|
#instructions {
|
|
position: absolute;
|
|
top: 50%; width: 100%;
|
|
text-align: center;
|
|
color: white;
|
|
pointer-events: none;
|
|
font-size: 24px;
|
|
transform: translateY(-50%);
|
|
background: rgba(0,0,0,0.5);
|
|
padding: 20px 0;
|
|
}
|
|
#reticle {
|
|
position: absolute;
|
|
top: 50%; left: 50%;
|
|
width: 10px; height: 10px;
|
|
background: yellow;
|
|
border-radius: 50%;
|
|
transform: translate(-50%, -50%);
|
|
pointer-events: none;
|
|
opacity: 0.8;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="reticle"></div>
|
|
<div id="instructions">CLICK TO START</div>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'https://unpkg.com/three@0.150.0/build/three.module.js';
|
|
|
|
// --- GLOBAL VARIABLES ---
|
|
let camera, scene, renderer;
|
|
let player, playerVelocity, playerDirection;
|
|
let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false, canJump = false;
|
|
let bullets = [];
|
|
let prevTime = performance.now();
|
|
|
|
// Configuration
|
|
const PLAYER_SPEED = 20.0;
|
|
const JUMP_FORCE = 15.0;
|
|
const GRAVITY = 40.0;
|
|
const BULLET_SPEED = 50.0;
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// 1. SETUP SCENE
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x87CEEB); // Sky Blue (Aether Sky)
|
|
scene.fog = new THREE.Fog(0x87CEEB, 10, 80); // Fog for that MML/Silo vibe
|
|
|
|
// 2. SETUP CAMERA (Third Person)
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
|
|
// 3. RENDERER
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// 4. LIGHTING
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambientLight);
|
|
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
dirLight.position.set(50, 100, 50);
|
|
dirLight.castShadow = true;
|
|
scene.add(dirLight);
|
|
|
|
// 5. THE WORLD (Ground & Obstacles)
|
|
// Ground
|
|
const floorGeo = new THREE.PlaneGeometry(200, 200);
|
|
const floorMat = new THREE.MeshStandardMaterial({ color: 0x555555 }); // Grey Concrete
|
|
const floor = new THREE.Mesh(floorGeo, floorMat);
|
|
floor.rotation.x = -Math.PI / 2;
|
|
floor.receiveShadow = true;
|
|
scene.add(floor);
|
|
|
|
// Obstacles (Random Blue Cubes)
|
|
const boxGeo = new THREE.BoxGeometry(4, 4, 4);
|
|
const boxMat = new THREE.MeshStandardMaterial({ color: 0x0044aa });
|
|
|
|
for (let i = 0; i < 50; i++) {
|
|
const box = new THREE.Mesh(boxGeo, boxMat);
|
|
// Random position spread out, avoiding center
|
|
const x = (Math.random() - 0.5) * 150;
|
|
const z = (Math.random() - 0.5) * 150;
|
|
// Keep center clear for spawn
|
|
if(Math.abs(x) < 10 && Math.abs(z) < 10) continue;
|
|
|
|
box.position.set(x, 2, z); // y=2 is half height
|
|
box.castShadow = true;
|
|
box.receiveShadow = true;
|
|
scene.add(box);
|
|
}
|
|
|
|
// 6. THE PLAYER
|
|
// Group holds the camera and mesh handles visual
|
|
player = new THREE.Group();
|
|
scene.add(player);
|
|
|
|
// Visual Mesh (Green Capsule)
|
|
const playerGeo = new THREE.CapsuleGeometry(1, 2, 4, 8);
|
|
const playerMat = new THREE.MeshStandardMaterial({ color: 0x33ff33 });
|
|
const playerMesh = new THREE.Mesh(playerGeo, playerMat);
|
|
playerMesh.position.y = 2; // Lift half height + radius so it stands on floor
|
|
playerMesh.castShadow = true;
|
|
player.add(playerMesh);
|
|
|
|
// Attach Camera to Player (The MML "Tight Follow")
|
|
// Positioned behind and slightly up
|
|
camera.position.set(0, 5, 8);
|
|
camera.lookAt(0, 2, 0); // Look at player's back/head
|
|
player.add(camera);
|
|
|
|
playerVelocity = new THREE.Vector3();
|
|
playerDirection = new THREE.Vector3();
|
|
|
|
// 7. EVENTS & CONTROLS
|
|
setupControls();
|
|
|
|
window.addEventListener('resize', onWindowResize);
|
|
}
|
|
|
|
function setupControls() {
|
|
// Pointer Lock (Click to capture mouse)
|
|
const instructions = document.getElementById('instructions');
|
|
|
|
document.addEventListener('click', () => {
|
|
document.body.requestPointerLock();
|
|
});
|
|
|
|
document.addEventListener('pointerlockchange', () => {
|
|
if (document.pointerlockElement === document.body) {
|
|
instructions.style.display = 'none';
|
|
} else {
|
|
instructions.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// Mouse Movement (Rotates Player Group)
|
|
document.addEventListener('mousemove', (event) => {
|
|
if (document.pointerlockElement === document.body) {
|
|
// Rotate player on Y axis (Yaw)
|
|
// Negative movementX because we want to turn left when moving mouse left
|
|
player.rotation.y -= event.movementX * 0.002;
|
|
|
|
// Simple vertical camera tilt (Optional constraint)
|
|
// (Skipping vertical look for pure MML feel, but usually desired)
|
|
}
|
|
});
|
|
|
|
// Keyboard
|
|
const onKeyDown = (event) => {
|
|
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 === true) {
|
|
playerVelocity.y += JUMP_FORCE;
|
|
canJump = false;
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
const onKeyUp = (event) => {
|
|
switch (event.code) {
|
|
case 'KeyW': moveForward = false; break;
|
|
case 'KeyA': moveLeft = false; break;
|
|
case 'KeyS': moveBackward = false; break;
|
|
case 'KeyD': moveRight = false; break;
|
|
}
|
|
};
|
|
|
|
// Shooting
|
|
const onMouseDown = (event) => {
|
|
if (document.pointerlockElement === document.body && event.button === 0) {
|
|
shootBullet();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
document.addEventListener('keyup', onKeyUp);
|
|
document.addEventListener('mousedown', onMouseDown);
|
|
}
|
|
|
|
function shootBullet() {
|
|
// Create Bullet Geometry
|
|
const bulletGeo = new THREE.SphereGeometry(0.3, 8, 8);
|
|
const bulletMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
|
|
const bullet = new THREE.Mesh(bulletGeo, bulletMat);
|
|
|
|
// Start bullet at player position (slightly in front and up)
|
|
// We need to get the player's world position and rotation
|
|
const spawnPos = new THREE.Vector3(0, 3, -2).applyMatrix4(player.matrixWorld);
|
|
bullet.position.copy(spawnPos);
|
|
|
|
// Calculate Velocity based on player rotation
|
|
const velocity = new THREE.Vector3(0, 0, -1);
|
|
velocity.applyQuaternion(player.quaternion);
|
|
velocity.multiplyScalar(BULLET_SPEED);
|
|
|
|
scene.add(bullet);
|
|
|
|
// Add to array
|
|
bullets.push({ mesh: bullet, velocity: velocity, creationTime: performance.now() });
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const time = performance.now();
|
|
const delta = (time - prevTime) / 1000;
|
|
|
|
// --- PLAYER MOVEMENT PHYSICS ---
|
|
|
|
// Friction/Damping
|
|
playerVelocity.x -= playerVelocity.x * 10.0 * delta;
|
|
playerVelocity.z -= playerVelocity.z * 10.0 * delta;
|
|
playerVelocity.y -= 9.8 * 2.0 * delta; // Gravity
|
|
|
|
// Input Direction
|
|
playerDirection.z = Number(moveForward) - Number(moveBackward);
|
|
playerDirection.x = Number(moveRight) - Number(moveLeft);
|
|
playerDirection.normalize(); // ensure consistent speed in all directions
|
|
|
|
if (moveForward || moveBackward) playerVelocity.z -= playerDirection.z * PLAYER_SPEED * delta;
|
|
if (moveLeft || moveRight) playerVelocity.x -= playerDirection.x * PLAYER_SPEED * delta;
|
|
|
|
// Apply movement (Local to World)
|
|
// We move the player Group based on its rotation
|
|
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion);
|
|
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion);
|
|
|
|
// Re-map inputs to forward/right vectors
|
|
// If pressing 'W' (playerDirection.z is 1), we move along forward vector
|
|
if(moveForward) player.position.addScaledVector(forward, PLAYER_SPEED * delta * 0.5);
|
|
if(moveBackward) player.position.addScaledVector(forward, -PLAYER_SPEED * delta * 0.5);
|
|
if(moveRight) player.position.addScaledVector(right, PLAYER_SPEED * delta * 0.5);
|
|
if(moveLeft) player.position.addScaledVector(right, -PLAYER_SPEED * delta * 0.5);
|
|
|
|
// Apply Jump/Gravity
|
|
player.position.y += playerVelocity.y * delta;
|
|
|
|
// Floor Collision
|
|
if (player.position.y < 0) {
|
|
playerVelocity.y = 0;
|
|
player.position.y = 0;
|
|
canJump = true;
|
|
}
|
|
|
|
// --- BULLET LOGIC ---
|
|
for (let i = bullets.length - 1; i >= 0; i--) {
|
|
const b = bullets[i];
|
|
// Move bullet
|
|
b.mesh.position.addScaledVector(b.velocity, delta);
|
|
|
|
// Remove if too old (2 seconds)
|
|
if (time - b.creationTime > 2000) {
|
|
scene.remove(b.mesh);
|
|
bullets.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
prevTime = time;
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |