<!DOCTYPE html><html lang="en"><head><meta http-equiv="x-dns-prefetch-control" content="off"><meta name="x-poe-allow-downloads" content="true"><script src="https://puc.poecdn.net/authenticated_preview_page/standard.3ef2c256959faf5a756d.js"></script> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Bank Heist Escape</title> <style> body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; font-family: Arial, sans-serif; touch-action: none; background-color: #000; } #gameCanvas { display: block; width: 100%; height: 100%; cursor: crosshair; } #ui-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; } /* Touch screen look controls */ #touch-look-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: auto; z-index: 6; display: none; touch-action: none; } #touch-instructions { position: absolute; top: 60%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 18px; text-align: center; padding: 20px; background-color: rgba(0, 0, 0, 0.7); border-radius: 10px; pointer-events: none; display: none; } #balance { position: absolute; top: 20px; right: 20px; color: #00ff00; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); } #health { position: absolute; top: 60px; right: 20px; color: #ff3333; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); } #notification { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 20px; border-radius: 10px; font-size: 20px; text-align: center; opacity: 0; transition: opacity 0.5s; max-width: 80%; } #position-display { position: absolute; bottom: 20px; left: 20px; color: white; font-size: 16px; background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; pointer-events: none; } #crosshair { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; border: 2px solid white; border-radius: 50%; pointer-events: none; z-index: 20; } #crosshair::after { content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 6px; height: 6px; background-color: white; border-radius: 50%; } #game-over { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.8); color: white; padding: 30px; border-radius: 10px; font-size: 24px; text-align: center; display: none; pointer-events: auto; z-index: 30; } #restart-btn { margin-top: 20px; padding: 10px 20px; font-size: 18px; background-color: #5D5CDE; color: white; border: none; border-radius: 5px; cursor: pointer; } #restart-btn:hover { background-color: #4a49b8; } #steal-money-btn { position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%); padding: 15px 30px; background-color: #00ff00; color: black; font-size: 24px; border: none; border-radius: 10px; cursor: pointer; pointer-events: auto; z-index: 20; display: none; } #escape-helicopter-btn { position: absolute; bottom: 150px; left: 50%; transform: translateX(-50%); padding: 20px 40px; background-color: #ff3333; color: white; font-size: 28px; font-weight: bold; border: none; border-radius: 15px; cursor: pointer; pointer-events: auto; z-index: 25; display: none; animation: pulse 2s infinite; box-shadow: 0 0 20px rgba(255, 0, 0, 0.7); } @keyframes pulse { 0% { transform: translateX(-50%) scale(1); } 50% { transform: translateX(-50%) scale(1.05); } 100% { transform: translateX(-50%) scale(1); } } #loading-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; font-size: 24px; z-index: 40; } .logo { font-size: 48px; font-weight: bold; margin-bottom: 30px; color: #ff3333; text-shadow: 0 0 10px rgba(255, 0, 0, 0.7); } #play-btn, #help-btn, .menu-btn { padding: 15px 50px; font-size: 28px; background-color: #5D5CDE; color: white; border: none; border-radius: 10px; cursor: pointer; margin-top: 15px; transition: all 0.3s ease; pointer-events: auto; } #play-btn:hover, #help-btn:hover, .menu-btn:hover { background-color: #4a49b8; transform: scale(1.05); } .difficulty-buttons { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; } .difficulty-btn { padding: 10px 20px; font-size: 18px; background-color: #444; color: white; border: 2px solid #666; border-radius: 5px; cursor: pointer; transition: all 0.2s ease; } .difficulty-btn:hover { background-color: #555; transform: scale(1.05); } .difficulty-btn.selected { background-color: #5D5CDE; border-color: #8A89FF; box-shadow: 0 0 10px rgba(93, 92, 222, 0.7); } .progress-bar { width: 300px; height: 20px; background-color: #333; border-radius: 10px; margin-top: 20px; overflow: hidden; } .progress { height: 100%; background-color: #5D5CDE; width: 0%; transition: width 0.3s; } #help-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; font-size: 18px; z-index: 45; display: none; } #help-content { width: 80%; max-width: 800px; background-color: rgba(50, 50, 50, 0.8); padding: 30px; border-radius: 10px; text-align: left; max-height: 80vh; overflow-y: auto; } #help-content h2 { color: #00aaff; text-align: center; margin-bottom: 20px; } #help-content ul { margin-left: 20px; margin-bottom: 15px; } #back-btn { margin-top: 20px; } #pause-btn { position: absolute; top: 20px; left: 20px; background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; padding: 5px 15px; font-size: 16px; cursor: pointer; pointer-events: auto; z-index: 25; white-space: nowrap; } #pause-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; justify-content: center; align-items: center; color: white; font-size: 36px; z-index: 35; display: none; } #guide-path { position: absolute; bottom: 20px; right: 20px; background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; padding: 5px 15px; font-size: 16px; cursor: pointer; pointer-events: auto; z-index: 25; } #performance-mode-btn { position: absolute; top: 20px; right: 200px; background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; padding: 5px 15px; font-size: 16px; cursor: pointer; pointer-events: auto; z-index: 25; white-space: nowrap; /* Prevent line breaks */ } #swat-mode-btn { position: absolute; top: 20px; right: 470px; /* Even more space for this button */ background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; padding: 5px 15px; font-size: 16px; cursor: pointer; pointer-events: auto; z-index: 25; white-space: nowrap; /* Prevent line breaks */ } #swat-mode-indicator { position: absolute; top: 100px; right: 20px; color: white; font-size: 16px; font-weight: bold; background-color: rgba(255, 0, 0, 0.7); padding: 10px; border-radius: 5px; pointer-events: none; display: none; } #in-game-help { position: absolute; top: 20px; left: 120px; background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; padding: 5px 15px; font-size: 16px; cursor: pointer; pointer-events: auto; z-index: 25; white-space: nowrap; /* Prevent line breaks */ } /* On-screen touch controls */ #touch-controls { position: absolute; bottom: 20px; left: 0; width: 100%; display: flex; justify-content: space-between; pointer-events: auto; z-index: 25; } /* Movement buttons */ #movement-buttons { display: grid; grid-template-columns: repeat(3, 60px); grid-template-rows: repeat(3, 60px); gap: 5px; margin-left: 20px; } /* Movement button styling */ .movement-btn { width: 60px; height: 60px; border-radius: 50%; background-color: rgba(93, 92, 222, 0.7); color: white; font-size: 24px; border: none; display: flex; justify-content: center; align-items: center; touch-action: manipulation; } /* Center button can be used as a placeholder or for other controls */ #center-btn { visibility: hidden; } /* Right side action buttons */ #action-buttons { display: flex; flex-direction: column; gap: 15px; margin-right: 20px; } /* Action button styling */ .action-btn { width: 70px; height: 70px; border-radius: 50%; background-color: rgba(93, 92, 222, 0.7); color: white; font-size: 20px; border: none; display: flex; justify-content: center; align-items: center; touch-action: manipulation; } /* Shoot button special styling */ #shoot-btn { background-color: rgba(255, 0, 0, 0.7); font-size: 24px; } #key-hints { position: absolute; top: 20px; left: 20px; color: white; font-size: 18px; background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; pointer-events: none; } /* AI Tactics display */ #ai-tactics { position: absolute; top: 100px; left: 20px; color: white; font-size: 14px; background-color: rgba(0, 0, 0, 0.7); padding: 10px; border-radius: 5px; max-width: 300px; pointer-events: none; opacity: 0; transition: opacity 0.5s; } /* SWAT Mode indicator */ #swat-mode-indicator { position: absolute; top: 100px; right: 20px; color: white; font-size: 16px; font-weight: bold; background-color: rgba(255, 0, 0, 0.7); padding: 10px; border-radius: 5px; pointer-events: none; display: none; } .dark { color-scheme: dark; } @media (hover: hover) { #mobile-controls { display: none; } .look-area { display: none; } } </style> <script src="https://puc.poecdn.net/authenticated_preview_page/disableWebRTC.9710cebe07429a9e8e06.js"></script><script src="https://puc.poecdn.net/authenticated_preview_page/miniApp.aeb34a137b051bfd4ac0.js"></script><script src="https://puc.poecdn.net/authenticated_preview_page/console.564bd151389dfff6cb98.js"></script><script src="https://puc.poecdn.net/authenticated_preview_page/interceptAndBubbleLinkClicks.0565894e0316ba77c36c.js"></script></head> <body> <!-- Loading Menu with Play Button --> <div id="loading-screen"> <div class="logo">BANK HEIST ESCAPE</div> <div>Can you steal the money and escape?</div> <div class="progress-bar"> <div class="progress" id="progress-bar"></div> </div> <div id="loading-status">Loading game assets...</div> <div id="difficulty-selector" style="display: none; margin: 15px 0;"> <div style="color: white; margin-bottom: 10px; font-size: 20px;">Select Difficulty:</div> <div class="difficulty-buttons"> <button id="easy-btn" class="difficulty-btn selected">Easy</button> <button id="normal-btn" class="difficulty-btn">Normal</button> <button id="hard-btn" class="difficulty-btn">Hard</button> <button id="impossible-btn" class="difficulty-btn">Impossible</button> </div> </div> <div style="display: flex; flex-direction: column; gap: 15px; margin-top: 10px; align-items: center;"> <button id="menu-performance-mode-btn" style="padding: 8px 15px; font-size: 16px; background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; cursor: pointer;">Low Cell-Service Mode: OFF</button> <button id="menu-swat-mode-btn" style="padding: 8px 15px; font-size: 16px; background-color: rgba(93, 92, 222, 0.7); color: white; border: none; border-radius: 5px; cursor: pointer;">SWAT Team Mode: OFF</button> </div> <button id="play-btn" style="display: none;">PLAY GAME</button> <button id="help-btn" style="display: none;">HELP</button> </div> <!-- Help Screen --> <div id="help-screen"> <div id="help-content"> <h2>Bank Heist Escape: Game Help</h2> <p>Welcome to Bank Heist Escape! Your goal is to steal as much money as possible and escape via helicopter before the cops catch you.</p> <h3>Controls:</h3> <ul> <li><strong>WASD or Arrow Keys:</strong> Move around</li> <li><strong>Mouse:</strong> Look around</li> <li><strong>Click/Space:</strong> Shoot or interact</li> <li><strong>Space:</strong> Jump</li> <li><strong>E:</strong> Enter helicopter when nearby</li> </ul> <h3>Game Phases:</h3> <ul> <li><strong>Bank Phase:</strong> Steal money from the computer. Cops arrive after 10 seconds!</li> <li><strong>Escape Phase:</strong> Fight your way out of the bank.</li> <li><strong>Forest Phase:</strong> Find the helicopter to escape!</li> </ul> <h3>Features:</h3> <ul> <li><strong>Guide Path:</strong> A blue line will lead you to the helicopter.</li> <li><strong>Low Cell-Service Mode:</strong> Optimizes the game for slower devices.</li> <li><strong>SWAT Team Mode:</strong> Cops wear bullet-proof vests (requiring headshots) and are faster and more aggressive.</li> <li><strong>Dynamic Cop Respawning:</strong> If you get too far from all cops, reinforcements will be deployed near you!</li> </ul> <h3>Tips:</h3> <ul> <li>Your shield blocks bullets from the front - face enemies when they shoot!</li> <li>The blue guide path will lead you to the helicopter.</li> <li>Look for hovering yellow arrows that point to the helicopter.</li> <li>Cops shoot slower the farther away they are.</li> <li>You have 10 health points - don't let them reach zero!</li> <li>In SWAT Team Mode, aim for the head to defeat enemies more efficiently.</li> <li>If all cops are more than 150 meters away, reinforcements will deploy near you!</li> </ul> <button id="back-btn" class="menu-btn">BACK TO MENU</button> </div> </div> <!-- Pause Screen --> <div id="pause-screen"> <h2>GAME PAUSED</h2> <button id="resume-btn" class="menu-btn">RESUME</button> <button id="pause-help-btn" class="menu-btn">HELP</button> <button id="pause-menu-btn" class="menu-btn">MAIN MENU</button> </div> <canvas id="gameCanvas"></canvas> <div id="touch-look-overlay"></div> <div id="touch-instructions"> <p>Drag anywhere to look around</p> <p>Use direction buttons to move</p> </div> <div id="ui-container"> <div id="balance">$0</div> <div id="health">Health: 10</div> <div id="swat-mode-indicator">⚠️ SWAT MODE ACTIVE ⚠️</div> <div id="notification"></div> <div id="crosshair"></div> <div id="position-display">Position: X: 0, Z: 0 | Distance to helicopter: ---</div> <div id="ai-tactics">AI Tactics: Initializing...</div> <div id="key-hints"> <div>Direction buttons: Move</div> <div>Drag to look around</div> <div>Action buttons: Shoot/Jump</div> </div> <div id="game-over"> <h2 id="game-over-title">Game Over</h2> <p id="game-over-message">You died! You stole $0</p> <button id="restart-btn">Play Again</button> <button id="menu-btn" style="margin-top: 10px; margin-left: 10px; padding: 10px 20px; font-size: 18px; background-color: #5D5CDE; color: white; border: none; border-radius: 5px; cursor: pointer;">Main Menu</button> </div> <!-- Game control buttons --> <button id="pause-btn">Pause</button> <button id="in-game-help">Help</button> <button id="performance-mode-btn">Low Cell-Service Mode: OFF</button> <button id="swat-mode-btn">SWAT Team Mode: OFF</button> <button id="steal-money-btn">STEAL MONEY ($1)</button> <button id="escape-helicopter-btn">ESCAPE THROUGH HELICOPTER!</button> <div id="touch-controls"> <div id="movement-buttons"> <div></div> <button id="up-btn" class="movement-btn">↑</button> <div></div> <button id="left-btn" class="movement-btn">←</button> <button id="center-btn" class="movement-btn"></button> <button id="right-btn" class="movement-btn">→</button> <div></div> <button id="down-btn" class="movement-btn">↓</button> <div></div> </div> <div id="action-buttons"> <button id="shoot-btn" class="action-btn">🔫</button> <button id="jump-btn" class="action-btn">↑</button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/three@0.143.0/build/three.min.js"></script> <script> // Initialize game const Game = { // Game state state: { money: 0, health: 10, // Reset to original value gamePhase: 'loading', // 'loading', 'bank', 'escape', 'forest', 'helicopter', 'gameover', 'win' cops: [], trees: [], bullets: [], enemyBullets: [], gameOver: false, nearComputer: false, lastPositionUpdate: 0, // For position updates positionUpdateInterval: 5000, // Update position every 5 seconds playerShield: { active: true, direction: new THREE.Vector3(0, 0, -1) }, lastShot: 0, lastMoneySteal: 0, copCount: 20, assetsLoaded: 0, totalAssets: 5, autoMoneyEnabled: false, autoMoneyInterval: null, playerPosition: new THREE.Vector3(), // Track last known player position copFormations: [], // For tactical cop formations // Game difficulty settings difficultyLevel: 'normal', // 'easy', 'normal', 'hard', 'impossible' swatMode: false, // New SWAT Team mode toggle difficultySettings: { easy: { copSpeed: 2.2, // Base speed for standard cops copSpeedFar: 15.0, // Maximum speed increased to higher value copHealth: 1, // Health multiplier shootAccuracy: 0.66, // Base accuracy shootDelay: 4500, // Base shoot delay teleportDistance: 0 // No teleport }, normal: { copSpeed: 3.3, copSpeedFar: 25.0, // Now can reach much higher speeds copHealth: 1, shootAccuracy: 0.77, shootDelay: 3600, teleportDistance: 0 // No teleport }, hard: { copSpeed: 4.4, copSpeedFar: 35.0, // Significantly higher max speed copHealth: 2, shootAccuracy: 0.88, shootDelay: 2700, teleportDistance: 45 // Teleport when player is close to helicopter }, impossible: { copSpeed: 5.5, copSpeedFar: 50.0, // Now up to 50 m/s as requested copHealth: 3, shootAccuracy: 0.99, // Nearly perfect accuracy shootDelay: 1800, teleportDistance: 40 // Teleport when player is close to helicopter } }, // SWAT specific adjustments swatAdjustments: { speedMultiplier: 1.25, // 25% faster fireRateMultiplier: 0.85, // 15% faster fire rate (lower delay) bulletProofVest: true, // Requires headshots vestColor: 0x222222, // Dark grey for vests helmColor: 0x444444, // Grey for helmets lastFarCopCheck: 0, // Last time we checked for far away cops farCopCheckInterval: 2000, // Check every 2 seconds maxFarCopDistance: 150 // Distance in meters to trigger failsafe }, aiLastRequest: 0, // Last time we sent an AI request aiRequestInterval: 120000, // Request AI update only every 2 minutes (reducing points by ~5000) aiTotalRequests: 0, // Track total AI requests per game aiMaxRequests: 2, // Maximum of only 2 AI requests per game (reducing points usage) aiLastResponse: "No AI decisions yet", // Last AI response aiResponseCache: {}, // Cache for AI responses aiCacheDuration: 300000, // Cache AI responses for 5 minutes (300 seconds) inHelicopter: false, // Track if player is in helicopter playerInHelicopterOffset: new THREE.Vector3(0, 0, 0), // Player position relative to helicopter lastTeleportCheck: 0, // Last time we checked for teleport teleportCheckInterval: 1000, // Check teleport every second guidePath: null, // Path to guide player to helicopter paused: false, // Game pause state showGuidePath: true, // Guide path is now always active by default debugMode: false // Debug mode for collision visualization }, // Player controller player: { height: 1.7, speed: 7.0, // Increased from 5.0 for better mobility velocity: new THREE.Vector3(), direction: new THREE.Vector3(), moveForward: false, moveBackward: false, moveLeft: false, moveRight: false, canJump: false, shootCooldown: 300, // ms // Look controls lookSpeed: 0.003, euler: new THREE.Euler(0, 0, 0, 'YXZ'), // Touch controls touchLook: { active: false, startX: 0, startY: 0 } }, // Three.js components three: { scene: null, camera: null, renderer: null, raycaster: null, clock: null, computer: null, moneyButton: null, // Materials materials: { bankFloor: null, bankWall: null, computer: null, button: null, copBody: null, copHead: null, copVest: null, // New vest material for SWAT mode copHelmet: null, // New helmet material for SWAT mode tree: null, treeTrunk: null, grass: null, helicopter: null, shield: null, bulletImpact: null // New material for bullet impacts } }, // DOM elements dom: { canvas: null, balance: null, health: null, poePoints: null, notification: null, positionDisplay: null, aiTactics: null, gameOver: null, gameOverTitle: null, gameOverMessage: null, restartBtn: null, loadingScreen: null, playBtn: null, helpBtn: null, loadingStatus: null, progressBar: null, stealMoneyBtn: null, escapeHelicopterBtn: null, keyHints: null, menuBtn: null, helpScreen: null, backBtn: null, pauseBtn: null, pauseScreen: null, resumeBtn: null, pauseHelpBtn: null, pauseMenuBtn: null, inGameHelpBtn: null, swatModeIndicator: null, // Mobile controls mobileControls: null, upBtn: null, downBtn: null, leftBtn: null, rightBtn: null, shootBtn: null, lookArea: null, // New mode toggles swatModeBtn: null, menuSwatModeBtn: null }, // Setup game environment init: function() { // Initialize performance mode settings this.state.performanceMode = false; this.state.frameSkip = 0; this.state.lastFrameTime = 0; this.state.frameCount = 0; // Check for dark mode if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.classList.add('dark'); } window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { if (event.matches) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } }); // Get DOM elements this.dom.canvas = document.getElementById('gameCanvas'); this.dom.balance = document.getElementById('balance'); this.dom.health = document.getElementById('health'); this.dom.poePoints = document.getElementById('poe-points'); this.dom.notification = document.getElementById('notification'); this.dom.positionDisplay = document.getElementById('position-display'); this.dom.aiTactics = document.getElementById('ai-tactics'); this.dom.gameOver = document.getElementById('game-over'); this.dom.gameOverTitle = document.getElementById('game-over-title'); this.dom.gameOverMessage = document.getElementById('game-over-message'); this.dom.restartBtn = document.getElementById('restart-btn'); this.dom.menuBtn = document.getElementById('menu-btn'); this.dom.loadingScreen = document.getElementById('loading-screen'); this.dom.playBtn = document.getElementById('play-btn'); this.dom.helpBtn = document.getElementById('help-btn'); this.dom.loadingStatus = document.getElementById('loading-status'); this.dom.progressBar = document.getElementById('progress-bar'); this.dom.stealMoneyBtn = document.getElementById('steal-money-btn'); this.dom.escapeHelicopterBtn = document.getElementById('escape-helicopter-btn'); this.dom.keyHints = document.getElementById('key-hints'); this.dom.swatModeIndicator = document.getElementById('swat-mode-indicator'); // New game UI elements this.dom.pauseBtn = document.getElementById('pause-btn'); this.dom.pauseScreen = document.getElementById('pause-screen'); this.dom.resumeBtn = document.getElementById('resume-btn'); this.dom.pauseHelpBtn = document.getElementById('pause-help-btn'); this.dom.pauseMenuBtn = document.getElementById('pause-menu-btn'); this.dom.inGameHelpBtn = document.getElementById('in-game-help'); this.dom.helpScreen = document.getElementById('help-screen'); this.dom.backBtn = document.getElementById('back-btn'); // New mode toggles this.dom.swatModeBtn = document.getElementById('swat-mode-btn'); this.dom.menuSwatModeBtn = document.getElementById('menu-swat-mode-btn'); // Touch controls this.dom.touchOverlay = document.getElementById('touch-look-overlay'); this.dom.touchInstructions = document.getElementById('touch-instructions'); // Mobile controls this.dom.mobileControls = document.getElementById('mobile-controls'); this.dom.upBtn = document.getElementById('up-btn'); this.dom.downBtn = document.getElementById('down-btn'); this.dom.leftBtn = document.getElementById('left-btn'); this.dom.rightBtn = document.getElementById('right-btn'); this.dom.shootBtn = document.getElementById('shoot-btn'); this.dom.lookArea = document.getElementById('look-area'); // Setup Three.js this.setupThreeJS(); // Create materials this.createMaterials(); // Initialize Poe AI for cops this.initPoeAI(); // Load game assets this.loadGameAssets(); // Setup play button this.dom.playBtn.addEventListener('click', () => { this.startGame(); }); // Start animation loop for loading screen // Setup additional UI components this.setupAdditionalUI = function() { console.log("Setting up additional UI components"); // Initialize menu buttons this.dom.helpBtn.style.display = "block"; // Setup menu button listeners this.dom.helpBtn.addEventListener('click', () => { this.showHelpScreen(); }); this.dom.backBtn.addEventListener('click', () => { this.hideHelpScreen(); }); // Setup in-game buttons this.dom.pauseBtn.addEventListener('click', () => { this.pauseGame(); }); this.dom.inGameHelpBtn.addEventListener('click', () => { this.pauseGame(); this.showHelpScreen(); }); this.dom.resumeBtn.addEventListener('click', () => { this.resumeGame(); }); this.dom.pauseHelpBtn.addEventListener('click', () => { this.showHelpScreen(); }); this.dom.pauseMenuBtn.addEventListener('click', () => { this.returnToMainMenu(); }); this.dom.menuBtn.addEventListener('click', () => { this.returnToMainMenu(); }); this.setupSwatModeButtons(); }; // COMPLETELY REWRITTEN: Setup SWAT mode buttons with global handlers to ensure reliability this.setupSwatModeButtons = function() { console.log("Setting up SWAT mode buttons with global handler approach"); // First, create a global reference to game instance for reliable access window._gameInstance = this; // Create a global handler function that will always work window._handleSwatModeToggle = function(event) { console.log("Global SWAT mode toggle handler called"); // Prevent default behavior if (event) event.preventDefault(); // Always use the global game instance if (window._gameInstance) { window._gameInstance.toggleSwatMode(); } else { console.error("Game instance not found in global scope"); } // Prevent event propagation return false; }; // Find DOM elements directly const swatModeBtn = document.getElementById('swat-mode-btn'); const menuSwatModeBtn = document.getElementById('menu-swat-mode-btn'); // Log elements to verify they exist console.log("Found in-game SWAT button:", !!swatModeBtn); console.log("Found menu SWAT button:", !!menuSwatModeBtn); // Process in-game button if (swatModeBtn) { // Clone to remove any existing handlers const newSwatBtn = swatModeBtn.cloneNode(true); if (swatModeBtn.parentNode) { swatModeBtn.parentNode.replaceChild(newSwatBtn, swatModeBtn); } // Store reference in game object this.dom.swatModeBtn = newSwatBtn; // Add global handler using direct assignment (most reliable method) newSwatBtn.onclick = window._handleSwatModeToggle; } // Process menu button if (menuSwatModeBtn) { // Clone to remove any existing handlers const newMenuSwatBtn = menuSwatModeBtn.cloneNode(true); if (menuSwatModeBtn.parentNode) { menuSwatModeBtn.parentNode.replaceChild(newMenuSwatBtn, menuSwatModeBtn); } // Store reference in game object this.dom.menuSwatModeBtn = newMenuSwatBtn; // Add global handler using direct assignment newMenuSwatBtn.onclick = window._handleSwatModeToggle; } // Update button visuals to match current state this.updateModeButtonStates(); // Log the current state to verify it's being applied properly console.log("After setup - SWAT mode state:", this.state.swatMode); console.log("Button text in-game:", this.dom.swatModeBtn ? this.dom.swatModeBtn.textContent : "N/A"); console.log("Button text menu:", this.dom.menuSwatModeBtn ? this.dom.menuSwatModeBtn.textContent : "N/A"); }; // Call setup for additional UI this.setupAdditionalUI(); // Create guide path to helicopter - now usable in all game states this.createGuidePath = function() { if (this.state.guidePath) { this.three.scene.remove(this.state.guidePath); } // Get appropriate start and end points based on game phase let startPoint = this.three.camera.position.clone(); startPoint.y = 0.1; // Slightly above ground let endPoint; if (this.state.gamePhase === 'bank') { // In bank phase, guide to the exit door endPoint = new THREE.Vector3(0, 0.1, 24); } else if (this.state.gamePhase === 'escape') { // In escape phase, guide through the door endPoint = new THREE.Vector3(0, 0.1, 30); } else { // Default to helicopter location endPoint = new THREE.Vector3(0, 0.1, -650); } // Create waypoints along path that avoid trees or walls const waypoints = this.findPathToTarget(startPoint, endPoint); // Create material for path - significantly brighter and much wider const pathMaterial = new THREE.LineBasicMaterial({ color: 0x00ffff, linewidth: 30, // Increased to 30px for maximum visibility opacity: 1.0, transparent: true }); // Add a second wider line underneath for better visibility const pathOutlineMaterial = new THREE.LineBasicMaterial({ color: 0x0088ff, linewidth: 40, // Even wider outline opacity: 0.7, transparent: true }); // Create geometry for path const pathGeometry = new THREE.BufferGeometry().setFromPoints(waypoints); // Create line for outline (thicker, under main path) const pathOutline = new THREE.Line(pathGeometry, pathOutlineMaterial); pathOutline.position.y = 0.01; // Position slightly below main path pathOutline.frustumCulled = false; // Create line for main path const path = new THREE.Line(pathGeometry, pathMaterial); path.position.y = 0.02; // Position slightly above outline path.frustumCulled = false; // Make sure it's always visible // Create a group to hold both lines const pathGroup = new THREE.Group(); pathGroup.add(pathOutline); pathGroup.add(path); // Add group to scene this.three.scene.add(pathGroup); // Store reference to path group this.state.guidePath = pathGroup; }; // Modified to find path to any target with performance optimizations this.findPathToTarget = function(start, end) { // Track last full path calculation time const now = performance.now(); if (this.state.lastFullPathCalc && now - this.state.lastFullPathCalc < 5000) { // Use simplified path calculation most of the time return this.findSimplifiedPath(start, end); } this.state.lastFullPathCalc = now; // Simple algorithm that moves away from nearest trees or walls const waypoints = [start.clone()]; // Reduce segments for better performance const numSegments = 20; // Reduced from 50 // Break path into segments for (let i = 1; i <= numSegments; i++) { const t = i / numSegments; // Start with a direct line point const directPoint = new THREE.Vector3(); directPoint.lerpVectors(start, end, t); // Reduce obstacle checks for performance - only check every 2nd segment if (i % 2 === 0) { let nearestObstacle = null; let nearestDistance = Infinity; let obstacleNormal = new THREE.Vector3(); // Check only a subset of trees/walls for better performance // Check distances to trees in forest phase if (this.state.gamePhase === 'forest' || this.state.gamePhase === 'escape') { // Only check trees within reasonable distance of this waypoint for (let j = 0; j < this.state.trees.length; j += 3) { // Check every 3rd tree const tree = this.state.trees[j]; const treePos = new THREE.Vector3( tree.position.x, 0, tree.position.z ); // Quick approximate distance check first const approxDist = Math.abs(directPoint.x - treePos.x) + Math.abs(directPoint.z - treePos.z); if (approxDist < 10) { // Only do exact check if roughly close enough const distance = directPoint.distanceTo(treePos) - tree.radius; if (distance < nearestDistance) { nearestDistance = distance; nearestObstacle = tree; obstacleNormal = new THREE.Vector3().subVectors(directPoint, treePos).normalize(); } } } } // Check distances to walls in bank phase if (this.state.gamePhase === 'bank') { for (const wall of this.bankWalls) { // Calculate point-to-box distance const pointInBox = new THREE.Vector3( Math.max(wall.minX, Math.min(directPoint.x, wall.maxX)), Math.max(wall.minY, Math.min(directPoint.y, wall.maxY)), Math.max(wall.minZ, Math.min(directPoint.z, wall.maxZ)) ); const distance = directPoint.distanceTo(pointInBox); if (distance < nearestDistance) { nearestDistance = distance; nearestObstacle = wall; obstacleNormal = wall.normal.clone(); } } } // If too close to an obstacle, move away if (nearestObstacle && nearestDistance < 3) { // Move point away from obstacle directPoint.add(obstacleNormal.multiplyScalar(3 - nearestDistance)); } } // Simplified hover effect - just use fixed height directPoint.y = 0.5; waypoints.push(directPoint); } // Add end point waypoints.push(end.clone()); return waypoints; }; // Simple path finder that uses fewer calculations this.findSimplifiedPath = function(start, end) { const waypoints = [start.clone()]; // Use many fewer segments for better performance const numSegments = 10; for (let i = 1; i <= numSegments; i++) { const t = i / numSegments; // Mostly direct line with slight randomization const directPoint = new THREE.Vector3(); directPoint.lerpVectors(start, end, t); // Add small random offset for visual variety if (i > 1 && i < numSegments) { directPoint.x += (Math.random() - 0.5) * 2; directPoint.z += (Math.random() - 0.5) * 2; } // Fixed height for simplicity directPoint.y = 0.5; waypoints.push(directPoint); } // Add end point waypoints.push(end.clone()); return waypoints; }; // Show the help screen this.showHelpScreen = function() { this.dom.helpScreen.style.display = 'flex'; }; // Hide the help screen this.hideHelpScreen = function() { this.dom.helpScreen.style.display = 'none'; // If game was paused, keep it paused if (this.state.paused) { this.dom.pauseScreen.style.display = 'flex'; } }; // Pause the game this.pauseGame = function() { if (this.state.gamePhase === 'loading' || this.state.gameOver) return; this.state.paused = true; this.dom.pauseScreen.style.display = 'flex'; }; // Resume the game this.resumeGame = function() { this.state.paused = false; this.dom.pauseScreen.style.display = 'none'; this.dom.helpScreen.style.display = 'none'; }; // Return to main menu this.returnToMainMenu = function() { // Reset game state this.state.money = 0; this.state.health = 10; this.state.gamePhase = 'loading'; this.state.gameOver = false; this.state.nearComputer = false; this.state.autoMoneyEnabled = false; this.helicopterReached = false; this.state.inHelicopter = false; this.state.paused = false; // Clear any active intervals if (this.state.autoMoneyInterval) { clearInterval(this.state.autoMoneyInterval); this.state.autoMoneyInterval = null; } // Clear existing entities for (const cop of this.state.cops) { this.three.scene.remove(cop.body); this.three.scene.remove(cop.head); this.three.scene.remove(cop.gun); if (cop.vest) this.three.scene.remove(cop.vest); if (cop.helmet) this.three.scene.remove(cop.helmet); } this.state.cops = []; for (const bullet of this.state.bullets) { this.three.scene.remove(bullet.mesh); } this.state.bullets = []; for (const bullet of this.state.enemyBullets) { this.three.scene.remove(bullet.mesh); } this.state.enemyBullets = []; if (this.state.guidePath) { this.three.scene.remove(this.state.guidePath); this.state.guidePath = null; } // Hide game screens this.dom.pauseScreen.style.display = 'none'; this.dom.helpScreen.style.display = 'none'; this.dom.gameOver.style.display = 'none'; // Show loading screen this.dom.loadingScreen.style.display = 'flex'; this.dom.playBtn.style.display = 'block'; this.dom.helpBtn.style.display = 'block'; // Reset camera position for next game this.three.camera.position.set(0, this.player.height, 0); this.three.camera.rotation.set(0, 0, 0); // Update UI this.updateMoneyDisplay(); this.updateHealthDisplay(); }; // Start animation loop this.animate(); }, // Toggle performance mode togglePerformanceMode: function() { this.state.performanceMode = !this.state.performanceMode; // Update both in-game and menu button text if (this.dom.performanceModeBtn) { this.dom.performanceModeBtn.textContent = this.state.performanceMode ? "Low Cell-Service Mode: ON" : "Low Cell-Service Mode: OFF"; // Change button color to indicate mode this.dom.performanceModeBtn.style.backgroundColor = this.state.performanceMode ? "rgba(255, 59, 48, 0.7)" : "rgba(93, 92, 222, 0.7)"; } if (this.dom.menuPerformanceModeBtn) { this.dom.menuPerformanceModeBtn.textContent = this.state.performanceMode ? "Low Cell-Service Mode: ON" : "Low Cell-Service Mode: OFF"; // Change menu button appearance this.dom.menuPerformanceModeBtn.style.backgroundColor = this.state.performanceMode ? "rgba(255, 59, 48, 0.7)" : "rgba(93, 92, 222, 0.7)"; } // Update renderer settings for performance mode if (this.state.performanceMode) { // Lower pixel ratio for better performance this.three.renderer.setPixelRatio(1); // Apply performance-oriented settings this.three.renderer.shadowMap.enabled = false; // Disable shadows // Reduce the number of active trees by hiding half of them let i = 0; for (const tree of this.state.trees) { if (i++ % 2 === 0) { tree.trunk.visible = false; tree.leaves.visible = false; // Also hide outlines if (tree.trunkOutline) tree.trunkOutline.visible = false; if (tree.leavesOutline) tree.leavesOutline.visible = false; } } this.showNotification("Low Cell-Service Mode activated. Performance optimized!", 3000); } else { // Restore normal settings this.three.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.three.renderer.shadowMap.enabled = true; // Show all trees again for (const tree of this.state.trees) { tree.trunk.visible = true; tree.leaves.visible = true; // Also show outlines if (tree.trunkOutline) tree.trunkOutline.visible = true; if (tree.leavesOutline) tree.leavesOutline.visible = true; } this.showNotification("Low Cell-Service Mode deactivated. Full graphics restored.", 3000); } }, // Toggle SWAT Team mode - COMPLETELY REWRITTEN for reliability toggleSwatMode: function() { console.log("SWAT mode toggle function called with global handler technique"); // Critical: Create a global reference to ensure this function always works if (!window._gameInstance) { window._gameInstance = this; } // Get fresh DOM references every time to avoid stale elements const swatModeBtn = document.getElementById('swat-mode-btn'); const menuSwatModeBtn = document.getElementById('menu-swat-mode-btn'); const swatModeIndicator = document.getElementById('swat-mode-indicator'); // Store these references in our object this.dom.swatModeBtn = swatModeBtn; this.dom.menuSwatModeBtn = menuSwatModeBtn; this.dom.swatModeIndicator = swatModeIndicator; // Debug log current state console.log("SWAT mode before toggle:", this.state.swatMode, typeof this.state.swatMode); // Toggle state with explicit boolean casting to prevent any type issues this.state.swatMode = this.state.swatMode === true ? false : true; // Debug log new state console.log("SWAT mode after toggle:", this.state.swatMode, typeof this.state.swatMode); // Define button states const onText = "SWAT Team Mode: ON"; const offText = "SWAT Team Mode: OFF"; const onColor = "rgba(255, 59, 48, 0.7)"; const offColor = "rgba(93, 92, 222, 0.7)"; // Update in-game button with direct DOM manipulation if (swatModeBtn) { swatModeBtn.innerText = this.state.swatMode ? onText : offText; swatModeBtn.style.backgroundColor = this.state.swatMode ? onColor : offColor; swatModeBtn.style.animation = this.state.swatMode ? "pulse 2s infinite" : "none"; } // Update menu button with direct DOM manipulation if (menuSwatModeBtn) { menuSwatModeBtn.innerText = this.state.swatMode ? onText : offText; menuSwatModeBtn.style.backgroundColor = this.state.swatMode ? onColor : offColor; menuSwatModeBtn.style.animation = this.state.swatMode ? "pulse 2s infinite" : "none"; } // Update indicator visibility if (swatModeIndicator) { swatModeIndicator.style.display = this.state.swatMode ? 'block' : 'none'; } // Critical: Create global handler for the toggle buttons // This ensures the function will always work even if 'this' context changes window.handleSwatModeToggle = function() { console.log("Global SWAT toggle handler called"); window._gameInstance.toggleSwatMode(); }; // Re-apply handlers with the global function if (swatModeBtn) { // Clone the button to remove any existing handlers const newBtn = swatModeBtn.cloneNode(true); swatModeBtn.parentNode.replaceChild(newBtn, swatModeBtn); this.dom.swatModeBtn = newBtn; // Use the global handler this.dom.swatModeBtn.onclick = window.handleSwatModeToggle; } if (menuSwatModeBtn) { // Clone the button to remove any existing handlers const newBtn = menuSwatModeBtn.cloneNode(true); menuSwatModeBtn.parentNode.replaceChild(newBtn, menuSwatModeBtn); this.dom.menuSwatModeBtn = newBtn; // Use the global handler this.dom.menuSwatModeBtn.onclick = window.handleSwatModeToggle; } // Update all existing cops with SWAT gear this.updateCopsForSwatMode(); // Show notification if (this.state.swatMode) { this.showNotification("⚠️ SWAT Team Mode activated! ⚠️\nCops now have bullet-proof vests, are 25% faster, and fire 15% more rapidly.\nAim for headshots!", 5000); } else { this.showNotification("SWAT Team Mode deactivated. Standard police deployed.", 2000); } // Save state to localStorage with explicit string conversion try { const valueToStore = String(this.state.swatMode); localStorage.setItem('swatMode', valueToStore); console.log("SWAT mode saved to localStorage as:", valueToStore); // Verify the saved value const savedValue = localStorage.getItem('swatMode'); console.log("Stored value confirmed as:", savedValue); } catch (e) { console.warn("Could not save SWAT mode to localStorage:", e); } }, // Load saved settings loadSavedSettings: function() { try { // Try to load SWAT mode setting from localStorage const savedSwatMode = localStorage.getItem('swatMode'); if (savedSwatMode !== null) { this.state.swatMode = savedSwatMode === 'true'; console.log("Loaded SWAT mode from localStorage:", this.state.swatMode); } // Update UI to match loaded settings this.updateModeButtonStates(); } catch (e) { console.warn("Could not load settings from localStorage:", e); } }, // Update all cops for SWAT mode - Fixed to ensure proper gear removal updateCopsForSwatMode: function() { console.log("Updating cops for SWAT mode. Current mode:", this.state.swatMode); // Apply SWAT features to all existing cops for (const cop of this.state.cops) { if (cop.dead) continue; // Skip dead cops // Add SWAT gear if mode is enabled, remove if disabled if (this.state.swatMode) { // Add SWAT gear if not present if (!cop.vest) { // Create vest - larger body section with dark color const vestGeometry = new THREE.CylinderGeometry(0.33, 0.33, 0.9, 8); const vest = new THREE.Mesh( vestGeometry, new THREE.MeshBasicMaterial({ color: this.state.swatAdjustments.vestColor }) ); vest.position.copy(cop.body.position); vest.position.y -= 0.1; // Position slightly lower for chest area this.three.scene.add(vest); cop.vest = vest; } // Add helmet if not present if (!cop.helmet) { // Create helmet - slightly larger than head with dark color const helmetGeometry = new THREE.SphereGeometry(0.28, 8, 8); const helmet = new THREE.Mesh( helmetGeometry, new THREE.MeshBasicMaterial({ color: this.state.swatAdjustments.helmColor }) ); helmet.position.copy(cop.head.position); this.three.scene.add(helmet); cop.helmet = helmet; } // Store original values before applying multipliers (only once) if (!cop.hasOwnProperty('originalSpeed')) { cop.originalSpeed = cop.speed; // Store original speed cop.originalShootDelay = cop.shootDelay; // Store original fire rate // Apply the multipliers cop.speed *= this.state.swatAdjustments.speedMultiplier; // 25% faster cop.shootDelay *= this.state.swatAdjustments.fireRateMultiplier; // 15% faster fire rate console.log("Enhanced cop speed from", cop.originalSpeed, "to", cop.speed); } // Add bullet-proof tag cop.bulletProofVest = true; } else { // Remove SWAT gear if present if (cop.vest) { this.three.scene.remove(cop.vest); cop.vest = null; } if (cop.helmet) { this.three.scene.remove(cop.helmet); cop.helmet = null; } // Restore original speed and fire rate if stored if (cop.hasOwnProperty('originalSpeed')) { console.log("Restoring cop speed from", cop.speed, "to", cop.originalSpeed); cop.speed = cop.originalSpeed; cop.shootDelay = cop.originalShootDelay; // Remove the stored original values to prevent issues with toggling delete cop.originalSpeed; delete cop.originalShootDelay; } // Remove bullet-proof tag cop.bulletProofVest = false; } } }, // Initialize Poe AI for cop tactics initPoeAI: function() { // Only initialize if we have window.Poe available if (!window.Poe) { console.warn("Poe API not available. Using default cop AI behavior."); return; } // Register the AI handler window.Poe.registerHandler("cop-ai-handler", (result, context) => { if (result.status === "error") { console.error("Error in AI response:", result.statusText); return; } if (result.responses && result.responses.length > 0) { const response = result.responses[0]; if (response.status === "complete") { // Try to extract JSON from the response try { // Look for a JSON object in the response const jsonMatch = response.content.match(/\{[\s\S]*\}/); if (jsonMatch) { const jsonStr = jsonMatch[0]; const tactics = JSON.parse(jsonStr); // Apply AI decisions to cops this.applyAIDecisionsToCops(tactics); // Display AI tactics if (tactics.tactics && tactics.tactics.notification) { this.dom.aiTactics.textContent = "AI Tactics: " + tactics.tactics.notification; this.dom.aiTactics.style.opacity = 1; setTimeout(() => { this.dom.aiTactics.style.opacity = 0.7; }, 5000); } } } catch (e) { console.error("Error parsing AI response:", e); } } } }); }, // Request AI decision for cops - extremely throttled to prevent lag requestAIDecision: function() { // Drastically reduce how often we update tactics const now = performance.now(); // Only update tactics once every 30 seconds (increased from 15 seconds) if (this.state.lastTacticsUpdate && now - this.state.lastTacticsUpdate < 30000) { return; // Skip frequent updates } this.state.lastTacticsUpdate = now; // Use ultra-simplified AI to prevent lag this.useUltraSimpleAI(); return; // Check cache first to avoid duplicate requests for similar situations const playerPos = this.three.camera.position; const cacheKey = this.generateAICacheKey(); if (this.state.aiResponseCache[cacheKey]) { // Use cached response if available for similar situation console.log("Using cached AI tactics"); this.applyAIDecisionsToCops(this.state.aiResponseCache[cacheKey]); return; } this.state.aiLastRequest = now; this.state.aiTotalRequests++; // Create a summary of the game state for the AI const aliveCops = this.state.cops.filter(cop => !cop.dead); // Skip if no alive cops if (aliveCops.length === 0) return; // Create a simplified situation state for the AI const situationData = { gamePhase: this.state.gamePhase, playerPosition: { x: Math.round(playerPos.x), z: Math.round(playerPos.z) }, playerMoney: this.state.money, playerHealth: this.state.health, activeCopCount: aliveCops.length, // Only include a sample of cops to reduce data size cops: aliveCops.slice(0, 10).map((cop, index) => ({ id: index, type: cop.type, position: { x: Math.round(cop.body.position.x), z: Math.round(cop.body.position.z) }, health: cop.health, distance: Math.round(cop.body.position.distanceTo(playerPos)) })) }; // Create prompt for Claude - using a more compact format const prompt = `You are the tactical AI director for police in a bank heist game. The player is a bank robber, and your job is to direct the cops to capture them. Current game state: ${JSON.stringify(situationData, null, 2)} Your task is to provide tactical instructions for the cops. Return ONLY raw JSON with this format without any explanation: { "tactics": { "cops": [ { "id": 0, "state": "chase", "tacticalPosition": {"x": 10, "z": 15} }, ...more cops ], "notification": "Short tactical message displayed to player" } } Valid states are: "chase", "flank", "cover", "retreat" Keep the notification message under 40 characters. `; // Show loading state for tactics this.dom.aiTactics.textContent = "AI Tactics: Requesting..."; this.dom.aiTactics.style.opacity = 0.7; // Send the request to Claude try { window.Poe.sendUserMessage("@Claude-3.5-Sonnet " + prompt, { handler: "cop-ai-handler", openChat: false }); } catch (e) { console.error("Error sending AI request:", e); // Fall back to local AI if API call fails this.useLocalAI(); } }, // Generate a cache key for similar game states generateAICacheKey: function() { const playerPos = this.three.camera.position; // Round to nearest 10 units to group similar positions const posX = Math.round(playerPos.x / 10) * 10; const posZ = Math.round(playerPos.z / 10) * 10; return `${this.state.gamePhase}_${posX}_${posZ}_${this.state.health}`; }, // Stable AI for consistent cop behavior - no random changes useUltraSimpleAI: function() { const playerPos = this.three.camera.position; const aliveCops = this.state.cops.filter(cop => !cop.dead); if (aliveCops.length === 0) return; // Create stable tactical positions - assign fixed roles to each cop const tactics = { tactics: { cops: [], notification: "Police in pursuit" } }; // Assign each cop a fixed role based on their index - THIS IS KEY FOR STABILITY aliveCops.forEach((cop, index) => { // Only change tactics if cop doesn't have a role yet or if player has moved very far const needsNewTactics = !cop.fixedRole || (cop.lastTargetPosition && cop.lastTargetPosition.distanceTo(new THREE.Vector3(playerPos.x, 0, playerPos.z)) > 50); if (needsNewTactics) { // Assign permanent role based on cop type and index (modulo to create repeating pattern) const roleIndex = index % 3; // Use 0, 1, or 2 as stable role assignment // Commander always covers, elite cops flank, standard cops alternate chase/flank by position let state; let tacticalPosition; if (cop.type === 'commander') { state = 'cover'; // Position behind other cops const angle = 3.14; // PI - behind player tacticalPosition = { x: playerPos.x + Math.cos(angle) * 25, z: playerPos.z + Math.sin(angle) * 25 }; } else if (cop.type === 'elite') { state = 'flank'; // Position to flank player - use stable angle based on cop index const angle = (index * 1.5) % (Math.PI * 2); // Spread elite cops evenly tacticalPosition = { x: playerPos.x + Math.cos(angle) * 20, z: playerPos.z + Math.sin(angle) * 20 }; } else { // Regular cops - alternate between chase and flank based on fixed pattern if (roleIndex === 0) { state = 'chase'; tacticalPosition = { x: playerPos.x, z: playerPos.z }; } else { state = 'flank'; // Position to flank player - use stable angle based on cop index const angle = (index * 0.8) % (Math.PI * 2); // Predictable angle tacticalPosition = { x: playerPos.x + Math.cos(angle) * 15, z: playerPos.z + Math.sin(angle) * 15 }; } } // Save the fixed role and target position for future reference cop.fixedRole = roleIndex; cop.fixedState = state; cop.lastTargetPosition = new THREE.Vector3(playerPos.x, 0, playerPos.z); // Push to tactics tactics.tactics.cops.push({ id: index, state: state, tacticalPosition: tacticalPosition }); } else { // Keep existing tactics but update position relative to player's new position // This maintains consistent behavior while still allowing cops to follow player const state = cop.state || 'chase'; let tacticalPosition; // Base tactical position on player's current position but keep the same relative approach if (state === 'chase') { tacticalPosition = { x: playerPos.x, z: playerPos.z }; } else if (state === 'flank') { // Keep same angle but update position relative to player const angle = (index * 0.8) % (Math.PI * 2); tacticalPosition = { x: playerPos.x + Math.cos(angle) * 15, z: playerPos.z + Math.sin(angle) * 15 }; } else { // For cover, stay behind player const angle = 3.14; // PI - behind player tacticalPosition = { x: playerPos.x + Math.cos(angle) * 25, z: playerPos.z + Math.sin(angle) * 25 }; } // Update last target position cop.lastTargetPosition = new THREE.Vector3(playerPos.x, 0, playerPos.z); // Push to tactics with same state but updated position tactics.tactics.cops.push({ id: index, state: state, tacticalPosition: tacticalPosition }); } }); // Apply the stable AI decisions this.applyAIDecisionsToCops(tactics); // Hide the tactics notification completely this.dom.aiTactics.style.opacity = 0; }, // Apply AI decisions to cops applyAIDecisionsToCops: function(aiDecisions) { if (!aiDecisions || !aiDecisions.tactics || !aiDecisions.tactics.cops) { return; } // Cache this response for future similar situations const cacheKey = this.generateAICacheKey(); this.state.aiResponseCache[cacheKey] = aiDecisions; // Limit cache size to prevent memory issues const cacheKeys = Object.keys(this.state.aiResponseCache); if (cacheKeys.length > 10) { // Remove oldest entry if cache gets too large delete this.state.aiResponseCache[cacheKeys[0]]; } const decisions = aiDecisions.tactics.cops; decisions.forEach(decision => { const copIndex = decision.id; if (copIndex < 0 || copIndex >= this.state.cops.length) return; const cop = this.state.cops[copIndex]; if (cop.dead) return; // Update cop state if (decision.state) { cop.state = decision.state; } // Update tactical position if provided if (decision.tacticalPosition) { cop.tacticalPosition = new THREE.Vector3( decision.tacticalPosition.x, 0, decision.tacticalPosition.z ); } }); // Store the AI response message if (aiDecisions.tactics.notification) { this.state.aiLastResponse = aiDecisions.tactics.notification; } }, // Load game assets loadGameAssets: function() { // Simulate asset loading const loadInterval = setInterval(() => { this.state.assetsLoaded++; this.updateLoadingProgress(); if (this.state.assetsLoaded >= this.state.totalAssets) { clearInterval(loadInterval); this.dom.loadingStatus.textContent = "Ready to play!"; this.dom.playBtn.style.display = "block"; // Show difficulty selector document.getElementById('difficulty-selector').style.display = 'block'; // Setup difficulty buttons const difficultyButtons = ['easy-btn', 'normal-btn', 'hard-btn', 'impossible-btn']; difficultyButtons.forEach(btnId => { const btn = document.getElementById(btnId); btn.addEventListener('click', () => { // Remove selected class from all buttons difficultyButtons.forEach(id => { document.getElementById(id).classList.remove('selected'); }); // Add selected class to clicked button btn.classList.add('selected'); // Set difficulty level const difficulty = btnId.split('-')[0]; this.state.difficultyLevel = difficulty; console.log(`Difficulty set to: ${difficulty}`); // Show notification about selected difficulty let message = ""; switch(difficulty) { case 'easy': message = "Easy mode: Take your time, cops move slower."; break; case 'normal': message = "Normal mode: Balanced challenge."; break; case 'hard': message = "Hard mode: Cops are faster and tougher!"; break; case 'impossible': message = "IMPOSSIBLE MODE: Only the best can escape!"; break; } // Show notification as overlay on loading screen const notification = document.createElement('div'); notification.style.position = 'absolute'; notification.style.top = '70%'; notification.style.left = '50%'; notification.style.transform = 'translate(-50%, -50%)'; notification.style.background = 'rgba(0, 0, 0, 0.7)'; notification.style.color = difficulty === 'impossible' ? '#ff0000' : 'white'; notification.style.padding = '15px'; notification.style.borderRadius = '10px'; notification.style.fontSize = '18px'; notification.style.textAlign = 'center'; notification.style.fontWeight = difficulty === 'impossible' ? 'bold' : 'normal'; notification.textContent = message; document.getElementById('loading-screen').appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); }); }); } }, 500); }, // Start the actual game startGame: function() { // Hide loading screen this.dom.loadingScreen.style.display = 'none'; // Set game phase to bank this.state.gamePhase = 'bank'; // Setup event listeners this.setupEventListeners(); // Create game environments this.createBankEnvironment(); this.createForestEnvironment(); // Create player shield this.createShield(); // Create guide path by default (auto-enabled) if (this.state.showGuidePath) { this.createGuidePath(); } // Show initial notification this.showNotification("Go to the computer and steal money! The cops are coming in 10 seconds!", 5000); // Set timer for cops arrival setTimeout(() => { if (this.state.gamePhase === 'bank') { this.showNotification("The cops have arrived! Run for the exit!", 3000); this.spawnCops(); this.state.gamePhase = 'escape'; // Update guide path to show exit if (this.state.showGuidePath) { this.createGuidePath(); } // Stop auto-money if it's running if (this.state.autoMoneyInterval) { clearInterval(this.state.autoMoneyInterval); this.state.autoMoneyInterval = null; } // Hide steal money button this.dom.stealMoneyBtn.style.display = 'none'; } }, 10000); }, // Setup Three.js components setupThreeJS: function() { // Create scene this.three.scene = new THREE.Scene(); this.three.scene.fog = new THREE.FogExp2(0x222222, 0.01); // Create camera this.three.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); this.three.camera.position.set(0, this.player.height, 0); this.three.camera.lookAt(0, this.player.height, -5); // Create renderer with alpha:true to fix black screen issues this.three.renderer = new THREE.WebGLRenderer({ canvas: this.dom.canvas, antialias: true, alpha: true // Enable transparency }); this.three.renderer.setClearColor(0x000000, 0); // Set clear color with 0 alpha this.three.renderer.setSize(window.innerWidth, window.innerHeight); this.three.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.three.renderer.shadowMap.enabled = true; this.three.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Better shadow quality // Create raycaster for interactions this.three.raycaster = new THREE.Raycaster(); // Create clock for timing this.three.clock = new THREE.Clock(); // Add lighting const ambientLight = new THREE.AmbientLight(0x404040, 0.7); this.three.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); directionalLight.position.set(50, 200, 100); directionalLight.castShadow = true; // Enhanced shadow settings directionalLight.shadow.camera.near = 0.1; directionalLight.shadow.camera.far = 300; directionalLight.shadow.camera.left = -100; directionalLight.shadow.camera.right = 100; directionalLight.shadow.camera.top = 100; directionalLight.shadow.camera.bottom = -100; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.bias = -0.0005; this.three.scene.add(directionalLight); // Add a spotlight for the computer const spotlight = new THREE.SpotLight(0xffffff, 1); spotlight.position.set(0, 5, -5); spotlight.target.position.set(0, 1.5, -5); spotlight.angle = Math.PI / 6; spotlight.penumbra = 0.2; spotlight.castShadow = true; spotlight.shadow.mapSize.width = 1024; spotlight.shadow.mapSize.height = 1024; this.three.scene.add(spotlight); this.three.scene.add(spotlight.target); }, // Create materials for the game createMaterials: function() { this.three.materials.bankFloor = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.7, metalness: 0.2 }); this.three.materials.bankWall = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.8, metalness: 0.1 }); this.three.materials.computer = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5, metalness: 0.7 }); this.three.materials.button = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 0.5, roughness: 0.4, metalness: 0.3 }); this.three.materials.copBody = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.7, metalness: 0.2 }); this.three.materials.copHead = new THREE.MeshStandardMaterial({ color: 0xFFCC99, roughness: 0.6, metalness: 0.1 }); // New materials for SWAT gear this.three.materials.copVest = new THREE.MeshStandardMaterial({ color: 0x222222, // Dark black for vest roughness: 0.5, metalness: 0.4 }); this.three.materials.copHelmet = new THREE.MeshStandardMaterial({ color: 0x444444, // Dark grey for helmet roughness: 0.4, metalness: 0.5 }); this.three.materials.tree = new THREE.MeshStandardMaterial({ color: 0x006400, roughness: 0.8, metalness: 0.1 }); this.three.materials.treeTrunk = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9, metalness: 0.05 }); this.three.materials.grass = new THREE.MeshStandardMaterial({ color: 0x7CFC00, roughness: 0.9, metalness: 0.0 }); this.three.materials.helicopter = new THREE.MeshStandardMaterial({ color: 0x000000, roughness: 0.6, metalness: 0.5 }); this.three.materials.shield = new THREE.MeshPhysicalMaterial({ color: 0x00ccff, transparent: true, opacity: 0.3, emissive: 0x00ccff, emissiveIntensity: 0.5, roughness: 0.2, metalness: 0.9, clearcoat: 1.0, clearcoatRoughness: 0.1, side: THREE.DoubleSide }); // New material for bullet impacts this.three.materials.bulletImpact = new THREE.MeshBasicMaterial({ color: 0x555555, transparent: true, opacity: 0.8 }); }, // Setup event listeners with touch-first approach setupEventListeners: function() { // Window resize handler window.addEventListener('resize', () => { this.three.camera.aspect = window.innerWidth / window.innerHeight; this.three.camera.updateProjectionMatrix(); this.three.renderer.setSize(window.innerWidth, window.innerHeight); }); // Setup performance mode toggle buttons (both in-game and menu) this.dom.performanceModeBtn = document.getElementById('performance-mode-btn'); if (this.dom.performanceModeBtn) { this.dom.performanceModeBtn.addEventListener('click', () => { this.togglePerformanceMode(); }); } this.dom.menuPerformanceModeBtn = document.getElementById('menu-performance-mode-btn'); if (this.dom.menuPerformanceModeBtn) { this.dom.menuPerformanceModeBtn.addEventListener('click', () => { this.togglePerformanceMode(); }); } // SWAT mode toggle handlers this.dom.swatModeBtn = document.getElementById('swat-mode-btn'); if (this.dom.swatModeBtn) { this.dom.swatModeBtn.addEventListener('click', () => { this.toggleSwatMode(); }); } this.dom.menuSwatModeBtn = document.getElementById('menu-swat-mode-btn'); if (this.dom.menuSwatModeBtn) { this.dom.menuSwatModeBtn.addEventListener('click', () => { this.toggleSwatMode(); }); } // Initialize controls this.controls = { isLocked: false, enabled: true, sensitivity: 0.0015, touchEnabled: true, touchSensitivity: 0.05 }; // Setup touch controls this.dom.touchOverlay.style.display = 'block'; this.dom.touchInstructions.style.display = 'block'; setTimeout(() => { this.dom.touchInstructions.style.display = 'none'; }, 5000); this.setupTouchControls(); this.setupVirtualJoystick(); this.setupActionButtons(); // Steal money button this.dom.stealMoneyBtn.addEventListener('click', () => { if (this.state.gamePhase === 'bank') { this.stealMoney(); } }); // Escape helicopter button this.dom.escapeHelicopterBtn.addEventListener('click', () => { console.log("Escape helicopter button clicked"); this.boardHelicopter(); }); // Restart button this.dom.restartBtn.addEventListener('click', () => { this.restartGame(); }); // Add keyboard support for helicopter boarding window.addEventListener('keydown', (e) => { if ((e.key === 'e' || e.key === 'E') && this.state.gamePhase === 'forest') { const distToHelicopter = new THREE.Vector3( this.three.camera.position.x, 0, this.three.camera.position.z ).distanceTo(new THREE.Vector3(0, 0, -650)); if (distToHelicopter < 30) { console.log("E key pressed near helicopter"); this.boardHelicopter(); } } // WASD and arrow key movement if (e.key === 'w' || e.key === 'W' || e.key === 'ArrowUp') { this.player.moveForward = true; } if (e.key === 's' || e.key === 'S' || e.key === 'ArrowDown') { this.player.moveBackward = true; } if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') { this.player.moveLeft = true; } if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') { this.player.moveRight = true; } // Space for jump if (e.key === ' ' && this.player.canJump) { this.player.velocity.y = 5; this.player.canJump = false; } // Debug mode toggle (only in development) if (e.key === 'b' && e.ctrlKey) { this.state.debugMode = !this.state.debugMode; this.showNotification("Debug mode: " + (this.state.debugMode ? "ON" : "OFF"), 2000); } }); // Key up events for movement window.addEventListener('keyup', (e) => { if (e.key === 'w' || e.key === 'W' || e.key === 'ArrowUp') { this.player.moveForward = false; } if (e.key === 's' || e.key === 'S' || e.key === 'ArrowDown') { this.player.moveBackward = false; } if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') { this.player.moveLeft = false; } if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') { this.player.moveRight = false; } }); // Mouse click handler this.dom.canvas.addEventListener('click', () => { // Handle shooting when clicked if (this.state.gamePhase !== 'loading' && !this.state.gameOver) { this.handleShoot(); } }); // Mouse movement handler document.addEventListener('mousemove', (event) => { this.onMouseMove(event); }); // Set up pointer lock for desktop this.setupPointerLock(); }, // Set up pointer lock API setupPointerLock: function() { this.dom.canvas.requestPointerLock = this.dom.canvas.requestPointerLock || this.dom.canvas.mozRequestPointerLock || this.dom.canvas.webkitRequestPointerLock; document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || document.webkitExitPointerLock; this.dom.canvas.addEventListener('click', () => { if (!this.controls.isLocked && this.state.gamePhase !== 'loading') { this.dom.canvas.requestPointerLock(); } }); const lockChangeHandler = () => { if (document.pointerLockElement === this.dom.canvas || document.mozPointerLockElement === this.dom.canvas || document.webkitPointerLockElement === this.dom.canvas) { this.controls.isLocked = true; } else { this.controls.isLocked = false; } }; document.addEventListener('pointerlockchange', lockChangeHandler); document.addEventListener('mozpointerlockchange', lockChangeHandler); document.addEventListener('webkitpointerlockchange', lockChangeHandler); }, // Mouse movement handler onMouseMove: function(event) { if (!this.controls.isLocked || this.state.gameOver || this.state.gamePhase === 'loading') return; const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; // Apply to camera this.player.euler.setFromQuaternion(this.three.camera.quaternion); this.player.euler.y -= movementX * this.controls.sensitivity; this.player.euler.x -= movementY * this.controls.sensitivity; // Clamp vertical rotation this.player.euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.player.euler.x)); // Apply rotation this.three.camera.quaternion.setFromEuler(this.player.euler); }, // Setup touch controls setupTouchControls: function() { // Touch state for look controls this.touchState = { active: false, lastX: 0, lastY: 0 }; // Touch start this.dom.touchOverlay.addEventListener('touchstart', (e) => { if (this.state.gameOver || this.state.gamePhase === 'loading') return; e.preventDefault(); const touch = e.touches[0]; this.touchState.active = true; this.touchState.lastX = touch.clientX; this.touchState.lastY = touch.clientY; }); // Touch move - handle camera rotation this.dom.touchOverlay.addEventListener('touchmove', (e) => { if (!this.touchState.active || this.state.gameOver || this.state.gamePhase === 'loading') return; e.preventDefault(); const touch = e.touches[0]; // Calculate movement delta const movementX = touch.clientX - this.touchState.lastX; const movementY = touch.clientY - this.touchState.lastY; // Apply smoothing const smoothedX = Math.sign(movementX) * Math.min(Math.abs(movementX), 20); const smoothedY = Math.sign(movementY) * Math.min(Math.abs(movementY), 20); // Update camera rotation this.player.euler.setFromQuaternion(this.three.camera.quaternion); this.player.euler.y -= smoothedX * (this.controls.touchSensitivity * 0.3); this.player.euler.x -= smoothedY * (this.controls.touchSensitivity * 0.3); // Clamp vertical rotation this.player.euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.player.euler.x)); // Apply rotation this.three.camera.quaternion.setFromEuler(this.player.euler); // Update last position this.touchState.lastX = touch.clientX; this.touchState.lastY = touch.clientY; }); // Touch end/cancel this.dom.touchOverlay.addEventListener('touchend', (e) => { e.preventDefault(); this.touchState.active = false; }); this.dom.touchOverlay.addEventListener('touchcancel', (e) => { e.preventDefault(); this.touchState.active = false; }); }, // Setup virtual joystick setupVirtualJoystick: function() { // Movement buttons const upBtn = document.getElementById('up-btn'); const downBtn = document.getElementById('down-btn'); const leftBtn = document.getElementById('left-btn'); const rightBtn = document.getElementById('right-btn'); // Movement button handlers upBtn.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.moveForward = true; }); upBtn.addEventListener('touchend', (e) => { e.preventDefault(); this.player.moveForward = false; }); downBtn.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.moveBackward = true; }); downBtn.addEventListener('touchend', (e) => { e.preventDefault(); this.player.moveBackward = false; }); leftBtn.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.moveLeft = true; }); leftBtn.addEventListener('touchend', (e) => { e.preventDefault(); this.player.moveLeft = false; }); rightBtn.addEventListener('touchstart', (e) => { e.preventDefault(); this.player.moveRight = true; }); rightBtn.addEventListener('touchend', (e) => { e.preventDefault(); this.player.moveRight = false; }); // Cancel events upBtn.addEventListener('touchcancel', (e) => { e.preventDefault(); this.player.moveForward = false; }); downBtn.addEventListener('touchcancel', (e) => { e.preventDefault(); this.player.moveBackward = false; }); leftBtn.addEventListener('touchcancel', (e) => { e.preventDefault(); this.player.moveLeft = false; }); rightBtn.addEventListener('touchcancel', (e) => { e.preventDefault(); this.player.moveRight = false; }); }, // Setup action buttons setupActionButtons: function() { const shootBtn = document.getElementById('shoot-btn'); const jumpBtn = document.getElementById('jump-btn'); // Shoot button shootBtn.addEventListener('touchstart', (e) => { e.preventDefault(); if (this.state.nearComputer && this.state.gamePhase === 'bank') { this.stealMoney(); } else { this.handleShoot(); } }); // Jump button jumpBtn.addEventListener('touchstart', (e) => { e.preventDefault(); if (this.player.canJump) { this.player.velocity.y = 5; this.player.canJump = false; } }); }, // Create bank environment createBankEnvironment: function() { // Bank floor const floor = new THREE.Mesh( new THREE.BoxGeometry(50, 0.1, 50), this.three.materials.bankFloor ); floor.position.y = -0.05; floor.receiveShadow = true; this.three.scene.add(floor); // IMPROVED BANK WALLS WITH COLLISION HANDLING // Bank walls are now thicker and have proper collision volumes // Bank walls collection for collision detection this.bankWalls = []; // Back wall const backWall = new THREE.Mesh( new THREE.BoxGeometry(50, 10, 1), // Thicker wall this.three.materials.bankWall ); backWall.position.set(0, 5, -25); backWall.castShadow = true; backWall.receiveShadow = true; this.three.scene.add(backWall); this.bankWalls.push({ mesh: backWall, minX: -25, maxX: 25, minY: 0, maxY: 10, minZ: -25.5, maxZ: -24.5, normal: new THREE.Vector3(0, 0, 1) // Normal facing into the bank }); // Front wall with doorway const frontWallLeft = new THREE.Mesh( new THREE.BoxGeometry(20, 10, 1), this.three.materials.bankWall ); frontWallLeft.position.set(-15, 5, 25); frontWallLeft.castShadow = true; frontWallLeft.receiveShadow = true; this.three.scene.add(frontWallLeft); this.bankWalls.push({ mesh: frontWallLeft, minX: -25, maxX: -5, minY: 0, maxY: 10, minZ: 24.5, maxZ: 25.5, normal: new THREE.Vector3(0, 0, -1) }); const frontWallRight = new THREE.Mesh( new THREE.BoxGeometry(20, 10, 1), this.three.materials.bankWall ); frontWallRight.position.set(15, 5, 25); frontWallRight.castShadow = true; frontWallRight.receiveShadow = true; this.three.scene.add(frontWallRight); this.bankWalls.push({ mesh: frontWallRight, minX: 5, maxX: 25, minY: 0, maxY: 10, minZ: 24.5, maxZ: 25.5, normal: new THREE.Vector3(0, 0, -1) }); const frontWallTop = new THREE.Mesh( new THREE.BoxGeometry(10, 5, 1), this.three.materials.bankWall ); frontWallTop.position.set(0, 7.5, 25); frontWallTop.castShadow = true; frontWallTop.receiveShadow = true; this.three.scene.add(frontWallTop); this.bankWalls.push({ mesh: frontWallTop, minX: -5, maxX: 5, minY: 5, maxY: 10, minZ: 24.5, maxZ: 25.5, normal: new THREE.Vector3(0, 0, -1) }); // Left wall const leftWall = new THREE.Mesh( new THREE.BoxGeometry(1, 10, 50), this.three.materials.bankWall ); leftWall.position.set(-25, 5, 0); leftWall.castShadow = true; leftWall.receiveShadow = true; this.three.scene.add(leftWall); this.bankWalls.push({ mesh: leftWall, minX: -25.5, maxX: -24.5, minY: 0, maxY: 10, minZ: -25, maxZ: 25, normal: new THREE.Vector3(1, 0, 0) }); // Right wall const rightWall = new THREE.Mesh( new THREE.BoxGeometry(1, 10, 50), this.three.materials.bankWall ); rightWall.position.set(25, 5, 0); rightWall.castShadow = true; rightWall.receiveShadow = true; this.three.scene.add(rightWall); this.bankWalls.push({ mesh: rightWall, minX: 24.5, maxX: 25.5, minY: 0, maxY: 10, minZ: -25, maxZ: 25, normal: new THREE.Vector3(-1, 0, 0) }); // Create computer with money button const desk = new THREE.Mesh( new THREE.BoxGeometry(3, 1, 2), this.three.materials.treeTrunk ); desk.position.set(0, 0.5, -5); desk.castShadow = true; desk.receiveShadow = true; this.three.scene.add(desk); const computer = new THREE.Mesh( new THREE.BoxGeometry(2, 1.5, 0.5), this.three.materials.computer ); computer.position.set(0, 1.75, -5); computer.castShadow = true; computer.receiveShadow = true; computer.name = "computer"; this.three.scene.add(computer); this.three.computer = computer; // Create money button const buttonGeometry = new THREE.BoxGeometry(1.5, 0.4, 1.0); const button = new THREE.Mesh( buttonGeometry, this.three.materials.button ); button.position.set(0, 1.5, -4.6); button.castShadow = true; button.name = "moneyButton"; this.three.scene.add(button); this.three.moneyButton = button; // Create button text const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 128; const context = canvas.getContext('2d'); context.fillStyle = 'black'; context.font = 'Bold 40px Arial'; context.textAlign = 'center'; context.fillText('+$1', 128, 70); const buttonTexture = new THREE.CanvasTexture(canvas); const buttonTextMaterial = new THREE.MeshBasicMaterial({ map: buttonTexture, transparent: true }); const buttonText = new THREE.Mesh( new THREE.PlaneGeometry(1.2, 0.4), buttonTextMaterial ); buttonText.position.set(0, 0, 0.51); button.add(buttonText); // Create a proximity trigger for the computer const computerProximity = new THREE.Mesh( new THREE.SphereGeometry(3, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.0 }) ); computerProximity.position.set(0, 1, -5); computerProximity.name = "computerProximity"; this.three.scene.add(computerProximity); // Add a highlight effect to button const buttonHighlight = () => { // Pulsate the button const time = Date.now() * 0.002; const scale = 1.0 + Math.sin(time * 3) * 0.1; button.scale.set(scale, scale, scale); // Rotate slightly button.rotation.z = Math.sin(time) * 0.1; // Change emissive intensity const material = button.material; material.emissiveIntensity = 0.5 + Math.sin(time * 2) * 0.3; // Continue animation if in bank phase if (this.state.gamePhase === 'bank') { requestAnimationFrame(buttonHighlight); } }; // Start button animation buttonHighlight(); // Enable auto-money collection after 5 seconds setTimeout(() => { if (this.state.gamePhase === 'bank' && !this.state.autoMoneyEnabled) { this.state.autoMoneyEnabled = true; this.state.autoMoneyInterval = setInterval(() => { if (this.state.nearComputer && this.state.gamePhase === 'bank') { this.stealMoney(); } }, 500); this.showNotification("Auto-money enabled: Your character will automatically steal money when near the computer!", 5000); } }, 5000); }, // Create forest environment createForestEnvironment: function() { // ENHANCED FIX FOR BLACK GROUND: Create multiple overlapping ground planes with higher density // Main ground - increased segments and size const mainGround = new THREE.Mesh( new THREE.PlaneGeometry(1500, 1500, 200, 200), // Increased size and segments this.three.materials.grass ); mainGround.rotation.x = -Math.PI / 2; mainGround.position.y = -0.01; mainGround.receiveShadow = true; this.three.scene.add(mainGround); // Additional helicopter area ground with higher density and size const helipadGround = new THREE.Mesh( new THREE.PlaneGeometry(300, 300, 80, 80), // More segments, larger area this.three.materials.grass ); helipadGround.rotation.x = -Math.PI / 2; helipadGround.position.set(0, -0.005, -650); helipadGround.receiveShadow = true; this.three.scene.add(helipadGround); // Extra ground sections to eliminate any black areas // Far-end ground extension const farGround = new THREE.Mesh( new THREE.PlaneGeometry(800, 300, 100, 100), // Much larger with more segments this.three.materials.grass ); farGround.rotation.x = -Math.PI / 2; farGround.position.set(0, -0.008, -800); farGround.receiveShadow = true; this.three.scene.add(farGround); // Extreme far-end ground to fully eliminate black edges const extremeFarGround = new THREE.Mesh( new THREE.PlaneGeometry(1000, 400, 100, 100), this.three.materials.grass ); extremeFarGround.rotation.x = -Math.PI / 2; extremeFarGround.position.set(0, -0.012, -1000); extremeFarGround.receiveShadow = true; this.three.scene.add(extremeFarGround); // Additional ground segments on sides to prevent black areas at edges const leftGround = new THREE.Mesh( new THREE.PlaneGeometry(400, 800, 80, 80), this.three.materials.grass ); leftGround.rotation.x = -Math.PI / 2; leftGround.position.set(-400, -0.009, -400); leftGround.receiveShadow = true; this.three.scene.add(leftGround); const rightGround = new THREE.Mesh( new THREE.PlaneGeometry(400, 800, 80, 80), this.three.materials.grass ); rightGround.rotation.x = -Math.PI / 2; rightGround.position.set(400, -0.009, -400); rightGround.receiveShadow = true; this.three.scene.add(rightGround); // Create trees this.createForest(); // Create helicopter landing zone const landingZone = new THREE.Mesh( new THREE.CircleGeometry(10, 32), new THREE.MeshStandardMaterial({ color: 0x333333 }) ); landingZone.rotation.x = -Math.PI / 2; landingZone.position.set(0, 0.01, -650); this.three.scene.add(landingZone); // Create helicopter this.createHelicopter(); // Create a bright spotlight above the helicopter pad const heliSpotlight = new THREE.SpotLight(0xffffff, 5); heliSpotlight.position.set(0, 50, -650); heliSpotlight.target.position.set(0, 0, -650); heliSpotlight.angle = Math.PI / 10; heliSpotlight.penumbra = 0.2; heliSpotlight.decay = 0; heliSpotlight.distance = 100; this.three.scene.add(heliSpotlight); this.three.scene.add(heliSpotlight.target); // Add colorful marker lights around helicopter pad const colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00]; for (let i = 0; i < 8; i++) { const angle = (i / 8) * Math.PI * 2; const x = Math.cos(angle) * 12; const z = -650 + Math.sin(angle) * 12; const markerLight = new THREE.PointLight(colors[i % 4], 1, 20); markerLight.position.set(x, 0.5, z); this.three.scene.add(markerLight); // Add a visible sphere for the light const markerSphere = new THREE.Mesh( new THREE.SphereGeometry(0.5, 16, 16), new THREE.MeshBasicMaterial({ color: colors[i % 4] }) ); markerSphere.position.copy(markerLight.position); this.three.scene.add(markerSphere); // Animate height and intensity for blinking effect const markerGroup = new THREE.Group(); markerGroup.add(markerLight); markerGroup.add(markerSphere); markerGroup.userData = { baseY: 0.5, offset: i * 0.3, animate: function(time) { const height = this.baseY + Math.sin(time * 3 + this.offset) * 0.3; markerLight.position.y = height; markerSphere.position.y = height; markerLight.intensity = 0.7 + Math.sin(time * 5 + this.offset) * 0.3; } }; this.three.scene.add(markerGroup); } // Create path markers this.createHelicopterPathMarkers(); }, // Create path markers to helicopter createHelicopterPathMarkers: function() { // Create path markers to the helicopter const pathMarkers = 20; const startPos = new THREE.Vector3(0, 0, 20); const endPos = new THREE.Vector3(0, 0, -650); for (let i = 0; i < pathMarkers; i++) { const t = Math.pow(i / (pathMarkers - 1), 1.5); const pos = new THREE.Vector3(); pos.lerpVectors(startPos, endPos, t); pos.x += (Math.random() - 0.5) * 10; // Create arrow marker const arrowMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.8 }); const arrowGroup = new THREE.Group(); // Arrow body const arrowBody = new THREE.Mesh( new THREE.CylinderGeometry(0.1, 0.1, 1, 8), arrowMaterial ); arrowBody.rotation.x = Math.PI / 2; arrowBody.position.set(0, 0, -0.5); arrowGroup.add(arrowBody); // Arrow head const arrowHead = new THREE.Mesh( new THREE.ConeGeometry(0.3, 0.7, 8), arrowMaterial ); arrowHead.rotation.x = Math.PI / 2; arrowHead.position.set(0, 0, -1.3); arrowGroup.add(arrowHead); // Position the arrow arrowGroup.position.set(pos.x, 3 + Math.sin(i) * 0.5, pos.z); // Add animation data arrowGroup.userData = { baseY: arrowGroup.position.y, offset: i * 0.3, animate: function(time) { // Hover animation arrowGroup.position.y = this.baseY + Math.sin(time * 2 + this.offset) * 0.3; // Point towards helicopter arrowGroup.lookAt(endPos.x, arrowGroup.position.y, endPos.z); } }; this.three.scene.add(arrowGroup); } }, // Create a dense forest createForest: function() { // Create trees in the forest (simplified) const treeCount = 600; // Create trees for (let i = 0; i < treeCount; i++) { const treeX = Math.random() * 600 - 300; const treeZ = -50 - Math.random() * 550; // Skip trees near bank or helicopter pad if ((Math.abs(treeX) < 30 && Math.abs(treeZ) < 30) || (Math.abs(treeX) < 15 && Math.abs(treeZ + 650) < 15)) { continue; } // Create tree this.createTree(treeX, 0, treeZ, 0.7 + Math.random() * 0.6); } }, // Create a tree with guaranteed visibility and shadows createTree: function(x, y, z, sizeFactor = 1.0) { const trunkHeight = (1.5 + Math.random() * 2.5) * sizeFactor; const trunkRadius = (0.15 + Math.random() * 0.25) * sizeFactor; const leafRadius = (0.8 + Math.random() * 1.7) * sizeFactor; // Create trunk with much more vivid colors (almost cartoon-like for visibility) const trunk = new THREE.Mesh( new THREE.CylinderGeometry(trunkRadius, trunkRadius * 1.2, trunkHeight, 8), new THREE.MeshLambertMaterial({ color: 0xA0522D, // Richer brown emissive: 0x3D2314, // Slight emissive glow emissiveIntensity: 0.3 }) ); trunk.position.set(x, y + trunkHeight / 2, z); trunk.castShadow = true; trunk.receiveShadow = true; // Add rim highlighting to trunk for better visibility const trunkOutline = new THREE.Mesh( new THREE.CylinderGeometry(trunkRadius * 1.05, trunkRadius * 1.25, trunkHeight * 1.01, 8), new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.7, side: THREE.BackSide }) ); trunkOutline.position.copy(trunk.position); this.three.scene.add(trunk); this.three.scene.add(trunkOutline); // Create leaves with extremely bright and visible colors const leaves = new THREE.Mesh( new THREE.SphereGeometry(leafRadius, 8, 8), new THREE.MeshLambertMaterial({ color: 0x32CD32, // Bright lime green emissive: 0x006400, // Dark green emissive emissiveIntensity: 0.4 }) ); leaves.position.set(x, y + trunkHeight + leafRadius * 0.5, z); leaves.castShadow = true; leaves.receiveShadow = true; // Add outline to leaves for visibility at distance const leavesOutline = new THREE.Mesh( new THREE.SphereGeometry(leafRadius * 1.05, 8, 8), new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.7, side: THREE.BackSide }) ); leavesOutline.position.copy(leaves.position); this.three.scene.add(leaves); this.three.scene.add(leavesOutline); // Create collision box const treeCollider = new THREE.Mesh( new THREE.CylinderGeometry(leafRadius, trunkRadius, trunkHeight + leafRadius * 2, 8), new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 }) ); treeCollider.position.set(x, y + (trunkHeight + leafRadius) / 2, z); treeCollider.name = "tree"; treeCollider.visible = false; this.three.scene.add(treeCollider); // Add to game state this.state.trees.push({ trunk, leaves, trunkOutline, leavesOutline, collider: treeCollider, position: new THREE.Vector3(x, y, z), radius: Math.max(leafRadius, trunkRadius), height: trunkHeight + leafRadius * 2 }); }, // Create helicopter createHelicopter: function() { const helicopter = new THREE.Group(); // Create more detailed helicopter with better colors // Body const body = new THREE.Mesh( new THREE.BoxGeometry(3, 1.5, 6), new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 0.5 }) ); body.castShadow = true; body.receiveShadow = true; helicopter.add(body); // Tail boom const tailBoom = new THREE.Mesh( new THREE.BoxGeometry(0.5, 0.5, 4), new THREE.MeshStandardMaterial({ color: 0xff0000 }) ); tailBoom.position.set(0, 0.5, -5); tailBoom.castShadow = true; tailBoom.receiveShadow = true; helicopter.add(tailBoom); // Main rotor const mainRotor = new THREE.Mesh( new THREE.BoxGeometry(10, 0.1, 0.5), new THREE.MeshStandardMaterial({ color: 0x888888 }) ); mainRotor.position.set(0, 1.5, 0); mainRotor.castShadow = true; mainRotor.receiveShadow = true; helicopter.add(mainRotor); // Tail rotor const tailRotor = new THREE.Mesh( new THREE.BoxGeometry(0.1, 2, 0.2), new THREE.MeshStandardMaterial({ color: 0x888888 }) ); tailRotor.position.set(0, 1, -7); tailRotor.castShadow = true; tailRotor.receiveShadow = true; helicopter.add(tailRotor); // Landing skids const leftSkid = new THREE.Mesh( new THREE.BoxGeometry(0.2, 0.2, 4), new THREE.MeshStandardMaterial({ color: 0x888888 }) ); leftSkid.position.set(-1.5, -1, 0); helicopter.add(leftSkid); const rightSkid = new THREE.Mesh( new THREE.BoxGeometry(0.2, 0.2, 4), new THREE.MeshStandardMaterial({ color: 0x888888 }) ); rightSkid.position.set(1.5, -1, 0); helicopter.add(rightSkid); // Add cockpit windows const cockpitGlass = new THREE.Mesh( new THREE.BoxGeometry(2.8, 1, 2), new THREE.MeshPhysicalMaterial({ color: 0x88ccff, transparent: true, opacity: 0.7, metalness: 0.2, roughness: 0.1 }) ); cockpitGlass.position.set(0, 0.5, 1.5); helicopter.add(cockpitGlass); // Add a passenger seat for player to sit in const passengerSeat = new THREE.Mesh( new THREE.BoxGeometry(1, 0.5, 1), new THREE.MeshStandardMaterial({ color: 0x333333 }) ); passengerSeat.position.set(0.8, 0, 0.5); helicopter.add(passengerSeat); // Add a bright beacon light on top const beaconGeometry = new THREE.SphereGeometry(0.5, 16, 16); const beaconMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); const beacon = new THREE.Mesh(beaconGeometry, beaconMaterial); beacon.position.set(0, 2.5, 0); helicopter.add(beacon); // Add point lights to make helicopter more visible const frontLight = new THREE.PointLight(0xff0000, 1, 10); frontLight.position.set(0, 0, 3); helicopter.add(frontLight); const rearLight = new THREE.PointLight(0x00ff00, 1, 10); rearLight.position.set(0, 0, -7); helicopter.add(rearLight); // Add animation data helicopter.userData = { mainRotorAngle: 0, tailRotorAngle: 0, animateRotors: function(delta) { this.mainRotorAngle += delta * 10; this.tailRotorAngle += delta * 15; mainRotor.rotation.y = this.mainRotorAngle; tailRotor.rotation.z = this.tailRotorAngle; // Animate the beacon light const time = Date.now() * 0.001; beacon.scale.set( 1 + Math.sin(time * 5) * 0.2, 1 + Math.sin(time * 5) * 0.2, 1 + Math.sin(time * 5) * 0.2 ); // Make navigation lights blink frontLight.intensity = Math.abs(Math.sin(time * 8)); rearLight.intensity = Math.abs(Math.sin(time * 8 + Math.PI)); } }; // Position helicopter helicopter.position.set(0, 2, -650); helicopter.name = "helicopter"; this.three.scene.add(helicopter); // Store reference to helicopter this.helicopter = helicopter; return helicopter; }, // Board helicopter function - IMPROVED TO KEEP PLAYER WITH HELICOPTER boardHelicopter: function() { console.log("Attempting to board helicopter"); // Double check that we're in forest phase and close to helicopter const distToHelicopter = this.three.camera.position.distanceTo(new THREE.Vector3(0, this.player.height, -650)); if (this.state.gamePhase !== 'forest' || distToHelicopter > 30) { return; } // Change game phase this.state.gamePhase = 'helicopter'; // Hide escape button this.dom.escapeHelicopterBtn.style.display = 'none'; // Show notification this.showNotification("Boarding helicopter! Preparing for takeoff...", 3000); // Get helicopter reference const helicopter = this.helicopter; if (!helicopter) { console.error("Could not find helicopter!"); // Create a new one if not found (fallback) const newHelicopter = this.createHelicopter(); this.helicopter = newHelicopter; setTimeout(() => { this.boardHelicopter(); // Try again with new helicopter }, 500); return; } // Move player into helicopter (passenger seat position) const helicopterOffset = new THREE.Vector3(0.8, 0.5, 0); this.three.camera.position.set( helicopter.position.x + helicopterOffset.x, helicopter.position.y + helicopterOffset.y + this.player.height, helicopter.position.z + helicopterOffset.z ); // Store offset from helicopter for maintaining position during flight this.state.playerInHelicopterOffset = new THREE.Vector3( helicopterOffset.x, helicopterOffset.y + this.player.height, helicopterOffset.z ); // Mark that player is in helicopter this.state.inHelicopter = true; // Helicopter takeoff animation const startPos = helicopter.position.clone(); const startTime = performance.now(); const duration = 8000; // 8 seconds // Animation function const animateTakeoff = () => { const now = performance.now(); const elapsed = now - startTime; const t = Math.min(elapsed / duration, 1); // Ease-out function const progress = 1 - Math.pow(1 - t, 3); // Move helicopter up helicopter.position.y = startPos.y + progress * 80; // After 50% of animation, start moving forward if (progress > 0.5) { const forwardMotion = (progress - 0.5) * 2; // 0-1 scale helicopter.position.z = startPos.z - forwardMotion * 50; // Tilt helicopter forward helicopter.rotation.x = -Math.PI * 0.15 * forwardMotion; } // Move player with helicopter maintaining offset // Calculate rotation matrix based on helicopter rotation const rotationMatrix = new THREE.Matrix4().makeRotationFromEuler(helicopter.rotation); const rotatedOffset = this.state.playerInHelicopterOffset.clone(); rotatedOffset.applyMatrix4(rotationMatrix); // Apply player position this.three.camera.position.set( helicopter.position.x + rotatedOffset.x, helicopter.position.y + rotatedOffset.y, helicopter.position.z + rotatedOffset.z ); // Make camera look in helicopter forward direction with slight down angle const lookTarget = new THREE.Vector3(0, 0, -1); lookTarget.applyQuaternion(helicopter.quaternion); this.three.camera.lookAt( this.three.camera.position.x + lookTarget.x, this.three.camera.position.y + lookTarget.y, this.three.camera.position.z + lookTarget.z ); // Animate rotors at higher speed during takeoff if (helicopter.userData && helicopter.userData.animateRotors) { helicopter.userData.animateRotors(0.05); // Faster rotation } // Continue animation until completion if (t < 1) { requestAnimationFrame(animateTakeoff); } else { // Win after animation completes setTimeout(() => { this.handleGameOver(true); }, 1000); } }; // Start takeoff animation animateTakeoff(); }, // Create player shield createShield: function() { const shieldGeometry = new THREE.CircleGeometry(0.5, 32); const shield = new THREE.Mesh(shieldGeometry, this.three.materials.shield); shield.position.set(0, 0, -0.5); this.three.camera.add(shield); this.three.scene.add(this.three.camera); }, // Create a cop with consistent height and minimal geometry createCop: function(x, y, z, type = 'standard') { // Get difficulty settings const diffSettings = this.state.difficultySettings[this.state.difficultyLevel]; // Define different cop types with scaling based on difficulty const copTypes = { standard: { bodyColor: 0x0000ff, speed: diffSettings.copSpeed, shootDelay: diffSettings.shootDelay, accuracy: diffSettings.shootAccuracy, health: Math.max(1, Math.round(1 * diffSettings.copHealth)), height: 2.1 // Increased standard cop height }, elite: { bodyColor: 0xff0000, // Pure red for visibility speed: diffSettings.copSpeed * 1.25, shootDelay: diffSettings.shootDelay * 0.85, accuracy: diffSettings.shootAccuracy * 1.15, health: Math.max(1, Math.round(2 * diffSettings.copHealth)), height: 2.2 // Increased elite cop height }, commander: { bodyColor: 0xffff00, // Pure yellow for visibility speed: diffSettings.copSpeed, shootDelay: diffSettings.shootDelay * 0.9, accuracy: diffSettings.shootAccuracy * 1.2, health: Math.max(1, Math.round(3 * diffSettings.copHealth)), height: 2.3 // Increased commander height } }; // Get cop specs based on type const specs = copTypes[type] || copTypes.standard; // Calculate consistent dimensions based on height const bodyHeight = specs.height * 0.75; // Body is 75% of total height const bodyRadius = 0.3; const headRadius = 0.25; // Calculate positions to ensure consistent total height const bodyY = y + bodyHeight / 2; // Position body with bottom at ground level const headY = y + bodyHeight + headRadius; // Position head above body const gunY = y + bodyHeight * 0.6; // Position gun at appropriate height on body // Create body - use basic material for best performance const bodyMaterial = new THREE.MeshBasicMaterial({ color: specs.bodyColor }); // Create taller, more consistent body const body = new THREE.Mesh( new THREE.CylinderGeometry(bodyRadius, bodyRadius, bodyHeight, 8), bodyMaterial ); body.position.set(x, bodyY, z); this.three.scene.add(body); // Create head with consistent size const headMaterial = new THREE.MeshBasicMaterial({ color: 0xffffcc // Light skin tone }); const head = new THREE.Mesh( new THREE.SphereGeometry(headRadius, 8, 8), headMaterial ); head.position.set(x, headY, z); this.three.scene.add(head); // Create gun const gunMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 // Black for visibility }); const gun = new THREE.Mesh( new THREE.BoxGeometry(0.1, 0.1, 0.3), gunMaterial ); gun.position.set(x + 0.3, gunY, z); this.three.scene.add(gun); // Create cop object with original structure for compatibility const cop = { body: body, head: head, gun: gun, position: new THREE.Vector3(x, y, z), direction: new THREE.Vector3(0, 0, 1), health: specs.health, speed: specs.speed, shootDelay: specs.shootDelay, accuracy: specs.accuracy, type: type, height: specs.height, // Store cop height dead: false, lastShot: 0, aimTime: 0, isAiming: false, state: 'chase', // chase, flank, cover, retreat tacticalPosition: null, // Target position from AI lastWallCollision: 0, // Timestamp of last collision parts: { body, head, gun } }; // Apply SWAT gear if mode is active if (this.state.swatMode) { // Create vest const vestGeometry = new THREE.CylinderGeometry(0.33, 0.33, 0.9, 8); const vest = new THREE.Mesh( vestGeometry, new THREE.MeshBasicMaterial({ color: this.state.swatAdjustments.vestColor }) ); vest.position.copy(cop.body.position); vest.position.y -= 0.1; this.three.scene.add(vest); cop.vest = vest; // Create helmet const helmetGeometry = new THREE.SphereGeometry(0.28, 8, 8); const helmet = new THREE.Mesh( helmetGeometry, new THREE.MeshBasicMaterial({ color: this.state.swatAdjustments.helmColor }) ); helmet.position.copy(cop.head.position); this.three.scene.add(helmet); cop.helmet = helmet; // Apply enhanced speed and fire rate cop.speed *= this.state.swatAdjustments.speedMultiplier; // 25% faster cop.shootDelay *= this.state.swatAdjustments.fireRateMultiplier; // 15% faster firing cop.bulletProofVest = true; // Store original values for toggling cop.originalSpeed = cop.speed / this.state.swatAdjustments.speedMultiplier; cop.originalShootDelay = cop.shootDelay / this.state.swatAdjustments.fireRateMultiplier; } this.state.cops.push(cop); return cop; }, // Check if all cops are too far from player (adding new failsafe mechanism) checkForFarCops: function() { // Only check periodically to reduce performance impact const now = performance.now(); if (now - this.state.swatAdjustments.lastFarCopCheck < this.state.swatAdjustments.farCopCheckInterval) { return; } this.state.swatAdjustments.lastFarCopCheck = now; // Only applicable in forest phase if (this.state.gamePhase !== 'forest' && this.state.gamePhase !== 'escape') { return; } const playerPos = this.three.camera.position; let allCopsFar = true; let aliveCopCount = 0; // Check all alive cops for (const cop of this.state.cops) { if (cop.dead) continue; aliveCopCount++; const distToPlayer = cop.body.position.distanceTo(playerPos); // If any cop is within range, we don't need to spawn more if (distToPlayer < this.state.swatAdjustments.maxFarCopDistance) { allCopsFar = false; break; } } // If all cops are too far away or there are none alive, deploy reinforcements if ((allCopsFar && aliveCopCount > 0) || aliveCopCount === 0) { this.deployReinforcementsNearPlayer(); } }, // Deploy close-range reinforcements for the failsafe mechanism deployReinforcementsNearPlayer: function() { console.log("Deploying reinforcements near player - all cops too far away"); this.showNotification("🚨 ALERT: Police reinforcements deploying nearby! 🚨", 3000); const playerPos = this.three.camera.position; const copCount = 7; // Exactly 7 new cops as requested const spawnRadius = 5; // 5m radius from player // Reinforcement types (mix of cop types) const copTypes = ['standard', 'standard', 'standard', 'standard', 'elite', 'elite', 'commander']; // Spawn cops in a circle around player for (let i = 0; i < copCount; i++) { const angle = (i / copCount) * Math.PI * 2; const distance = 5 + Math.random(); // Distance between 5-6m const x = playerPos.x + Math.cos(angle) * distance; const z = playerPos.z + Math.sin(angle) * distance; // Create cop with appropriate type const cop = this.createCop(x, 0, z, copTypes[i]); // Add spawn effect const spawnEffect = new THREE.Mesh( new THREE.SphereGeometry(1.5, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8 }) ); spawnEffect.position.copy(cop.body.position); this.three.scene.add(spawnEffect); // Animate and remove effect const startTime = performance.now(); const duration = 1000; // 1 second effect const animateSpawn = () => { const elapsed = performance.now() - startTime; const t = elapsed / duration; if (t < 1) { spawnEffect.scale.set(1 + t * 3, 1 + t * 3, 1 + t * 3); spawnEffect.material.opacity = 0.8 * (1 - t); requestAnimationFrame(animateSpawn); } else { this.three.scene.remove(spawnEffect); } }; animateSpawn(); // Set cop to aggressive chase state cop.state = 'chase'; cop.tacticalPosition = new THREE.Vector3(playerPos.x, 0, playerPos.z); } }, // Check if a point is inside a wall (for both player and cop collision) isPointInWall: function(position) { if (this.state.gamePhase !== 'bank' && this.state.gamePhase !== 'escape') { return false; } // Check each wall for collision for (const wall of this.bankWalls) { // Check if point is within wall bounds - added small buffer for better collision if (position.x >= wall.minX - 0.1 && position.x <= wall.maxX + 0.1 && position.y >= wall.minY && position.y <= wall.maxY && position.z >= wall.minZ - 0.1 && position.z <= wall.maxZ + 0.1) { // Debug visualization if debug mode is on if (this.state.debugMode) { this.showNotification("Wall collision detected!", 500); } return wall; // Return the wall that was hit } } // Special case for the doorway - check if trying to exit through walls if (position.z > 24.5 && (position.x < -5 || position.x > 5 || position.y > 5)) { return this.bankWalls[2]; // Return front wall } return false; // No collision }, // Advanced bullet-to-wall intersection test using raycasting isBulletHittingWall: function(bullet) { if (this.state.gamePhase !== 'bank' && this.state.gamePhase !== 'escape') { return false; } // Create a ray from the bullet's current position in its direction of travel const bulletPosition = bullet.mesh.position.clone(); const bulletDirection = bullet.direction.clone(); // Create a raycaster const raycaster = new THREE.Raycaster(bulletPosition, bulletDirection); // Get the walls to test against const wallMeshes = this.bankWalls.map(wall => wall.mesh); // Use a much higher testing distance to reliably catch all walls // This fixes bullets passing through walls by increasing detection range const testDistance = bullet.speed * 0.05; // Increased from 0.015 // Check for intersections with walls const intersections = raycaster.intersectObjects(wallMeshes); if (intersections.length > 0 && intersections[0].distance < testDistance) { // Return the wall and the point of impact if (this.state.debugMode) { this.showNotification("Bullet hit wall!", 300); } return { wall: this.bankWalls.find(wall => wall.mesh === intersections[0].object), point: intersections[0].point, face: intersections[0].face, normal: intersections[0].face ? intersections[0].face.normal : null }; } return false; }, // Create a bullet createBullet: function(position, direction, isEnemy = false, accuracy = 1.0) { // Apply accuracy variation to enemy bullets if (isEnemy && accuracy < 1.0) { // Add random deviation based on inverse of accuracy const deviation = (1 - accuracy) * 0.2; direction.x += (Math.random() * 2 - 1) * deviation; direction.y += (Math.random() * 2 - 1) * deviation; direction.z += (Math.random() * 2 - 1) * deviation; direction.normalize(); } const bullet = new THREE.Mesh( new THREE.SphereGeometry(0.05, 8, 8), new THREE.MeshBasicMaterial({ color: isEnemy ? 0xff0000 : 0xffff00 }) ); // Position slightly in front to avoid self-collision const offset = direction.clone().multiplyScalar(isEnemy ? 0.5 : 0.5); bullet.position.copy(position).add(offset); // Check if this bullet will immediately hit a wall const initialRay = new THREE.Raycaster(position, direction, 0, 1); const wallMeshes = this.bankWalls.map(wall => wall.mesh); const initialHits = initialRay.intersectObjects(wallMeshes); // If bullet would spawn inside or hitting a wall, don't create it if (initialHits.length > 0 && initialHits[0].distance < 0.5) { // Create impact effect at this position instead this.createBulletImpact(initialHits[0].point, initialHits[0].face.normal); return null; } this.three.scene.add(bullet); const bulletObj = { mesh: bullet, direction: direction.clone().normalize(), speed: 30, isEnemy, age: 0, maxAge: 3, // seconds previousPosition: position.clone() // Store previous position for better collision }; if (isEnemy) { this.state.enemyBullets.push(bulletObj); } else { this.state.bullets.push(bulletObj); } return bulletObj; }, // Create bullet impact effect - optimized version createBulletImpact: function(position, normal) { // Create a circular impact decal const impactGeometry = new THREE.CircleGeometry(0.1 + Math.random() * 0.05, 8); const impact = new THREE.Mesh( impactGeometry, this.three.materials.bulletImpact ); // Position at hit point with small offset to prevent z-fighting impact.position.copy(position); impact.position.addScaledVector(normal, 0.01); // Orient facing outward from wall impact.lookAt( impact.position.x + normal.x, impact.position.y + normal.y, impact.position.z + normal.z ); this.three.scene.add(impact); // Remove after some time - use shorter duration to prevent memory issues setTimeout(() => { this.three.scene.remove(impact); }, 5000); // Fixed shorter duration // Reduced particle effect chance and count to improve performance if (Math.random() < 0.3) { // Reduced from 0.7 const particleCount = 2 + Math.floor(Math.random() * 2); // Reduced from 5+5 // Reuse geometries and materials for particles const particleGeometry = new THREE.SphereGeometry(0.02, 4, 4); const particleMaterial = new THREE.MeshBasicMaterial({ color: 0x888888, transparent: true, opacity: 0.8 }); for (let i = 0; i < particleCount; i++) { const particle = new THREE.Mesh(particleGeometry, particleMaterial); // Start at impact position particle.position.copy(position); // Create a random direction biased toward the normal const particleDir = normal.clone(); particleDir.x += (Math.random() - 0.5) * 2; particleDir.y += (Math.random() - 0.5) * 2; particleDir.z += (Math.random() - 0.5) * 2; particleDir.normalize(); // Add particle to scene this.three.scene.add(particle); // Animate particle with fewer animation frames const speed = 1 + Math.random() * 2; const startTime = performance.now(); const duration = 200; // Fixed shorter duration const animateParticle = () => { const now = performance.now(); const elapsed = now - startTime; const t = elapsed / duration; if (t < 1) { // Move particle outward particle.position.addScaledVector(particleDir, speed * 0.02); // Apply gravity particle.position.y -= 0.01; // Fade out particle.material.opacity = 0.8 * (1 - t); // Use less frequent animation frames - only request every other frame setTimeout(() => requestAnimationFrame(animateParticle), 32); } else { this.three.scene.remove(particle); } }; animateParticle(); } } }, // Handle shooting and interaction handleShoot: function() { if (this.state.nearComputer && this.state.gamePhase === 'bank') { this.stealMoney(); return; } if (this.state.gamePhase !== 'bank' && this.state.gamePhase !== 'loading' && !this.state.inHelicopter) { // Fire gun const now = performance.now(); if (now - this.state.lastShot > this.player.shootCooldown) { const direction = new THREE.Vector3(); this.three.camera.getWorldDirection(direction); const bulletPosition = this.three.camera.position.clone(); bulletPosition.y -= 0.2; // Adjust for gun position this.createBullet(bulletPosition, direction); this.state.lastShot = now; } } }, // Steal money function stealMoney: function() { const now = performance.now(); if (now - this.state.lastMoneySteal > 100) { // Limit how quickly money can be stolen this.state.money += 1; this.updateMoneyDisplay(); // Animate money button if (this.three.moneyButton) { this.three.moneyButton.position.y -= 0.05; setTimeout(() => { if (this.three.moneyButton) { this.three.moneyButton.position.y += 0.05; } }, 100); } // Show feedback this.showNotification("+$1", 300); this.state.lastMoneySteal = now; } }, // Spawn cops at the entrance with tactical formations (reduced count for better performance) spawnCops: function() { // Create fewer cops for better performance // Lower cop count based on if performance mode is enabled const copLimit = this.state.performanceMode ? 10 : 15; const formations = [ // Center group, main force { x: 0, z: 20, radius: 5, count: 3, type: 'standard' }, // Left flank { x: -10, z: 15, radius: 4, count: 2, type: 'standard' }, // Right flank { x: 10, z: 15, radius: 4, count: 2, type: 'standard' }, // Elite officers at the front { x: -8, z: 10, radius: 3, count: 1, type: 'elite' }, { x: 8, z: 10, radius: 3, count: 1, type: 'elite' }, // Elite marksmen at various positions - only one on each side { x: -15, z: 20, radius: 2, count: 1, type: 'elite' } ]; // Create one commander this.createCop(0, 0, 15, 'commander'); // Create formations - making sure we don't exceed 20 cops total let copCount = 1; // We already made one commander for (const formation of formations) { if (copCount >= 20) break; // Stop if we've reached 20 cops const copsToMake = Math.min(formation.count, 20 - copCount); for (let i = 0; i < copsToMake; i++) { const angle = (i / formation.count) * Math.PI * 2; const x = formation.x + Math.cos(angle) * formation.radius * Math.random(); const z = formation.z + Math.sin(angle) * formation.radius * Math.random(); this.createCop(x, 0, z, formation.type); copCount++; } } console.log(`Total cops spawned: ${copCount}`); }, // Check if cops should teleport near player checkCopTeleport: function() { // Only check periodically to avoid performance hit const now = performance.now(); if (now - this.state.lastTeleportCheck < this.state.teleportCheckInterval) { return; } this.state.lastTeleportCheck = now; // Only teleport if in forest and near helicopter if (this.state.gamePhase !== 'forest') return; const playerPos = this.three.camera.position; const helicopterPos = new THREE.Vector3(0, 0, -650); const distToHelicopter = new THREE.Vector3(playerPos.x, 0, playerPos.z).distanceTo(helicopterPos); // Get difficulty settings const diffSettings = this.state.difficultySettings[this.state.difficultyLevel]; // Check if teleportation should happen if (distToHelicopter < 15 && diffSettings.teleportDistance > 0) { // Find all cops that are far away const farCops = this.state.cops.filter(cop => { if (cop.dead) return false; const distToPlayer = cop.body.position.distanceTo(playerPos); return distToPlayer > diffSettings.teleportDistance; }); if (farCops.length > 0) { // Teleport them close to player for (const cop of farCops) { // Generate random position around player (10m radius) const angle = Math.random() * Math.PI * 2; const radius = 10 + Math.random() * 3; const teleportPos = new THREE.Vector3( playerPos.x + Math.cos(angle) * radius, 0, playerPos.z + Math.sin(angle) * radius ); // Move cop to new position cop.body.position.set(teleportPos.x, playerPos.y + 0.75, teleportPos.z); cop.head.position.set(teleportPos.x, playerPos.y + 1.6, teleportPos.z); cop.gun.position.set(teleportPos.x + 0.3, playerPos.y + 1.1, teleportPos.z); // Update SWAT gear position if present if (cop.vest) { cop.vest.position.copy(cop.body.position); cop.vest.position.y -= 0.1; } if (cop.helmet) { cop.helmet.position.copy(cop.head.position); } // Visual teleport effect const teleportEffect = new THREE.Mesh( new THREE.SphereGeometry(1, 16, 16), new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.6 }) ); teleportEffect.position.copy(cop.body.position); this.three.scene.add(teleportEffect); // Animate and remove const startTime = now; const animateTeleport = () => { const elapsed = performance.now() - startTime; const t = elapsed / 500; // 500ms animation if (t < 1) { teleportEffect.scale.set(1 + t * 2, 1 + t * 2, 1 + t * 2); teleportEffect.material.opacity = 0.6 * (1 - t); requestAnimationFrame(animateTeleport); } else { this.three.scene.remove(teleportEffect); } }; animateTeleport(); } // Show warning to player this.showNotification("Warning! Cops have arrived to block your escape!", 2000); } } }, // Function to ensure minimum cop counts are maintained - with performance optimizations ensureMinimumCops: function() { // Only run this check every 3 seconds to reduce performance impact const now = performance.now(); if (!this.state.lastCopCheck || now - this.state.lastCopCheck < 3000) return; this.state.lastCopCheck = now; // Only run this in escape or forest phases if (this.state.gamePhase !== 'escape' && this.state.gamePhase !== 'forest') return; // Count alive cops by type let aliveStandard = 0; let aliveElite = 0; let aliveCommander = 0; for (const cop of this.state.cops) { if (!cop.dead) { if (cop.type === 'standard') aliveStandard++; else if (cop.type === 'elite') aliveElite++; else if (cop.type === 'commander') aliveCommander++; } } const totalAlive = aliveStandard + aliveElite + aliveCommander; // If we have fewer than 5 total cops alive or missing elite/commander types if (totalAlive < 5 || aliveElite < 1 || aliveCommander < 1) { // Create respawn message this.showNotification("Police reinforcements arriving!", 2000); // Determine how many of each type to spawn const needStandard = Math.max(0, 3 - aliveStandard); // at least 3 standard cops const needElite = Math.max(0, 1 - aliveElite); // at least 1 elite cop const needCommander = Math.max(0, 1 - aliveCommander); // at least 1 commander // Get player position to spawn reinforcements around const playerPos = this.three.camera.position; // Spawn standard cops for (let i = 0; i < needStandard; i++) { const angle = Math.random() * Math.PI * 2; const distance = 25 + Math.random() * 10; // spawn 25-35 units away const x = playerPos.x + Math.cos(angle) * distance; const z = playerPos.z + Math.sin(angle) * distance; // Create standard cop with double health const cop = this.createCop(x, 0, z, 'standard'); cop.health *= 2; // Make them tougher } // Spawn elite cops if needed for (let i = 0; i < needElite; i++) { const angle = Math.random() * Math.PI * 2; const distance = 25 + Math.random() * 10; const x = playerPos.x + Math.cos(angle) * distance; const z = playerPos.z + Math.sin(angle) * distance; // Create elite cop with additional health boost const cop = this.createCop(x, 0, z, 'elite'); cop.health *= 2; // Double the already higher elite health } // Spawn commander if needed for (let i = 0; i < needCommander; i++) { const angle = Math.random() * Math.PI * 2; const distance = 25 + Math.random() * 15; // Spawn commanders further back const x = playerPos.x + Math.cos(angle) * distance; const z = playerPos.z + Math.sin(angle) * distance; // Create commander with extra health boost const cop = this.createCop(x, 0, z, 'commander'); cop.health *= 3; // Triple the already higher commander health } // Create cool teleport effect for each new cop for (let i = this.state.cops.length - (needStandard + needElite + needCommander); i < this.state.cops.length; i++) { const cop = this.state.cops[i]; // Create teleport effect const teleportEffect = new THREE.Mesh( new THREE.SphereGeometry(1.5, 16, 16), new THREE.MeshBasicMaterial({ color: cop.type === 'commander' ? 0xFFD700 : cop.type === 'elite' ? 0xff0000 : 0x0000ff, transparent: true, opacity: 0.7 }) ); teleportEffect.position.copy(cop.body.position); this.three.scene.add(teleportEffect); // Animate and remove const startTime = performance.now(); const duration = 1000; // 1 second effect const animateTeleport = () => { const elapsed = performance.now() - startTime; const t = elapsed / duration; if (t < 1) { teleportEffect.scale.set(1 + t * 2, 1 + t * 2, 1 + t * 2); teleportEffect.material.opacity = 0.7 * (1 - t); requestAnimationFrame(animateTeleport); } else { this.three.scene.remove(teleportEffect); } }; animateTeleport(); } } }, // Update cops with AI-based behavior and wall collision detection - optimized updateCops: function(delta) { const playerPosition = this.three.camera.position.clone(); this.state.playerPosition.copy(playerPosition); // Check if all cops are too far away (new failsafe) this.checkForFarCops(); // Get difficulty settings const diffSettings = this.state.difficultySettings[this.state.difficultyLevel]; // DEBUG - highlight bank walls for debug if enabled if (this.state.debugMode) { for (const wall of this.bankWalls) { if (wall.mesh && wall.mesh.material && typeof wall.mesh.material.emissive !== 'undefined') { wall.mesh.material.emissive = new THREE.Color(0x00ff00); wall.mesh.material.emissiveIntensity = 0.3; } } } // Check if we need to respawn cops to maintain minimum count this.ensureMinimumCops(); // Check if cops should teleport this.checkCopTeleport(); // Only update a subset of cops each frame to reduce lag // This creates a "rolling update" pattern where not all cops update every frame const totalCops = this.state.cops.length; const copsPerFrame = Math.min(5, totalCops); // Process at most 5 cops per frame const startIndex = Math.floor(performance.now() / 100) % totalCops; // Vary starting point // Update only a subset of cops each frame for (let count = 0; count < copsPerFrame; count++) { const i = (startIndex + count) % totalCops; const cop = this.state.cops[i]; if (cop.dead) continue; // Calculate stats const now = performance.now(); const distance = cop.body.position.distanceTo(playerPosition); const dirToPlayer = new THREE.Vector3().subVectors(playerPosition, cop.body.position).normalize(); // Update cop orientation based on state if (cop.state === 'chase' || !cop.tacticalPosition) { // Look directly at player when chasing cop.body.lookAt(playerPosition.x, cop.body.position.y, playerPosition.z); cop.head.lookAt(playerPosition.x, cop.head.position.y, playerPosition.z); } else { // When using tactical position, use a blend of target and player positions const targetPos = cop.tacticalPosition.clone(); const blendPos = new THREE.Vector3(); // If close to tactical position, look more at player const distToTactical = cop.body.position.distanceTo(targetPos); const blendFactor = Math.min(distToTactical / 10, 1); // 0 when at tactical position, 1 when far blendPos.lerpVectors(playerPosition, targetPos, blendFactor * 0.7); // 70% towards tactical position when far cop.body.lookAt(blendPos.x, cop.body.position.y, blendPos.z); cop.head.lookAt(playerPosition.x, cop.head.position.y, playerPosition.z); // Head always looks at player } // Determine movement based on state and AI input let moveAmount; if (cop.tacticalPosition && cop.state !== 'chase') { // Move toward tactical position const distToTactical = cop.body.position.distanceTo(cop.tacticalPosition); if (distToTactical > 2) { // Still moving to tactical position const dirToTactical = new THREE.Vector3() .subVectors(cop.tacticalPosition, cop.body.position) .normalize(); moveAmount = dirToTactical.multiplyScalar(cop.speed * delta); } else { // At tactical position, use behavior specific to state switch(cop.state) { case 'flank': // Slowly circle the player const circleDir = new THREE.Vector3( Math.sin(now * 0.001 + i), 0, Math.cos(now * 0.001 + i) ).normalize(); moveAmount = circleDir.multiplyScalar(cop.speed * delta * 0.3); break; case 'cover': // Minimal movement when in cover moveAmount = new THREE.Vector3( (Math.random() - 0.5) * 0.05, 0, (Math.random() - 0.5) * 0.05 ); break; case 'retreat': // Move away from player slightly moveAmount = dirToPlayer.clone().negate().multiplyScalar(cop.speed * delta * 0.1); break; default: moveAmount = new THREE.Vector3(0, 0, 0); } } } else { // Default chase behavior // Scale speed based on distance to player - faster when farther let baseSpeed = diffSettings.copSpeed; let maxSpeed = diffSettings.copSpeedFar; // Linear interpolation between baseSpeed and maxSpeed based on distance // Start increasing speed at 20 units, reach max at 100 units let distanceFactor = Math.min(1, Math.max(0, (distance - 20) / 80)); let dynamicSpeed = baseSpeed + (maxSpeed - baseSpeed) * distanceFactor; // New speed cap of 50 m/s as requested dynamicSpeed = Math.min(dynamicSpeed, 50); // Elite and commander cops get type-based modifiers if (cop.type !== 'standard') { if (distance < 10) { // Step back a bit if too close dynamicSpeed *= -0.5; } else if (cop.type === 'elite') { dynamicSpeed *= 1.1; // Elite cops are slightly faster } else if (cop.type === 'commander') { dynamicSpeed *= 0.9; // Commanders are slightly slower but tougher } } // Apply delta time to get frame-independent movement moveAmount = dirToPlayer.clone().multiplyScalar(dynamicSpeed * delta); } // Calculate new position const newPosition = cop.body.position.clone().add(moveAmount); // Check for wall collisions - IMPORTANT NEW CODE if (this.state.gamePhase === 'bank' || this.state.gamePhase === 'escape') { // Check if the new position would be inside a wall const hitWall = this.isPointInWall(newPosition); if (hitWall) { // Wall collision handling const now = performance.now(); // Don't process too many collisions at once (performance) if (now - cop.lastWallCollision > 300) { cop.lastWallCollision = now; // Calculate bounce direction based on wall normal const normal = hitWall.normal.clone(); const dotProduct = moveAmount.dot(normal); if (dotProduct < 0) { // Moving into the wall - push back along the normal const correction = normal.clone().multiplyScalar(-dotProduct * 1.5); moveAmount.add(correction); // Add some randomness to prevent stuck cops moveAmount.x += (Math.random() - 0.5) * 0.1; moveAmount.z += (Math.random() - 0.5) * 0.1; // Apply adjusted movement cop.body.position.add(moveAmount); } else { // Already moving away from wall, allow movement cop.body.position.add(moveAmount); } } else { // If too many collisions, make a more dramatic correction const doorway = new THREE.Vector3(0, 0, 24); const dirToDoor = new THREE.Vector3() .subVectors(doorway, cop.body.position) .normalize(); // Move directly toward door moveAmount = dirToDoor.multiplyScalar(cop.speed * delta); cop.body.position.add(moveAmount); } } else { // No collision, apply normal movement cop.body.position.add(moveAmount); } } else { // In forest, just apply movement normally cop.body.position.add(moveAmount); } // Update head position to follow body cop.head.position.copy(cop.body.position); cop.head.position.y += 0.85; // Height offset for head // Update SWAT gear position if present if (cop.vest) { cop.vest.position.copy(cop.body.position); cop.vest.position.y -= 0.1; cop.vest.rotation.copy(cop.body.rotation); } if (cop.helmet) { cop.helmet.position.copy(cop.head.position); cop.helmet.rotation.copy(cop.head.rotation); } // Update gun position const gunOffset = new THREE.Vector3(0.3, -0.5, 0); gunOffset.applyQuaternion(cop.head.quaternion); cop.gun.position.copy(cop.head.position).add(gunOffset); cop.gun.lookAt(playerPosition); // Handle shooting based on difficulty settings const maxShootRange = cop.type === 'standard' ? 30 : 40; if (distance <= maxShootRange) { // Adjust shoot delay and accuracy based on difficulty const baseShootDelay = diffSettings.shootDelay; const baseAccuracy = diffSettings.shootAccuracy; // Reduced shoot delay based on distance const effectiveShootDelay = Math.max(baseShootDelay * 0.9, baseShootDelay * (distance / maxShootRange)); // Aiming logic if (!cop.isAiming) { // Start aiming if enough time has passed if (now - cop.lastShot > effectiveShootDelay) { cop.isAiming = true; cop.aimTime = now; // Visual feedback for aiming if (distance < 30 && Math.random() < 0.5) { const laserGeometry = new THREE.BoxGeometry(0.02, 0.02, distance); const laserMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.7 }); const laser = new THREE.Mesh(laserGeometry, laserMaterial); laser.position.copy(cop.gun.position); laser.lookAt(playerPosition); laser.translateZ(distance / 2); this.three.scene.add(laser); // Remove laser after a brief time setTimeout(() => { this.three.scene.remove(laser); }, 200); } } } else { // Elite cops aim faster const aimDelay = cop.type === 'standard' ? 300 + Math.random() * 300 : 200 + Math.random() * 150; if (now - cop.aimTime > aimDelay) { // Fire shots with slight angle variations const bulletDir = dirToPlayer.clone(); // Even stronger wall check - first check with a very long ray // We extend both backward and forward to ensure we don't miss any walls // This addresses the problem of cops shooting through walls // Create extended origin and destination points const extendedOrigin = cop.gun.position.clone().sub(bulletDir.clone().multiplyScalar(1)); const extendedDestination = playerPosition.clone().add(bulletDir.clone().multiplyScalar(1)); // Create a new directory vector from our extended points const extendedBulletDir = new THREE.Vector3() .subVectors(extendedDestination, extendedOrigin) .normalize(); // Use the extended ray for testing const ray = new THREE.Raycaster(extendedOrigin, extendedBulletDir); ray.far = extendedOrigin.distanceTo(extendedDestination) + 2; // Make sure ray is long enough const wallMeshes = this.bankWalls.map(wall => wall.mesh); const intersections = ray.intersectObjects(wallMeshes); // Add debug visualization for line of sight rays if debug mode is on if (this.state.debugMode) { // Draw the exact ray path we're testing const lineGeometry = new THREE.BufferGeometry().setFromPoints([ extendedOrigin.clone(), extendedDestination.clone() ]); const lineMaterial = new THREE.LineBasicMaterial({ color: intersections.length === 0 ? 0x00ff00 : 0xff0000, transparent: true, opacity: 0.7, linewidth: 3 }); const line = new THREE.Line(lineGeometry, lineMaterial); this.three.scene.add(line); // Remove debug line after 200ms setTimeout(() => { this.three.scene.remove(line); }, 200); // Draw spheres at each intersection point for better visualization if (intersections.length > 0) { for (const intersection of intersections) { const hitMarker = new THREE.Mesh( new THREE.SphereGeometry(0.2, 8, 8), new THREE.MeshBasicMaterial({ color: 0xff0000 }) ); hitMarker.position.copy(intersection.point); this.three.scene.add(hitMarker); setTimeout(() => { this.three.scene.remove(hitMarker); }, 200); } } } // Only consider cop having line of sight if there are absolutely no walls between const hasLineOfSight = (intersections.length === 0); // Only shoot if there's a clear line of sight and player isn't in helicopter if (hasLineOfSight && !this.state.inHelicopter) { // Accuracy improves at closer ranges const rangeAccuracyBonus = 1 - (distance / maxShootRange) * 0.3; const effectiveAccuracy = Math.min(1, baseAccuracy * rangeAccuracyBonus); // Fire shot this.createBullet(cop.gun.position, bulletDir, true, effectiveAccuracy); // Elite cops rarely fire multiple shots if (cop.type !== 'standard' && Math.random() < 0.1) { setTimeout(() => { const secondBulletDir = dirToPlayer.clone(); secondBulletDir.x += (Math.random() - 0.5) * 0.05; secondBulletDir.y += (Math.random() - 0.5) * 0.05; secondBulletDir.normalize(); this.createBullet(cop.gun.position, secondBulletDir, true, effectiveAccuracy); }, 100); } // Visual effect for firing const flashGeometry = new THREE.SphereGeometry(0.1, 8, 8); const flashMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.8 }); const flash = new THREE.Mesh(flashGeometry, flashMaterial); flash.position.copy(cop.gun.position); flash.position.add(bulletDir.clone().multiplyScalar(0.2)); this.three.scene.add(flash); // Remove flash after brief time setTimeout(() => { this.three.scene.remove(flash); }, 50); } cop.lastShot = now; cop.isAiming = false; } } } } // Request updated AI decisions this.requestAIDecision(); }, // Update bullets with advanced wall collision detection updateBullets: function(delta) { // Update player bullets for (let i = this.state.bullets.length - 1; i >= 0; i--) { const bullet = this.state.bullets[i]; // Store current position before moving bullet.previousPosition = bullet.mesh.position.clone(); // Move bullet const movement = bullet.direction.clone().multiplyScalar(bullet.speed * delta); bullet.mesh.position.add(movement); // Check age bullet.age += delta; if (bullet.age > bullet.maxAge) { this.three.scene.remove(bullet.mesh); this.state.bullets.splice(i, 1); continue; } // Check for bullet-wall collisions with raycasting const hitResult = this.isBulletHittingWall(bullet); if (hitResult) { // Create impact effect this.createBulletImpact( hitResult.point || bullet.mesh.position.clone(), hitResult.normal || hitResult.wall.normal ); // Remove bullet this.three.scene.remove(bullet.mesh); this.state.bullets.splice(i, 1); continue; } // Check collision with cops for (const cop of this.state.cops) { if (cop.dead) continue; // Check for bullet hit let hitDetected = false; let isHeadshot = false; // Check for headshot const distToHead = bullet.mesh.position.distanceTo(cop.head.position); if (distToHead < 0.3) { // Headshot always deals damage regardless of bullet-proof vest hitDetected = true; isHeadshot = true; // Extra damage for headshots cop.health -= 2; // Show headshot notification and effect this.showNotification("HEADSHOT!", 800); // Create spark effect for headshot with helmet if (cop.helmet) { const particleCount = 5; const particleGeometry = new THREE.SphereGeometry(0.03, 4, 4); const particleMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.9 }); for (let i = 0; i < particleCount; i++) { const particle = new THREE.Mesh(particleGeometry, particleMaterial); particle.position.copy(cop.head.position); // Random direction const angle = Math.random() * Math.PI * 2; const speed = 0.3 + Math.random() * 0.7; const direction = new THREE.Vector3( Math.cos(angle) * speed, Math.random() * 0.5, Math.sin(angle) * speed ); this.three.scene.add(particle); // Animate particle const startTime = performance.now(); const duration = 500; const animateParticle = () => { const elapsed = performance.now() - startTime; const t = elapsed / duration; if (t < 1) { particle.position.add(direction.clone().multiplyScalar(0.05)); particle.position.y -= 0.01; // Gravity particle.material.opacity = 0.9 * (1 - t); requestAnimationFrame(animateParticle); } else { this.three.scene.remove(particle); } }; animateParticle(); } } } // Check for body hit else { const distToBody = bullet.mesh.position.distanceTo(cop.body.position); if (distToBody < 0.5) { // Body hit hitDetected = true; if (cop.bulletProofVest && this.state.swatMode) { // SWAT armor absorbs the hit - no damage this.showNotification("Bullet absorbed by armor!", 300); // Create spark effect for armor hit const particleCount = 8; const particleGeometry = new THREE.SphereGeometry(0.02, 4, 4); const particleMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, // Yellow for sparks transparent: true, opacity: 0.9 }); for (let i = 0; i < particleCount; i++) { const particle = new THREE.Mesh(particleGeometry, particleMaterial); particle.position.copy(bullet.mesh.position); // Random direction const angle = Math.random() * Math.PI * 2; const speed = 0.2 + Math.random() * 0.5; const direction = new THREE.Vector3( Math.cos(angle) * speed, 0.2 + Math.random() * 0.3, // Mostly upward Math.sin(angle) * speed ); this.three.scene.add(particle); // Animate particle const startTime = performance.now(); const duration = 300; const animateParticle = () => { const elapsed = performance.now() - startTime; const t = elapsed / duration; if (t < 1) { particle.position.add(direction.clone().multiplyScalar(0.05)); particle.position.y -= 0.005; // Little gravity particle.material.opacity = 0.9 * (1 - t); requestAnimationFrame(animateParticle); } else { this.three.scene.remove(particle); } }; animateParticle(); } } else { // Regular hit cop.health--; } } } // Process hit if detected if (hitDetected) { // Remove bullet this.three.scene.remove(bullet.mesh); this.state.bullets.splice(i, 1); // Check if cop is defeated if (cop.health <= 0) { // Kill cop cop.dead = true; // Make sure the body and its children exist before accessing material if (cop.body && cop.body.material) { cop.body.material.color.set(0x555555); } // Make cop fall down cop.body.rotation.x = Math.PI / 2; cop.body.position.y = 0.4; cop.head.position.y = 0.4; cop.gun.visible = false; // Handle SWAT gear if (cop.vest) { cop.vest.rotation.x = Math.PI / 2; cop.vest.position.y = 0.5; } if (cop.helmet) { cop.helmet.position.y = 0.45; } } else { // Wounded effect - just change color for MeshBasicMaterial if (cop.parts && cop.parts.body && cop.parts.body.material) { // Store original color const originalColor = cop.parts.body.material.color.clone(); // Change to red for hit effect cop.parts.body.material.color.set(0xff0000); // Clear effect after a moment setTimeout(() => { // Only restore if the material still exists if (cop.parts.body && cop.parts.body.material) { // Restore original color cop.parts.body.material.color.copy(originalColor); } }, 300); } } break; } } } // Update enemy bullets for (let i = this.state.enemyBullets.length - 1; i >= 0; i--) { const bullet = this.state.enemyBullets[i]; // Store current position before moving bullet.previousPosition = bullet.mesh.position.clone(); // Move bullet const movement = bullet.direction.clone().multiplyScalar(bullet.speed * delta); bullet.mesh.position.add(movement); // Check age bullet.age += delta; if (bullet.age > bullet.maxAge) { this.three.scene.remove(bullet.mesh); this.state.enemyBullets.splice(i, 1); continue; } // Check for bullet-wall collisions with raycasting const hitResult = this.isBulletHittingWall(bullet); if (hitResult) { // Create impact effect this.createBulletImpact( hitResult.point || bullet.mesh.position.clone(), hitResult.normal || hitResult.wall.normal ); // Remove bullet this.three.scene.remove(bullet.mesh); this.state.enemyBullets.splice(i, 1); continue; } // Check collision with player (if not in helicopter) if (!this.state.inHelicopter) { const distToPlayer = bullet.mesh.position.distanceTo(this.three.camera.position); if (distToPlayer < 0.5) { this.three.scene.remove(bullet.mesh); this.state.enemyBullets.splice(i, 1); // Check if bullet is coming from direction player is facing (shield blocks it) const bulletDir = bullet.direction.clone(); const playerDir = new THREE.Vector3(); this.three.camera.getWorldDirection(playerDir); const negPlayerDir = playerDir.clone().negate(); const angle = bulletDir.angleTo(negPlayerDir); if (angle < Math.PI / 3) { // Bullet is blocked by shield this.showNotification("Bullet blocked by shield!", 500); } else { // Player takes damage this.state.health--; this.updateHealthDisplay(); if (this.state.health <= 0) { this.handleGameOver(false); } else { this.showNotification("You've been shot! Health: " + this.state.health, 1000); } } } } // Check collision with helicopter if (this.helicopter && this.state.inHelicopter) { const distToHelicopter = bullet.mesh.position.distanceTo(this.helicopter.position); if (distToHelicopter < 3) { // Helicopter is larger than player this.three.scene.remove(bullet.mesh); this.state.enemyBullets.splice(i, 1); // Helicopter is armored, but still show impact this.showNotification("Bullet hit helicopter!", 500); } } } }, // Check if player is near computer checkComputerProximity: function() { if (!this.three.computer || this.state.gamePhase !== 'bank') return false; const computerPos = this.three.computer.position.clone(); const playerPos = this.three.camera.position.clone(); const distance = playerPos.distanceTo(computerPos); // If player is within 3 units of computer if (distance < 3) { if (!this.state.nearComputer) { this.state.nearComputer = true; this.showNotification("Press SPACE or click to steal money!", 2000); this.dom.stealMoneyBtn.style.display = 'block'; } } else { if (this.state.nearComputer) { this.state.nearComputer = false; this.dom.stealMoneyBtn.style.display = 'none'; } } return this.state.nearComputer; }, // IMPROVED BANK WALL COLLISION HANDLING checkBankWallCollisions: function(newPosition, oldPosition) { if (this.state.gamePhase !== 'bank' && this.state.gamePhase !== 'escape') { return false; } // Check each wall for collision using isPointInWall const hitWall = this.isPointInWall(newPosition); if (hitWall) { // Hit a wall - apply collision response const normal = hitWall.normal.clone(); // Move player back to old position const moveBack = newPosition.clone().sub(oldPosition); const dotProduct = moveBack.dot(normal); if (dotProduct < 0) { // Moving into the wall - push back along the normal const correction = normal.clone().multiplyScalar(-dotProduct * 1.1); newPosition.add(correction); } return true; } return false; }, // Update player position display (now with regular updates) updatePositionDisplay: function() { const now = performance.now(); if (now - this.state.lastPositionUpdate > this.state.positionUpdateInterval) { this.state.lastPositionUpdate = now; const playerPos = this.three.camera.position; let distToHelicopter = "---"; if (this.state.gamePhase === 'forest') { distToHelicopter = Math.round( new THREE.Vector3(playerPos.x, 0, playerPos.z) .distanceTo(new THREE.Vector3(0, 0, -650)) ); } this.dom.positionDisplay.textContent = `Position: X: ${Math.round(playerPos.x)}, Z: ${Math.round(playerPos.z)} | Distance to helicopter: ${distToHelicopter}m`; } }, // Update player movement updatePlayer: function(delta) { if (this.state.gamePhase === 'loading' || this.state.gameOver) { return; } // Skip player movement when in helicopter if (this.state.inHelicopter) { // Player movement is controlled by helicopter animation return; } // Apply gravity this.player.velocity.y -= 7.5 * delta; // Reduced gravity for higher jumps // Get player direction this.player.direction.z = Number(this.player.moveForward) - Number(this.player.moveBackward); this.player.direction.x = Number(this.player.moveRight) - Number(this.player.moveLeft); this.player.direction.normalize(); // Calculate movement velocity if (this.player.moveForward || this.player.moveBackward) { this.player.velocity.z = this.player.direction.z * this.player.speed; } else { this.player.velocity.z = 0; } if (this.player.moveLeft || this.player.moveRight) { this.player.velocity.x = -this.player.direction.x * this.player.speed; } else { this.player.velocity.x = 0; } // Store old position for collision detection const oldPosition = this.three.camera.position.clone(); // Calculate new position const newPosition = oldPosition.clone(); // Apply movement if (this.player.velocity.x !== 0) { // Move right/left based on camera direction const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.three.camera.quaternion); const right = new THREE.Vector3().crossVectors(this.three.camera.up, forward).normalize(); newPosition.addScaledVector(right, this.player.velocity.x * delta); } if (this.player.velocity.z !== 0) { // Move forward/backward based on camera direction but keep y level const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.three.camera.quaternion); forward.y = 0; forward.normalize(); newPosition.addScaledVector(forward, this.player.velocity.z * delta); } // Apply gravity newPosition.y += this.player.velocity.y * delta; // Ensure player doesn't fall below ground if (newPosition.y < this.player.height) { this.player.velocity.y = 0; newPosition.y = this.player.height; this.player.canJump = true; } // Check for bank wall collisions const hitWall = this.checkBankWallCollisions(newPosition, oldPosition); // Check for tree collisions in forest let hitTree = false; if ((this.state.gamePhase === 'escape' || this.state.gamePhase === 'forest') && !hitWall) { for (const tree of this.state.trees) { const treePos = new THREE.Vector3( tree.collider.position.x, 0, tree.collider.position.z ); const playerPos = new THREE.Vector3( newPosition.x, 0, newPosition.z ); const distToTree = playerPos.distanceTo(treePos); if (distToTree < tree.radius + 0.5) { // Move player back from tree const bounceDir = new THREE.Vector3().subVectors(playerPos, treePos).normalize(); newPosition.copy(oldPosition); newPosition.add(bounceDir.multiplyScalar(0.1)); this.player.velocity.x *= 0.4; this.player.velocity.z *= 0.4; hitTree = true; break; } } } // Apply new position if no collisions this.three.camera.position.copy(newPosition); // Check if near computer this.checkComputerProximity(); // Update shield direction this.state.playerShield.direction.set(0, 0, -1); this.three.camera.getWorldDirection(this.state.playerShield.direction); // Check for phase transitions if (this.state.gamePhase === 'escape' && this.three.camera.position.z > 20) { this.state.gamePhase = 'forest'; this.showNotification("You've escaped the bank! Navigate through the forest to the helicopter!", 5000); // Update guide path for the new phase if (this.state.showGuidePath) { this.createGuidePath(); } // Update key hints this.dom.keyHints.innerHTML = ` <div>WASD/Arrow Keys: Move</div> <div>Mouse: Look</div> <div>Click: Shoot</div> <div>Find the escape helicopter!</div> `; } // Check for helicopter proximity if (this.state.gamePhase === 'forest') { const distToHelicopter = new THREE.Vector3( this.three.camera.position.x, 0, this.three.camera.position.z ).distanceTo(new THREE.Vector3(0, 0, -650)); // Show distance notification when far away if (distToHelicopter > 200 && Math.random() < 0.01) { this.showNotification("Helicopter is " + Math.round(distToHelicopter) + " meters away. Follow the arrows!", 1500); } // Show escape button when close to helicopter if (distToHelicopter < 30) { // Show notification if first time if (!this.helicopterReached) { this.helicopterReached = true; this.showNotification("You've reached the helicopter! Click ESCAPE THROUGH HELICOPTER to get away!", 5000); } // Show escape button with animation this.dom.escapeHelicopterBtn.style.display = 'block'; // Reminder notification occasionally if (Math.random() < 0.005) { this.showNotification("Click ESCAPE THROUGH HELICOPTER! button to escape!", 2000); } } else { // Hide boarding button when too far this.dom.escapeHelicopterBtn.style.display = 'none'; } } // Update position display this.updatePositionDisplay(); }, // Show notification message showNotification: function(message, duration = 3000) { this.dom.notification.textContent = message; this.dom.notification.style.opacity = 1; setTimeout(() => { this.dom.notification.style.opacity = 0; }, duration); }, // Update money display updateMoneyDisplay: function() { this.dom.balance.textContent = `$${this.state.money}`; }, // Update health display updateHealthDisplay: function() { this.dom.health.textContent = `Health: ${this.state.health}`; }, // Update loading progress updateLoadingProgress: function() { const progress = Math.min(100, Math.round((this.state.assetsLoaded / this.state.totalAssets) * 100)); this.dom.progressBar.style.width = `${progress}%`; this.dom.loadingStatus.textContent = `Loading game assets... ${progress}%`; }, // Handle game over handleGameOver: function(win = false) { this.state.gameOver = true; // Clear auto-money interval if active if (this.state.autoMoneyInterval) { clearInterval(this.state.autoMoneyInterval); this.state.autoMoneyInterval = null; } if (win) { this.dom.gameOverTitle.textContent = "You Won!"; this.dom.gameOverMessage.textContent = `Congratulations! You escaped with $${this.state.money}!`; } else { this.dom.gameOverTitle.textContent = "Game Over"; this.dom.gameOverMessage.textContent = `You died! You stole $${this.state.money}`; } this.dom.gameOver.style.display = 'block'; this.dom.stealMoneyBtn.style.display = 'none'; this.dom.escapeHelicopterBtn.style.display = 'none'; }, // Restart the game - completely reworked to reliably reset everything restartGame: function() { console.log("Restarting game with comprehensive reset. Current SWAT mode:", this.state.swatMode); // Store SWAT mode setting before reset const currentSwatMode = this.state.swatMode; const currentPerformanceMode = this.state.performanceMode; // Reset game state this.state.money = 0; this.state.health = 10; this.state.gamePhase = 'bank'; this.state.gameOver = false; this.state.nearComputer = false; this.state.autoMoneyEnabled = false; this.helicopterReached = false; this.state.inHelicopter = false; // Reset AI tracking for new game this.state.aiLastRequest = 0; this.state.aiTotalRequests = 0; this.state.aiResponseCache = {}; // Clear any active intervals if (this.state.autoMoneyInterval) { clearInterval(this.state.autoMoneyInterval); this.state.autoMoneyInterval = null; } // Clear existing entities for (const cop of this.state.cops) { this.three.scene.remove(cop.body); this.three.scene.remove(cop.head); this.three.scene.remove(cop.gun); if (cop.vest) this.three.scene.remove(cop.vest); if (cop.helmet) this.three.scene.remove(cop.helmet); } this.state.cops = []; for (const bullet of this.state.bullets) { this.three.scene.remove(bullet.mesh); } this.state.bullets = []; for (const bullet of this.state.enemyBullets) { this.three.scene.remove(bullet.mesh); } this.state.enemyBullets = []; if (this.state.guidePath) { this.three.scene.remove(this.state.guidePath); this.state.guidePath = null; } // Reset position this.three.camera.position.set(0, this.player.height, 0); this.three.camera.rotation.set(0, 0, 0); // Restore saved settings this.state.swatMode = currentSwatMode; this.state.performanceMode = currentPerformanceMode; // Re-setup the SWAT mode buttons to ensure proper event handlers console.log("Rebuilding all event handlers for SWAT mode buttons"); this.setupSwatModeButtons(); // Update UI displays this.updateMoneyDisplay(); this.updateHealthDisplay(); // Reset key hints this.dom.keyHints.innerHTML = ` <div>WASD/Arrow Keys: Move</div> <div>Mouse: Look</div> <div>Click: Interact</div> <div>Press SPACE near computer to steal money</div> `; // Hide game over screen this.dom.gameOver.style.display = 'none'; // Recreate helicopter at original position if (this.helicopter) { this.helicopter.position.set(0, 2, -650); this.helicopter.rotation.set(0, 0, 0); } // Show initial notification this.showNotification("Go to the computer and steal money! The cops are coming in 10 seconds!", 5000); // Auto-enable guide path if (this.state.showGuidePath) { this.createGuidePath(); } // Enable auto-money collection after 5 seconds setTimeout(() => { if (this.state.gamePhase === 'bank' && !this.state.autoMoneyEnabled) { this.state.autoMoneyEnabled = true; this.state.autoMoneyInterval = setInterval(() => { if (this.state.nearComputer && this.state.gamePhase === 'bank') { this.stealMoney(); } }, 500); this.showNotification("Auto-money enabled: Your character will automatically steal money when near the computer!", 5000); } }, 5000); // Reset timer for cops arrival setTimeout(() => { if (this.state.gamePhase === 'bank') { this.showNotification("The cops have arrived! Run for the exit!", 3000); this.spawnCops(); this.state.gamePhase = 'escape'; // Update guide path for new phase if (this.state.showGuidePath) { this.createGuidePath(); } // Stop auto-money if it's running if (this.state.autoMoneyInterval) { clearInterval(this.state.autoMoneyInterval); this.state.autoMoneyInterval = null; } // Hide steal money button this.dom.stealMoneyBtn.style.display = 'none'; } }, 10000); // Log final state for debugging console.log("Game reset complete. SWAT mode now:", this.state.swatMode); }, // Update mode button states to match current settings updateModeButtonStates: function() { console.log("Updating button states. SWAT mode:", this.state.swatMode); // Update SWAT mode buttons if (this.dom.swatModeBtn) { this.dom.swatModeBtn.textContent = this.state.swatMode ? "SWAT Team Mode: ON" : "SWAT Team Mode: OFF"; this.dom.swatModeBtn.style.backgroundColor = this.state.swatMode ? "rgba(255, 59, 48, 0.7)" : "rgba(93, 92, 222, 0.7)"; this.dom.swatModeBtn.style.animation = this.state.swatMode ? "pulse 2s infinite" : "none"; } if (this.dom.menuSwatModeBtn) { this.dom.menuSwatModeBtn.textContent = this.state.swatMode ? "SWAT Team Mode: ON" : "SWAT Team Mode: OFF"; this.dom.menuSwatModeBtn.style.backgroundColor = this.state.swatMode ? "rgba(255, 59, 48, 0.7)" : "rgba(93, 92, 222, 0.7)"; this.dom.menuSwatModeBtn.style.animation = this.state.swatMode ? "pulse 2s infinite" : "none"; } if (this.dom.swatModeIndicator) { this.dom.swatModeIndicator.style.display = this.state.swatMode ? 'block' : 'none'; } // Update performance mode buttons if (this.dom.performanceModeBtn) { this.dom.performanceModeBtn.textContent = this.state.performanceMode ? "Low Cell-Service Mode: ON" : "Low Cell-Service Mode: OFF"; this.dom.performanceModeBtn.style.backgroundColor = this.state.performanceMode ? "rgba(255, 59, 48, 0.7)" : "rgba(93, 92, 222, 0.7)"; } if (this.dom.menuPerformanceModeBtn) { this.dom.menuPerformanceModeBtn.textContent = this.state.performanceMode ? "Low Cell-Service Mode: ON" : "Low Cell-Service Mode: OFF"; this.dom.menuPerformanceModeBtn.style.backgroundColor = this.state.performanceMode ? "rgba(255, 59, 48, 0.7)" : "rgba(93, 92, 222, 0.7)"; } }, // Main animation loop - with performance mode animate: function() { requestAnimationFrame(() => this.animate()); // Add frame skipping for extremely slow devices in performance mode if (this.state.performanceMode && !this.state.lastFrameTime) { this.state.lastFrameTime = performance.now(); this.state.frameCount = 0; } else if (this.state.performanceMode) { this.state.frameCount++; const now = performance.now(); const elapsed = now - this.state.lastFrameTime; // Calculate FPS and skip frames if necessary if (elapsed > 1000) { // Check every second const fps = this.state.frameCount / (elapsed / 1000); this.state.lastFrameTime = now; this.state.frameCount = 0; // If FPS is very low, increase frame skipping if (fps < 15) { this.state.frameSkip = 2; // Skip 2 frames } else if (fps < 25) { this.state.frameSkip = 1; // Skip 1 frame } else { this.state.frameSkip = 0; // Don't skip frames } } // Skip this frame if needed if (this.state.frameSkip > 0 && (this.state.frameCount % (this.state.frameSkip + 1) !== 0)) { return; } } // Use a smaller max delta in normal mode, larger in performance mode const maxDelta = this.state.performanceMode ? 0.2 : 0.1; const delta = Math.min(maxDelta, this.three.clock.getDelta()); const time = performance.now() * 0.001; // Skip updates if in loading screen or paused if (this.state.gamePhase !== 'loading' && !this.state.paused) { // Always update player for responsive controls this.updatePlayer(delta); // Reduce cop and bullet update frequency in performance mode const updateRate = this.state.performanceMode ? 4 : 2; // Update every 4 seconds in performance mode const updateCritical = Math.floor(time) % updateRate < 1; // Update cops if ((this.state.gamePhase === 'escape' || this.state.gamePhase === 'forest') && !this.state.gameOver) { if (this.state.performanceMode) { // In performance mode, update only every other frame if (this.state.frameCount % 2 === 0) { this.updateCops(delta); } } else { // Normal mode - update every frame this.updateCops(delta); } } // Always update bullets - critical for gameplay this.updateBullets(delta); // Update helicopter rotors and other animated objects // But only update a subset of objects each frame let animationCounter = 0; // In performance mode, limit animations more aggressively const maxAnimationsPerFrame = this.state.performanceMode ? 3 : 5; this.three.scene.traverse(object => { // Only process a few objects per frame to distribute load if (animationCounter++ > maxAnimationsPerFrame) return; if (object.userData && object.userData.animateRotors) { object.userData.animateRotors(delta); } // Animate path markers - but only when they're likely visible if (object.userData && object.userData.animate) { // Only animate if it's our turn or if it's a critical object if (updateCritical || object.name === "helicopter") { object.userData.animate(time); } } }); // Update guide path less frequently in performance mode const guidePathInterval = this.state.performanceMode ? 10 : 5; if (this.state.showGuidePath && time % guidePathInterval < delta) { this.createGuidePath(); } } // Lower pixel ratio in performance mode for better rendering speed if (this.state.performanceMode && this.three.renderer.getPixelRatio() > 1) { this.three.renderer.setPixelRatio(1); } else if (!this.state.performanceMode) { // Restore normal pixel ratio this.three.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); } // Render scene this.three.renderer.render(this.three.scene, this.three.camera); } }; // Initialize game Game.init(); </script> </body></html>