🎮ArcadeLab

Road Fighter v4.9 - Handbrake Ram AI Edition

by RocketKoala70
1950 lines82.3 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Road Fighter v4.9 - Handbrake Ram AI Edition</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { overflow: hidden; }
body {
  background: #08080c; display: flex; flex-direction: column;
  justify-content: center; align-items: center; min-height: 100vh;
  font-family: 'Courier New', monospace; color: #fff; user-select: none;
}
#game-container {
  position: relative; border: 4px solid #333;
  box-shadow: 0 0 50px rgba(0,0,0,0.95);
  background: #000; overflow: hidden;
}
canvas { display: block; }
.panel-info { margin-top: 10px; text-align: center; font-size: 13px; color: #bbb; line-height: 1.5; }
.panel-info b { color: #f1c40f; }
.music-slots {
  margin-top: 8px; display: flex; gap: 8px; align-items: center;
  background: #141721; padding: 8px 14px; border-radius: 6px;
  border: 1px solid #2d3446; font-size: 11px;
}
.slot-btn { background: #2b334a; color: #fff; padding: 5px 9px; border-radius: 4px; border: 1px solid #4a5578; cursor: pointer; }
.slot-btn:hover { background: #3e4b6d; }
input[type="file"] { display: none; }
</style>
</head>
<body>

<div id="game-container"><canvas id="gameCanvas" width="620" height="660"></canvas></div>

<div class="music-slots">
  <span>Слоты музыки:</span>
  <label class="slot-btn" for="file1">🎵 Трек 1</label><input type="file" id="file1" accept="audio/*">
  <label class="slot-btn" for="file2">🎵 Трек 2</label><input type="file" id="file2" accept="audio/*">
  <label class="slot-btn" for="file3">🎵 Трек 3</label><input type="file" id="file3" accept="audio/*">
</div>

<div class="panel-info">
  Меню: <b>Y</b> или <b>ESC / M</b> | Меню: <b>Стрелки</b> + <b>ENTER</b><br>
  <b>P1:</b> Руль <b>Ф/В</b>, HIGH <b>Ц(W)</b>, LOW <b>Ы(S)</b>, Ручник <b>ПРОБЕЛ</b>, Трюк <b>E</b><br>
  <b>P2:</b> IJKL (I=газ J=влево K=тормоз L=вправо) трюк <b>O</b> ручник <b>M</b><br>
  <b>Сальто</b> = 5 сек нитро 480 | <b>Щит</b> = 355 | <b>За дорогой = смерть!</b><br>
  <b>Розовая машина</b> умеет дёргать ручник для тарана!<br>
  <b>Ъ</b> — рестарт
</div>

<audio id="bgAudio" loop></audio>

<script>
const GAME_VERSION = "v4.9";
const MAX_LIVES = 3;
const DIST_PX = 2.2;
const PLAYER_SCREEN_Y = 520;

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

let ROAD_LEFT = 145;
let ROAD_WIDTH = 330;
let currentRoadLeft = ROAD_LEFT;
let currentRoadWidth = ROAD_WIDTH;

function applyRoadDimensions() {
  if (isTwoPlayer) { ROAD_LEFT = 65; ROAD_WIDTH = 180; }
  else { ROAD_LEFT = 145; ROAD_WIDTH = 330; }
  currentRoadLeft = ROAD_LEFT;
  currentRoadWidth = ROAD_WIDTH;
}

const STAGES = [
  { name:'STAGE 1: ПРИГОРОД', distance:Math.round(25000*1.7*1.5), spawnRate:34, grassColor:'#0b5313', trafficSpeed:1.05, night:false, rain:false, hasBoss:false },
  { name:'STAGE 2: СКОРОСТНАЯ ТРАССА', distance:Math.round(30000*1.7*1.5), spawnRate:28, grassColor:'#192a56', trafficSpeed:1.25, night:false, rain:false, hasBoss:'tanker' },
  { name:'STAGE 3: АДСКИЙ КАНЬОН', distance:Math.round(36000*1.7*1.5), spawnRate:23, grassColor:'#4b1313', trafficSpeed:1.45, night:false, rain:false, hasBoss:'heli' },
  { name:'STAGE 4: НОЧНОЙ ЛИВЕНЬ', distance:Math.round(42000*1.7*1.5), spawnRate:19, grassColor:'#151b29', trafficSpeed:1.65, night:true, rain:true, hasBoss:false },
  { name:'STAGE 5: РЕМОНТНАЯ ЗОНА', distance:Math.round(44000*1.7*1.5), spawnRate:18, grassColor:'#634832', trafficSpeed:1.70, night:false, rain:false, hasBoss:false }
];

const TRAFFIC_PALETTE = ['#f1c40f','#00cec9','#e84393','#6c5ce7','#00b894','#fdcb6e','#fab1a0','#dfe6e9','#fd79a8','#ffeaa7'];

const CAR_CLASSES = [
  { name:"СПОРТКАР", color:"#ff2222", maxSpeed:410, accel:155, steer:235, fuelRate:1.0, stability:1.0, desc:"Сбалансированная скорость и манёвренность" },
  { name:"ДРИФТЕР", color:"#00e5ff", maxSpeed:425, accel:175, steer:260, fuelRate:1.15, stability:0.75, desc:"Быстрее разгон и руль, но строже к заносам" },
  { name:"ТЯЖЕЛОВЕС", color:"#e67e22", maxSpeed:385, accel:130, steer:205, fuelRate:0.85, stability:1.5, desc:"Тяжёлый кузов, стойкий к крену, экономит топливо" }
];

let selectedCarClassP1 = 0;
let selectedCarClassP2 = 1;

let unlockedStageIndex = parseInt(localStorage.getItem('road_fighter_checkpoint') || '0', 10);
if (unlockedStageIndex >= STAGES.length) unlockedStageIndex = 0;

let deathCount = parseInt(localStorage.getItem('road_fighter_deaths') || '0', 10);
if (isNaN(deathCount) || deathCount < 0) deathCount = 0;

let livesEnabled = true;
let currentLevel = unlockedStageIndex;
let isTwoPlayer = false;
let gameState = 'PRESS_START';
let menuSelection = 0;
let gameOverSelection = 0;
let selectedMusicTrack = 1;
let timeLeft = 90;
let roadScroll = 0;
let screenShakeTimer = 0;
let screenShakeMagnitude = 0;
let skidmarks = [];
let roadNarrowTimer = 0;
let roadNarrowPhase = 0;
let roadConeProps = [];
let activeBoss = null;
let highScores = JSON.parse(localStorage.getItem('road_fighter_records') || '[]');

function p2KeyMap() {
  return {
    left:     ['KeyJ', 'ArrowLeft', 'Numpad4', 'Digit4', 'Home'],
    right:    ['KeyL', 'ArrowRight', 'Numpad6', 'Digit6', 'End'],
    low:      ['KeyK', 'ArrowDown', 'Numpad2', 'Numpad5', 'Digit2', 'Digit5'],
    high:     ['KeyI', 'ArrowUp', 'Numpad8', 'Digit8'],
    handbrake:['KeyM', 'Numpad0', 'ShiftRight', 'NumpadDecimal']
  };
}
function p2StuntKeys(code, key) {
  return code === 'KeyO' || key === 'o' || key === 'щ' ||
         code === 'Slash' || code === 'NumpadDivide' || code === 'NumpadMultiply' ||
         key === '/' || key === '*';
}

function saveHighScore(score, timeSpent) {
  highScores.push({ score, time: Math.round(timeSpent), date: new Date().toLocaleDateString() });
  highScores.sort((a,b)=>b.score-a.score);
  highScores = highScores.slice(0,5);
  localStorage.setItem('road_fighter_records', JSON.stringify(highScores));
}
function triggerScreenShake(d, m) { screenShakeTimer = d; screenShakeMagnitude = m; }
function getLivesLeft() { return Math.max(0, MAX_LIVES - deathCount); }
function resetDeaths() { deathCount = 0; localStorage.setItem('road_fighter_deaths', '0'); }

// ===== AUDIO =====
const bgAudio = document.getElementById('bgAudio');
const trackUrls = { 1:null, 2:null, 3:null };
function setupAudioLoader(id, n) {
  document.getElementById(id).addEventListener('change', e => {
    const f = e.target.files[0]; if (!f) return;
    if (trackUrls[n]) URL.revokeObjectURL(trackUrls[n]);
    trackUrls[n] = URL.createObjectURL(f);
    if (selectedMusicTrack === n) playSelectedMusic();
  });
}
setupAudioLoader('file1',1); setupAudioLoader('file2',2); setupAudioLoader('file3',3);

function playSelectedMusic() {
  if (selectedMusicTrack === 0) { bgAudio.pause(); return; }
  const url = trackUrls[selectedMusicTrack];
  if (url) { bgAudio.src = url; bgAudio.volume = 0.55; bgAudio.play().catch(()=>{}); }
  else bgAudio.pause();
}

const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let engineOsc = null, engineGain = null;
function initEngineSound() {
  try {
    engineOsc = audioCtx.createOscillator();
    engineGain = audioCtx.createGain();
    engineOsc.type = 'sawtooth';
    engineOsc.frequency.setValueAtTime(42, audioCtx.currentTime);
    engineGain.gain.setValueAtTime(0.012, audioCtx.currentTime);
    engineOsc.connect(engineGain); engineGain.connect(audioCtx.destination);
    engineOsc.start();
  } catch(e){}
}
function updateEngineSound(s) {
  if (!engineOsc) return;
  engineOsc.frequency.setTargetAtTime(45 + (s/410)*105, audioCtx.currentTime, 0.05);
}
function playSfx(f, d, t='square', ramp=0.001) {
  try {
    const o = audioCtx.createOscillator(), g = audioCtx.createGain();
    o.type = t; o.frequency.setValueAtTime(f, audioCtx.currentTime);
    g.gain.setValueAtTime(0.08, audioCtx.currentTime);
    g.gain.exponentialRampToValueAtTime(ramp, audioCtx.currentTime + d);
    o.connect(g); g.connect(audioCtx.destination);
    o.start(); o.stop(audioCtx.currentTime + d);
  } catch(e){}
}

let synthTick = 0;
const synthNotes = [220, 220, 261.6, 293.7, 329.6, 293.7, 261.6, 196];
function updateSynthMusic() {
  if (selectedMusicTrack === 0 || trackUrls[selectedMusicTrack]) return;
  synthTick++;
  if (synthTick % 18 === 0 && (gameState === 'PLAY' || gameState === 'MENU')) {
    const n = synthNotes[Math.floor(synthTick/18) % synthNotes.length];
    playSfx(n, 0.12, 'triangle', 0.015);
  }
}

// ===== PLAYER =====
function createPlayer(id, x, classIndex, keyMap) {
  const cc = CAR_CLASSES[classIndex];
  return {
    id, x, y: PLAYER_SCREEN_Y, w: 24, h: 46,
    color: cc.color, carClass: cc, keyMap,
    dist: 0, catchUpBoost: 1.0,
    speed: 0, fuel: 100, score: 0,
    crashed: false, crashTimer: 0,
    tiltAngle: 0, driftVel: 0, angularVel: 0, tiltGraceTimer: 0, isSpinningOut: false,
    spinCharges: 2, spinRechargeTimer: 0,
    stuntAngle: 0, isDoingStunt360: false, stuntDir: 1, stuntTimer: 0, stuntDuration: 0.2, stuntDecelTarget: 0,
    isDoingFlip: false, flipScaleY: 1.0,
    dashDir: 0, dashTimer: 0, lastLeftTapTime: 0, lastRightTapTime: 0,
    shieldTimer: 0, nitroTimer: 0, invulnerableTimer: 0,
    jumpTimer: 0, jumpScale: 1.0,
    rainbowOilTimer: 0, isInTurboTunnel: false,
    handbrakeDebuffTotal: 0, handbrakeCooldown: 0
  };
}

let players = [];
let traffic = [];
let pickups = [];
let bonusCars = [];
let obstacles = [];
let ramps = [];
let cops = [];
let pitVans = [];
let tunnels = [];
let raindrops = [];
let particles = [];
let stageTimePassed = 0;

function syFor(worldDist, playerDist) {
  return PLAYER_SCREEN_Y - (worldDist - playerDist) * DIST_PX;
}
function collidesPlayerObj(p, obj) {
  const objY = syFor(obj.dist, p.dist);
  return !(p.x + p.w < obj.x || p.x > obj.x + obj.w || p.y + p.h < objY || p.y > objY + obj.h);
}

// ===== KEYS =====
const activeKeys = {};
const PREVENT_DEFAULT_CODES = new Set([
  'ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space','Slash','NumpadDivide','NumpadMultiply',
  'Numpad0','Numpad1','Numpad2','Numpad4','Numpad5','Numpad6','Numpad8','NumpadDecimal',
  'Home','End',
  'Backquote','Enter','Escape',
  'KeyW','KeyA','KeyS','KeyD','KeyE','KeyY','KeyU','KeyM','KeyC','KeyF','KeyZ',
  'KeyI','KeyJ','KeyK','KeyL','KeyO',
  'Digit2','Digit4','Digit5','Digit6','Digit8'
]);

window.addEventListener('keydown', e => {
  const key = e.key ? e.key.toLowerCase() : '';
  const code = e.code || '';
  activeKeys[code] = true;
  if (key) activeKeys[key] = true;
  if (PREVENT_DEFAULT_CODES.has(code)) e.preventDefault();
  if (e.repeat) return;

  if (code === 'Backquote' || key === 'ъ' || key === '~' || key === 'ё') {
    if (audioCtx.state === 'suspended') audioCtx.resume();
    if (!engineOsc) initEngineSound();
    if (players.length === 0) initPlayersForRestart();
    quickRestart();
    return;
  }

  if (code === 'KeyY' || key === 'y' || key === 'н' || code === 'KeyU' || key === 'u' || key === 'г' || code === 'Escape' || code === 'KeyM' || key === 'ь') {
    if (audioCtx.state === 'suspended') audioCtx.resume();
    if (!engineOsc) initEngineSound();
    gameState = 'MENU'; playSelectedMusic(); playSfx(420, 0.15);
    return;
  }

  if (gameState === 'PRESS_START') {
    if (audioCtx.state === 'suspended') audioCtx.resume();
    if (!engineOsc) initEngineSound();
    gameState = 'MENU'; playSelectedMusic(); playSfx(420, 0.15);
    return;
  }

  if (gameState === 'MENU') {
    const totalMenuRows = isTwoPlayer ? 6 : 5;
    const startRowIndex = isTwoPlayer ? 5 : 4;
    if (code === 'ArrowUp' || code === 'KeyW' || key === 'ц') { menuSelection = (menuSelection - 1 + totalMenuRows) % totalMenuRows; playSfx(330, 0.08); }
    if (code === 'ArrowDown' || code === 'KeyS' || key === 'ы') { menuSelection = (menuSelection + 1) % totalMenuRows; playSfx(330, 0.08); }
    if (code === 'ArrowLeft' || code === 'KeyA' || key === 'ф') handleMenuAdjust(-1);
    if (code === 'ArrowRight' || code === 'KeyD' || key === 'в') handleMenuAdjust(1);
    if (code === 'Enter') {
      if (menuSelection === startRowIndex) startGame(); else handleMenuAdjust(1);
      playSfx(480, 0.1);
    }
  }

  if (gameState === 'GAMEOVER') {
    if (code === 'ArrowUp' || code === 'ArrowDown' || code === 'KeyW' || code === 'KeyS' || key === 'ц' || key === 'ы') {
      gameOverSelection = 1 - gameOverSelection; playSfx(330, 0.08);
    }
    if (code === 'Enter' || code === 'Space') {
      if (gameOverSelection === 0) loadStage(currentLevel);
      else { currentLevel = 0; localStorage.setItem('road_fighter_checkpoint','0'); resetDeaths(); loadStage(0); }
      gameState = 'PLAY'; playSfx(520, 0.2);
    }
  }

  if (gameState === 'ALL_LIVES_LOST') {
    if (code === 'Enter' || code === 'Space') {
      currentLevel = 0; localStorage.setItem('road_fighter_checkpoint','0'); resetDeaths();
      loadStage(0); gameState = 'PLAY'; playSfx(520, 0.2);
    }
  }

  if (gameState === 'PLAY') {
    if (code === 'KeyE' || key === 'e' || key === 'у') triggerEAction(players[0]);
    if (players[1] && p2StuntKeys(code, key)) triggerEAction(players[1]);

    const now = performance.now();
    if (players[0]) {
      if (code === 'KeyA' || code === 'KeyC' || key === 'ф' || key === 'с') {
        if (now - players[0].lastLeftTapTime < 280) performSideBash(players[0], -1);
        players[0].lastLeftTapTime = now;
      }
      if (code === 'KeyD' || code === 'KeyF' || key === 'в' || key === 'f') {
        if (now - players[0].lastRightTapTime < 280) performSideBash(players[0], 1);
        players[0].lastRightTapTime = now;
      }
    }
  }

  if (gameState === 'STAGE_CLEAR') {
    if (code === 'Enter' || code === 'Space') {
      currentLevel++;
      if (currentLevel >= STAGES.length) currentLevel = 0;
      localStorage.setItem('road_fighter_checkpoint', currentLevel.toString());
      loadStage(currentLevel); gameState = 'PLAY';
    }
  } else if (gameState === 'VICTORY') {
    if (code === 'Enter' || code === 'Space') { resetDeaths(); gameState = 'MENU'; }
  }
});

function handleMenuAdjust(dir) {
  if (menuSelection === 0) { isTwoPlayer = !isTwoPlayer; if (!isTwoPlayer && menuSelection > 4) menuSelection = 4; }
  else if (menuSelection === 1) livesEnabled = !livesEnabled;
  else if (menuSelection === 2) selectedCarClassP1 = (selectedCarClassP1 + dir + CAR_CLASSES.length) % CAR_CLASSES.length;
  else if (isTwoPlayer && menuSelection === 3) selectedCarClassP2 = (selectedCarClassP2 + dir + CAR_CLASSES.length) % CAR_CLASSES.length;
  else if ((!isTwoPlayer && menuSelection === 3) || (isTwoPlayer && menuSelection === 4)) {
    selectedMusicTrack = (selectedMusicTrack + dir + 4) % 4; playSelectedMusic();
  }
}

window.addEventListener('keyup', e => {
  const key = e.key ? e.key.toLowerCase() : '';
  const code = e.code || '';
  activeKeys[code] = false;
  if (key) activeKeys[key] = false;
});
function clearAllKeys() { for (const k in activeKeys) activeKeys[k] = false; }
window.addEventListener('blur', clearAllKeys);
window.addEventListener('visibilitychange', () => { if (document.hidden) clearAllKeys(); });
function isKeyActive(l) { return l.some(k => activeKeys[k]); }

// ===== ACTIONS =====
function triggerEAction(p) {
  if (!p || p.crashed) return;
  if (p.jumpTimer > 0) performFlip(p); else performStunt360(p);
}
function performSideBash(p, dir) {
  if (!p || p.crashed || p.dashTimer > 0 || p.isSpinningOut) return;
  p.dashDir = dir; p.dashTimer = 0.2;
  p.invulnerableTimer = Math.max(p.invulnerableTimer, 0.25);
  playSfx(500, 0.15, 'sawtooth');
  for (let i = 0; i < 8; i++) {
    particles.push({ x: p.x + (dir<0?0:p.w), y: p.y + p.h/2 + (Math.random()-0.5)*15,
      vx: dir*(3+Math.random()*4), vy: (Math.random()-0.5)*2, dist: p.dist,
      size: 3+Math.random()*3, life: 14, color: '#fff' });
  }
}
function performStunt360(p) {
  if (!p || p.crashed || p.isDoingStunt360 || p.isSpinningOut || p.speed <= 30) return;
  if (p.spinCharges <= 0) { playSfx(120, 0.15, 'square'); return; }
  p.spinCharges--;
  if (p.spinRechargeTimer <= 0) p.spinRechargeTimer = 10.0;
  let dir = Math.random() > 0.5 ? 1 : -1;
  if (isKeyActive(p.keyMap.left)) dir = -1;
  else if (isKeyActive(p.keyMap.right) || isKeyActive(p.keyMap.high)) dir = 1;
  p.isDoingStunt360 = true;
  p.stuntDir = dir; p.stuntAngle = 0; p.stuntTimer = 0.2; p.stuntDuration = 0.2;
  p.stuntDecelTarget = p.speed * 0.96;
  p.invulnerableTimer = Math.max(p.invulnerableTimer, 3.0);
  playSfx(540, 0.2, 'sine');
}
function performFlip(p) {
  if (!p || p.crashed || p.jumpTimer <= 0 || p.isDoingFlip) return;
  p.isDoingFlip = true; p.flipScaleY = 1.0;
  p.nitroTimer = Math.max(p.nitroTimer, 5.0);
  p.speed = Math.max(p.speed, 480);
  playSfx(680, 0.3, 'sine');
}

// ===== START =====
function initPlayersForRestart() {
  applyRoadDimensions();
  players = [ createPlayer(1, ROAD_LEFT + (isTwoPlayer ? 50 : ROAD_WIDTH/2 - 12), selectedCarClassP1, {
    left: ['KeyA','KeyC','ф','с'], right: ['KeyD','KeyF','в','f','а'],
    low: ['KeyS','KeyZ','ы','я'], high: ['KeyW','ц'], handbrake: ['Space']
  })];
  if (isTwoPlayer) {
    players.push(createPlayer(2, ROAD_LEFT + (ROAD_WIDTH - 74), selectedCarClassP2, p2KeyMap()));
  }
}

function quickRestart() {
  currentLevel = 0; unlockedStageIndex = 0;
  localStorage.setItem('road_fighter_checkpoint','0'); resetDeaths();
  initPlayersForRestart();
  stageTimePassed = 0; loadStage(0); gameState = 'PLAY';
  triggerScreenShake(0.5, 8); playSfx(300, 0.3, 'triangle'); playSfx(520, 0.3, 'sine');
}

function startGame() {
  if (!engineOsc) initEngineSound();
  playSelectedMusic();
  applyRoadDimensions();
  stageTimePassed = 0;
  initPlayersForRestart();
  if (!livesEnabled) resetDeaths();
  loadStage(currentLevel);
  gameState = 'PLAY';
  playSfx(520, 0.25);
}

function loadStage(lvl) {
  // +15% времени от базового + 10 секунд дополнительно
  timeLeft = Math.floor((100 + Math.floor(Math.random() * 21)) * 1.15) + 10;
  applyRoadDimensions();
  traffic = []; pickups = []; bonusCars = []; obstacles = []; ramps = [];
  cops = []; pitVans = []; tunnels = []; particles = []; raindrops = [];
  skidmarks = []; roadConeProps = []; roadNarrowPhase = 0; activeBoss = null;
  roadScroll = 0;

  const stage = STAGES[lvl];
  if (stage.rain) {
    for (let i = 0; i < 90; i++) {
      raindrops.push({ x: Math.random()*canvas.width, y: Math.random()*canvas.height,
        len: 12+Math.random()*8, speed: 16+Math.random()*8 });
    }
  }
  players.forEach((p, idx) => {
    p.x = ROAD_LEFT + (isTwoPlayer ? (idx === 0 ? 40 : ROAD_WIDTH - 64) : ROAD_WIDTH/2 - 12);
    p.dist = 0; p.speed = 0; p.tiltAngle = 0; p.driftVel = 0; p.angularVel = 0;
    p.tiltGraceTimer = 0; p.isSpinningOut = false; p.crashed = false;
    p.shieldTimer = 0; p.nitroTimer = 0; p.jumpTimer = 0; p.jumpScale = 1.0;
    p.isDoingStunt360 = false; p.stuntAngle = 0; p.stuntTimer = 0;
    p.spinCharges = 2; p.spinRechargeTimer = 0; p.dashTimer = 0;
    p.isDoingFlip = false; p.rainbowOilTimer = 0; p.isInTurboTunnel = false;
    p.invulnerableTimer = 0; p.handbrakeDebuffTotal = 0; p.handbrakeCooldown = 0;
    p.fuel = 100; p.catchUpBoost = 1.0;
  });
}

// ===== SPAWN =====
let ticker = 0;
function spawnWorld() {
  const stage = STAGES[currentLevel];
  ticker++;
  const maxP = players.reduce((m,p)=>Math.max(m,p.dist), 0);

  if (stage.hasBoss && !activeBoss && maxP > stage.distance * 0.68) {
    if (stage.hasBoss === 'tanker') {
      activeBoss = { type:'tanker', x: currentRoadLeft + 30, dist: maxP + 220,
        w: 68, h: 120, speed: 140, hp: 12, actionTick: 0 };
      playSfx(220, 0.6, 'sawtooth');
    } else if (stage.hasBoss === 'heli') {
      activeBoss = { type:'heli', x: (isTwoPlayer ? 155 : canvas.width/2) - 35, dist: maxP + 220,
        w: 70, h: 90, speed: 155, hp: 10, actionTick: 0, rotorAngle: 0 };
      playSfx(620, 0.6, 'sine');
    }
  }

  const isFifthLevel = (currentLevel + 1) % 5 === 0;
  if (isFifthLevel && roadNarrowPhase === 0 && Math.random() < 0.003) {
    roadNarrowPhase = 1; roadNarrowTimer = 9.0;
  }

  if (ticker % stage.spawnRate === 0) {
    const laneX = currentRoadLeft + 24 + Math.random() * (currentRoadWidth - 72);
    const tooClose = traffic.some(c => Math.abs(c.dist - maxP) < 120 && Math.abs(c.x - laneX) < 45);

    if (!tooClose) {
      const r = Math.random();
      const color = TRAFFIC_PALETTE[Math.floor(Math.random()*TRAFFIC_PALETTE.length)];
      const isCounter = (laneX < currentRoadLeft + currentRoadWidth/3) && (Math.random() < 0.35);
      const spawnDist = maxP + 300;

      const base = {
        x: laneX, dist: spawnDist, w: 24, h: 44,
        speed: isCounter ? -160 : 130 * stage.trafficSpeed,
        color: isCounter ? '#ff3838' : color,
        type: isCounter ? 'counter' : 'car',
        tiltAngle: isCounter ? Math.PI : 0,
        driftVel: 0, angularVel: 0, isSpinning: false,
        isRevengeReady: false, revengeTimer: 0, revengeAttempts: 0, revengeActive: false,
        revengeTactic: 'BLOCK', ambushTriggered: false,
        angryRacer: Math.random() < 0.15, angryTriggered: false,
        isPurpleAgressor: false, purpleCooldown: 0,
        cutByPlayer: false, cutTimer: 0, isRevengeAI: false
      };

      if (isCounter) traffic.push(base);
      else if (r < 0.20) traffic.push(base);
      else if (r < 0.38) { base.speed = 165 * stage.trafficSpeed; base.color = '#0984e3'; base.type = 'shifter'; base.dir = Math.random()>0.5?1:-1; traffic.push(base); }
      else if (r < 0.52) { base.speed = 145 * stage.trafficSpeed; base.color = '#8e44ad'; base.type = 'purple'; base.isPurpleAgressor = true; traffic.push(base); }
      else if (r < 0.64) { base.speed = 110 * stage.trafficSpeed; base.color = '#e17055'; base.type = 'taxi'; traffic.push(base); }
      else if (r < 0.74) { base.w = 28; base.h = 66; base.speed = 95 * stage.trafficSpeed; base.color = '#ff6b00'; base.type = 'truck'; traffic.push(base); }
      else if (r < 0.82) obstacles.push({ x: laneX, dist: spawnDist, w: 32, h: 22, type: 'oil_black' });
      else if (r < 0.90) obstacles.push({ x: laneX, dist: spawnDist, w: 34, h: 24, type: 'oil_rainbow', pulse: 0 });
      else if (r < 0.95) obstacles.push({ x: laneX, dist: spawnDist, w: 24, h: 22, type: 'rock' });
      else ramps.push({ x: laneX, dist: spawnDist, w: 30, h: 24 });
    }

    if (pitVans.length === 0 && Math.random() < 0.05) {
      pitVans.push({ x: currentRoadLeft + 25 + Math.random()*(currentRoadWidth-85),
        dist: maxP + 300, w: 32, h: 75, speed: 120, arrowTick: 0 });
    }
    if (tunnels.length === 0 && Math.random() < 0.04) {
      tunnels.push({ dist: maxP + 900, height: 900, neonTick: 0 });
    }
    if (cops.length === 0 && Math.random() < 0.08 && currentLevel >= 1) {
      cops.push({ x: currentRoadLeft + (Math.random()>0.5 ? 20 : currentRoadWidth-44),
        dist: maxP - 200, w: 24, h: 46, speed: 460, sirenTick: 0 });
      playSfx(780, 0.4, 'sawtooth');
    }

    const bonusRoll = Math.random();
    if (bonusRoll < 0.40) {
      const bX = currentRoadLeft + 22 + Math.random() * (currentRoadWidth - 64);
      const bR = Math.random();
      let bType = 'FUEL';
      if (bR > 0.75) { const o = Math.random(); if (o > 0.66) bType = 'NITRO'; else if (o > 0.33) bType = 'TIME'; else bType = 'SHIELD'; }
      pickups.push({ x: bX, dist: maxP + 300, w: 22, h: 22, type: bType, pulse: 0 });
    } else if (bonusRoll < 0.46) {
      const bX = currentRoadLeft + 22 + Math.random() * (currentRoadWidth - 64);
      bonusCars.push({ x: bX, dist: maxP + 300, w: 22, h: 42, speed: 135, tick: 0 });
    }
  }
}

// ===== EXPLOSIONS =====
function createExplosion(x, y, worldDist) {
  triggerScreenShake(0.35, 6);
  playSfx(75, 0.45, 'sawtooth');
  for (let i = 0; i < 28; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = 2 + Math.random() * 6;
    particles.push({ x, y, vx: Math.cos(a)*s, vy: Math.sin(a)*s, dist: worldDist,
      size: 4+Math.random()*5, life: 30+Math.random()*18,
      color: ['#ff1111','#ff9900','#ffff00','#ffffff'][Math.floor(Math.random()*4)] });
  }
}
function emitSpeedFire(p) {
  for (let i = 0; i < 2; i++) {
    const ox = (Math.random()-0.5)*14;
    particles.push({ x: p.x + p.w/2 + ox, y: p.y + p.h - 2,
      vx: (Math.random()-0.5)*1.5, vy: 4+Math.random()*6, dist: p.dist,
      size: 3+Math.random()*5, life: 14+Math.random()*8,
      color: ['#ff1e00','#ff6100','#ffb700','#ffffff'][Math.floor(Math.random()*4)] });
  }
}
function createPitStopSparkles(x, y, worldDist) {
  for (let i = 0; i < 20; i++) {
    const a = Math.random() * Math.PI * 2;
    const s = 2 + Math.random() * 5;
    particles.push({ x, y, vx: Math.cos(a)*s, vy: Math.sin(a)*s, dist: worldDist,
      size: 3+Math.random()*4, life: 25+Math.random()*10,
      color: ['#00ff66','#00e5ff','#ffffff'][Math.floor(Math.random()*3)] });
  }
}
function updateParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const pt = particles[i];
    pt.x += pt.vx; pt.y += pt.vy; pt.life--;
    if (pt.life <= 0) particles.splice(i, 1);
  }
}

// ===== UPDATE =====
function update(dt) {
  if (gameState !== 'PLAY') return;
  const stage = STAGES[currentLevel];
  updateSynthMusic();
  stageTimePassed += dt;
  if (screenShakeTimer > 0) screenShakeTimer -= dt;

  timeLeft -= dt;
  if (timeLeft <= 0) {
    timeLeft = 0;
    saveHighScore(players[0].score, stageTimePassed);
    if (livesEnabled) {
      deathCount++;
      localStorage.setItem('road_fighter_deaths', deathCount.toString());
      if (deathCount >= MAX_LIVES) {
        currentLevel = 0; localStorage.setItem('road_fighter_checkpoint','0');
        gameState = 'ALL_LIVES_LOST'; playSfx(90, 0.8, 'sawtooth');
      } else gameState = 'GAMEOVER';
    } else gameState = 'GAMEOVER';
    return;
  }

  if ((currentLevel + 1) % 5 === 0) {
    if (roadNarrowPhase === 1) {
      roadNarrowTimer -= dt;
      currentRoadWidth = Math.max(isTwoPlayer ? 110 : 170, currentRoadWidth - 65 * dt);
      currentRoadLeft = ROAD_LEFT + (ROAD_WIDTH - currentRoadWidth) / 2;
      if (Math.random() < 0.12) {
        roadConeProps.push({ x: currentRoadLeft - 8, dist: players[0].dist + 300 });
        roadConeProps.push({ x: currentRoadLeft + currentRoadWidth + 4, dist: players[0].dist + 300 });
      }
      if (roadNarrowTimer <= 0) roadNarrowPhase = 2;
    } else if (roadNarrowPhase === 2) {
      currentRoadWidth = Math.min(ROAD_WIDTH, currentRoadWidth + 65 * dt);
      currentRoadLeft = ROAD_LEFT + (ROAD_WIDTH - currentRoadWidth) / 2;
      if (currentRoadWidth >= ROAD_WIDTH) { currentRoadWidth = ROAD_WIDTH; currentRoadLeft = ROAD_LEFT; roadNarrowPhase = 0; }
    }
  } else { currentRoadLeft = ROAD_LEFT; currentRoadWidth = ROAD_WIDTH; }

  if (stage.rain) {
    for (let d of raindrops) { d.y += d.speed; d.x -= 2; if (d.y > canvas.height) { d.y = -10; d.x = Math.random()*canvas.width; } }
  }

  if (isTwoPlayer && players.length === 2) {
    if (players[0].dist > players[1].dist + 5) { players[0].catchUpBoost = 1.0; players[1].catchUpBoost = 1.10; }
    else if (players[1].dist > players[0].dist + 5) { players[0].catchUpBoost = 1.10; players[1].catchUpBoost = 1.0; }
    else { players[0].catchUpBoost = 1.0; players[1].catchUpBoost = 1.0; }
  } else if (players[0]) players[0].catchUpBoost = 1.0;

  let highestSpeed = 0;

  players.forEach(p => {
    p.isInTurboTunnel = tunnels.some(t => p.dist >= t.dist && p.dist <= t.dist + t.height / DIST_PX);
  });

  players.forEach(p => {
    if (p.shieldTimer > 0) p.shieldTimer -= dt;
    if (p.nitroTimer > 0) p.nitroTimer -= dt;
    if (p.invulnerableTimer > 0) p.invulnerableTimer -= dt;
    if (p.handbrakeCooldown > 0) p.handbrakeCooldown -= dt;
    if (p.rainbowOilTimer > 0) p.rainbowOilTimer -= dt;

    if (p.spinCharges < 2) {
      p.spinRechargeTimer -= dt;
      if (p.spinRechargeTimer <= 0) { p.spinCharges++; if (p.spinCharges < 2) p.spinRechargeTimer = 10.0; }
    }
    if (p.dashTimer > 0) { p.dashTimer -= dt; p.x += p.dashDir * 420 * dt; }
    if (p.jumpTimer > 0) {
      p.jumpTimer -= dt;
      p.jumpScale = 1.0 + Math.sin((1.0 - p.jumpTimer) * Math.PI) * 0.45;
      if (p.jumpTimer <= 0) { p.jumpTimer = 0; p.jumpScale = 1.0; p.isDoingFlip = false; triggerScreenShake(0.18, 4); playSfx(150, 0.15); }
    }
    if (p.isDoingStunt360) {
      p.stuntTimer -= dt;
      const prog = Math.min(1, 1 - p.stuntTimer / p.stuntDuration);
      p.stuntAngle = p.stuntDir * prog * Math.PI * 2;
      p.speed += (p.stuntDecelTarget - p.speed) * (dt / 0.2);
      if (p.stuntTimer <= 0) { p.isDoingStunt360 = false; p.stuntAngle = 0; p.speed = p.stuntDecelTarget; }
    }
    if (p.isDoingFlip) p.flipScaleY = Math.cos((1 - p.jumpTimer) * Math.PI * 2);

    if (p.crashed) {
      p.crashTimer -= dt;
      if (p.crashTimer <= 0) {
        p.crashed = false;
        p.x = ROAD_LEFT + (isTwoPlayer ? (p.id === 1 ? 40 : ROAD_WIDTH - 64) : ROAD_WIDTH/2 - 12);
        p.speed = 80; p.tiltAngle = 0; p.driftVel = 0; p.angularVel = 0;
        p.tiltGraceTimer = 0; p.isSpinningOut = false;
        p.invulnerableTimer = 2.5; p.fuel = Math.max(p.fuel, 55);
      }
      return;
    }

    if (p.speed > highestSpeed) highestSpeed = p.speed;
    if (p.speed >= 400) emitSpeedFire(p);

    if (p.speed > 0 && !p.isInTurboTunnel) {
      const bm = p.carClass.fuelRate;
      p.fuel -= (0.016 + (p.speed/410)*0.046) * bm;
      if (p.fuel <= 0) { p.fuel = 0; p.speed = Math.max(0, p.speed - 120*dt); }
    }

    const keyL = isKeyActive(p.keyMap.left);
    const keyR = isKeyActive(p.keyMap.right);
    const keyLow = isKeyActive(p.keyMap.low);
    const keyHigh = isKeyActive(p.keyMap.high);
    const keyHB = isKeyActive(p.keyMap.handbrake);

    if ((keyHB || p.tiltAngle !== 0 || p.isSpinningOut) && p.speed > 80) {
      skidmarks.push({ x: p.x + 3, dist: p.dist, alpha: 0.7 });
      skidmarks.push({ x: p.x + p.w - 5, dist: p.dist, alpha: 0.7 });
    }

    if (keyHB) {
      p.speed = 0;
      playSfx(150, 0.05, 'sawtooth', 0.01);
      if (p.handbrakeCooldown <= 0) {
        p.handbrakeCooldown = 1.2;
        if (Math.random() < 0.63) {
          const add = 0.01 + Math.random() * 0.07;
          p.handbrakeDebuffTotal = Math.min(0.35, p.handbrakeDebuffTotal + add);
          playSfx(110, 0.35, 'square');
        }
      }
    }

    let baseMax = p.carClass.maxSpeed;
    if (p.isInTurboTunnel) baseMax += 30;
    if (p.shieldTimer > 0) baseMax = Math.min(baseMax, 355);
    else if (p.nitroTimer > 0) baseMax = 480 + (p.isInTurboTunnel ? 30 : 0);

    baseMax *= p.catchUpBoost;

    const epf = 1.0 - p.handbrakeDebuffTotal;
    const targetMax = baseMax * epf;

    let targetSpeed = 0;
    if (p.fuel > 0 || p.isInTurboTunnel) {
      if (keyHigh || p.nitroTimer > 0 || p.isInTurboTunnel) targetSpeed = targetMax;
      else if (keyLow) targetSpeed = 0;
    }

    if (p.isSpinningOut) p.speed = Math.max(0, p.speed * (1 - 0.15 * dt));
    else if (p.tiltAngle !== 0) { const sd = 320 / 1.65; p.speed = Math.max(0, p.speed - sd * dt); }
    else if (!keyHB && !p.isDoingStunt360) {
      const accel = (p.nitroTimer > 0 ? 260 : (p.isInTurboTunnel ? 220 : p.carClass.accel)) * dt;
      if (p.speed < targetSpeed) p.speed = Math.min(targetSpeed, p.speed + accel);
      else if (p.speed > targetSpeed) {
        let br = (p.shieldTimer > 0 ? 250 : 170) * dt;
        if (keyLow) br = 450 * dt;
        p.speed = Math.max(targetSpeed, p.speed - br);
      }
    }

    const rainGrip = stage.rain ? 0.72 : 1.0;
    const stab = p.carClass.stability * rainGrip;

    if (p.isSpinningOut) {
      p.tiltAngle += p.angularVel * dt;
      p.x += p.driftVel * 1.5 * dt;
      if (p.speed <= 90) {
        p.angularVel *= 0.88;
        if (Math.abs(p.angularVel) < 1.0) {
          p.isSpinningOut = false; p.tiltAngle = 0; p.driftVel = 0; p.angularVel = 0; p.tiltGraceTimer = 0;
        }
      }
    } else if (p.tiltAngle !== 0) {
      p.tiltGraceTimer += dt;
      p.tiltAngle += p.angularVel * dt;
      p.driftVel = p.tiltAngle * 210;
      p.x += p.driftVel * dt;
      if (p.tiltAngle > 0 && keyL) {
        p.angularVel -= 3.8 * stab * dt; p.tiltAngle -= 1.4 * stab * dt;
        if (p.tiltAngle <= 0) { p.tiltAngle = 0; p.driftVel = 0; p.angularVel = 0; p.tiltGraceTimer = 0; }
      } else if (p.tiltAngle < 0 && keyR) {
        p.angularVel += 3.8 * stab * dt; p.tiltAngle += 1.4 * stab * dt;
        if (p.tiltAngle >= 0) { p.tiltAngle = 0; p.driftVel = 0; p.angularVel = 0; p.tiltGraceTimer = 0; }
      } else if (p.tiltGraceTimer >= 1.8 || Math.abs(p.tiltAngle) > 0.75 * stab) {
        p.isSpinningOut = true;
        p.angularVel = (p.tiltAngle > 0 ? 1 : -1) * 13.5;
        p.driftVel = (p.tiltAngle > 0 ? 1 : -1) * 220;
        playSfx(140, 0.35, 'sawtooth');
      }
    } else {
      const sf = p.rainbowOilTimer > 0 ? 0.45 : 1.0;
      const ss = p.carClass.steer * sf * dt;
      if (keyL) p.x -= ss;
      if (keyR) p.x += ss;
    }

    if (p.x <= currentRoadLeft + 6 || p.x + p.w >= currentRoadLeft + currentRoadWidth - 6) { crashPlayerInstant(p); return; }

    p.dist += p.speed * dt;

    const isCounter = p.x < currentRoadLeft + currentRoadWidth / 3;
    const mult = isCounter ? 0.48 : 0.16;
    p.score += Math.floor((p.speed * dt) * mult);
  });

  updateEngineSound(highestSpeed);

  const worldSpeed = Math.max(50, highestSpeed);
  const minP = players.reduce((m,p)=>Math.min(m,p.dist), Infinity);
  const maxP = players.reduce((m,p)=>Math.max(m,p.dist), 0);

  // ===== Traffic =====
  for (let i = traffic.length - 1; i >= 0; i--) {
    const car = traffic[i];
    car.dist += car.speed * dt;

    if (car.angryRacer && !car.angryTriggered && car.dist < maxP - 30) {
      car.angryTriggered = true; car.speed = 400; car.color = '#ff0033';
      playSfx(480, 0.3, 'sawtooth');
    }

    if (car.cutByPlayer) {
      car.cutTimer -= dt;
      if (car.cutTimer <= 0) {
        car.cutByPlayer = false;
        car.isPurpleAgressor = true;
        car.isRevengeAI = true;
        car.color = '#ff69b4';
        car.aiTimer = 0;
        car.aiTactic = 'PURSUE';
        car.sideDir = Math.random() > 0.5 ? 1 : -1;
        car.speed = Math.max(420, worldSpeed * 1.1);
        car.dist = maxP + 150;
        car.tiltAngle = 0; car.angularVel = 0; car.driftVel = 0; car.isSpinning = false;
        car.hbPhase = 0; car.hbTimer = 0;
        playSfx(550, 0.5, 'sawtooth');
      }
    }

    // ===== УМНЫЙ ИИ РОЗОВОЙ МАШИНЫ =====
    if (car.isPurpleAgressor && !car.isSpinning && car.tiltAngle === 0) {
      if (car.aiTimer === undefined) car.aiTimer = 0;
      if (car.aiTactic === undefined) car.aiTactic = 'PURSUE';
      if (car.sideDir === undefined) car.sideDir = Math.random() > 0.5 ? 1 : -1;
      if (car.hbPhase === undefined) car.hbPhase = 0;
      if (car.hbTimer === undefined) car.hbTimer = 0;

      car.aiTimer -= dt;
      const smart = car.isRevengeAI === true;

      let target = null;
      let bestDist = Infinity;
      for (const p of players) {
        if (p.crashed) continue;
        const d = Math.abs(car.dist - p.dist);
        if (d < bestDist) { bestDist = d; target = p; }
      }

      if (target) {
        const distDiff = target.dist - car.dist;
        const dx = target.x - car.x;
        const closeRange = Math.abs(distDiff) < (smart ? 260 : 180) / DIST_PX;

        if (car.aiTimer <= 0) {
          car.aiTimer = (smart ? 0.8 : 1.5) + Math.random() * (smart ? 0.8 : 1.5);
          // Сброс фазы ручника при смене тактики
          car.hbPhase = 0; car.hbTimer = 0;
          const r = Math.random();
          if (closeRange) {
            if (smart) {
              // Умный AI умеет дёргать ручник для тарана
              if (r < 0.30) car.aiTactic = 'HANDBRAKE_RAM';
              else if (r < 0.55) car.aiTactic = 'RAM';
              else if (r < 0.80) car.aiTactic = 'SIDE';
              else car.aiTactic = 'BLOCK';
            } else {
              if (r < 0.35) car.aiTactic = 'RAM';
              else if (r < 0.65) car.aiTactic = 'SIDE';
              else car.aiTactic = 'BLOCK';
            }
          } else if (distDiff > 0) {
            car.aiTactic = 'PURSUE';
          } else {
            car.aiTactic = smart ? 'PURSUE' : 'WAIT';
          }
        }

        switch (car.aiTactic) {
          case 'PURSUE':
            car.speed = (smart ? 455 : 420) + Math.random() * 40;
            car.x += Math.sign(dx) * (smart ? 160 : 130) * dt;
            break;
          case 'WAIT':
            car.speed = Math.max(90, target.speed * (smart ? 0.9 : 0.75));
            car.x += Math.sign(dx) * 100 * dt;
            break;
          case 'RAM':
            car.speed = target.speed * (smart ? 1.05 : 0.98);
            if (Math.abs(dx) > (smart ? 3 : 6)) {
              car.x += Math.sign(dx) * (smart ? 240 : 200) * dt;
            } else {
              if (distDiff > 25 / DIST_PX) car.speed = target.speed * (smart ? 1.30 : 1.20);
              else if (distDiff < -25 / DIST_PX) car.speed = target.speed * (smart ? 0.70 : 0.80);
            }
            break;
          // === НОВАЯ ТАКТИКА: дёрнуть ручник и протаранить ===
          case 'HANDBRAKE_RAM':
            if (car.hbPhase === 0) {
              // Фаза 0: резко сбрасываем скорость как ручником
              car.speed = Math.max(20, car.speed - 1400 * dt);
              car.hbTimer += dt;
              // Дым из-под колёс
              if (Math.random() < 0.6) {
                particles.push({
                  x: car.x + (Math.random()-0.5) * car.w,
                  y: PLAYER_SCREEN_Y - (car.dist - players[0].dist) * DIST_PX + car.h * 0.5,
                  vx: (Math.random()-0.5)*3, vy: 1.5 + Math.random()*3,
                  dist: car.dist, size: 3 + Math.random()*3, life: 14,
                  color: ['#666','#888','#aaa'][Math.floor(Math.random()*3)]
                });
              }
              if (car.hbTimer > 0.5 || car.speed <= 30) {
                car.hbPhase = 1;
                car.hbTimer = 0;
                car.ramTargetSpeed = target.speed * 1.45 + 60;
                playSfx(200, 0.2, 'sawtooth');
              }
            } else {
              // Фаза 1: стремительный рывок в игрока
              car.speed = Math.min(car.ramTargetSpeed, car.speed + 1200 * dt);
              if (Math.abs(dx) > 3) car.x += Math.sign(dx) * 340 * dt;
              car.hbTimer += dt;
              if (car.hbTimer > 1.2) {
                car.hbPhase = 0; car.hbTimer = 0;
              }
            }
            break;
          case 'SIDE':
            car.speed = target.speed * (smart ? 1.02 : 1.0);
            if (Math.abs(distDiff) < 50 / DIST_PX) {
              car.x += car.sideDir * (smart ? 260 : 220) * dt;
              if (car.sideDir > 0 && car.x > target.x + 20) car.sideDir = -1;
              if (car.sideDir < 0 && car.x < target.x - 20) car.sideDir = 1;
            } else {
              car.x += Math.sign(dx) * 150 * dt;
            }
            break;
          case 'BLOCK':
            car.speed = target.speed * (smart ? 0.95 : 0.85);
            car.x += Math.sign(dx) * (smart ? 180 : 120) * dt;
            break;
        }

        if (car.x < currentRoadLeft + 6) car.x = currentRoadLeft + 6;
        if (car.x + car.w > currentRoadLeft + currentRoadWidth - 6) car.x = currentRoadLeft + currentRoadWidth - 6 - car.w;
      } else {
        car.speed = 130 * STAGES[currentLevel].trafficSpeed;
      }
    }

    if (car.isRevengeReady && !car.revengeActive) {
      car.revengeTimer -= dt;
      if (car.revengeTimer <= 0) {
        car.revengeActive = true; car.color = '#ff1493'; car.type = 'revenge';
        car.revengeTactic = Math.random() < 0.5 ? 'SIDE_AMBUSH' : 'BLOCK';
        car.ambushTriggered = false; playSfx(550, 0.35, 'sawtooth');
      }
    }
    if (car.revengeActive && car.revengeAttempts > 0) {
      const tp = players[0];
      if (tp && !tp.crashed) {
        if (car.revengeTactic === 'SIDE_AMBUSH') {
          const dY = Math.abs(car.dist - tp.dist) * DIST_PX;
          if (dY < 55) car.ambushTriggered = true;
          if (car.ambushTriggered) {
            const sd = car.x < tp.x ? 1 : -1;
            car.x += sd * 280 * dt;
          }
          car.dist = tp.dist + (car.ambushTriggered ? 45/DIST_PX : 100/DIST_PX);
        } else {
          if (Math.abs(car.dist - tp.dist) < 12/DIST_PX) car.speed = worldSpeed * 0.95;
          else car.speed = worldSpeed * 1.05;
          const sd = car.x < tp.x ? 1 : -1;
          car.x += sd * 125 * dt;
        }
      }
    }
    if (car.isSpinning) {
      car.tiltAngle += car.angularVel * dt;
      car.x += car.driftVel * dt;
      car.speed = Math.max(30, car.speed - 120 * dt);
      if (car.x <= currentRoadLeft + 4 || car.x + car.w >= currentRoadLeft + currentRoadWidth - 4) {
        createExplosion(car.x + car.w/2, PLAYER_SCREEN_Y, car.dist);
        traffic.splice(i, 1); continue;
      }
    } else if (car.tiltAngle !== 0) {
      car.tiltAngle += car.angularVel * dt;
      car.driftVel = car.tiltAngle * 170;
      car.x += car.driftVel * dt;
      if (car.canRecover) {
        if (car.tiltAngle > 0) { car.angularVel -= 3.2*dt; car.tiltAngle -= 1.3*dt; if (car.tiltAngle <= 0) finishRecovery(car); }
        else if (car.tiltAngle < 0) { car.angularVel += 3.2*dt; car.tiltAngle -= 1.3*dt; if (car.tiltAngle >= 0) finishRecovery(car); }
      }
      if (Math.abs(car.tiltAngle) > 0.7) { car.isSpinning = true; car.angularVel = (car.tiltAngle > 0 ? 1 : -1) * 11.0; }
      if (car.x <= currentRoadLeft + 4 || car.x + car.w >= currentRoadLeft + currentRoadWidth - 4) {
        createExplosion(car.x + car.w/2, PLAYER_SCREEN_Y, car.dist);
        traffic.splice(i, 1); continue;
      }
    } else if (car.type === 'shifter') {
      car.x += car.dir * 55 * dt;
      if (car.x < currentRoadLeft + 20) { car.x = currentRoadLeft + 20; car.dir = 1; }
      if (car.x > currentRoadLeft + currentRoadWidth - car.w - 20) { car.x = currentRoadLeft + currentRoadWidth - car.w - 20; car.dir = -1; }
    }

    players.forEach(p => {
      if (!p.crashed && p.jumpTimer === 0 && collidesPlayerObj(p, car)) {
        if ((p.dashTimer > 0 || p.invulnerableTimer > 0) && car.type !== 'truck') {
          createExplosion(car.x + car.w/2, PLAYER_SCREEN_Y, car.dist);
          p.score += 800; traffic.splice(i, 1); return;
        }
        if (p.invulnerableTimer > 0 && car.type === 'truck') {
          createExplosion(car.x + car.w/2, PLAYER_SCREEN_Y, car.dist);
          traffic.splice(i, 1); return;
        }
        if (car.revengeActive && car.revengeAttempts > 0) {
          car.revengeAttempts--; car.ambushTriggered = false;
          if (p.speed < 120) { p.speed = Math.max(40, p.speed - 30); car.x += (car.x > p.x ? 45 : -45); playSfx(200, 0.2); }
          else {
            p.speed = Math.max(40, p.speed - 90);
            p.tiltAngle = (p.x < car.x ? -1 : 1) * 0.35;
            p.angularVel = (p.x < car.x ? -1 : 1) * 1.3;
            p.tiltGraceTimer = 0; playSfx(140, 0.3, 'sawtooth');
          }
          if (car.revengeAttempts <= 0) { car.revengeActive = false; car.color = '#555'; }
          return;
        }
        if (car.type === 'truck' || car.type === 'counter') { crashPlayer(p); return; }
        if (p.shieldTimer > 0) {
          createExplosion(car.x + car.w/2, PLAYER_SCREEN_Y, car.dist);
          p.score += 600; traffic.splice(i, 1);
        } else {
          const isPurple = car.isPurpleAgressor === true;
          const isSmartPink = car.isRevengeAI === true;
          const isHB = car.aiTactic === 'HANDBRAKE_RAM';
          p.speed = Math.max(50, p.speed - (isHB ? 150 : (isSmartPink ? 130 : (isPurple ? 110 : 75))));
          const hd = p.x < car.x ? -1 : 1;
          const bumpStrength = isHB ? 0.45 : (isSmartPink ? 0.38 : (isPurple ? 0.32 : 0.22));
          if (!p.isSpinningOut) { p.tiltAngle = hd * (bumpStrength / p.carClass.stability); p.angularVel = hd * (isHB ? 1.7 : (isSmartPink ? 1.45 : (isPurple ? 1.25 : 0.95))); p.tiltGraceTimer = 0; }

          if (Math.random() < 0.65 && !car.isSpinning) {
            car.tiltAngle = -hd * 0.24; car.angularVel = -hd * 0.95;
            car.canRecover = Math.random() < 0.60;

            if (Math.random() < 0.35 && !car.isPurpleAgressor && !car.revengeActive && !car.cutByPlayer && car.type !== 'truck' && car.type !== 'counter') {
              car.cutByPlayer = true;
              car.cutTimer = 5 + Math.random() * 5;
              playSfx(680, 0.4, 'sawtooth');
            }
          } else car.x += (hd < 0 ? 25 : -25);

          // После ручника-тарана сбрасываем фазу, чтобы не залипал
          if (isHB) { car.hbPhase = 0; car.hbTimer = 0; }

          playSfx(160, 0.25, 'sawtooth');
        }
      }
    });
  }
  for (let i = traffic.length - 1; i >= 0; i--) {
    const c = traffic[i];
    if (c.dist < minP - 500 || c.dist > maxP + 2500) traffic.splice(i, 1);
  }

  // ===== Obstacles =====
  for (let i = obstacles.length - 1; i >= 0; i--) {
    const ob = obstacles[i];
    if (ob.type === 'oil_rainbow') ob.pulse += dt * 3;
    players.forEach(p => {
      if (!p.crashed && p.jumpTimer === 0 && p.invulnerableTimer <= 0 && collidesPlayerObj(p, ob)) {
        if (ob.type === 'oil_black') {
          if (!p.isSpinningOut && p.tiltAngle === 0) {
            const d = Math.random() > 0.5 ? 1 : -1;
            p.tiltAngle = d * (0.24 / p.carClass.stability);
            p.angularVel = d * 0.95; p.tiltGraceTimer = 0;
            playSfx(210, 0.2, 'sawtooth');
          }
        } else if (ob.type === 'oil_rainbow') { p.rainbowOilTimer = 3.5; playSfx(380, 0.25, 'triangle'); }
        else if (ob.type === 'rock') {
          if (p.shieldTimer > 0) { createExplosion(ob.x + ob.w/2, PLAYER_SCREEN_Y, ob.dist); obstacles.splice(i, 1); }
          else crashPlayer(p);
        }
      }
    });
  }
  for (let i = obstacles.length - 1; i >= 0; i--) {
    const ob = obstacles[i];
    if (ob.dist < minP - 500 || ob.dist > maxP + 2500) obstacles.splice(i, 1);
  }

  // ===== Pickups =====
  for (let i = pickups.length - 1; i >= 0; i--) {
    const pk = pickups[i];
    pk.pulse += dt * 5;
    players.forEach(p => {
      if (!p.crashed && p.jumpTimer === 0 && collidesPlayerObj(p, pk)) {
        if (pk.type === 'FUEL') { p.fuel = Math.min(100, p.fuel + 45); p.score += 800; playSfx(650, 0.15); }
        else if (pk.type === 'TIME') { timeLeft = Math.min(200, timeLeft + 12); p.score += 1000; playSfx(800, 0.18); }
        else if (pk.type === 'NITRO') { p.nitroTimer = 4.5; p.speed = 460; p.score += 1500; playSfx(950, 0.3); }
        else if (pk.type === 'SHIELD') { p.shieldTimer = 8.0; p.score += 1200; playSfx(880, 0.25); }
        pickups.splice(i, 1);
      }
    });
  }
  for (let i = pickups.length - 1; i >= 0; i--) {
    const pk = pickups[i];
    if (pk.dist < minP - 500 || pk.dist > maxP + 2500) pickups.splice(i, 1);
  }

  // ===== Ramps =====
  for (let i = ramps.length - 1; i >= 0; i--) {
    const rmp = ramps[i];
    players.forEach(p => {
      if (!p.crashed && p.jumpTimer === 0 && collidesPlayerObj(p, rmp)) {
        p.jumpTimer = 1.0; playSfx(480, 0.3, 'sine');
      }
    });
    if (rmp.dist < minP - 500 || rmp.dist > maxP + 2500) ramps.splice(i, 1);
  }

  // ===== Cops =====
  for (let i = cops.length - 1; i >= 0; i--) {
    const cop = cops[i];
    cop.dist += cop.speed * dt;
    cop.sirenTick++;
    if (cop.sirenTick % 12 === 0) playSfx(cop.sirenTick % 24 === 0 ? 820 : 640, 0.1, 'square');
    const tp = players[0];
    if (tp && !tp.crashed) {
      const pushLeft = tp.x < currentRoadLeft + currentRoadWidth / 2;
      const desiredX = pushLeft ? tp.x + 20 : tp.x - 20;
      cop.x += (desiredX - cop.x) * 3.5 * dt;
      if (collidesPlayerObj(tp, cop) && tp.jumpTimer === 0) {
        if (tp.dashTimer > 0 || tp.invulnerableTimer > 0) {
          createExplosion(cop.x + cop.w/2, PLAYER_SCREEN_Y, cop.dist); cops.splice(i, 1); continue;
        }
        tp.x += (pushLeft ? -1 : 1) * 110 * dt;
        tp.speed = Math.max(50, tp.speed - 80 * dt);
        playSfx(160, 0.15, 'sawtooth');
        if (tp.shieldTimer > 0) { createExplosion(cop.x + cop.w/2, PLAYER_SCREEN_Y, cop.dist); cops.splice(i, 1); continue; }
      }
    }
    if (cop.dist < minP - 500 || cop.dist > maxP + 2500) cops.splice(i, 1);
  }

  // ===== Pit vans =====
  for (let i = pitVans.length - 1; i >= 0; i--) {
    const van = pitVans[i];
    van.dist += van.speed * dt;
    van.arrowTick++;
    players.forEach(p => {
      if (!p.crashed && p.jumpTimer === 0 && collidesPlayerObj(p, van)) {
        p.handbrakeDebuffTotal = 0; p.fuel = 100; p.speed = Math.max(p.speed, 280);
        playSfx(950, 0.35, 'triangle'); playSfx(1200, 0.35, 'sine');
        createPitStopSparkles(van.x + van.w/2, PLAYER_SCREEN_Y, van.dist);
        pitVans.splice(i, 1);
      }
    });
    if (van.dist < minP - 500 || van.dist > maxP + 2500) pitVans.splice(i, 1);
  }

  // ===== Tunnels =====
  for (let i = tunnels.length - 1; i >= 0; i--) {
    const t = tunnels[i];
    t.neonTick += dt * 4;
    if (t.dist + t.height / DIST_PX < minP - 500 || t.dist > maxP + 2500) tunnels.splice(i, 1);
  }

  // ===== Road cones =====
  for (let i = roadConeProps.length - 1; i >= 0; i--) {
    const c = roadConeProps[i];
    if (c.dist < minP - 500 || c.dist > maxP + 2500) roadConeProps.splice(i, 1);
  }

  // ===== Skidmarks =====
  for (let i = skidmarks.length - 1; i >= 0; i--) {
    const s = skidmarks[i];
    s.alpha -= dt * 0.45;
    if (s.alpha <= 0 || s.dist < minP - 500) skidmarks.splice(i, 1);
  }

  // ===== Boss =====
  if (activeBoss) {
    const b = activeBoss;
    if (b.type === 'tanker') {
      if (b.dist - maxP > 100) b.dist += (b.speed - worldSpeed) * dt;
      else b.dist += (b.speed - worldSpeed * 0.9) * dt;
      if (b.dist - maxP > 200) b.dist -= 40 * dt;
      b.actionTick++;
      if (b.actionTick % 45 === 0) {
        obstacles.push({ x: b.x + 10 + Math.random()*(b.w-30), dist: b.dist + 20, w: 48, h: 30, type: 'oil_black' });
        playSfx(140, 0.2, 'sawtooth');
      }
      players.forEach(p => {
        if (!p.crashed && p.jumpTimer === 0 && collidesPlayerObj(p, b)) {
          if (p.dashTimer > 0) {
            b.hp--; createExplosion(p.x + p.w/2, PLAYER_SCREEN_Y, b.dist); playSfx(280, 0.2);
            if (b.hp <= 0) { createExplosion(b.x + b.w/2, PLAYER_SCREEN_Y, b.dist); p.score += 5000; activeBoss = null; }
          } else if (p.invulnerableTimer <= 0) crashPlayer(p);
        }
      });
    } else if (b.type === 'heli') {
      b.rotorAngle += dt * 35;
      b.dist += (b.speed - worldSpeed) * dt;
      if (b.dist - maxP > 250) b.dist -= 30 * dt;
      const tp = players[0];
      b.x += (tp.x - b.x) * 1.5 * dt;
      b.actionTick++;
      if (b.actionTick % 95 === 0) {
        const dropX = b.x + b.w/2;
        playSfx(380, 0.3, 'sawtooth');
        setTimeout(() => { createExplosion(dropX, PLAYER_SCREEN_Y - 40, tp.dist); }, 600);
      }
    }
  }

  spawnWorld();

  const leader = players.reduce((a,b)=>a.dist > b.dist ? a : b);
  if (leader.dist >= stage.distance) {
    if (currentLevel + 1 < STAGES.length) {
      gameState = 'STAGE_CLEAR'; playSfx(600, 0.4);
    } else {
      saveHighScore(players[0].score, stageTimePassed);
      resetDeaths(); gameState = 'VICTORY'; playSfx(750, 0.6);
    }
    return;
  }

  updateParticles();
}

function finishRecovery(car) {
  car.tiltAngle = 0; car.angularVel = 0; car.driftVel = 0; car.canRecover = false;
  car.isRevengeReady = true; car.speed += 95;
  car.revengeTimer = 5 + Math.random() * 10; car.revengeAttempts = 3;
}
function crashPlayer(p) {
  if (p.crashed || p.invulnerableTimer > 0) return;
  p.crashed = true; p.crashTimer = 1.1; p.speed = 0;
  p.jumpTimer = 0; p.jumpScale = 1.0; p.nitroTimer = 0;
  p.isDoingStunt360 = false; p.stuntAngle = 0; p.isDoingFlip = false; p.rainbowOilTimer = 0;
  triggerScreenShake(0.4, 7);
  createExplosion(p.x + p.w/2, PLAYER_SCREEN_Y, p.dist);
}
function crashPlayerInstant(p) {
  if (p.crashed) return;
  p.crashed = true; p.crashTimer = 1.1; p.speed = 0;
  p.jumpTimer = 0; p.jumpScale = 1.0; p.nitroTimer = 0;
  p.isDoingStunt360 = false; p.stuntAngle = 0; p.isDoingFlip = false; p.rainbowOilTimer = 0;
  p.invulnerableTimer = 0; p.shieldTimer = 0;
  triggerScreenShake(0.5, 9);
  createExplosion(p.x + p.w/2, PLAYER_SCREEN_Y, p.dist);
  playSfx(60, 0.6, 'sawtooth');
}

// ===== DRAW =====
function draw() {
  ctx.fillStyle = '#08080c';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  if (isTwoPlayer && players.length === 2) {
    drawViewport(players[0], 0, 310);
    drawViewport(players[1], 310, 310);
    ctx.fillStyle = '#000'; ctx.fillRect(308, 0, 4, 660);
    ctx.strokeStyle = '#444'; ctx.lineWidth = 2;
    ctx.beginPath(); ctx.moveTo(310, 0); ctx.lineTo(310, 660); ctx.stroke();
    ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center';
    ctx.fillStyle = 'rgba(255,71,87,0.85)'; ctx.fillText('P1', 155, 14);
    ctx.fillStyle = 'rgba(0,229,255,0.7)';  ctx.fillText('P2', 465, 14);
    ctx.textAlign = 'left';
  } else {
    drawViewport(players[0], 0, 620);
  }

  if (gameState === 'PRESS_START') drawPressStartScreen();
  else if (gameState === 'MENU') drawMenuScreen();
  else if (gameState === 'STAGE_CLEAR') drawOverlay('ЭТАП ПРОЙДЕН!', 'ЧЕКПОИНТ СОХРАНЁН! [ENTER]', '#2ecc71');
  else if (gameState === 'GAMEOVER') drawGameOverMenu();
  else if (gameState === 'ALL_LIVES_LOST') drawAllLivesLostScreen();
  else if (gameState === 'VICTORY') drawOverlay('ЧЕМПИОН ВСЕХ ТРАСС!', 'ИГРА ПОЛНОСТЬЮ ПРОЙДЕНА! [ENTER]', '#f1c40f');
}

function drawViewport(player, vx, vw) {
  if (!player) return;
  ctx.save();
  ctx.beginPath();
  ctx.rect(vx, 0, vw, 660);
  ctx.clip();
  ctx.translate(vx, 0);

  if (screenShakeTimer > 0) {
    const sx = (Math.random()-0.5) * screenShakeMagnitude;
    const sy = (Math.random()-0.5) * screenShakeMagnitude;
    ctx.translate(sx, sy);
  }

  const stage = STAGES[currentLevel] || STAGES[0];
  const pd = player.dist;
  const sy = (d) => PLAYER_SCREEN_Y - (d - pd) * DIST_PX;
  const myScroll = (pd * DIST_PX) % 60;

  ctx.fillStyle = stage.grassColor;
  ctx.fillRect(0, 0, vw, 660);

  ctx.fillStyle = stage.night ? '#1e2430' : '#2c3e50';
  ctx.fillRect(currentRoadLeft, 0, currentRoadWidth, 660);

  for (let sm of skidmarks) {
    const smY = sy(sm.dist);
    if (smY > -20 && smY < 700) {
      ctx.fillStyle = `rgba(10,10,15,${sm.alpha})`;
      ctx.fillRect(sm.x, smY, 4, 12);
    }
  }

  const curbStep = 30;
  for (let y = -curbStep; y < 660 + curbStep; y += curbStep) {
    const isRed = ((y + myScroll) % (curbStep*2)) < curbStep;
    ctx.fillStyle = isRed ? '#e74c3c' : '#ecf0f1';
    ctx.fillRect(currentRoadLeft - 12, y + (myScroll % curbStep), 12, curbStep);
    ctx.fillRect(currentRoadLeft + currentRoadWidth, y + (myScroll % curbStep), 12, curbStep);
  }

  const laneW = currentRoadWidth / 3;
  ctx.fillStyle = '#ffffff';
  for (let y = -40; y < 660 + 40; y += 45) {
    ctx.fillRect(currentRoadLeft + laneW - 2, y + (myScroll % 45), 4, 24);
    ctx.fillRect(currentRoadLeft + laneW*2 - 2, y + (myScroll % 45), 4, 24);
  }

  for (let c of roadConeProps) {
    const cY = sy(c.dist);
    if (cY > -30 && cY < 700) {
      ctx.fillStyle = '#e67e22';
      ctx.beginPath();
      ctx.moveTo(c.x, cY + 14); ctx.lineTo(c.x + 6, cY); ctx.lineTo(c.x + 12, cY + 14);
      ctx.closePath(); ctx.fill();
    }
  }

  for (let t of tunnels) {
    const tY = sy(t.dist);
    const tH = t.height;
    if (tY + tH > -100 && tY < 800) {
      ctx.fillStyle = 'rgba(10,15,30,0.88)';
      ctx.fillRect(currentRoadLeft, tY, currentRoadWidth, tH);
      const nc = (Math.floor(t.neonTick) % 2 === 0) ? '#00e5ff' : '#d500f9';
      ctx.strokeStyle = nc; ctx.lineWidth = 4;
      ctx.strokeRect(currentRoadLeft, tY, currentRoadWidth, tH);
      for (let ay = tY; ay < tY + tH; ay += 75) {
        if (ay > -50 && ay < 700) {
          ctx.strokeStyle = 'rgba(0,229,255,0.35)'; ctx.lineWidth = 2;
          ctx.beginPath(); ctx.moveTo(currentRoadLeft, ay); ctx.lineTo(currentRoadLeft + currentRoadWidth, ay); ctx.stroke();
        }
      }
      ctx.fillStyle = '#00e5ff'; ctx.font = 'bold 11px monospace'; ctx.textAlign = 'center';
      ctx.fillText('>> TURBO TUNNEL (+30) >>', vw/2, tY + 20);
      ctx.textAlign = 'left';
    }
  }

  for (let rmp of ramps) {
    const y = sy(rmp.dist);
    if (y > -60 && y < 700) {
      ctx.fillStyle = '#f1c40f';
      ctx.beginPath();
      ctx.moveTo(rmp.x, y + rmp.h);
      ctx.lineTo(rmp.x + rmp.w/2, y);
      ctx.lineTo(rmp.x + rmp.w, y + rmp.h);
      ctx.lineTo(rmp.x + rmp.w/2, y + rmp.h * 0.5);
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle = '#d35400'; ctx.lineWidth = 2; ctx.stroke();
    }
  }

  for (let ob of obstacles) {
    const y = sy(ob.dist);
    if (y > -60 && y < 700) {
      if (ob.type === 'oil_black') {
        ctx.fillStyle = '#111';
        ctx.beginPath(); ctx.ellipse(ob.x + ob.w/2, y + ob.h/2, ob.w/2, ob.h/2, 0, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fill();
      } else if (ob.type === 'oil_rainbow') {
        const g = ctx.createLinearGradient(ob.x, y, ob.x + ob.w, y + ob.h);
        g.addColorStop(0, '#ff007f'); g.addColorStop(0.33, '#00ffff');
        g.addColorStop(0.66, '#ffea00'); g.addColorStop(1, '#76ff03');
        ctx.fillStyle = g;
        ctx.beginPath(); ctx.ellipse(ob.x + ob.w/2, y + ob.h/2, ob.w/2, ob.h/2, 0, 0, Math.PI*2); ctx.fill();
      } else if (ob.type === 'rock') {
        ctx.fillStyle = '#7f8c8d'; ctx.fillRect(ob.x, y, ob.w, ob.h);
        ctx.fillStyle = '#bdc3c7'; ctx.fillRect(ob.x + 3, y + 3, ob.w - 6, ob.h - 6);
      }
    }
  }

  if (activeBoss) {
    const y = sy(activeBoss.dist);
    if (y > -400 && y < 800) {
      ctx.save();
      ctx.translate(activeBoss.x, y);
      if (activeBoss.type === 'tanker') {
        ctx.fillStyle = '#2c3e50'; ctx.fillRect(0, 0, activeBoss.w, activeBoss.h);
        ctx.fillStyle = '#c0392b'; ctx.fillRect(6, 20, activeBoss.w - 12, activeBoss.h - 35);
        ctx.fillStyle = '#f1c40f'; ctx.font = 'bold 11px monospace';
        ctx.fillText(`HP: ${activeBoss.hp}`, 12, -8);
      } else if (activeBoss.type === 'heli') {
        ctx.fillStyle = '#2d3436'; ctx.fillRect(0, 0, activeBoss.w, activeBoss.h);
        ctx.save(); ctx.translate(activeBoss.w/2, activeBoss.h/2); ctx.rotate(activeBoss.rotorAngle);
        ctx.fillStyle = '#636e72'; ctx.fillRect(-45, -3, 90, 6); ctx.restore();
        ctx.fillStyle = 'rgba(255,255,255,0.22)';
        ctx.beginPath();
        ctx.moveTo(activeBoss.w/2, activeBoss.h);
        ctx.lineTo(-40, activeBoss.h + 260);
        ctx.lineTo(activeBoss.w + 40, activeBoss.h + 260);
        ctx.closePath(); ctx.fill();
      }
      ctx.restore();
    }
  }

  for (let van of pitVans) {
    const y = sy(van.dist);
    if (y > -120 && y < 700) {
      ctx.save();
      ctx.translate(van.x + van.w/2, y + van.h/2);
      ctx.fillStyle = '#00b894'; ctx.fillRect(-van.w/2, -van.h/2, van.w, van.h);
      ctx.fillStyle = '#27ae60'; ctx.fillRect(-van.w/2 + 3, -van.h/2 + 10, van.w - 6, van.h - 25);
      ctx.fillStyle = '#111'; ctx.fillRect(-van.w/2 + 2, -van.h/2 + 4, van.w - 4, 8);
      ctx.fillRect(-van.w/2 + 4, van.h/2 - 12, van.w - 8, 12);
      const ab = Math.floor(van.arrowTick/6) % 2 === 0;
      ctx.fillStyle = ab ? '#00ff66' : '#fff';
      ctx.beginPath(); ctx.moveTo(0, van.h/2 - 2); ctx.lineTo(-6, van.h/2 - 9); ctx.lineTo(6, van.h/2 - 9);
      ctx.closePath(); ctx.fill();
      ctx.restore();
    }
  }

  for (let pk of pickups) {
    const y = sy(pk.dist);
    if (y > -40 && y < 700) {
      const scale = 1 + Math.sin(pk.pulse) * 0.12;
      ctx.save(); ctx.translate(pk.x + pk.w/2, y + pk.h/2); ctx.scale(scale, scale);
      if (pk.type === 'FUEL') { ctx.fillStyle = '#27ae60'; ctx.fillRect(-pk.w/2, -pk.h/2, pk.w, pk.h); ctx.fillStyle='#fff'; ctx.fillText('F',-4,4); }
      else if (pk.type === 'TIME') { ctx.fillStyle = '#f39c12'; ctx.fillRect(-pk.w/2, -pk.h/2, pk.w, pk.h); ctx.fillStyle='#fff'; ctx.fillText('T',-4,4); }
      else if (pk.type === 'NITRO') { ctx.fillStyle = '#9b59b6'; ctx.fillRect(-pk.w/2, -pk.h/2, pk.w, pk.h); ctx.fillStyle='#fff'; ctx.fillText('N',-4,4); }
      else if (pk.type === 'SHIELD') { ctx.fillStyle = '#2980b9'; ctx.fillRect(-pk.w/2, -pk.h/2, pk.w, pk.h); ctx.fillStyle='#fff'; ctx.fillText('S',-4,4); }
      ctx.restore();
    }
  }

  for (let car of traffic) {
    const y = sy(car.dist);
    if (y > -100 && y < 700) {
      ctx.save();
      ctx.translate(car.x + car.w/2, y + car.h/2);
      if (car.tiltAngle) ctx.rotate(car.tiltAngle);
      if (car.revengeActive) { ctx.strokeStyle = '#ff007f'; ctx.lineWidth = 2; ctx.strokeRect(-car.w/2 - 3, -car.h/2 - 3, car.w + 6, car.h + 6); }

      // Розовая обводка для умных
      if (car.isRevengeAI) {
        ctx.strokeStyle = '#ff1493';
        ctx.lineWidth = 3;
        ctx.strokeRect(-car.w/2 - 5, -car.h/2 - 5, car.w + 10, car.h + 10);
        ctx.strokeStyle = 'rgba(255,255,255,0.85)';
        ctx.lineWidth = 1;
        ctx.strokeRect(-car.w/2 - 7, -car.h/2 - 7, car.w + 14, car.h + 14);
      } else if (car.isPurpleAgressor) {
        ctx.strokeStyle = 'rgba(255,105,180,0.65)';
        ctx.lineWidth = 2;
        ctx.strokeRect(-car.w/2 - 4, -car.h/2 - 4, car.w + 8, car.h + 8);
      }
      // Жёлтая пунктирная обводка "порезан"
      if (car.cutByPlayer) {
        ctx.strokeStyle = 'rgba(255,235,59,0.9)';
        ctx.lineWidth = 2;
        ctx.setLineDash([4, 4]);
        ctx.strokeRect(-car.w/2 - 6, -car.h/2 - 6, car.w + 12, car.h + 12);
        ctx.setLineDash([]);
      }
      // Красная обводка во время ручника-тарана
      if (car.aiTactic === 'HANDBRAKE_RAM') {
        ctx.strokeStyle = 'rgba(255,60,60,0.9)';
        ctx.lineWidth = 2;
        ctx.strokeRect(-car.w/2 - 6, -car.h/2 - 6, car.w + 12, car.h + 12);
      }

      ctx.fillStyle = car.color; ctx.fillRect(-car.w/2, -car.h/2, car.w, car.h);
      ctx.fillStyle = '#111';
      ctx.fillRect(-car.w/2 - 2, -car.h/2 + 4, 2, 10); ctx.fillRect(car.w/2, -car.h/2 + 4, 2, 10);
      ctx.fillRect(-car.w/2 - 2, car.h/2 - 14, 2, 10); ctx.fillRect(car.w/2, car.h/2 - 14, 2, 10);
      if (car.type === 'truck') {
        ctx.fillStyle = '#111'; ctx.fillRect(-car.w/2 + 2, -car.h/2 + 6, car.w - 4, 10);
      } else {
        ctx.fillStyle = '#222';
        ctx.fillRect(-car.w/2 + 3, -car.h/2 + 9, car.w - 6, 8);
        ctx.fillRect(-car.w/2 + 4, car.h/2 - 11, car.w - 8, 5);
      }
      ctx.restore();
    }
  }

  for (let cop of cops) {
    const y = sy(cop.dist);
    if (y > -100 && y < 700) {
      ctx.save(); ctx.translate(cop.x + cop.w/2, y + cop.h/2);
      ctx.fillStyle = '#111'; ctx.fillRect(-cop.w/2, -cop.h/2, cop.w, cop.h);
      ctx.fillStyle = '#f5f6fa'; ctx.fillRect(-cop.w/2, -cop.h/4, cop.w, cop.h/2);
      const red = (Math.floor(cop.sirenTick/6) % 2 === 0);
      ctx.fillStyle = red ? '#e74c3c' : '#0984e3';
      ctx.fillRect(-cop.w/3, -4, cop.w * 0.66, 8);
      ctx.restore();
    }
  }

  for (let pt of particles) {
    const py = pt.dist !== undefined ? sy(pt.dist) + (pt.y - PLAYER_SCREEN_Y) : pt.y;
    if (py > -20 && py < 700) {
      ctx.fillStyle = pt.color;
      ctx.fillRect(pt.x, py, pt.size, pt.size);
    }
  }

  if (isTwoPlayer && players.length === 2) {
    const other = (player.id === 1) ? players[1] : players[0];
    if (other && !other.crashed) {
      const otherY = sy(other.dist);
      if (otherY > -200 && otherY < 900) {
        drawPlayerAt(other, otherY, { ghost: true, label: `P${other.id}` });
      }
    }
  }

  if (!player.crashed) {
    drawPlayerAt(player, PLAYER_SCREEN_Y, { ghost: false, label: isTwoPlayer ? `P${player.id}` : null });
  }

  if (stage.night) drawNightEffectForViewport(player, vw);

  if (stage.rain) {
    ctx.strokeStyle = 'rgba(173,216,230,0.4)'; ctx.lineWidth = 1.5;
    ctx.beginPath();
    for (let drop of raindrops) {
      ctx.moveTo(drop.x - vx, drop.y);
      ctx.lineTo(drop.x - vx - 4, drop.y + drop.len);
    }
    ctx.stroke();
  }

  drawViewportHUD(player, vw);

  if (isTwoPlayer && player.id === 2) drawP2KeyIndicator(vw);

  ctx.restore();
}

function drawP2KeyIndicator(vw) {
  const keys = [
    { code: 'KeyI', label: 'I', alt: ['ArrowUp', 'Numpad8', 'Digit8'] },
    { code: 'KeyJ', label: 'J', alt: ['ArrowLeft', 'Numpad4', 'Digit4', 'Home'] },
    { code: 'KeyK', label: 'K', alt: ['ArrowDown', 'Numpad2', 'Numpad5', 'Digit2', 'Digit5'] },
    { code: 'KeyL', label: 'L', alt: ['ArrowRight', 'Numpad6', 'Digit6', 'End'] }
  ];
  const bx = vw - 52;
  const by = 660 - 60;
  ctx.save();
  ctx.font = 'bold 8px monospace';
  ctx.fillStyle = 'rgba(0,0,0,0.55)';
  ctx.fillRect(bx - 4, by - 12, 50, 52);
  ctx.strokeStyle = '#2d3446'; ctx.lineWidth = 1;
  ctx.strokeRect(bx - 4, by - 12, 50, 52);
  ctx.fillStyle = '#888';
  ctx.fillText('P2 КЕЙС', bx, by - 3);
  for (let i = 0; i < keys.length; i++) {
    const k = keys[i];
    const on = activeKeys[k.code] || k.alt.some(c => activeKeys[c]);
    const kx = bx + (i % 2) * 22;
    const ky = by + 10 + Math.floor(i / 2) * 18;
    ctx.fillStyle = on ? '#00e5ff' : '#333';
    ctx.fillRect(kx, ky, 18, 14);
    ctx.fillStyle = on ? '#000' : '#666';
    ctx.font = 'bold 11px monospace';
    ctx.fillText(k.label, kx + 6, ky + 11);
    ctx.font = 'bold 8px monospace';
  }
  ctx.restore();
}

function drawPlayerAt(p, screenY, opts = {}) {
  const ghost = !!opts.ghost;
  const label = opts.label || null;

  if (!ghost && p.invulnerableTimer > 0 && p.invulnerableTimer <= 2.5 && !p.isDoingStunt360 && Math.floor(Date.now()/100) % 2 === 0) return;

  ctx.save();
  if (ghost) ctx.globalAlpha = 0.55;

  if (p.jumpScale > 1.0) {
    ctx.fillStyle = 'rgba(0,0,0,0.4)';
    ctx.beginPath();
    ctx.ellipse(p.x + p.w/2, screenY + p.h/2 + 15, p.w*0.7, p.h*0.4, 0, 0, Math.PI*2);
    ctx.fill();
  }

  ctx.translate(p.x + p.w/2, screenY + p.h/2);
  ctx.rotate(p.tiltAngle + p.stuntAngle);
  ctx.scale(p.jumpScale, p.jumpScale * p.flipScaleY);

  if (p.invulnerableTimer > 0 && !ghost) {
    ctx.strokeStyle = Math.floor(Date.now()/70) % 2 === 0 ? '#fffa65' : '#00ffff';
    ctx.lineWidth = 3;
    ctx.beginPath(); ctx.arc(0, 0, p.h*0.75, 0, Math.PI*2); ctx.stroke();
  }

  if (p.speed >= 400) {
    const fh = 14 + Math.random()*12;
    ctx.fillStyle = '#ff3838';
    ctx.beginPath(); ctx.moveTo(-p.w/3, p.h/2); ctx.lineTo(-p.w/6, p.h/2 + fh); ctx.lineTo(0, p.h/2); ctx.fill();
    ctx.beginPath(); ctx.moveTo(0, p.h/2); ctx.lineTo(p.w/6, p.h/2 + fh); ctx.lineTo(p.w/3, p.h/2); ctx.fill();
    ctx.fillStyle = '#fff200'; ctx.fillRect(-p.w/4, p.h/2, p.w/2, fh*0.5);
  }

  if (p.shieldTimer > 0) {
    ctx.strokeStyle = Math.floor(Date.now()/80) % 2 === 0 ? '#00ffff' : '#e056fd';
    ctx.lineWidth = 3;
    ctx.beginPath(); ctx.arc(0, 0, p.h*0.72, 0, Math.PI*2); ctx.stroke();
  }

  ctx.fillStyle = p.color;
  ctx.fillRect(-p.w/2, -p.h/2, p.w, p.h);

  ctx.fillStyle = '#111';
  ctx.fillRect(-p.w/2 + 3, -p.h/2 + 10, p.w - 6, 9);
  ctx.fillRect(-p.w/2 + 4, p.h/2 - 12, p.w - 8, 6);
  ctx.fillStyle = '#000';
  ctx.fillRect(-p.w/2 - 2, -p.h/2 + 5, 3, 10); ctx.fillRect(p.w/2 - 1, -p.h/2 + 5, 3, 10);
  ctx.fillRect(-p.w/2 - 2, p.h/2 - 15, 3, 10); ctx.fillRect(p.w/2 - 1, p.h/2 - 15, 3, 10);
  ctx.fillStyle = '#ffff66';
  ctx.fillRect(-p.w/2 + 2, -p.h/2, 4, 3); ctx.fillRect(p.w/2 - 6, -p.h/2, 4, 3);

  ctx.restore();

  if (label) {
    ctx.save();
    ctx.font = 'bold 10px monospace';
    ctx.textAlign = 'center';
    if (ghost) {
      ctx.strokeStyle = 'rgba(0,0,0,0.85)';
      ctx.lineWidth = 3;
      ctx.strokeText(label, p.x + p.w/2, screenY - 6);
      ctx.fillStyle = '#ffffff';
      ctx.fillText(label, p.x + p.w/2, screenY - 6);
    } else {
      ctx.strokeStyle = 'rgba(0,0,0,0.9)';
      ctx.lineWidth = 3;
      ctx.strokeText(label, p.x + p.w/2, screenY - 8);
      ctx.fillStyle = '#ffe066';
      ctx.fillText(label, p.x + p.w/2, screenY - 8);
    }
    ctx.textAlign = 'left';
    ctx.restore();
  }

  if (!ghost) {
    if (Math.abs(p.tiltAngle) > 0.08 && !p.isSpinningOut) {
      ctx.fillStyle = '#f1c40f';
      ctx.font = 'bold 10px monospace';
      ctx.textAlign = 'center';
      const rem = Math.max(0, 1.8 - p.tiltGraceTimer).toFixed(1);
      const hint = p.tiltAngle > 0 ? `<< ВЫРАВНИВАЙ (${rem}с)` : `(${rem}с) ВЫРАВНИВАЙ >>`;
      ctx.fillText(hint, p.x + p.w/2, screenY - 22);
      ctx.textAlign = 'left';
    } else if (p.isSpinningOut) {
      ctx.fillStyle = '#e74c3c';
      ctx.font = 'bold 10px monospace';
      ctx.textAlign = 'center';
      ctx.fillText('ТОРМОЗИ ДО 90!', p.x + p.w/2, screenY - 22);
      ctx.textAlign = 'left';
    }
  }
}

function drawNightEffectForViewport(viewPlayer, vw) {
  ctx.save();
  const nc = document.createElement('canvas');
  nc.width = vw; nc.height = 660;
  const nctx = nc.getContext('2d');
  nctx.fillStyle = 'rgba(5,7,15,0.91)';
  nctx.fillRect(0, 0, vw, 660);
  nctx.globalCompositeOperation = 'destination-out';
  players.forEach(p => {
    if (!p.crashed) {
      const sY = syFor(p.dist, viewPlayer.dist);
      nctx.beginPath();
      nctx.moveTo(p.x + p.w/2, sY + p.h/2);
      nctx.lineTo(p.x - 70, sY - 280);
      nctx.lineTo(p.x + p.w + 70, sY - 280);
      nctx.closePath(); nctx.fill();
      nctx.beginPath();
      nctx.arc(p.x + p.w/2, sY + p.h/2, 60, 0, Math.PI*2); nctx.fill();
    }
  });
  ctx.drawImage(nc, 0, 0);
  ctx.restore();
}

function drawViewportHUD(player, vw) {
  const stage = STAGES[currentLevel] || STAGES[0];

  ctx.fillStyle = '#1e272e';
  const barX = 8, barY = 90, barW = 10, barH = isTwoPlayer ? 300 : 440;
  ctx.fillRect(barX, barY, barW, barH);
  ctx.strokeStyle = '#485460'; ctx.lineWidth = 1;
  ctx.strokeRect(barX, barY, barW, barH);
  ctx.fillStyle = '#e74c3c'; ctx.fillRect(barX - 2, barY - 4, barW + 4, 3);
  ctx.fillStyle = '#2ecc71'; ctx.fillRect(barX - 2, barY + barH + 1, barW + 4, 3);

  const prog = Math.min(1, player.dist / stage.distance);
  const my = barY + barH - prog * barH;
  ctx.fillStyle = '#f5f6fa'; ctx.fillRect(barX - 2, my, barW + 4, 6);

  ctx.fillStyle = '#00e5ff';
  ctx.font = 'bold 9px monospace';
  ctx.fillText(GAME_VERSION, 8, 14);

  const livesLeft = getLivesLeft();
  if (livesEnabled) {
    let h = '';
    for (let i = 0; i < MAX_LIVES; i++) h += (i < livesLeft) ? '❤' : '✖';
    ctx.fillStyle = livesLeft > 1 ? '#ff4757' : '#e74c3c';
    ctx.font = 'bold 10px monospace';
    ctx.fillText(h, 8, 28);
  } else {
    ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 10px monospace';
    ctx.fillText('∞', 8, 28);
  }

  const rx = vw - 6;
  ctx.textAlign = 'right';
  ctx.fillStyle = '#f1c40f'; ctx.font = 'bold 10px monospace';
  ctx.fillText(`ST.${currentLevel + 1}/${STAGES.length}`, rx, 14);
  ctx.fillStyle = timeLeft < 15 ? '#e74c3c' : '#fff';
  ctx.font = 'bold 14px monospace';
  ctx.fillText(`${Math.ceil(timeLeft)}s`, rx, 32);

  ctx.fillStyle = player.color;
  ctx.font = 'bold 11px monospace';
  const tag = isTwoPlayer ? (player.id === 1 ? 'P1' : 'P2') : 'P1';
  ctx.fillText(`${tag} [${player.carClass.name}]`, rx, 52);
  ctx.font = 'bold 16px monospace';
  ctx.fillStyle = player.speed >= 400 ? '#e056fd' : (player.speed >= 330 ? '#ff4757' : (player.isInTurboTunnel ? '#00e5ff' : (player.shieldTimer > 0 ? '#00ffff' : '#2ecc71')));
  ctx.fillText(`${Math.round(player.speed)} km/h`, rx, 72);
  ctx.font = 'bold 10px monospace';
  ctx.fillStyle = '#f1c40f';
  ctx.fillText(`SCORE: ${player.score}`, rx, 88);

  ctx.fillStyle = '#fff'; ctx.font = '9px monospace';
  ctx.fillText('FUEL', rx, 102);
  const fw = 60;
  ctx.strokeStyle = '#fff'; ctx.lineWidth = 1;
  ctx.strokeRect(rx - fw, 106, fw, 8);
  ctx.fillStyle = player.fuel < 25 ? '#e74c3c' : '#2ecc71';
  ctx.fillRect(rx - fw + 1, 107, (player.fuel/100) * (fw - 2), 6);

  let oy = 126;
  const sk = (player.id === 2) ? 'O' : 'E';
  ctx.fillStyle = player.spinCharges > 0 ? '#00e5ff' : '#7f8c8d';
  ctx.font = 'bold 8px monospace';
  ctx.fillText(`360[${sk}]:${player.spinCharges}/2`, rx, oy); oy += 11;
  if (player.catchUpBoost > 1 && isTwoPlayer) {
    ctx.fillStyle = '#ffeb3b'; ctx.fillText(`CATCH-UP +10%`, rx, oy); oy += 11;
  }
  if (player.jumpTimer > 0) { ctx.fillStyle = '#f1c40f'; ctx.fillText(`FLY ${player.jumpTimer.toFixed(1)}s`, rx, oy); oy += 11; }
  if (player.shieldTimer > 0) { ctx.fillStyle = '#00ffff'; ctx.fillText(`SHLD 355 ${player.shieldTimer.toFixed(1)}s`, rx, oy); oy += 11; }
  if (player.nitroTimer > 0) { ctx.fillStyle = '#e056fd'; ctx.fillText(`NITRO 480 ${player.nitroTimer.toFixed(1)}s`, rx, oy); oy += 11; }
  if (player.handbrakeDebuffTotal > 0) { ctx.fillStyle = '#e74c3c'; ctx.fillText(`HB -${Math.round(player.handbrakeDebuffTotal*100)}%`, rx, oy); oy += 11; }

  ctx.textAlign = 'left';
}

// ===== SCREENS =====
function drawPressStartScreen() {
  ctx.fillStyle = 'rgba(0,0,0,0.9)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#00e5ff'; ctx.font = 'bold 14px Courier New';
  ctx.fillText(`ROAD FIGHTER ${GAME_VERSION}`, canvas.width/2, 145);
  ctx.fillStyle = '#f1c40f'; ctx.font = 'bold 20px Courier New';
  ctx.fillText('HANDBRAKE RAM AI', canvas.width/2, 180);
  ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 13px Courier New';
  ctx.fillText('2P: слева P1, справа P2', canvas.width/2, 212);
  ctx.fillStyle = '#ff6b00'; ctx.font = 'bold 14px Courier New';
  ctx.fillText('P2: IJKL (I=газ J=влево K=тормоз L=вправо)', canvas.width/2, 240);
  ctx.fillStyle = '#fff'; ctx.font = 'bold 12px Courier New';
  ctx.fillText('Трюк: O   Ручник: M', canvas.width/2, 262);
  ctx.fillStyle = '#e056fd'; ctx.font = 'bold 13px Courier New';
  ctx.fillText('САЛЬТО = 5 сек NITRO 480!', canvas.width/2, 288);
  ctx.fillStyle = '#00ffff'; ctx.font = 'bold 13px Courier New';
  ctx.fillText('ЩИТ = макс 355', canvas.width/2, 308);
  ctx.fillStyle = '#ff3333'; ctx.font = 'bold 13px Courier New';
  ctx.fillText('ЗА ДОРОГОЙ = СМЕРТЬ!', canvas.width/2, 328);
  ctx.fillStyle = '#ff69b4'; ctx.font = 'bold 12px Courier New';
  ctx.fillText('Розовая машина дёргает РУЧНИК для тарана!', canvas.width/2, 350);
  ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 16px Courier New';
  if (Math.floor(Date.now()/500) % 2 === 0) ctx.fillText('НАЖМИТЕ ЛЮБУЮ КЛАВИШУ', canvas.width/2, 390);
  ctx.font = '13px Courier New'; ctx.fillStyle = '#aaa';
  ctx.fillText('(Y или ESC — меню)', canvas.width/2, 418);
  ctx.textAlign = 'left';
}

function drawMenuScreen() {
  ctx.fillStyle = 'rgba(0,0,0,0.95)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#00e5ff'; ctx.font = 'bold 13px Courier New';
  ctx.fillText(`ROAD FIGHTER ${GAME_VERSION}`, canvas.width/2, 55);

  if (livesEnabled) {
    const ll = getLivesLeft();
    let h = '';
    for (let i = 0; i < MAX_LIVES; i++) h += (i < ll) ? '❤ ' : '✖ ';
    ctx.fillStyle = ll > 1 ? '#2ecc71' : '#e74c3c';
    ctx.font = 'bold 12px Courier New';
    ctx.fillText(`ЖИЗНИ: ${h}`, canvas.width/2, 80);
  } else {
    ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 12px Courier New';
    ctx.fillText('РЕЖИМ: БЕЗ ЖИЗНЕЙ (∞)', canvas.width/2, 80);
  }

  const optMode = isTwoPlayer ? 'РЕЖИМ: 2 ИГРОКА (SPLIT)' : 'РЕЖИМ: 1 ИГРОК';
  const optLives = livesEnabled ? 'ЖИЗНИ: < ВКЛЮЧЕНЫ (3 смерти) >' : 'ЖИЗНИ: < ОТКЛЮЧЕНЫ (∞) >';
  const optCar1 = `P1 АВТО: < ${CAR_CLASSES[selectedCarClassP1].name} >`;
  const optCar2 = `P2 АВТО: < ${CAR_CLASSES[selectedCarClassP2].name} >`;
  const musicNames = ['[ ВЫКЛЮЧЕНА ]', '[ ТРЕК 1 ]', '[ ТРЕК 2 ]', '[ ТРЕК 3 ]'];
  const optMusic = `МУЗЫКА: < ${musicNames[selectedMusicTrack]} >`;
  const optStart = `▶ СТАРТ (ЭТАП ${currentLevel + 1}) ◀`;

  const rows = [];
  rows.push({ text: optMode, id: 0 });
  rows.push({ text: optLives, id: 1, isLives: true });
  rows.push({ text: optCar1, id: 2, sub: CAR_CLASSES[selectedCarClassP1].desc });
  if (isTwoPlayer) {
    rows.push({ text: optCar2, id: 3, sub: CAR_CLASSES[selectedCarClassP2].desc });
    rows.push({ text: optMusic, id: 4 });
    rows.push({ text: optStart, id: 5, isStart: true });
  } else {
    rows.push({ text: optMusic, id: 3 });
    rows.push({ text: optStart, id: 4, isStart: true });
  }

  let startY = 125;
  rows.forEach((r, idx) => {
    const sel = menuSelection === idx;
    ctx.font = 'bold 15px Courier New';
    if (r.isStart) {
      ctx.fillStyle = sel ? '#2ecc71' : '#7f8c8d';
      ctx.fillText(sel ? `★  ${r.text}  ★` : r.text, canvas.width/2, startY);
    } else if (r.isLives) {
      const bc = livesEnabled ? '#ff4757' : '#2ecc71';
      ctx.fillStyle = sel ? '#f1c40f' : bc;
      ctx.fillText(sel ? `▶  ${r.text}  ◀` : r.text, canvas.width/2, startY);
    } else {
      ctx.fillStyle = sel ? '#f1c40f' : '#888';
      ctx.fillText(sel ? `▶  ${r.text}  ◀` : r.text, canvas.width/2, startY);
      if (r.sub && sel) {
        ctx.font = '11px Courier New'; ctx.fillStyle = '#00e5ff';
        ctx.fillText(r.sub, canvas.width/2, startY + 18);
      }
    }
    startY += r.sub ? 52 : 44;
  });

  ctx.font = '11px Courier New'; ctx.fillStyle = '#aaa';
  ctx.fillText('ВВЕРХ/ВНИЗ — строки | ВЛЕВО/ВПРАВО — выбор | ENTER — старт', canvas.width/2, 520);
  ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 11px Courier New';
  ctx.fillText('P1 трюк [E] | P2 трюк [O] | Сальто = NITRO 480/5сек', canvas.width/2, 545);
  ctx.fillStyle = '#00ffff'; ctx.font = 'bold 11px Courier New';
  ctx.fillText('Щит = макс 355 | За дорогой = мгновенная смерть', canvas.width/2, 565);
  ctx.fillStyle = '#ff69b4'; ctx.font = 'bold 11px Courier New';
  ctx.fillText('Розовая дёргает ручник и таранит + возвращается злее!', canvas.width/2, 585);
  ctx.fillStyle = '#f1c40f'; ctx.font = 'bold 10px Courier New';
  ctx.fillText('P2 руль: IJKL / Стрелки / Numpad / Цифры 8-4-6-2-5', canvas.width/2, 608);
  ctx.textAlign = 'left';
}

function drawOverlay(title, subtitle, color) {
  ctx.fillStyle = 'rgba(0,0,0,0.85)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.textAlign = 'center';
  ctx.font = 'bold 26px Courier New'; ctx.fillStyle = color;
  ctx.fillText(title, canvas.width/2, canvas.height/2 - 20);
  ctx.font = '13px Courier New'; ctx.fillStyle = '#fff';
  ctx.fillText(subtitle, canvas.width/2, canvas.height/2 + 25);
  ctx.textAlign = 'left';
}

function drawGameOverMenu() {
  ctx.fillStyle = 'rgba(0,0,0,0.9)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#e74c3c'; ctx.font = 'bold 28px Courier New';
  ctx.fillText('ВРЕМЯ ВЫШЛО / РАЗБИТ!', canvas.width/2, 170);
  ctx.fillStyle = '#ecf0f1'; ctx.font = '14px Courier New';
  ctx.fillText(`ЭТАП ${currentLevel + 1}`, canvas.width/2, 205);
  if (livesEnabled) {
    const ll = getLivesLeft();
    ctx.fillStyle = '#f1c40f'; ctx.font = 'bold 13px Courier New';
    ctx.fillText(`ЖИЗНЕЙ: ${ll} / ${MAX_LIVES}`, canvas.width/2, 240);
    let h = '';
    for (let i = 0; i < MAX_LIVES; i++) h += (i < ll) ? '❤ ' : '✖ ';
    ctx.fillStyle = ll > 1 ? '#2ecc71' : '#e74c3c'; ctx.font = 'bold 22px Courier New';
    ctx.fillText(h, canvas.width/2, 275);
  } else {
    ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 13px Courier New';
    ctx.fillText('РЕЖИМ БЕЗ ЖИЗНЕЙ', canvas.width/2, 255);
  }
  const o1 = '▶ ПРОДОЛЖИТЬ С ТЕКУЩЕГО ◀';
  const o2 = '▶ НАЧАТЬ С 1 УРОВНЯ ◀';
  ctx.font = 'bold 15px Courier New';
  ctx.fillStyle = gameOverSelection === 0 ? '#f1c40f' : '#7f8c8d';
  ctx.fillText(gameOverSelection === 0 ? `★ ${o1} ★` : o1, canvas.width/2, 350);
  ctx.fillStyle = gameOverSelection === 1 ? '#f1c40f' : '#7f8c8d';
  ctx.fillText(gameOverSelection === 1 ? `★ ${o2} ★` : o2, canvas.width/2, 400);
  ctx.font = '11px Courier New'; ctx.fillStyle = '#aaa';
  ctx.fillText('↑/↓ + ENTER', canvas.width/2, 470);
  ctx.fillStyle = '#00e5ff'; ctx.font = 'bold 12px Courier New';
  ctx.fillText('Или [Ъ] для рестарта', canvas.width/2, 510);
  ctx.textAlign = 'left';
}

function drawAllLivesLostScreen() {
  ctx.fillStyle = 'rgba(0,0,0,0.92)'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#e74c3c'; ctx.font = 'bold 30px Courier New';
  ctx.fillText('ВСЕ ЖИЗНИ ПОТЕРЯНЫ!', canvas.width/2, 200);
  ctx.fillStyle = '#fff'; ctx.font = 'bold 15px Courier New';
  ctx.fillText(`Умерли ${MAX_LIVES} раза`, canvas.width/2, 250);
  ctx.fillStyle = '#f1c40f'; ctx.font = 'bold 17px Courier New';
  ctx.fillText('СТАРТ С 1-го УРОВНЯ', canvas.width/2, 310);
  if (Math.floor(Date.now()/500) % 2 === 0) {
    ctx.fillStyle = '#2ecc71'; ctx.font = 'bold 16px Courier New';
    ctx.fillText('НАЖМИ [ENTER]', canvas.width/2, 390);
  }
  ctx.fillStyle = '#00e5ff'; ctx.font = 'bold 12px Courier New';
  ctx.fillText('Или [Ъ]', canvas.width/2, 440);
  ctx.textAlign = 'left';
}

let lastTime = performance.now();
function gameLoop(time) {
  const dt = Math.min((time - lastTime) / 1000, 0.1);
  lastTime = time;
  update(dt);
  draw();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
</script>

</body>
</html>

Game Source: Road Fighter v4.9 - Handbrake Ram AI Edition

Creator: RocketKoala70

Libraries: none

Complexity: complex (1950 lines, 82.3 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: road-fighter-v4-9-handbrake-ram-ai-editi-rocketkoala70" to link back to the original. Then publish at arcadelab.ai/publish.