! SEVERE WARNING !
EXTREME STORM CELL DETECTED
FUNDS
$0
FUEL
100%
DEATHS
0
SPEED
0
MPH
RADAR
SCANNING...
ACTIVE MISSION
Deploy drone in EF1+ storm
Full-Vortex Scanner
--- MPH
SCANNING
COMMAND BEACON
TEAM RADIO
System
Radio online. Link with other chasers.
STORM CENTER
WEATHER MODIFICATION DIVISION
SCANNING FOR BIOMETRICS...
MULTIPLAYER HUB
TEAMMATES IN AREA:
SOLO OPERATIONS
VEHICLE FLEET
AERODYNAMICS
AI MEDIA LAB
STABILIZERS & ANCHORS
ARMOR & FLAPS
SPIKE MOUNTS
DEPLOYABLE DEFENSE
CHASSIS MODS
MISSIONS
CASUALTY LOG
SESSION TERMINATED
Strongest Cell Encountered
--
Total Casualties
0
Fallen Team Members
STORM CHASER: INTERCEPT • UNIT LOG
R}, ${grassG}, ${grassB})`; ctx.fillRect(0, 0, canvas.width, canvas.height); // Lightning Flash Effect if (window.appState.atmosphere.lightningActive) { ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.save(); // Center of screen, apply zoom, then offset by player pos ctx.translate(cx + shakeX, cy + shakeY); ctx.scale(zoom, zoom); ctx.translate(-window.appState.player.x, -window.appState.player.y); const startX = window.appState.player.x - (cx / zoom); const startY = window.appState.player.y - (cy / zoom); const endX = startX + (canvas.width / zoom); const endY = startY + (canvas.height / zoom); // Grass Variation Tiling (Revamped Map) const tileSize = 600; for (let x = Math.floor(startX / tileSize) * tileSize; x < endX + tileSize; x += tileSize) { for (let y = Math.floor(startY / tileSize) * tileSize; y < endY + tileSize; y += tileSize) { const seed = Math.abs(x * 1337 + y * 7331); const plotType = seed % 5; if (plotType === 0) ctx.fillStyle = '#14532d'; // Forest else if (plotType === 1) ctx.fillStyle = '#166534'; // Grass else if (plotType === 2) ctx.fillStyle = '#854d0e'; // Dirt patch else if (plotType === 3) ctx.fillStyle = '#ca8a04'; // Wheat field else ctx.fillStyle = '#3f6212'; // Meadow ctx.fillRect(x, y, tileSize, tileSize); // Add "Farming" grid lines for texture if (plotType === 3) { ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.lineWidth = 2; for(let k=10; k { // Simple viewport culling if (tree.x < startX - 100 || tree.x > endX + 100 || tree.y < startY - 100 || tree.y > endY + 100) return; ctx.save(); ctx.translate(tree.x, tree.y); ctx.rotate(tree.rotation); // Tree Shadow ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.ellipse(tree.size*0.2, tree.size*0.2, tree.size*0.6, tree.size*0.3, 0, 0, Math.PI*2); ctx.fill(); // Tree Foliage (Simple fluffy shapes) ctx.fillStyle = tree.color; ctx.beginPath(); ctx.arc(0, 0, tree.size * 0.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.05)'; ctx.beginPath(); ctx.arc(-tree.size*0.1, -tree.size*0.1, tree.size * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.restore(); }); // Towns window.appState.towns.forEach(t => { // Draw Town Foundation if (t.status === 'ripped') { ctx.fillStyle = '#262626'; ctx.beginPath(); ctx.rect(t.x - 450, t.y - 450, 900, 900); ctx.fill(); // Scoured earth effect ctx.fillStyle = '#1a1a1a'; ctx.beginPath(); ctx.ellipse(t.x, t.y, 420, 320, Math.PI/4, 0, Math.PI * 2); ctx.fill(); // Ripped Animation: Spinning Debris const time = Date.now() / 1000; ctx.save(); ctx.translate(t.x, t.y); for(let d=0; d<15; d++) { const angle = time * 2 + (d * (Math.PI*2/15)); const dist = 150 + Math.sin(time + d) * 100; const dx = Math.cos(angle) * dist; const dy = Math.sin(angle) * dist; ctx.fillStyle = d % 2 === 0 ? '#4b5563' : '#1e293b'; ctx.save(); ctx.translate(dx, dy); ctx.rotate(angle * 5); ctx.fillRect(-10, -10, 20, 20); // Debris pieces ctx.restore(); } ctx.restore(); } else { // Draw Buildings t.buildings.forEach(b => { ctx.save(); ctx.translate(t.x + b.dx, t.y + b.dy); if (t.status === 'rubble') { ctx.rotate(b.rotation + (Math.random() * 0.2)); ctx.fillStyle = '#334155'; ctx.fillRect(-b.w/4, -b.h/4, b.w/2, b.h/2); } else { ctx.rotate(b.rotation); // Better Building Design - Multi-layered with details ctx.fillStyle = '#1e293b'; // Base Shadow ctx.fillRect(-b.w/2, -b.h/2, b.w, b.h); ctx.fillStyle = b.color || '#94a3b8'; // Main Facade Color ctx.fillRect(-b.w/2 + 8, -b.h/2 + 8, b.w - 16, b.h - 16); // Roof detail (X pattern) ctx.strokeStyle = 'rgba(0,0,0,0.15)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-b.w/2+10, -b.h/2+10); ctx.lineTo(b.w/2-10, b.h/2-10); ctx.stroke(); ctx.beginPath(); ctx.moveTo(b.w/2-10, -b.h/2+10); ctx.lineTo(-b.w/2+10, b.h/2-10); ctx.stroke(); // Windows (Grid of squares) ctx.fillStyle = 'rgba(255, 255, 220, 0.4)'; // Lit windows const winGrid = 2; const winSize = Math.min(b.w, b.h) / 5; for(let r=0; r 0.4) { ctx.fillStyle = (Math.random() > 0.5) ? '#ef4444' : '#f97316'; ctx.globalAlpha = 0.7; ctx.beginPath(); ctx.moveTo(-b.w/2, -b.h/2); ctx.lineTo(0, -b.h * 1.5); ctx.lineTo(b.w/2, -b.h/2); ctx.fill(); ctx.globalAlpha = 1.0; } ctx.restore(); }); } // Town Text Label (Always Rendered at World Coordinates) ctx.save(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 36px Courier New'; ctx.textAlign = 'center'; ctx.shadowColor = 'black'; ctx.shadowBlur = 15; let suffix = ""; if (t.status === 'fire') { suffix = " [ON FIRE]"; ctx.fillStyle = '#ff8888'; } else if (t.status === 'rubble') { suffix = " [RUINED]"; ctx.fillStyle = '#aaaaaa'; } else if (t.status === 'ripped') { suffix = " [WIPED OUT]"; ctx.fillStyle = '#666666'; } ctx.fillText(t.name.toUpperCase() + suffix, t.x, t.y - 750); ctx.restore(); }); // AI Chasers window.appState.aiChasers.forEach(ai => { if (ai.dead) return; ctx.save(); ctx.translate(ai.x, ai.y); ctx.rotate(ai.rotation); const aiVehicleId = ai.vehicleType || 'v1'; if (ai.isDying) { drawDestroyedVehicle(ctx, aiVehicleId, ai.color, ai.deathTimer); } else { drawVehicleShape(ctx, aiVehicleId, ai.color); } ctx.restore(); ctx.fillStyle = 'white'; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'center'; ctx.shadowColor = 'black'; ctx.shadowBlur = 4; ctx.fillText(ai.name, ai.x, ai.y - 20); ctx.shadowBlur = 0; }); // Lost Chasers (Memorial Markers) window.appState.lostChasers.forEach(c => { ctx.save(); ctx.translate(c.x, c.y); // Red Cross Marker ctx.fillStyle = 'rgba(239, 68, 68, 0.6)'; ctx.fillRect(-2, -10, 4, 20); ctx.fillRect(-10, -2, 20, 4); ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.font = 'bold 10px Arial'; ctx.textAlign = 'center'; ctx.fillText(c.name.toUpperCase(), 0, 18); ctx.restore(); }); // Player Car (Rendered before storms to be visually "under" them) ctx.save(); ctx.translate(window.appState.player.x, window.appState.player.y); const currentVehicleId = window.appState.currentVehicleId; // If player is dying as a human, the car model should remain on the road if (window.appState.player.isDying && window.appState.player.deathType === 'human') { ctx.save(); const roadInt = 1200; const carX = Math.round(window.appState.player.deathOriginX / roadInt) * roadInt; const carY = Math.round(window.appState.player.deathOriginY / roadInt) * roadInt; ctx.translate(carX, carY); ctx.rotate(window.appState.player.rotation); drawVehicleShape(ctx, currentVehicleId, '#0ea5e9'); ctx.restore(); } // Car Body Design & Destruction if (window.appState.player.isDying) { if (window.appState.player.deathType === 'human') { drawDestroyedHuman(ctx, window.appState.player.humanDeathVariant, window.appState.player.deathTimer, window.appState.player.ditchAnim.color); } else { drawDestroyedVehicle(ctx, currentVehicleId, '#0ea5e9', window.appState.player.deathTimer); } } else { // Ditch Animation Logic (Drawn in world space relative to car translation) const p = window.appState.player; if (p.ditchAnim.active && p.ditchAnim.timer > 0) { ctx.save(); const progress = 1.0 - p.ditchAnim.timer; const curX = p.ditchAnim.startX + (p.ditchAnim.targetX - p.ditchAnim.startX) * progress; const curY = p.ditchAnim.startY + (p.ditchAnim.targetY - p.ditchAnim.startY) * progress; ctx.globalAlpha = Math.max(0, p.ditchAnim.timer * 2.0 - 1.0); const localX = curX - p.x; const localY = curY - p.y; ctx.fillStyle = p.ditchAnim.color || '#fff'; ctx.fillRect(localX - 8, localY - 4, 16, 12); ctx.fillStyle = '#ffdbac'; ctx.beginPath(); ctx.arc(localX, localY - 12, 6, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } ctx.rotate(window.appState.player.rotation); drawVehicleShape(ctx, currentVehicleId, '#0ea5e9'); // Drawing the stabilizers overlay (Inside rotation block) if (window.appState.stabsDeployed) { const stabId = window.appState.currentStabId; ctx.save(); ctx.fillStyle = '#94a3b8'; ctx.strokeStyle = '#334155'; ctx.lineWidth = 1; if (stabId === 's1') { // Better fitment on sides of car [-14, 14].forEach(y => { ctx.fillRect(-2, y - (y>0?0:15), 4, 15); ctx.strokeRect(-2, y - (y>0?0:15), 4, 15); }); } else { const isS3 = stabId === 's3'; const pW = isS3 ? 8 : 4; const pH = isS3 ? 25 : 18; // Deployed sticks on sides [-14, 14].forEach(y => { ctx.fillRect(4 - pW/2, y - (y>0?0:pH), pW, pH); ctx.fillRect(-10 - pW/2, y - (y>0?0:pH), pW, pH); }); } ctx.restore(); } // Determine correct headlight offset based on vehicle length let hX = 14; if (currentVehicleId === 'v1') hX = 10; // Insight else if (currentVehicleId === 'v2') hX = 10; // F150 else if (currentVehicleId === 'v3') hX = 14; // Big Rig else if (currentVehicleId === 'v4') hX = 14; // TIV else if (currentVehicleId === 'v5') hX = 18; // Gaboi // Headlights (2 Beams merging into 1) const beamAlpha = 0.5; const beamGrad = ctx.createLinearGradient(hX, 0, 150, 0); beamGrad.addColorStop(0, `rgba(255, 255, 230, ${beamAlpha})`); beamGrad.addColorStop(1, 'transparent'); ctx.fillStyle = beamGrad; // Two beams that overlap and merge further out // Left Beam ctx.beginPath(); ctx.moveTo(hX, -6); ctx.lineTo(150, -45); ctx.lineTo(150, 5); // Overlap center line ctx.closePath(); ctx.fill(); // Right Beam ctx.beginPath(); ctx.moveTo(hX, 6); ctx.lineTo(150, 45); ctx.lineTo(150, -5); // Overlap center line ctx.closePath(); ctx.fill(); // Headlight Bulbs (Functional Glows) ctx.fillStyle = '#fff'; ctx.shadowColor = '#fff'; ctx.shadowBlur = 10; ctx.beginPath(); ctx.arc(hX, -6, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(hX, 6, 2, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } ctx.restore(); // Restores state before Player Car translate/rotate // Storms (Rendered after car to visually overlap/tower over it) window.appState.activeStorms.forEach(s => { const pulse = Math.sin(Date.now() / 200) * (s.grade + 1) * 2; const currentRadius = s.radius + pulse; // Calculate gradient radius to cover non-circular shapes let maxDim = currentRadius; if (s.shape === 'wedge') maxDim = currentRadius * 3.5; else if (s.shape === 'cone') maxDim = currentRadius * 2.5; else if (s.shape === 'rope') maxDim = currentRadius * 3.0; // Base glow (drawn in world space) const grad = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, Math.max(0.1, maxDim)); grad.addColorStop(0, s.color); grad.addColorStop(0.5, s.color + 'cc'); grad.addColorStop(0.8, s.color + '40'); grad.addColorStop(1, 'transparent'); ctx.fillStyle = grad; ctx.fillRect(s.x - maxDim, s.y - maxDim, maxDim * 2, maxDim * 2); // Draw Main Vortex Body drawVortexVisuals(ctx, s.x, s.y, s.grade, s.shape, s.color, s.radius, false); // Labels (drawn in world space relative to storm) ctx.save(); ctx.translate(s.x, s.y); ctx.globalAlpha = 1.0; ctx.fillStyle = 'white'; ctx.font = s.isPrimary ? 'bold 14px Courier New' : '12px Courier New'; ctx.textAlign = 'center'; if (s.isPrimary) { ctx.fillText(`TARGET CELL`, 0, 15); } ctx.restore(); }); ctx.restore(); // Restores world transform scale/shake // Intel Directional Arrow if (window.appState.intelArrow.timer > 0) { const arrowX = window.appState.intelArrow.x; const arrowY = window.appState.intelArrow.y; const dx = arrowX - window.appState.player.x; const dy = arrowY - window.appState.player.y; const dist = Math.sqrt(dx*dx + dy*dy); // Only show if storm is far away (out of immediate sight) if (dist > 300) { const angle = Math.atan2(dy, dx); const margin = 60; const pointerDist = 120; // Radius from screen center ctx.save(); ctx.translate(cx, cy); ctx.rotate(angle); // Draw Pulsing Arrow const arrowPulse = Math.sin(Date.now() / 150) * 5; const arrowColor = '#bae6fd'; // Light Blue ctx.fillStyle = arrowColor; ctx.shadowColor = arrowColor; ctx.shadowBlur = 10; ctx.beginPath(); ctx.moveTo(pointerDist + arrowPulse, 0); ctx.lineTo(pointerDist - 20 + arrowPulse, -15); ctx.lineTo(pointerDist - 20 + arrowPulse, 15); ctx.closePath(); ctx.fill(); // Intel Text ctx.rotate(-angle); // Straighten text ctx.fillStyle = 'white'; ctx.font = 'bold 12px Courier New'; ctx.textAlign = 'center'; ctx.fillText("STORM INTEL", (pointerDist + 30) * Math.cos(angle), (pointerDist + 30) * Math.sin(angle)); ctx.restore(); } } // Radar Sweep Overlay (Static on screen) const sweepAngle = (Date.now() / 1000) % (Math.PI * 2); ctx.save(); ctx.translate(cx, cy); ctx.rotate(sweepAngle); ctx.fillStyle = 'rgba(34, 197, 94, 0.1)'; ctx.beginPath(); ctx.moveTo(0,0); ctx.arc(0,0, Math.max(canvas.width, canvas.height), 0, 0.2); ctx.fill(); ctx.restore(); } function processInterceptSuccess(grade, isSuper = false) { const pGrade = Math.floor(grade); const speedRange = window.appState.windSpeedRanges[pGrade] || "??? mph"; const reward = 250 + (pGrade * 150); window.appState.money += reward; playSound('success'); showNotification(`INTERCEPT SUCCESS! EF${pGrade} (${speedRange}) +$${reward}`, 'success'); if (grade >= 5) { if (!window.appState.unlockedGaboi) { window.appState.unlockedGaboi = true; showNotification("LEGENDARY UNLOCK: GABOI INTERCEPTOR IS NOW AVAILABLE!", "success"); } } window.appState.missions.forEach(m => { if (!m.completed) { let missionSuccess = false; if (m.reqVehicle) { if (window.appState.currentVehicleId === m.reqVehicle && grade >= m.reqEF) { missionSuccess = true; } } else if (m.reqNoAIDeaths) { const allAlive = window.appState.aiChasers.every(ai => !ai.dead); if (allAlive && grade >= m.reqEF) { missionSuccess = true; } } else if (m.reqEF <= grade) { missionSuccess = true; } if (missionSuccess) { m.completed = true; window.appState.money += m.reward; showNotification(`MISSION COMPLETE: ${m.desc} +$${m.reward}`, 'success'); } } }); saveGame(); updateDashboard(); // Small knockback/reposition after success to prevent immediate re-triggering and simulate turbulence if (grade >= 2) { const player = window.appState.player; const currentStorm = window.appState.activeStorms.find(s => s.intercepted && Math.sqrt((s.x-player.x)**2 + (s.y-player.y)**2) < s.radius); if (currentStorm) { const angle = Math.atan2(player.y - currentStorm.y, player.x - currentStorm.x); const dist = currentStorm.radius * 0.8; player.x = currentStorm.x + Math.cos(angle) * dist; player.y = currentStorm.y + Math.sin(angle) * dist; } } } // Initialize Application on DOM Ready document.addEventListener('DOMContentLoaded', () => { initApp(); });