Spaces:
Running
Running
| <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> |