D-day-game-3d / index.html
Keeby-smilyai's picture
Update index.html
77517f4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SAVING PRIVATE RYAN - REDUX</title>
<style>
* {
margin: 0; padding: 0; box-sizing: border-box;
-webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
body {
overflow: hidden; font-family: 'Courier New', monospace; background: #000;
touch-action: none; position: fixed; width: 100%; height: 100%;
}
#game-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
/* START MENU */
#start-menu {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); z-index: 500;
display: flex; flex-direction: column; align-items: center; justify-content: flex-start; padding: 20px; overflow-y: auto;
}
#start-menu.hidden { display: none; }
#start-menu h1 { color: #c4a000; font-size: 24px; text-align: center; margin-bottom: 5px; text-shadow: 2px 2px 4px black; letter-spacing: 3px; }
#start-menu h2 { color: #888; font-size: 12px; margin-bottom: 20px; }
.menu-section { background: rgba(0,0,0,0.5); border: 2px solid #444; padding: 15px; margin: 10px 0; width: 100%; max-width: 350px; }
.menu-section h3 { color: #ffaa00; font-size: 14px; margin-bottom: 10px; border-bottom: 1px solid #444; padding-bottom: 5px; }
.weapon-option { display: flex; align-items: center; padding: 12px; margin: 8px 0; background: rgba(255,255,255,0.1); border: 2px solid transparent; cursor: pointer; transition: all 0.2s; }
.weapon-option:hover, .weapon-option.selected { background: rgba(255,200,0,0.2); border-color: #ffaa00; }
.weapon-option.selected { box-shadow: 0 0 15px rgba(255,170,0,0.5); }
.weapon-icon { width: 80px; height: 30px; background: #333; margin-right: 15px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
.weapon-info { flex: 1; }
.weapon-name { color: white; font-size: 14px; font-weight: bold; }
.weapon-stats { color: #aaa; font-size: 10px; margin-top: 3px; }
.stat-bar { display: inline-block; width: 50px; height: 6px; background: #333; margin-left: 5px; vertical-align: middle; }
.stat-fill { height: 100%; background: #ffaa00; }
#start-btn { margin-top: 20px; padding: 15px 50px; font-size: 18px; font-weight: bold; background: linear-gradient(180deg, #4a7c20 0%, #2d5a10 100%); border: 3px solid #6a9c30; color: white; cursor: pointer; text-shadow: 1px 1px 2px black; letter-spacing: 2px; }
#start-btn:active { transform: scale(0.98); background: linear-gradient(180deg, #5a8c30 0%, #3d6a20 100%); }
.mission-brief { color: #aaa; font-size: 11px; line-height: 1.5; }
/* GAME UI */
#ui { position: absolute; top: 0; left: 0; color: white; z-index: 10; padding: 10px; pointer-events: none; }
#ui.hidden { display: none; }
h1.game-title { font-size: 12px; letter-spacing: 2px; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); color: #ccc; }
#hp_bar { width: 120px; height: 10px; background: rgba(255, 0, 0, 0.3); border: 2px solid #fff; margin-top: 5px; }
#hp_fill { width: 100%; height: 100%; background: linear-gradient(90deg, #ff0000, #ff4444); transition: width 0.3s; }
#status { margin-top: 5px; color: #aaa; font-size: 10px; max-width: 200px; }
#phase { position: absolute; top: 10px; right: 10px; color: #ffaa00; font-size: 12px; font-weight: bold; z-index: 10; pointer-events: none; text-align: right; }
#weapon-display { position: absolute; top: 30px; right: 10px; color: #888; font-size: 10px; z-index: 10; pointer-events: none; text-align: right; }
#ammo { position: absolute; top: 45px; right: 10px; color: white; font-size: 18px; font-weight: bold; z-index: 10; pointer-events: none; }
#level-display { position: absolute; top: 70px; right: 10px; color: #4a9; font-size: 10px; z-index: 10; pointer-events: none; }
#blood-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; background: radial-gradient(circle, transparent 30%, rgba(255,0,0,0.7) 100%); z-index: 5; opacity: 0; transition: opacity 0.15s; }
#blood-overlay.hit { opacity: 1; }
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 10; }
#crosshair.normal { width: 30px; height: 30px; }
#crosshair.normal::before, #crosshair.normal::after { content: ''; position: absolute; background: rgba(255,255,255,0.9); box-shadow: 0 0 3px black; }
#crosshair.normal::before { top: 50%; left: 3px; right: 3px; height: 2px; transform: translateY(-50%); }
#crosshair.normal::after { left: 50%; top: 3px; bottom: 3px; width: 2px; transform: translateX(-50%); }
#crosshair.scoped { width: 100%; height: 100%; background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100"><defs><radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"><stop offset="50%" style="stop-color:transparent;stop-opacity:1" /><stop offset="51%" style="stop-color:black;stop-opacity:1" /></radialGradient></defs><rect width="100%" height="100%" fill="url(%23grad1)" /><line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="0.5" /><line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="0.5" /></svg>') no-repeat center center; background-size: cover; }
/* MOBILE CONTROLS */
#mobile-controls { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 100; pointer-events: none; }
#mobile-controls.hidden { display: none; }
#joystick-zone { position: absolute; bottom: 20px; left: 20px; width: 130px; height: 130px; pointer-events: auto; touch-action: none; }
#joystick-base { position: absolute; width: 130px; height: 130px; background: rgba(255, 255, 255, 0.2); border: 4px solid rgba(255, 255, 255, 0.6); border-radius: 50%; }
#joystick-thumb { position: absolute; width: 55px; height: 55px; background: rgba(255, 255, 255, 0.7); border: 3px solid white; border-radius: 50%; top: 37.5px; left: 37.5px; box-shadow: 0 0 15px rgba(255,255,255,0.4); }
#look-zone { position: absolute; top: 100px; right: 0; width: 55%; height: calc(100% - 220px); pointer-events: auto; touch-action: none; }
#buttons-zone { position: absolute; bottom: 15px; right: 15px; display: flex; flex-direction: column; gap: 12px; align-items: flex-end; pointer-events: auto; }
.game-btn { display: flex; align-items: center; justify-content: center; border-radius: 50%; font-weight: bold; color: white; text-shadow: 1px 1px 2px black; cursor: pointer; touch-action: manipulation; pointer-events: auto; }
#fire-btn { width: 90px; height: 90px; background: rgba(255, 0, 0, 0.5); border: 4px solid rgba(255, 100, 100, 0.9); font-size: 16px; box-shadow: 0 0 25px rgba(255,0,0,0.4); }
#fire-btn:active, #fire-btn.pressed { background: rgba(255, 0, 0, 0.8); transform: scale(0.95); }
.btn-row { display: flex; gap: 12px; }
#reload-btn, #action-btn, #scope-btn { width: 60px; height: 60px; background: rgba(255, 255, 255, 0.35); border: 3px solid rgba(255, 255, 255, 0.7); font-size: 10px; text-align: center; line-height: 1.2; }
#reload-btn:active, #action-btn:active, #scope-btn:active { background: rgba(255, 255, 255, 0.7); transform: scale(0.95); }
#action-btn { background: rgba(255, 170, 0, 0.4); border-color: rgba(255, 200, 0, 0.8); }
#action-btn.ready { animation: pulse 0.8s infinite; }
#scope-btn { background: rgba(100, 100, 255, 0.4); border-color: rgba(150, 150, 255, 0.8); }
#scope-btn.hidden { display: none; }
#scope-btn.active { background: rgba(100, 100, 255, 0.8); }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 10px rgba(255,170,0,0.4); }
50% { box-shadow: 0 0 25px rgba(255,170,0,1); }
}
#debug { position: absolute; top: 100px; left: 10px; background: rgba(0,0,0,0.8); color: lime; padding: 5px 10px; font-size: 9px; z-index: 200; pointer-events: none; max-width: 150px; display: none; }
/* LEVEL TRANSITION */
#level-transition { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: black; z-index: 400; display: none; flex-direction: column; align-items: center; justify-content: center; color: white; }
#level-transition.show { display: flex; }
#level-transition h2 { color: #ffaa00; font-size: 20px; margin-bottom: 10px; }
#level-transition p { color: #aaa; font-size: 12px; text-align: center; max-width: 300px; line-height: 1.5; }
/* SCOPE OVERLAY */
#scope-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; pointer-events: none; z-index: 6; }
#scope-overlay.active { display: block; }
</style>
</head>
<body>
<!-- START MENU -->
<div id="start-menu">
<h1>SAVING PRIVATE RYAN</h1>
<h2>NORMANDY CAMPAIGN - REDUX</h2>
<div class="menu-section">
<h3>SELECT YOUR WEAPON</h3>
<div class="weapon-option selected" data-weapon="garand">
<div class="weapon-icon">🔫</div>
<div class="weapon-info">
<div class="weapon-name">BAR (SCOPED)</div>
<div class="weapon-stats"> DMG: <span class="stat-bar"><span class="stat-fill" style="width:70%"></span></span> ROF: <span class="stat-bar"><span class="stat-fill" style="width:50%"></span></span></div>
</div>
</div>
<div class="weapon-option" data-weapon="carbine">
<div class="weapon-icon">🔫</div>
<div class="weapon-info">
<div class="weapon-name">M1 CARBINE (SCOPED)</div>
<div class="weapon-stats"> DMG: <span class="stat-bar"><span class="stat-fill" style="width:45%"></span></span> ROF: <span class="stat-bar"><span class="stat-fill" style="width:75%"></span></span></div>
</div>
</div>
<div class="weapon-option" data-weapon="springfield">
<div class="weapon-icon">🎯</div>
<div class="weapon-info">
<div class="weapon-name">M1903 SPRINGFIELD (SNIPER)</div>
<div class="weapon-stats"> DMG: <span class="stat-bar"><span class="stat-fill" style="width:100%"></span></span> ROF: <span class="stat-bar"><span class="stat-fill" style="width:20%"></span></span></div>
</div>
</div>
<div class="weapon-option" data-weapon="thompson">
<div class="weapon-icon">💥</div>
<div class="weapon-info">
<div class="weapon-name">M1 THOMPSON (SCOPED)</div>
<div class="weapon-stats"> DMG: <span class="stat-bar"><span class="stat-fill" style="width:40%"></span></span> ROF: <span class="stat-bar"><span class="stat-fill" style="width:95%"></span></span></div>
</div>
</div>
</div>
<button id="start-btn">BEGIN MISSION</button>
</div>
<!-- LEVEL TRANSITION -->
<div id="level-transition">
<h2 id="level-title">MISSION COMPLETE</h2>
<p id="level-desc">Proceeding to next objective...</p>
</div>
<!-- GAME -->
<div id="game-canvas"></div>
<div id="ui" class="hidden">
<h1 class="game-title">SGT. MILLER - 2ND RANGERS</h1>
<div id="hp_bar"><div id="hp_fill"></div></div>
<div id="status">Ready for combat</div>
</div>
<div id="phase" class="hidden">PHASE: APPROACH</div>
<div id="weapon-display" class="hidden">M1 GARAND</div>
<div id="ammo" class="hidden">8 / 80</div>
<div id="level-display" class="hidden">OMAHA BEACH</div>
<div id="blood-overlay"></div>
<div id="scope-overlay"></div>
<div id="crosshair" class="normal"></div>
<div id="debug"></div>
<!-- MOBILE CONTROLS -->
<div id="mobile-controls" class="hidden">
<div id="joystick-zone">
<div id="joystick-base"></div>
<div id="joystick-thumb"></div>
</div>
<div id="look-zone"></div>
<div id="buttons-zone">
<div id="fire-btn" class="game-btn">FIRE</div>
<div class="btn-row">
<div id="scope-btn" class="game-btn">SCOPE</div>
<div id="action-btn" class="game-btn">DROP<br>RAMP</div>
<div id="reload-btn" class="game-btn">RELOAD</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Debug function
function debug(msg) {
const d = document.getElementById('debug');
d.innerText = msg;
d.style.display = 'block';
console.log(msg);
}
// WEAPON DEFINITIONS - ALL SCOPED NOW
const WEAPONS = {
garand: { name: 'BAR(very op)', magSize: 70000, totalAmmo: 7000000000000000000000, damage: 500, fireRate: 10, auto: true, scoped: false, scopeZoom: 2.0, reloadTime: 50 },
carbine: { name: 'M1 CARBINE', magSize: 15, totalAmmo: 1200, damage: 22, fireRate: 180, auto: false, scoped: true, scopeZoom: 1.8, reloadTime: 1800 },
springfield: { name: 'M1903 SPRINGFIELD', magSize: 5, totalAmmo: 400, damage: 100, fireRate: 1200, auto: false, scoped: true, scopeZoom: 4.0, reloadTime: 3000 },
thompson: { name: 'M1 THOMPSON', magSize: 3000, totalAmmo: 18000, damage: 18, fireRate: 80, auto: true, scoped: true, scopeZoom: 1.4, reloadTime: 2500 }
};
// LEVEL DEFINITIONS
const LEVELS = {
beach: { name: 'OMAHA BEACH', subtitle: 'D-DAY LANDING', fog: 0x6699bb, fogNear: 30, fogFar: 180 },
village: { name: 'NEUVILLE', subtitle: 'SNIPER ALLEY', fog: 0x889988, fogNear: 40, fogFar: 150 },
forest: { name: 'ARDENNES FOREST', subtitle: 'MG NEST ASSAULT', fog: 0x445544, fogNear: 20, fogFar: 100 },
city: { name: 'RAMELLE', subtitle: 'FINAL STAND', fog: 0x777777, fogNear: 50, fogFar: 200 }
};
// GAME STATE
let selectedWeapon = 'garand';
let currentLevel = 'beach';
let gameStarted = false;
let isLevelLoading = false; // Prevents crash on level switch
// THREE.JS
let scene, camera, renderer, clock;
let boats = [], ocean, player;
let allies = [], enemies = [], particles = [], buildings = [];
// Player state
let playerHP = 3000; // Boosted HP for survival
let maxHP = 3000;
let ammo, totalAmmo, magSize;
let lastShotTime = 0;
let isReloading = false;
let isScoped = false;
let shake = 0;
let phase = 'SAILING';
let boatSpeed = 0.3;
// Camera
let mouseX = 0, mouseY = 0;
let baseFOV = 75;
// Movement
let moveX = 0, moveZ = 0;
// Touch
let joystickTouch = null;
let lookTouch = null;
let joystickCenter = { x: 0, y: 0 };
let lastLookX = 0, lastLookY = 0;
// Auto fire
let isFiring = false;
let fireInterval = null;
// ===================== MENU =====================
document.querySelectorAll('.weapon-option').forEach(opt => {
opt.addEventListener('click', function() {
document.querySelectorAll('.weapon-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
selectedWeapon = this.dataset.weapon;
});
opt.addEventListener('touchstart', function(e) {
e.preventDefault();
document.querySelectorAll('.weapon-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
selectedWeapon = this.dataset.weapon;
}, { passive: false });
});
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('start-btn').addEventListener('touchstart', function(e) {
e.preventDefault();
startGame();
}, { passive: false });
function startGame() {
gameStarted = true;
document.getElementById('start-menu').classList.add('hidden');
// Setup weapon
const weapon = WEAPONS[selectedWeapon];
magSize = weapon.magSize;
ammo = magSize;
totalAmmo = weapon.totalAmmo;
document.getElementById('weapon-display').innerText = weapon.name;
// Show UI
document.getElementById('ui').classList.remove('hidden');
document.getElementById('phase').classList.remove('hidden');
document.getElementById('weapon-display').classList.remove('hidden');
document.getElementById('ammo').classList.remove('hidden');
document.getElementById('level-display').classList.remove('hidden');
document.getElementById('mobile-controls').classList.remove('hidden');
updateUI();
initGame();
}
// ===================== MOBILE CONTROLS =====================
const joystickZone = document.getElementById('joystick-zone');
const joystickThumb = document.getElementById('joystick-thumb');
const lookZone = document.getElementById('look-zone');
const fireBtn = document.getElementById('fire-btn');
const reloadBtn = document.getElementById('reload-btn');
const actionBtn = document.getElementById('action-btn');
const scopeBtn = document.getElementById('scope-btn');
// JOYSTICK
joystickZone.addEventListener('touchstart', function(e) {
e.preventDefault();
const touch = e.changedTouches[0];
joystickTouch = touch.identifier;
const rect = joystickZone.getBoundingClientRect();
joystickCenter.x = rect.left + rect.width / 2;
joystickCenter.y = rect.top + rect.height / 2;
handleJoystickMove(touch);
}, { passive: false });
joystickZone.addEventListener('touchmove', function(e) {
e.preventDefault();
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === joystickTouch) {
handleJoystickMove(e.changedTouches[i]);
}
}
}, { passive: false });
joystickZone.addEventListener('touchend', handleJoystickEnd, { passive: false });
joystickZone.addEventListener('touchcancel', handleJoystickEnd, { passive: false });
function handleJoystickEnd(e) {
e.preventDefault();
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === joystickTouch) {
joystickTouch = null;
moveX = 0; moveZ = 0;
joystickThumb.style.left = '37.5px';
joystickThumb.style.top = '37.5px';
}
}
}
function handleJoystickMove(touch) {
let dx = touch.clientX - joystickCenter.x;
let dy = touch.clientY - joystickCenter.y;
const maxDist = 37;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > maxDist) {
dx = (dx / dist) * maxDist;
dy = (dy / dist) * maxDist;
}
moveX = dx / maxDist;
moveZ = dy / maxDist;
joystickThumb.style.left = (37.5 + dx) + 'px';
joystickThumb.style.top = (37.5 + dy) + 'px';
}
// LOOK ZONE - FIXED VERTICAL INVERSION
lookZone.addEventListener('touchstart', function(e) {
e.preventDefault();
const touch = e.changedTouches[0];
lookTouch = touch.identifier;
lastLookX = touch.clientX;
lastLookY = touch.clientY;
}, { passive: false });
lookZone.addEventListener('touchmove', function(e) {
e.preventDefault();
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === lookTouch) {
const touch = e.changedTouches[i];
const dx = touch.clientX - lastLookX;
const dy = touch.clientY - lastLookY;
mouseX -= dx * 0.0035;
mouseY -= dy * 0.0035;
mouseY = Math.max(-1.3, Math.min(1.3, mouseY));
lastLookX = touch.clientX;
lastLookY = touch.clientY;
}
}
}, { passive: false });
lookZone.addEventListener('touchend', function(e) {
e.preventDefault();
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === lookTouch) {
lookTouch = null;
}
}
}, { passive: false });
// FIRE BUTTON
fireBtn.addEventListener('touchstart', function(e) {
e.preventDefault(); e.stopPropagation();
fireBtn.classList.add('pressed');
const weapon = WEAPONS[selectedWeapon];
if (weapon.auto) {
isFiring = true;
shoot();
fireInterval = setInterval(shoot, weapon.fireRate);
} else {
shoot();
}
}, { passive: false });
fireBtn.addEventListener('touchend', function(e) {
e.preventDefault();
fireBtn.classList.remove('pressed');
isFiring = false;
if (fireInterval) {
clearInterval(fireInterval);
fireInterval = null;
}
}, { passive: false });
// RELOAD BUTTON
reloadBtn.addEventListener('touchstart', function(e) {
e.preventDefault(); e.stopPropagation();
reload();
}, { passive: false });
// ACTION BUTTON
actionBtn.addEventListener('touchstart', function(e) {
e.preventDefault(); e.stopPropagation();
doAction();
}, { passive: false });
// SCOPE BUTTON
scopeBtn.addEventListener('touchstart', function(e) {
e.preventDefault(); e.stopPropagation();
toggleScope();
}, { passive: false });
// ===================== GAME FUNCTIONS =====================
function shoot() {
if (isReloading) return;
if (phase === 'SAILING') return;
const weapon = WEAPONS[selectedWeapon];
const now = Date.now();
if (now - lastShotTime < weapon.fireRate) return;
if (ammo <= 0) {
document.getElementById('status').innerText = 'RELOAD!';
return;
}
lastShotTime = now;
ammo--;
shake = isScoped ? 0.05 : 0.12;
updateUI();
// Muzzle flash
const flash = new THREE.PointLight(0xffaa00, 3, 10);
flash.position.copy(camera.position);
const dir = new THREE.Vector3();
camera.getWorldDirection(dir);
flash.position.add(dir.multiplyScalar(1));
scene.add(flash);
setTimeout(() => scene.remove(flash), 40);
// Raycast
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const allObjects = [...enemies.map(e => e.group)];
const hits = raycaster.intersectObjects(scene.children, true);
for (let i = 0; i < hits.length; i++) {
const hit = hits[i];
// Check enemies
for (let j = enemies.length - 1; j >= 0; j--) {
let obj = hit.object;
while (obj) {
if (obj === enemies[j].group) {
enemies[j].hp -= weapon.damage;
createBlood(hit.point);
if (enemies[j].hp <= 0) {
scene.remove(enemies[j].group);
enemies.splice(j, 1);
document.getElementById('status').innerText = 'ENEMY DOWN! ' + enemies.length + ' remaining';
checkLevelComplete();
}
return;
}
obj = obj.parent;
}
}
// Hit something else - spark
if (hit.distance < 100) {
const spark = new THREE.PointLight(0xffff00, 1, 2);
spark.position.copy(hit.point);
scene.add(spark);
setTimeout(() => scene.remove(spark), 50);
return;
}
}
}
function reload() {
if (isReloading) return;
if (totalAmmo <= 0) {
document.getElementById('status').innerText = 'NO AMMO!';
return;
}
if (ammo >= magSize) return;
isReloading = true;
document.getElementById('status').innerText = 'Reloading...';
// Unscope on reload
if(isScoped) toggleScope();
const weapon = WEAPONS[selectedWeapon];
setTimeout(function() {
const needed = magSize - ammo;
const amount = Math.min(needed, totalAmmo);
ammo += amount;
totalAmmo -= amount;
isReloading = false;
updateUI();
document.getElementById('status').innerText = 'Ready';
}, weapon.reloadTime);
}
function doAction() {
if (currentLevel === 'beach') {
if (phase === 'SAILING' && boats.length > 0 && boats[0].position.z <= -75) {
dropRamp();
}
}
}
function toggleScope() {
const weapon = WEAPONS[selectedWeapon];
isScoped = !isScoped;
const crosshair = document.getElementById('crosshair');
const scopeOverlay = document.getElementById('scope-overlay');
const scopeBtnEl = document.getElementById('scope-btn');
if (isScoped) {
crosshair.className = 'scoped';
scopeOverlay.classList.add('active');
scopeBtnEl.classList.add('active');
camera.fov = baseFOV / weapon.scopeZoom;
} else {
crosshair.className = 'normal';
scopeOverlay.classList.remove('active');
scopeBtnEl.classList.remove('active');
camera.fov = baseFOV;
}
camera.updateProjectionMatrix();
}
function dropRamp() {
if (phase !== 'SAILING') return;
phase = 'LANDING';
document.getElementById('phase').innerText = 'PHASE: LANDING';
document.getElementById('status').innerText = 'GO GO GO!';
actionBtn.classList.remove('ready');
actionBtn.innerHTML = '---';
boats.forEach((boat, boatIdx) => {
const ramp = boat.userData.ramp;
let angle = 0;
const interval = setInterval(function() {
angle += 0.1;
ramp.rotation.x = -angle;
// Ramp extends OUT further to bridge gap
ramp.position.z += 0.25;
ramp.position.y -= 0.12;
if (angle >= Math.PI / 1.5) { // Lower ramp
clearInterval(interval);
// Move allies out
allies.forEach(function(ally, allyIdx) {
if (ally.parent === boat) {
setTimeout(function() {
const pos = new THREE.Vector3();
ally.getWorldPosition(pos);
boat.remove(ally);
scene.add(ally);
ally.position.copy(pos);
ally.position.z -= 4 + Math.random() * 5;
ally.position.x += (Math.random() - 0.5) * 6;
ally.position.y = 1;
ally.userData.offBoat = true;
}, 200 + (allyIdx + boatIdx * 10) * 150);
}
});
}
}, 30);
});
phase = 'COMBAT';
document.getElementById('phase').innerText = 'PHASE: COMBAT';
// Move player out
const worldPos = new THREE.Vector3();
player.getWorldPosition(worldPos);
boats[0].remove(player);
scene.add(player);
player.position.copy(worldPos);
player.position.z -= 6;
player.position.y = 1.7;
}
function takeDamage(amount) {
playerHP -= amount;
if (playerHP < 0) playerHP = 0;
document.getElementById('blood-overlay').classList.add('hit');
setTimeout(function() {
document.getElementById('blood-overlay').classList.remove('hit');
}, 150);
updateUI();
if (playerHP <= 0) {
document.getElementById('status').innerText = 'KIA - MISSION FAILED';
document.getElementById('phase').innerText = 'GAME OVER';
setTimeout(function() { location.reload(); }, 2500);
}
}
function createBlood(pos) {
const geo = new THREE.SphereGeometry(0.08);
const mat = new THREE.MeshBasicMaterial({ color: 0xaa0000 });
for (let i = 0; i < 6; i++) {
const p = new THREE.Mesh(geo, mat);
p.position.copy(pos);
p.userData.vel = new THREE.Vector3(
(Math.random() - 0.5) * 0.25,
Math.random() * 0.35,
(Math.random() - 0.5) * 0.25
);
p.userData.life = 1.2;
particles.push(p);
scene.add(p);
}
}
function updateUI() {
document.getElementById('hp_fill').style.width = (playerHP / maxHP * 100) + '%';
document.getElementById('ammo').innerText = ammo + ' / ' + totalAmmo;
}
// CRITICAL FIX: isLevelLoading flag to prevent infinite loops/freeze
function checkLevelComplete() {
if (enemies.length === 0 && !isLevelLoading) {
isLevelLoading = true;
if (currentLevel === 'beach') {
setTimeout(() => loadLevel('village'), 2000);
} else if (currentLevel === 'village') {
setTimeout(() => loadLevel('forest'), 2000);
} else if (currentLevel === 'forest') {
setTimeout(() => loadLevel('city'), 2000);
} else if (currentLevel === 'city') {
document.getElementById('status').innerText = 'MISSION COMPLETE! RYAN SAVED!';
document.getElementById('phase').innerText = 'VICTORY!';
}
}
}
function showLevelTransition(levelKey, callback) {
const level = LEVELS[levelKey];
document.getElementById('level-title').innerText = level.name;
document.getElementById('level-desc').innerText = level.subtitle;
document.getElementById('level-transition').classList.add('show');
// Reset scoped view on transition
if(isScoped) toggleScope();
setTimeout(function() {
document.getElementById('level-transition').classList.remove('show');
if (callback) callback();
}, 2500);
}
function loadLevel(levelKey) {
showLevelTransition(levelKey, function() {
currentLevel = levelKey;
clearLevel();
const level = LEVELS[levelKey];
scene.fog = new THREE.Fog(level.fog, level.fogNear, level.fogFar);
scene.background = new THREE.Color(level.fog);
document.getElementById('level-display').innerText = level.name;
document.getElementById('phase').innerText = 'PHASE: COMBAT';
phase = 'COMBAT';
// Reset player position
player.position.set(0, 1.7, 0);
mouseX = 0; mouseY = 0;
// Refill some ammo and health
const weapon = WEAPONS[selectedWeapon];
totalAmmo = Math.min(totalAmmo + weapon.magSize * 3, weapon.totalAmmo);
playerHP = maxHP; // Full heal
updateUI();
if (levelKey === 'village') {
createVillage();
} else if (levelKey === 'forest') {
createForest();
} else if (levelKey === 'city') {
createCity();
}
isLevelLoading = false; // Reset lock
});
}
function clearLevel() {
// Remove enemies
enemies.forEach(e => scene.remove(e.group));
enemies = [];
// Remove buildings
buildings.forEach(b => scene.remove(b));
buildings = [];
// Remove boats and ocean
boats.forEach(b => scene.remove(b));
boats = [];
if (ocean) {
scene.remove(ocean);
ocean = null;
}
// Keep player and allies
}
// ===================== LEVEL CREATION =====================
function initGame() {
scene = new THREE.Scene();
const level = LEVELS[currentLevel];
scene.background = new THREE.Color(level.fog);
scene.fog = new THREE.Fog(level.fog, level.fogNear, level.fogFar);
camera = new THREE.PerspectiveCamera(baseFOV, window.innerWidth / window.innerHeight, 0.1, 1000);
clock = new THREE.Clock();
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('game-canvas').appendChild(renderer.domElement);
// Lights
scene.add(new THREE.AmbientLight(0x555555));
const sun = new THREE.DirectionalLight(0xffffee, 0.8);
sun.position.set(50, 100, 30);
scene.add(sun);
createBeachLevel();
animate();
}
function createBeachLevel() {
// Ocean - LARGER
const oceanGeo = new THREE.PlaneGeometry(800, 800, 32, 32);
const oceanMat = new THREE.MeshStandardMaterial({ color: 0x1e4a6e });
ocean = new THREE.Mesh(oceanGeo, oceanMat);
ocean.rotation.x = -Math.PI / 2;
ocean.position.y = -0.5;
scene.add(ocean);
// Beach - LARGER
const beachGeo = new THREE.PlaneGeometry(400, 180);
const beachMat = new THREE.MeshStandardMaterial({ color: 0xc4a572 });
const beach = new THREE.Mesh(beachGeo, beachMat);
beach.rotation.x = -Math.PI / 2;
// Beach starts at -160, extends back
beach.position.set(0, 0.1, -160);
scene.add(beach);
buildings.push(beach);
// Beach obstacles (hedgehogs) - MORE
for (let i = 0; i < 60; i++) {
const obs = createHedgehog();
obs.position.set((Math.random() - 0.5) * 150, 0, -130 + Math.random() * 40);
scene.add(obs);
buildings.push(obs);
}
// Create MULTIPLE BOATS
createMultipleBoats();
// Create player
createPlayer();
// Create MORE allies
createAllies();
// Create WAY MORE enemies
createBeachEnemies();
document.getElementById('level-display').innerText = 'OMAHA BEACH';
}
function createMultipleBoats() {
boats = [];
for (let i = 0; i < 5; i++) { // 5 boats total
const boat = createBoat();
boat.position.set((i - 2) * 25, 0, 100); // Spread out horizontally
boat.userData.ramp = boat.children.find(c => c.geometry.parameters.width === 7);
scene.add(boat);
boats.push(boat);
}
}
function createHedgehog() {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: 0x333333 });
for (let i = 0; i < 3; i++) {
const beam = new THREE.Mesh(new THREE.BoxGeometry(0.3, 3, 0.3), mat);
beam.rotation.z = (i / 3) * Math.PI;
beam.rotation.x = 0.5;
group.add(beam);
}
return group;
}
function createBoat() {
const boat = new THREE.Group();
const hullMat = new THREE.MeshStandardMaterial({ color: 0x556b2f });
const hull = new THREE.Mesh(new THREE.BoxGeometry(7, 2.5, 12), hullMat);
hull.position.y = 0.5;
boat.add(hull);
const sideMat = new THREE.MeshStandardMaterial({ color: 0x3d4f2a });
const leftSide = new THREE.Mesh(new THREE.BoxGeometry(0.4, 2, 11), sideMat);
leftSide.position.set(-3.5, 1.8, 0);
boat.add(leftSide);
const rightSide = new THREE.Mesh(new THREE.BoxGeometry(0.4, 2, 11), sideMat);
rightSide.position.set(3.5, 1.8, 0);
boat.add(rightSide);
const ramp = new THREE.Mesh(new THREE.BoxGeometry(7, 2.5, 0.6), new THREE.MeshStandardMaterial({ color: 0x4a5f3a }));
ramp.position.set(0, 1.2, 6);
boat.add(ramp);
return boat;
}
function createPlayer() {
player = new THREE.Group();
player.position.set(0, 2.2, 3);
const body = new THREE.Mesh(
new THREE.BoxGeometry(0.6, 1.6, 0.5),
new THREE.MeshStandardMaterial({ color: 0x4a6b3a })
);
body.position.y = -0.8;
body.visible = false; // First person
player.add(body);
boats[0].add(player); // Player in first boat
}
function createAllies() {
allies = [];
const allyCount = 25; // MORE allies
for (let i = 0; i < allyCount; i++) {
const ally = new THREE.Mesh(
new THREE.BoxGeometry(0.6, 1.6, 0.5),
new THREE.MeshStandardMaterial({ color: 0x4a7c59 })
);
const boatIdx = Math.floor(i / 5); // 5 per boat
const boatPos = i % 5;
const row = Math.floor(boatPos / 2);
const col = boatPos % 2;
ally.position.set((col - 0.5) * 2.5, 1.5, 1 - row * 2);
const helmet = new THREE.Mesh(
new THREE.SphereGeometry(0.32, 8, 8),
new THREE.MeshStandardMaterial({ color: 0x3a4a2a })
);
helmet.scale.y = 0.7;
helmet.position.y = 1.05;
ally.add(helmet);
ally.userData = { hp: 50, offBoat: false, lastShot: 0 };
boats[boatIdx % boats.length].add(ally);
allies.push(ally);
}
}
function createBeachEnemies() {
const enemyCount = 35; // WAY MORE enemies
for (let i = 0; i < enemyCount; i++) {
// Multiple bunker types + individual soldiers
if (i < 12) {
// Heavy bunkers
const bunker = createHeavyBunker();
bunker.position.set((i - 5.5) * 18, 0, -175 + (i % 3) * 8);
enemies.push({ group: bunker, hp: 180, lastShot: 0, type: 'heavy_bunker', damage: 18 });
scene.add(bunker);
} else if (i < 22) {
// Regular bunkers
const bunker = createBunker();
bunker.position.set((i - 12) * 14 + Math.sin(i) * 8, 0, -168);
enemies.push({ group: bunker, hp: 120, lastShot: 0, type: 'bunker', damage: 14 });
scene.add(bunker);
} else {
// Patrol soldiers on beach
const soldier = createBeachSoldier();
soldier.position.set((i - 22) * 8 + Math.random() * 20, 1.2, -145 + Math.random() * 25);
soldier.lookAt(0, 1, 0);
enemies.push({ group: soldier, hp: 45, lastShot: 0, type: 'soldier', damage: 10 });
scene.add(soldier);
}
}
}
function createHeavyBunker() {
const group = new THREE.Group();
// Larger base
const base = new THREE.Mesh(new THREE.BoxGeometry(12, 6, 8), new THREE.MeshStandardMaterial({ color: 0x444444 }));
base.position.y = 3;
group.add(base);
// Thicker walls
const walls = new THREE.Mesh(new THREE.BoxGeometry(12, 4, 2), new THREE.MeshStandardMaterial({ color: 0x333333 }));
walls.position.set(0, 5, 3.5);
group.add(walls);
// Multiple MGs
for (let i = 0; i < 2; i++) {
const mg = createMG42();
mg.position.set(i * 4 - 2, 5.5, 4.5);
group.add(mg);
}
// Gunner
const gunner = createEnemySoldier();
gunner.position.set(0, 5.8, 3);
group.add(gunner);
return group;
}
function createBunker() {
const group = new THREE.Group();
const base = new THREE.Mesh(new THREE.BoxGeometry(10, 5, 7), new THREE.MeshStandardMaterial({ color: 0x555555 }));
base.position.y = 2.5;
group.add(base);
const opening = new THREE.Mesh(new THREE.BoxGeometry(3, 1.5, 2), new THREE.MeshStandardMaterial({ color: 0x222222 }));
opening.position.set(0, 4, 3);
group.add(opening);
const enemy = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.5, 0.5), new THREE.MeshStandardMaterial({ color: 0x444433 }));
enemy.position.set(0, 4.8, 2.5);
group.add(enemy);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.38, 8, 8), new THREE.MeshStandardMaterial({ color: 0x2a2a2a }));
helmet.scale.set(1, 0.65, 1);
helmet.position.set(0, 5.7, 2.5);
group.add(helmet);
const mg = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 1.8), new THREE.MeshStandardMaterial({ color: 0x111111 }));
mg.rotation.z = Math.PI / 2;
mg.position.set(0, 4.5, 3.8);
group.add(mg);
return group;
}
function createBeachSoldier() {
const group = createSoldierEnemy();
// Beach uniform
group.children[0].material.color.setHex(0x665544);
return group;
}
function createSoldierEnemy() {
const group = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.6, 0.5), new THREE.MeshStandardMaterial({ color: 0x444433 }));
body.position.y = 0.8;
group.add(body);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.35, 8, 8), new THREE.MeshStandardMaterial({ color: 0x2a2a2a }));
helmet.scale.y = 0.65;
helmet.position.y = 1.9;
group.add(helmet);
const gun = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.1, 0.8), new THREE.MeshStandardMaterial({ color: 0x222222 }));
gun.position.set(0.4, 0.7, 0.3);
group.add(gun);
return group;
}
function createEnemySoldier() {
const group = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.2, 0.5), new THREE.MeshStandardMaterial({ color: 0x444433 }));
body.position.y = 0.6;
group.add(body);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.35, 8, 8), new THREE.MeshStandardMaterial({ color: 0x2a2a2a }));
helmet.scale.y = 0.65;
helmet.position.y = 1.4;
group.add(helmet);
return group;
}
function createMG42() {
const group = new THREE.Group();
const barrel = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 2.2), new THREE.MeshStandardMaterial({ color: 0x111111 }));
barrel.rotation.z = Math.PI / 2;
barrel.position.y = 0.5;
group.add(barrel);
const body = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.4, 1.2), new THREE.MeshStandardMaterial({ color: 0x222222 }));
body.position.set(0, 0.2, 0.3);
group.add(body);
return group;
}
function createVillage() {
const ground = new THREE.Mesh(new THREE.PlaneGeometry(250, 250), new THREE.MeshStandardMaterial({ color: 0x665544 }));
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
buildings.push(ground);
// MORE buildings with cover
for (let i = 0; i < 25; i++) {
const building = createBuilding();
const side = i % 2 === 0 ? -1 : 1;
building.position.set(side * (15 + Math.random() * 12), 0, -90 + i * 12);
scene.add(building);
buildings.push(building);
}
// Road
const road = new THREE.Mesh(new THREE.PlaneGeometry(12, 250), new THREE.MeshStandardMaterial({ color: 0x444444 }));
road.rotation.x = -Math.PI / 2;
road.position.y = 0.05;
scene.add(road);
buildings.push(road);
// MORE enemies
for (let i = 0; i < 18; i++) {
const soldier = createSoldierEnemy();
soldier.position.set((Math.random() - 0.5) * 50, 1, -70 + i * 10);
enemies.push({ group: soldier, hp: 50, lastShot: 0, type: 'soldier', damage: 10 });
scene.add(soldier);
}
// Sniper
const sniperBuilding = buildings[8];
const sniper = createSoldierEnemy();
sniper.position.copy(sniperBuilding.position);
sniper.position.y = 10;
enemies.push({ group: sniper, hp: 45, lastShot: 0, type: 'sniper', damage: 45 });
scene.add(sniper);
// MORE allies
for (let i = 0; i < 12; i++) {
const ally = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.6, 0.5), new THREE.MeshStandardMaterial({ color: 0x4a7c59 }));
ally.position.set(-8 + i * 1.8, 1, 15);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.32, 8, 8), new THREE.MeshStandardMaterial({ color: 0x3a4a2a }));
helmet.scale.y = 0.7;
helmet.position.y = 1.05;
ally.add(helmet);
ally.userData = { hp: 50, offBoat: true, lastShot: 0 };
allies.push(ally);
scene.add(ally);
}
}
function createBuilding() {
const group = new THREE.Group();
const height = 8 + Math.random() * 8;
const width = 10 + Math.random() * 6;
const depth = 10 + Math.random() * 6;
const building = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth), new THREE.MeshStandardMaterial({ color: 0x887766 }));
building.position.y = height / 2;
group.add(building);
const roof = new THREE.Mesh(new THREE.ConeGeometry(width * 0.8, 4, 4), new THREE.MeshStandardMaterial({ color: 0x553322 }));
roof.position.y = height + 2;
roof.rotation.y = Math.PI / 4;
group.add(roof);
for (let y = 0; y < 3; y++) {
for (let x = -1; x <= 1; x += 2) {
const window = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.8, 0.2), new THREE.MeshStandardMaterial({ color: 0x333344 }));
window.position.set(x * 3, 3 + y * 3.5, depth / 2 + 0.1);
group.add(window);
}
}
return group;
}
function createForest() {
const ground = new THREE.Mesh(new THREE.PlaneGeometry(350, 350), new THREE.MeshStandardMaterial({ color: 0x334422 }));
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
buildings.push(ground);
// MORE trees for cover
for (let i = 0; i < 120; i++) {
const tree = createTree();
tree.position.set((Math.random() - 0.5) * 150, 0, -120 + Math.random() * 200);
scene.add(tree);
buildings.push(tree);
}
// MORE MG nests
for (let i = 0; i < 8; i++) {
const nest = createMGNest();
nest.group.position.set((Math.random() - 0.5) * 60, 0, -70 + i * 20);
enemies.push(nest);
scene.add(nest.group);
}
// MORE soldiers
for (let i = 0; i < 25; i++) {
const soldier = createSoldierEnemy();
soldier.position.set((Math.random() - 0.5) * 80, 1, -60 + Math.random() * 120);
enemies.push({ group: soldier, hp: 50, lastShot: 0, type: 'soldier', damage: 10 });
scene.add(soldier);
}
}
function createTree() {
const group = new THREE.Group();
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.6, 5), new THREE.MeshStandardMaterial({ color: 0x4a3020 }));
trunk.position.y = 2.5;
group.add(trunk);
const leaves = new THREE.Mesh(new THREE.ConeGeometry(4, 10, 8), new THREE.MeshStandardMaterial({ color: 0x2a4a20 }));
leaves.position.y = 8.5;
group.add(leaves);
return group;
}
function createMGNest() {
const group = new THREE.Group();
for (let i = 0; i < 8; i++) {
const sandbag = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.6, 1), new THREE.MeshStandardMaterial({ color: 0x8b7355 }));
const angle = (i / 8) * Math.PI * 2;
sandbag.position.set(Math.cos(angle) * 2.5, 0.3 + (i % 2) * 0.4, Math.sin(angle) * 2.5);
sandbag.rotation.y = angle;
group.add(sandbag);
}
const gunner = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.2, 0.5), new THREE.MeshStandardMaterial({ color: 0x444433 }));
gunner.position.y = 1;
group.add(gunner);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.35, 8, 8), new THREE.MeshStandardMaterial({ color: 0x2a2a2a }));
helmet.scale.y = 0.65;
helmet.position.y = 1.8;
group.add(helmet);
const mg = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1.5), new THREE.MeshStandardMaterial({ color: 0x111111 }));
mg.rotation.x = Math.PI / 2;
mg.position.set(0, 1.2, 2);
group.add(mg);
return { group, hp: 90, lastShot: 0, type: 'mg_nest', damage: 10, fireRate: 0.12 };
}
function createCity() {
const ground = new THREE.Mesh(new THREE.PlaneGeometry(450, 450), new THREE.MeshStandardMaterial({ color: 0x555555 }));
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
buildings.push(ground);
// TONS of ruined buildings for cover
for (let i = 0; i < 35; i++) {
const building = createRuinedBuilding();
building.position.set((Math.random() - 0.5) * 200, 0, -180 + Math.random() * 280);
scene.add(building);
buildings.push(building);
}
// TONS of enemies
for (let i = 0; i < 40; i++) {
const soldier = createSoldierEnemy();
soldier.position.set((Math.random() - 0.5) * 120, 1, -100 + Math.random() * 180);
enemies.push({ group: soldier, hp: 50, lastShot: 0, type: 'soldier', damage: 10 });
scene.add(soldier);
}
// Tank
const tank = createTank();
tank.position.set(0, 0, -120);
enemies.push({ group: tank, hp: 350, lastShot: 0, type: 'tank', damage: 55 });
scene.add(tank);
// LOTS of allies
for (let i = 0; i < 25; i++) {
const ally = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.6, 0.5), new THREE.MeshStandardMaterial({ color: 0x4a7c59 }));
ally.position.set((Math.random() - 0.5) * 40, 1, 10 + Math.random() * 30);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.32, 8, 8), new THREE.MeshStandardMaterial({ color: 0x3a4a2a }));
helmet.scale.y = 0.7;
helmet.position.y = 1.05;
ally.add(helmet);
ally.userData = { hp: 50, offBoat: true, lastShot: 0 };
allies.push(ally);
scene.add(ally);
}
}
function createRuinedBuilding() {
const group = new THREE.Group();
const height = 6 + Math.random() * 12;
const width = 8 + Math.random() * 10;
const wall1 = new THREE.Mesh(new THREE.BoxGeometry(width, height, 0.6), new THREE.MeshStandardMaterial({ color: 0x666655 }));
wall1.position.set(0, height / 2, width / 2);
group.add(wall1);
if (Math.random() > 0.2) {
const wall2 = new THREE.Mesh(new THREE.BoxGeometry(0.6, height * 0.8, width * 0.7), new THREE.MeshStandardMaterial({ color: 0x666655 }));
wall2.position.set(width / 2, height * 0.4, 0);
group.add(wall2);
}
for (let i = 0; i < 8; i++) {
const rubble = new THREE.Mesh(new THREE.BoxGeometry(1.5 + Math.random(), 0.8 + Math.random(), 1.5 + Math.random()), new THREE.MeshStandardMaterial({ color: 0x555544 }));
rubble.position.set((Math.random() - 0.5) * width, 0.5, (Math.random() - 0.5) * width);
rubble.rotation.y = Math.random() * Math.PI;
group.add(rubble);
}
return group;
}
function createTank() {
const group = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(5, 2.2, 7), new THREE.MeshStandardMaterial({ color: 0x3a3a2a }));
body.position.y = 1.6;
group.add(body);
const turret = new THREE.Mesh(new THREE.BoxGeometry(3, 1.4, 3.5), new THREE.MeshStandardMaterial({ color: 0x3a3a2a }));
turret.position.y = 3.3;
group.add(turret);
const cannon = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.25, 5), new THREE.MeshStandardMaterial({ color: 0x222222 }));
cannon.rotation.x = Math.PI / 2;
cannon.position.set(0, 3.5, 4.2);
group.add(cannon);
const trackMat = new THREE.MeshStandardMaterial({ color: 0x222222 });
const leftTrack = new THREE.Mesh(new THREE.BoxGeometry(1, 1.4, 7.5), trackMat);
leftTrack.position.set(-2.5, 0.7, 0);
group.add(leftTrack);
const rightTrack = new THREE.Mesh(new THREE.BoxGeometry(1, 1.4, 7.5), trackMat);
rightTrack.position.set(2.5, 0.7, 0);
group.add(rightTrack);
const cross = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0.1), new THREE.MeshBasicMaterial({ color: 0xffffff }));
cross.position.set(0, 2.2, 3.6);
group.add(cross);
return group;
}
// ===================== GAME LOOP =====================
function animate() {
requestAnimationFrame(animate);
if (!gameStarted) return;
const dt = clock.getDelta();
const time = clock.elapsedTime;
// === SAILING PHASE ===
if (phase === 'SAILING' && boats.length > 0) {
boats.forEach(boat => {
boat.position.z -= boatSpeed;
boat.position.y = Math.sin(time * 0.6 + boat.position.x * 0.01) * 0.4;
boat.rotation.z = Math.sin(time * 0.8 + boat.position.x * 0.01) * 0.04;
boat.rotation.x = Math.sin(time * 0.5) * 0.02;
});
// Status updates
if (boats[0].position.z > -40) {
document.getElementById('status').innerText = 'Approaching Omaha Beach...';
} else if (boats[0].position.z > -90) {
document.getElementById('status').innerText = 'Enemy shore defenses spotted!';
} else if (boats[0].position.z > -110) {
document.getElementById('status').innerText = 'INCOMING FIRE! Stay down!';
}
// FIXED STOPPING POINT: -115 is water, not sand (-140 was clipping)
if (boats[0].position.z <= -75) {
boatSpeed = 0;
document.getElementById('status').innerText = 'TAP "DROP RAMP" NOW!';
document.getElementById('phase').innerText = 'READY TO LAND';
actionBtn.classList.add('ready');
}
// First person camera
camera.position.copy(player.position);
const worldPos = new THREE.Vector3();
player.getWorldPosition(worldPos);
camera.position.copy(worldPos);
camera.position.y += 0.4;
camera.rotation.order = 'YXZ';
camera.rotation.y = mouseX;
camera.rotation.x = mouseY;
}
// === COMBAT PHASE ===
if (phase === 'COMBAT' || phase === 'LANDING') {
// Player movement
const speed = 0.12;
const forward = new THREE.Vector3(0, 0, -1);
const right = new THREE.Vector3(1, 0, 0);
forward.applyQuaternion(camera.quaternion);
right.applyQuaternion(camera.quaternion);
forward.y = 0; right.y = 0;
forward.normalize(); right.normalize();
player.position.add(forward.clone().multiplyScalar(-moveZ * speed));
player.position.add(right.clone().multiplyScalar(moveX * speed));
player.position.y = 1.7;
// Camera follows player
camera.position.copy(player.position);
camera.position.y += 0.4;
camera.rotation.order = 'YXZ';
camera.rotation.y = mouseX;
camera.rotation.x = mouseY;
// IMPROVED ENEMY AI - Targets ALL allies + player
enemies.forEach(function(enemy) {
const fireRate = enemy.fireRate || 2.2;
if (time - enemy.lastShot < fireRate) return;
// Possible targets: player + living allies
const possibleTargets = [player, ...allies.filter(a => a.userData.hp > 0)];
let bestTarget = null;
let bestDistance = Infinity;
const ePos = new THREE.Vector3();
enemy.group.getWorldPosition(ePos);
ePos.y += 1.1;
for (const target of possibleTargets) {
const tPos = new THREE.Vector3();
if (target === player) {
tPos.copy(player.position);
tPos.y += 0.4;
} else {
target.getWorldPosition(tPos);
tPos.y += 1.0;
}
const toTarget = tPos.clone().sub(ePos);
const distance = toTarget.length();
if (distance > 95) continue;
// Simple line-of-sight check
const ray = new THREE.Raycaster(ePos, toTarget.normalize(), 0, distance);
const hits = ray.intersectObjects(buildings, true);
if (hits.length > 0 && hits[0].distance < distance - 1.5) continue;
if (distance < bestDistance) {
bestDistance = distance;
bestTarget = target;
}
}
if (!bestTarget) return;
enemy.lastShot = time + Math.random() * 0.7;
let hitChance = 0.35;
if (enemy.type === 'sniper') hitChance = 0.65;
if (enemy.type === 'mg_nest') hitChance = 0.28;
if (enemy.type === 'tank' || enemy.type === 'heavy_bunker') hitChance = 0.48;
hitChance *= Math.max(0.25, 1 - bestDistance / 110);
const willHit = Math.random() < hitChance;
const tPos = new THREE.Vector3();
if (bestTarget === player) {
tPos.copy(player.position);
tPos.y += 0.4;
} else {
bestTarget.getWorldPosition(tPos);
tPos.y += 1.0;
}
// Tracer
const color = (bestTarget === player) ? 0xffff00 : 0xff8800;
const tracerGeo = new THREE.BufferGeometry();
tracerGeo.setAttribute('position', new THREE.Float32BufferAttribute([
ePos.x, ePos.y, ePos.z, tPos.x, tPos.y, tPos.z
], 3));
const tracer = new THREE.Line(tracerGeo, new THREE.LineBasicMaterial({ color }));
scene.add(tracer);
setTimeout(() => scene.remove(tracer), 80);
if (willHit) {
if (bestTarget === player) {
takeDamage(enemy.damage || 12);
} else {
bestTarget.userData.hp -= enemy.damage || 10;
if (bestTarget.userData.hp <= 0) {
scene.remove(bestTarget);
allies = allies.filter(a => a !== bestTarget);
}
}
}
});
// IMPROVED ALLIES - Actually damage enemies
allies.forEach(function(ally) {
if (!ally.userData.offBoat || ally.userData.hp <= 0) return;
// Movement
if (Math.random() < 0.01) {
ally.position.z -= 0.7 + Math.random() * 0.5;
}
// Shooting
if (!ally.userData.lastShot) ally.userData.lastShot = 0;
if (time - ally.userData.lastShot > 1.6 + Math.random() * 2.5 && enemies.length > 0) {
const target = enemies[Math.floor(Math.random() * enemies.length)];
const aPos = ally.position.clone();
aPos.y += 1.1;
const tPos = new THREE.Vector3();
target.group.getWorldPosition(tPos);
tPos.y += 1.2 + Math.random() * 0.6;
const distance = aPos.distanceTo(tPos);
let hitChance = 0.11;
hitChance *= Math.max(0.3, 1 - distance / 130);
// Green tracer
const tracerGeo = new THREE.BufferGeometry().setFromPoints([aPos, tPos]);
const tracer = new THREE.Line(tracerGeo, new THREE.LineBasicMaterial({
color: 0x88ff88, transparent: true, opacity: 0.8
}));
scene.add(tracer);
setTimeout(() => scene.remove(tracer), 70);
if (Math.random() < hitChance) {
target.hp -= 30 + Math.random() * 10;
if (target.hp <= 0) {
scene.remove(target.group);
enemies = enemies.filter(e => e !== target);
document.getElementById('status').innerText = 'ENEMY DOWN! ' + enemies.length + ' left';
checkLevelComplete();
}
}
ally.userData.lastShot = time;
}
});
}
// Screen shake
if (shake > 0) {
camera.rotation.z = (Math.random() - 0.5) * shake * 0.15;
shake *= 0.92;
if (shake < 0.01) shake = 0;
}
// Particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.position.add(p.userData.vel);
p.userData.vel.y -= 0.015;
p.userData.life -= dt;
if (p.userData.life <= 0 || p.position.y < 0) {
scene.remove(p);
particles.splice(i, 1);
}
}
// Ocean waves
if (ocean && ocean.geometry) {
const pos = ocean.geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
pos.setY(i, Math.sin(x * 0.04 + time * 0.8) * Math.cos(z * 0.04 + time * 0.6) * 1.2);
}
pos.needsUpdate = true;
}
renderer.render(scene, camera);
}
// Window resize
window.addEventListener('resize', function() {
if (camera && renderer) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
});
// Keyboard support
window.addEventListener('keydown', function(e) {
if (!gameStarted) return;
if (e.key === ' ' && phase === 'SAILING' && boats.length > 0 && boats[0].position.z <= -115) {
e.preventDefault();
dropRamp();
}
if (e.key.toLowerCase() === 'r') reload();
if (e.key === 'w') moveZ = -1;
if (e.key === 's') moveZ = 1;
if (e.key === 'a') moveX = -1;
if (e.key === 'd') moveX = 1;
});
window.addEventListener('keyup', function(e) {
if (e.key === 'w' || e.key === 's') moveZ = 0;
if (e.key === 'a' || e.key === 'd') moveX = 0;
});
// Mouse support
document.addEventListener('mousemove', function(e) {
if (document.pointerLockElement && gameStarted) {
mouseX -= e.movementX * 0.002;
mouseY -= e.movementY * 0.002;
mouseY = Math.max(-1.3, Math.min(1.3, mouseY));
}
});
document.addEventListener('click', function(e) {
if (!gameStarted) return;
if (!document.pointerLockElement && e.target.tagName === 'CANVAS') {
renderer.domElement.requestPointerLock();
} else if (document.pointerLockElement) {
shoot();
}
});
debug('Game ready! - 5 Boats, 35+ Beach Enemies, Tons of Cover!');
</script>
</body>
</html>