<!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>