๐ŸŽฎArcadeLab

Hero Pilot

by StealthWolf75
1035 lines41.0 KB
โ–ถ Play
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Hero Pilot</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@700;800&display=swap');
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;}
html,body{width:100%;height:100%;overflow:hidden;background:#1a1a2e;touch-action:none;user-select:none;}
canvas{position:fixed;top:0;left:0;}

/* OVERLAYS */
.ov{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:400;background:rgba(0,0,0,0.6);padding:12px;}
.ov.hide{display:none!important;}
.card{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:24px;padding:28px 32px;text-align:center;color:#fff;max-width:460px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,0.6);border:3px solid rgba(255,255,255,0.2);max-height:92vh;overflow-y:auto;}
.card.ora{background:linear-gradient(135deg,#f39c12,#d35400);}
.card.grn{background:linear-gradient(135deg,#27ae60,#1e8449);}
.card h1{font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(24px,6vw,42px);margin-bottom:8px;text-shadow:2px 2px 0 rgba(0,0,0,0.25);}
.card h2{font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(18px,4.5vw,28px);margin-bottom:10px;}
.card p{font-size:clamp(12px,2.8vw,15px);line-height:1.55;margin-bottom:12px;opacity:.92;}
.btn{display:inline-block;margin:4px;background:#FFD700;color:#333;border:none;border-radius:50px;padding:clamp(9px,2vw,13px) clamp(18px,4vw,30px);font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(14px,3.5vw,20px);cursor:pointer;box-shadow:0 5px 0 #b8960c;transition:transform .1s,box-shadow .1s;}
.btn:active{transform:translateY(4px);box-shadow:0 1px 0 #b8960c;}
.btn.g{background:#2ecc71;box-shadow:0 5px 0 #1a8a4a;color:#fff;}
.btn.b{background:#3498db;box-shadow:0 5px 0 #1a6fa0;color:#fff;}

/* HUD */
#hud{position:fixed;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;padding:env(safe-area-inset-top,6px) 10px 0;z-index:200;pointer-events:none;}
.hb{background:rgba(255,255,255,0.93);border-radius:14px;padding:5px 11px;font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(11px,2.8vw,16px);box-shadow:0 2px 8px rgba(0,0,0,0.2);margin-top:6px;}
.hb.mo{color:#2d8a4e;}.hb.lv{color:#8e44ad;text-align:center;}.hb.pa{color:#2980b9;}
#savBar{position:fixed;top:clamp(46px,9vw,66px);left:50%;transform:translateX(-50%);background:rgba(255,255,255,0.93);border-radius:14px;padding:4px 12px;display:flex;align-items:center;gap:6px;font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(10px,2.4vw,13px);color:#e67e22;z-index:200;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,0.2);white-space:nowrap;}
.bOut{width:clamp(70px,18vw,130px);height:11px;background:#f0e0c8;border-radius:6px;overflow:hidden;}
.bIn{height:100%;background:linear-gradient(90deg,#f39c12,#e74c3c);border-radius:6px;transition:width .4s;}

/* SPEECH */
#speech{position:fixed;top:clamp(82px,16vw,110px);left:50%;transform:translateX(-50%);background:#fff;border-radius:18px;padding:8px 16px;font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(12px,2.8vw,16px);color:#333;max-width:min(350px,88vw);text-align:center;box-shadow:0 4px 18px rgba(0,0,0,0.3);z-index:200;pointer-events:none;transition:opacity .35s;width:max-content;}
#speech.hide{opacity:0;}

/* COMBO */
#combo{position:fixed;top:40%;left:50%;transform:translate(-50%,-50%);font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(26px,7vw,50px);color:#FFD700;text-shadow:0 0 20px rgba(255,200,0,0.9),3px 3px 0 rgba(0,0,0,0.3);z-index:300;pointer-events:none;text-align:center;transition:opacity .3s;}
#combo.hide{opacity:0;}

/* CONTROLLER */
#ctrl{position:fixed;bottom:0;left:0;right:0;height:clamp(140px,32vw,190px);display:flex;align-items:center;justify-content:space-between;z-index:200;padding:0 clamp(10px,3vw,20px) env(safe-area-inset-bottom,10px);}

/* JOYSTICK */
#joyWrap{position:relative;width:clamp(110px,28vw,160px);height:clamp(110px,28vw,160px);}
#joyBase{position:absolute;inset:0;border-radius:50%;background:rgba(255,255,255,0.13);border:2px solid rgba(255,255,255,0.38);backdrop-filter:blur(5px);}
#joyKnob{position:absolute;border-radius:50%;background:radial-gradient(circle at 35% 35%,rgba(255,255,255,0.92),rgba(200,200,200,0.7));box-shadow:0 3px 12px rgba(0,0,0,0.35);}
#joyLabel{position:absolute;bottom:-22px;left:50%;transform:translateX(-50%);font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:11px;color:rgba(255,255,255,0.55);white-space:nowrap;}

/* ACTION BUTTON */
#actBtn{width:clamp(88px,22vw,125px);height:clamp(88px,22vw,125px);border-radius:50%;background:radial-gradient(circle at 38% 32%,#FFE566,#FFB700);border:3px solid rgba(255,255,255,0.65);display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:โ€˜Fredoka Oneโ€™,cursive;font-size:clamp(9px,2.2vw,13px);color:#333;box-shadow:0 6px 0 rgba(0,0,0,0.28),0 8px 20px rgba(0,0,0,0.3);cursor:pointer;line-height:1.25;text-align:center;transition:transform .08s,box-shadow .08s;}
#actBtn.hit{transform:scale(0.9) translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,0.28);}
#actIcon{font-size:clamp(20px,5.5vw,30px);display:block;}

/* CHAR SELECT */
.cp{display:inline-flex;align-items:center;justify-content:center;width:clamp(46px,11vw,62px);height:clamp(46px,11vw,62px);margin:3px;border-radius:50%;border:3px solid rgba(255,255,255,0.28);cursor:pointer;font-size:clamp(22px,5.5vw,30px);transition:transform .15s,border-color .15s;background:rgba(255,255,255,0.1);}
.cp.sel{border-color:#FFD700;transform:scale(1.18);background:rgba(255,215,0,0.22);}
</style>

</head>
<body>
<canvas id="c"></canvas>

<!-- HUD -->

<div id="hud">
  <div class="hb mo">๐Ÿ’ฐ$<span id="hMoney">0</span></div>
  <div class="hb lv"><div id="hLevel" style="font-size:clamp(13px,3vw,18px)">Level 1</div><div id="hLoc" style="font-size:clamp(9px,2vw,11px);color:#888">๐ŸŒ</div></div>
  <div class="hb pa">๐Ÿง<span id="hPass">0</span>/<span id="hTotal">0</span></div>
</div>
<div id="savBar">๐Ÿฆ<div class="bOut"><div class="bIn" id="savFill" style="width:0%"></div></div><span id="hSav">$0</span></div>
<div id="speech" class="hide"></div>
<div id="combo" class="hide"></div>

<!-- CONTROLLER -->

<div id="ctrl">
  <div id="joyWrap">
    <div id="joyBase"></div>
    <div id="joyKnob"></div>
    <div id="joyLabel">MOVE</div>
  </div>
  <div id="actBtn">
    <span id="actIcon">๐Ÿค</span>
    PICK UP /<br>DROP OFF
  </div>
</div>

<!-- START -->

<div class="ov" id="scrStart">
  <div class="card">
    <h1>๐Ÿš—โœจ Hero Pilot!</h1>
    <p>Fly your magical <strong>winged car</strong> around the world! Pick up passengers, help communities, and learn to <strong>save money</strong> for big rewards!</p>
    <p><strong>Choose your pilot:</strong></p>
    <div id="charGrid" style="margin-bottom:14px"></div>
    <p style="opacity:.7;font-size:clamp(11px,2.5vw,13px);margin-bottom:14px">Use the joystick โ† โ†’ to fly โ€ข Gold button to pick up &amp; drop off</p>
    <button class="btn g" id="startBtn">๐Ÿš€ Let's Fly!</button>
  </div>
</div>

<!-- LEVEL COMPLETE -->

<div class="ov hide" id="scrLevel">
  <div class="card ora">
    <h2 id="lcTitle">๐ŸŽ‰ Level Complete!</h2>
    <p id="lcText"></p>
    <div id="lcSave">
      <p style="color:#FFD700;font-family:'Fredoka One',cursive;font-size:clamp(13px,3vw,18px)">๐Ÿ’ก Delay Gratification!</p>
      <p>You earned <strong id="lcAmt"></strong>! What will you do?</p>
      <button class="btn" id="btnSpend">๐Ÿฆ Spend Now!</button>
      <button class="btn g" id="btnSave">๐Ÿฆ Save it! (+50%!)</button>
    </div>
    <div id="lcNext"><button class="btn b" id="btnNext">Next Level โžก๏ธ</button></div>
  </div>
</div>

<!-- WIN -->

<div class="ov hide" id="scrWin">
  <div class="card grn">
    <h1>๐Ÿ† YOU WIN!</h1>
    <p id="winText"></p>
    <button class="btn" id="btnReplay">Play Again! ๐Ÿ”„</button>
  </div>
</div>

<script>
(function() {
'use strict';

// โ”€โ”€ PILOTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var PILOTS = [
  { emoji:'๐Ÿ‘ฆ๐Ÿป', hat:'๐Ÿงข', carCol:'#FF6B35', wingCol:'#FFD700', name:'Alex' },
  { emoji:'๐Ÿ‘ง๐Ÿฝ', hat:'๐ŸŽ€', carCol:'#9B59B6', wingCol:'#F1C40F', name:'Maya' },
  { emoji:'๐Ÿง’๐Ÿฟ', hat:'โญ', carCol:'#27AE60', wingCol:'#F39C12', name:'Sam'  },
  { emoji:'๐Ÿ‘ฉ๐Ÿผ', hat:'๐ŸŒธ', carCol:'#E74C3C', wingCol:'#fff',    name:'Zara' },
  { emoji:'๐Ÿ‘จ๐Ÿพ', hat:'๐ŸŽฉ', carCol:'#2980B9', wingCol:'#1ABC9C', name:'Dre'  },
  { emoji:'๐Ÿ‘ฑ๐Ÿปโ€โ™€๏ธ',hat:'๐ŸŒˆ', carCol:'#D81B60', wingCol:'#FFD700', name:'Kai'  },
];
var pilotIdx = 0;

// Build character grid
var charGrid = document.getElementById('charGrid');
PILOTS.forEach(function(p, i) {
  var el = document.createElement('span');
  el.className = 'cp' + (i === 0 ? ' sel' : '');
  el.textContent = p.emoji;
  el.title = p.name;
  el.addEventListener('click', function() {
    document.querySelectorAll('.cp').forEach(function(e){ e.classList.remove('sel'); });
    el.classList.add('sel');
    pilotIdx = i;
  });
  charGrid.appendChild(el);
});

// โ”€โ”€ LEVELS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var LEVELS = [
  {
    name:'Level 1', loc:'๐Ÿ™๏ธ Sunny City',
    sky:['#87CEEB','#B0E0E6'], gTop:'#5D8A3C', gFill:'#4CAF50',
    pax:[
      {fx:.14, emoji:'๐Ÿ‘ด', dest:'hospital', say:'Take me to the hospital!'},
      {fx:.40, emoji:'๐Ÿ‘ฉโ€๐Ÿซ', dest:'school',   say:'I need to get to school!'},
      {fx:.70, emoji:'๐Ÿ‘ถ', dest:'park',     say:'Wahh! I wanna go to the park!'},
    ],
    dests:[
      {fx:.25, type:'hospital', emoji:'๐Ÿฅ', label:'Hospital',  col:'#e74c3c'},
      {fx:.53, type:'school',   emoji:'๐Ÿซ', label:'School',    col:'#3498db'},
      {fx:.84, type:'park',     emoji:'๐ŸŒณ', label:'Park',      col:'#2ecc71'},
    ],
    wind:.04, numCoins:4,
    tip:'Welcome! ๐ŸŽ‰ Use the joystick to fly. Get near a โ— person and tap the gold button!'
  },
  {
    name:'Level 2', loc:'๐Ÿ”๏ธ Mountain Valley',
    sky:['#B0C4DE','#C8D8E8'], gTop:'#7B8B6F', gFill:'#8B7355',
    pax:[
      {fx:.09, emoji:'โ›ท๏ธ',    dest:'lodge',  say:'Take me to the ski lodge!'},
      {fx:.32, emoji:'๐Ÿง‘โ€๐ŸŒพ', dest:'market', say:'My veggies need the market!'},
      {fx:.57, emoji:'๐Ÿง‘โ€โš•๏ธ', dest:'clinic', say:'Emergency at the clinic!'},
      {fx:.80, emoji:'๐Ÿ“ฆ',    dest:'house',  say:'Deliver me home!'},
    ],
    dests:[
      {fx:.20, type:'lodge',  emoji:'๐Ÿ ', label:'Ski Lodge', col:'#8B4513'},
      {fx:.43, type:'market', emoji:'๐Ÿ›’', label:'Market',    col:'#FF8C00'},
      {fx:.66, type:'clinic', emoji:'โš•๏ธ', label:'Clinic',    col:'#DC143C'},
      {fx:.88, type:'house',  emoji:'๐Ÿก', label:'House',     col:'#9370DB'},
    ],
    wind:.18, numCoins:6,
    tip:'Mountain gusts! Push joystick UP to climb. Collect ๐Ÿช™ coins for bonus cash!'
  },
  {
    name:'Level 3', loc:'๐ŸŒŠ Tropical Islands',
    sky:['#00CED1','#40E0D0'], gTop:'#DEB887', gFill:'#F4A460',
    pax:[
      {fx:.08, emoji:'๐Ÿคฟ',    dest:'reef',    say:'To the coral reef!'},
      {fx:.27, emoji:'๐Ÿ‘ฉโ€๐Ÿ”ฌ', dest:'lab',     say:'Science samples for the lab!'},
      {fx:.47, emoji:'๐Ÿง‘โ€๐ŸŽจ', dest:'gallery', say:'Art show today!'},
      {fx:.67, emoji:'๐Ÿ‘ต',    dest:'home',    say:'Take me home dear!'},
      {fx:.87, emoji:'๐Ÿ„',    dest:'beach',   say:"Surf's up at the north beach!"},
    ],
    dests:[
      {fx:.17, type:'reef',    emoji:'๐Ÿ ', label:'Coral Reef', col:'#FF6347'},
      {fx:.35, type:'lab',     emoji:'๐Ÿ”ฌ', label:'Lab',        col:'#4169E1'},
      {fx:.55, type:'gallery', emoji:'๐Ÿ–ผ๏ธ', label:'Gallery',    col:'#DA70D6'},
      {fx:.74, type:'home',    emoji:'๐Ÿ ', label:'Home',       col:'#32CD32'},
      {fx:.92, type:'beach',   emoji:'๐Ÿ–๏ธ', label:'Beach',      col:'#FFD700'},
    ],
    wind:-.13, numCoins:8,
    tip:'Ocean breeze blows LEFT! Pick up all 5 passengers! ๐ŸŒŠ'
  },
  {
    name:'Level 4', loc:'๐ŸŒ† Mega City Rush',
    sky:['#FF8C69','#FFA07A'], gTop:'#696969', gFill:'#808080',
    pax:[
      {fx:.07, emoji:'๐Ÿ‘จโ€๐Ÿ’ผ', dest:'office',     say:'Late for my meeting!'},
      {fx:.24, emoji:'๐Ÿ‘ฉโ€๐Ÿณ', dest:'restaurant', say:'Lunch rush at the restaurant!'},
      {fx:.43, emoji:'๐Ÿง‘โ€๐Ÿš’', dest:'station',    say:'Back to the fire station!'},
      {fx:.62, emoji:'๐ŸŽค',    dest:'stage',      say:'Concert starts in 5 minutes!'},
      {fx:.82, emoji:'๐Ÿ‘จโ€๐Ÿ”ง', dest:'garage',     say:'Cars need fixing!'},
    ],
    dests:[
      {fx:.15, type:'office',      emoji:'๐Ÿข', label:'Office',      col:'#4682B4'},
      {fx:.33, type:'restaurant',  emoji:'๐Ÿฝ๏ธ', label:'Restaurant',  col:'#FF4500'},
      {fx:.51, type:'station',     emoji:'๐Ÿš’', label:'Fire Station', col:'#DC143C'},
      {fx:.70, type:'stage',       emoji:'๐ŸŽญ', label:'Stage',        col:'#9400D3'},
      {fx:.88, type:'garage',      emoji:'๐Ÿ”ง', label:'Garage',       col:'#A0522D'},
    ],
    wind:.22, numCoins:10,
    tip:'City air pockets! Stay steady and collect all the coins! ๐Ÿ™๏ธ'
  },
  {
    name:'Level 5', loc:'๐ŸŒ Around the World!',
    sky:['#0d0d2b','#1a1a5e'], gTop:'#2F4F4F', gFill:'#3CB371',
    pax:[
      {fx:.07, emoji:'๐Ÿง‘โ€๐Ÿš€', dest:'launch', say:'To the launch pad!'},
      {fx:.22, emoji:'๐Ÿ‘ธ',    dest:'castle', say:'My castle awaits!'},
      {fx:.37, emoji:'๐Ÿง™',    dest:'tower',  say:'Magic books at the tower!'},
      {fx:.53, emoji:'๐Ÿฆธ',    dest:'hq',     say:'Superhero HQ please!'},
      {fx:.70, emoji:'๐ŸŒบ',    dest:'garden', say:'Rare flowers for the garden!'},
      {fx:.87, emoji:'๐Ÿง‘โ€๐ŸŽ“', dest:'hall',   say:'Graduation ceremony!'},
    ],
    dests:[
      {fx:.14, type:'launch', emoji:'๐Ÿš€', label:'Launch Pad',  col:'#FF6347'},
      {fx:.29, type:'castle', emoji:'๐Ÿฐ', label:'Castle',       col:'#9370DB'},
      {fx:.45, type:'tower',  emoji:'๐Ÿ—ผ', label:'Magic Tower',  col:'#20B2AA'},
      {fx:.61, type:'hq',     emoji:'โšก', label:'Hero HQ',      col:'#FFD700'},
      {fx:.77, type:'garden', emoji:'๐ŸŒธ', label:'Garden',       col:'#FF69B4'},
      {fx:.92, type:'hall',   emoji:'๐ŸŽ“', label:'Grad Hall',    col:'#4169E1'},
    ],
    wind:-.20, numCoins:12,
    tip:'THE FINAL LEVEL! You are the ultimate Hero Pilot! ๐ŸŒŸ'
  }
];

// โ”€โ”€ CANVAS & SIZING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var cv = document.getElementById('c');
var ctx = cv.getContext('2d');
var W = 0, H = 0, DPR = 1;

function resize() {
  DPR = Math.min(window.devicePixelRatio || 1, 2.5);
  W = window.innerWidth;
  H = window.innerHeight;
  cv.width  = Math.round(W * DPR);
  cv.height = Math.round(H * DPR);
  cv.style.width  = W + 'px';
  cv.style.height = H + 'px';
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}
resize();
window.addEventListener('resize', function() {
  resize();
  if (gs && gs.started) recalcLayout();
});

function ctrlTop() {
  var el = document.getElementById('ctrl');
  return el ? el.getBoundingClientRect().top : H - 160;
}
function sc() { return Math.min(W, H) / 420; }  // global scale factor

// โ”€โ”€ GAME STATE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var gs = { level:0, money:0, savings:0, bonus:false, started:false, streak:0, coins:0 };

// Car object
var car = { x:0, y:0, vx:0, vy:0, angle:0, w:72, h:34, pax:[] };

// Level dynamic data
var ld = null;

// Animation frame counter, timers
var fr = 0;
var spTimer    = 0;
var comboTimer = 0;
var lvlDone    = false;
var actPending = false;

// โ”€โ”€ JOYSTICK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var joyWrap  = document.getElementById('joyWrap');
var joyKnob  = document.getElementById('joyKnob');
var joy = { x:0, y:0, tid:-1 };

function updateKnob() {
  var sz  = joyWrap.offsetWidth;
  var ksz = joyKnob.offsetWidth;
  var maxR = sz * 0.36;
  var kx = sz/2 + joy.x * maxR - ksz/2;
  var ky = sz/2 + joy.y * maxR - ksz/2;
  joyKnob.style.left = kx + 'px';
  joyKnob.style.top  = ky + 'px';
}

function setKnobSize() {
  var sz  = joyWrap.offsetWidth;
  var ksz = Math.round(sz * 0.42);
  joyKnob.style.width  = ksz + 'px';
  joyKnob.style.height = ksz + 'px';
  joyKnob.style.left   = (sz/2 - ksz/2) + 'px';
  joyKnob.style.top    = (sz/2 - ksz/2) + 'px';
}

function joyMove(clientX, clientY) {
  var r   = joyWrap.getBoundingClientRect();
  var cx2 = r.left + r.width  / 2;
  var cy2 = r.top  + r.height / 2;
  var dx  = clientX - cx2;
  var dy  = clientY - cy2;
  var dist = Math.sqrt(dx*dx + dy*dy);
  var maxD = r.width * 0.38;
  if (dist > maxD) { dx = dx/dist*maxD; dy = dy/dist*maxD; }
  joy.x = dx / maxD;
  joy.y = dy / maxD;
  updateKnob();
}

function joyReset() {
  joy.x = 0; joy.y = 0; joy.tid = -1;
  updateKnob();
}

joyWrap.addEventListener('touchstart', function(e) {
  e.preventDefault();
  var t = e.changedTouches[0];
  joy.tid = t.identifier;
  joyMove(t.clientX, t.clientY);
}, { passive: false });

joyWrap.addEventListener('touchmove', function(e) {
  e.preventDefault();
  for (var i = 0; i < e.changedTouches.length; i++) {
    var t = e.changedTouches[i];
    if (t.identifier === joy.tid) { joyMove(t.clientX, t.clientY); break; }
  }
}, { passive: false });

joyWrap.addEventListener('touchend',    function(e){ e.preventDefault(); joyReset(); }, { passive:false });
joyWrap.addEventListener('touchcancel', function(e){ e.preventDefault(); joyReset(); }, { passive:false });
joyWrap.addEventListener('mousedown',   function(e){ joyMove(e.clientX, e.clientY); });
window.addEventListener( 'mousemove',   function(e){ if (joy.tid !== -1 || e.buttons) joyMove(e.clientX, e.clientY); });
window.addEventListener( 'mouseup',     function()  { joyReset(); });

// โ”€โ”€ ACTION BUTTON โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var actBtn  = document.getElementById('actBtn');
var actIcon = document.getElementById('actIcon');

function triggerAction() {
  actPending = true;
  actBtn.classList.add('hit');
  actIcon.style.transform = 'scale(1.3)';
  setTimeout(function() {
    actBtn.classList.remove('hit');
    actIcon.style.transform = '';
  }, 160);
}

actBtn.addEventListener('touchstart', function(e){ e.preventDefault(); triggerAction(); }, { passive:false });
actBtn.addEventListener('mousedown',  function()  { triggerAction(); });

// โ”€โ”€ KEYBOARD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var keys = {};
document.addEventListener('keydown', function(e) {
  keys[e.key] = true;
  if ([' ','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(e.key) >= 0) e.preventDefault();
  if (e.key === ' ') triggerAction();
});
document.addEventListener('keyup', function(e) { keys[e.key] = false; });

function goU() { return joy.y < -0.2 || keys['ArrowUp']    || keys['w'] || keys['W']; }
function goD() { return joy.y >  0.2 || keys['ArrowDown']  || keys['s'] || keys['S']; }
function goL() { return joy.x < -0.2 || keys['ArrowLeft']  || keys['a'] || keys['A']; }
function goR() { return joy.x >  0.2 || keys['ArrowRight'] || keys['d'] || keys['D']; }

// โ”€โ”€ BUTTON WIRING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('btnSpend').addEventListener('click', spendNow);
document.getElementById('btnSave').addEventListener('click',  saveIt);
document.getElementById('btnNext').addEventListener('click',  nextLevel);
document.getElementById('btnReplay').addEventListener('click', function(){ location.reload(); });

// โ”€โ”€ HELPERS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function clamp(v, mn, mx) { return v < mn ? mn : v > mx ? mx : v; }

function showEl(id) { document.getElementById(id).classList.remove('hide'); }
function hideEl(id) { document.getElementById(id).classList.add('hide'); }

function updateHUD() {
  document.getElementById('hMoney').textContent = gs.money;
  document.getElementById('hPass').textContent  = ld.delivered;
  document.getElementById('hTotal').textContent = ld.total;
  document.getElementById('hSav').textContent   = '$' + gs.savings;
  document.getElementById('savFill').style.width = clamp((gs.savings / 700) * 100, 0, 100) + '%';
}

function say(txt, ms) {
  ms = ms || 3000;
  var el = document.getElementById('speech');
  el.textContent = txt;
  el.classList.remove('hide');
  spTimer = ms;
}

function showCombo(txt) {
  var el = document.getElementById('combo');
  el.textContent = txt;
  el.classList.remove('hide');
  comboTimer = 1800;
}

// โ”€โ”€ LAYOUT RECALC โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function recalcLayout() {
  var s = sc();
  car.w = Math.round(72 * s);
  car.h = Math.round(34 * s);
  setKnobSize();
  if (ld) ld.gY = ctrlTop() - 8;
}

// โ”€โ”€ GENERATE COINS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function genCoins(n, worldW, gY) {
  var coins = [];
  for (var i = 0; i < n; i++) {
    coins.push({
      wx: 80 + Math.random() * (worldW - 200),
      wy: gY * 0.12 + Math.random() * gY * 0.5,
      r:  clamp(9 * sc(), 7, 16),
      ph: Math.random() * Math.PI * 2,
      collected: false
    });
  }
  return coins;
}

// โ”€โ”€ LOAD LEVEL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function loadLevel(idx) {
  lvlDone = false;
  actPending = false;
  var lv = LEVELS[idx];
  document.getElementById('hLevel').textContent = lv.name;
  document.getElementById('hLoc').textContent   = lv.loc;

  recalcLayout();
  var gY     = ctrlTop() - 8;
  var worldW = W * 2.6;

  // Convert fractional positions to world pixels
  function wx(f) { return f * worldW; }

  var passengers = lv.pax.map(function(p, i) {
    return { wx:wx(p.fx), wy:gY-32, emoji:p.emoji, dest:p.dest, say:p.say, id:i, picked:false, done:false };
  });

  var dests = lv.dests.map(function(d) {
    return { wx:wx(d.fx), wy:gY, type:d.type, emoji:d.emoji, label:d.label, col:d.col };
  });

  var clouds = [];
  for (var i = 0; i < 16; i++) {
    clouds.push({ wx: Math.random()*worldW, wy: 40+Math.random()*(gY*0.5), r: clamp(30+Math.random()*55,28,90), spd: 0.3+Math.random()*0.35 });
  }

  var buildings = [];
  var numB = 28;
  for (var i = 0; i < numB; i++) {
    buildings.push({ wx: i*(worldW/numB)+15, bw: clamp(22+Math.random()*55,22,76), bh: clamp(28+Math.random()*110,28,130), col: 'hsl('+Math.round(Math.random()*360)+',32%,'+Math.round(38+Math.random()*20)+'%)' });
  }

  var stars = [];
  if (lv.sky[0].charAt(1) === '0' || lv.sky[0].charAt(1) === '1') {
    for (var i = 0; i < 80; i++) {
      stars.push({ wx: Math.random()*worldW, wy: Math.random()*(gY*0.7), r: Math.random()*1.8, ph: Math.random()*Math.PI*2 });
    }
  }

  ld = {
    worldW:  worldW,
    gY:      gY,
    scrollX: 0,
    wind:    lv.wind,
    delivered: 0,
    total:   passengers.length,
    pax:     passengers,
    dests:   dests,
    coins:   genCoins(lv.numCoins, worldW, gY),
    clouds:  clouds,
    buildings: buildings,
    stars:   stars,
    parts:   []    // particles
  };

  car.x = 80;
  car.y = gY - car.h - 20;
  car.vx = 0;
  car.vy = 0;
  car.angle = 0;
  car.pax = [];

  updateHUD();
  say(lv.tip, 5000);
}

// โ”€โ”€ START GAME โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function startGame() {
  hideEl('scrStart');
  gs.started = true;
  loadLevel(0);
  requestAnimationFrame(loop);
}

// โ”€โ”€ PHYSICS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function physics() {
  var s      = sc();
  var thrust = 0.40 * s;
  var grav   = 0.23 * s;
  var drag   = 0.974;
  var maxV   = 9  * s;

  // Horizontal
  var jx = clamp(joy.x, -1, 1);
  var jy = clamp(joy.y, -1, 1);
  if (goL()) jx = Math.min(jx, -0.65);
  if (goR()) jx = Math.max(jx,  0.65);
  if (goU()) jy = Math.min(jy, -0.65);
  if (goD()) jy = Math.max(jy,  0.30);

  if (jx < -0.1) {
    car.vx -= thrust * Math.abs(jx);
    car.angle = Math.max(car.angle - 0.04, -0.40);
  } else if (jx > 0.1) {
    car.vx += thrust * Math.abs(jx);
    car.angle = Math.min(car.angle + 0.04,  0.40);
  } else {
    car.angle *= 0.87;
  }

  if (jy < -0.1) {
    car.vy -= thrust * 1.1 * Math.abs(jy);
    car.vx += Math.sin(car.angle) * thrust * 0.45;
  }
  if (jy >  0.1) { car.vy += thrust * 0.5 * Math.abs(jy); }

  // Speed-based lift
  var spd = Math.abs(car.vx);
  if (spd > 1.5 * s) car.vy -= 0.15 * s * Math.min(spd / maxV, 1);

  // Wind
  car.vx += ld.wind * s;

  // Gravity & drag
  car.vy += grav;
  car.vx *= drag;
  car.vy *= drag;
  car.vx = clamp(car.vx, -maxV, maxV);
  car.vy = clamp(car.vy, -maxV * 1.2, maxV * 1.4);

  car.x += car.vx;
  car.y += car.vy;
  car.x = clamp(car.x, 0, ld.worldW - car.w);

  // Ground
  var gnd = ld.gY - car.h / 2;
  if (car.y >= gnd) { car.y = gnd; car.vy = 0; car.vx *= 0.80; }
  if (car.y < 30)   { car.y = 30;  car.vy = Math.abs(car.vy) * 0.4; }

  // Coin collection
  var ccx = car.x + car.w / 2;
  var ccy = car.y + car.h / 2;
  ld.coins.forEach(function(coin) {
    if (coin.collected) return;
    var dx = ccx - coin.wx;
    var dy = ccy - coin.wy;
    if (Math.sqrt(dx*dx + dy*dy) < coin.r + car.h / 2 + 6) {
      coin.collected = true;
      var earn = 10 + (gs.bonus ? 5 : 0);
      gs.money   += earn;
      gs.savings += earn;
      gs.coins++;
      burst(coin.wx, coin.wy, '#FFD700', 8);
      updateHUD();
      showCombo('๐Ÿช™ +$' + earn + '!');
    }
  });
}

// โ”€โ”€ INTERACT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function interact() {
  if (!actPending) return;
  actPending = false;

  var ccx   = car.x + car.w / 2;
  var ccy   = car.y + car.h / 2;
  var reach = car.h * 2.4;

  // Try pickup first
  if (car.pax.length < 4) {
    for (var i = 0; i < ld.pax.length; i++) {
      var p = ld.pax[i];
      if (p.picked || p.done) continue;
      var dx = (p.wx + 16) - ccx;
      var dy = p.wy - ccy;
      if (Math.sqrt(dx*dx + dy*dy) < reach + 28) {
        p.picked = true;
        car.pax.push(p);
        gs.streak++;
        say(p.emoji + ' ' + p.say);
        burst(p.wx, p.wy, '#FFD700', 12);
        if (gs.streak >= 3) showCombo('๐Ÿ”ฅ ' + gs.streak + ' pickups!');
        return;
      }
    }
  }

  // Try dropoff
  if (car.pax.length > 0) {
    for (var i = 0; i < ld.dests.length; i++) {
      var d = ld.dests[i];
      var dx = (d.wx + 44) - ccx;
      var dy = d.wy - ccy;
      if (Math.sqrt(dx*dx + dy*dy) < reach + 45) {
        var hits = car.pax.filter(function(p){ return p.dest === d.type; });
        if (hits.length > 0) {
          var earnPer = gs.bonus ? 45 : 30;
          hits.forEach(function(p) {
            p.done = true;
            car.pax = car.pax.filter(function(pp){ return pp.id !== p.id; });
            ld.delivered++;
            gs.money   += earnPer;
            gs.savings += earnPer;
            burst(d.wx + 44, d.wy - 40, d.col, 15);
          });
          updateHUD();
          say('๐ŸŽ‰ Delivered! +$' + (earnPer * hits.length) + '! Community thanks you!');
          if (gs.streak >= 2) showCombo('โœจ STREAK x' + gs.streak + '!');
          gs.streak = 0;
          if (ld.delivered >= ld.total && !lvlDone) {
            lvlDone = true;
            setTimeout(showLvlDone, 1100);
          }
        } else {
          say('๐Ÿ—บ๏ธ Wrong stop! Check where this passenger needs to go!');
          gs.streak = 0;
        }
        return;
      }
    }
  }

  // Nothing nearby
  if (car.pax.length === 0) say('Fly close to a โ— person, then tap the gold button!');
  else                       say('Fly to a glowing pad, then tap the gold button!');
  gs.streak = 0;
}

// โ”€โ”€ PARTICLES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function burst(x, y, col, n) {
  n = n || 14;
  for (var i = 0; i < n; i++) {
    ld.parts.push({
      x: x, y: y,
      vx: (Math.random() - .5) * 9,
      vy: (Math.random() - .5) * 9 - 2,
      life: 1,
      col: col,
      sz: 4 + Math.random() * 9
    });
  }
}

function tickParts() {
  ld.parts = ld.parts.filter(function(p){ return p.life > 0; });
  ld.parts.forEach(function(p){ p.x+=p.vx; p.y+=p.vy; p.vy+=0.12; p.life-=0.024; });
}

// โ”€โ”€ LEVEL DONE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function showLvlDone() {
  document.getElementById('lcNext').style.display = 'none';
  document.getElementById('lcSave').style.display = 'none';
  if (gs.level < 4) {
    document.getElementById('lcTitle').textContent = '๐ŸŽ‰ Level Complete!';
    document.getElementById('lcText').textContent  = 'You delivered all ' + ld.total + ' passengers in ' + LEVELS[gs.level].loc + '! ๐Ÿช™ Coins: ' + gs.coins + ' Your community loves you! ๐ŸŒŸ';
    document.getElementById('lcAmt').textContent   = '$' + gs.savings;
    document.getElementById('lcSave').style.display = 'block';
  } else {
    document.getElementById('lcTitle').textContent = '๐ŸŽŠ You Beat the Game!';
    document.getElementById('lcText').textContent  = 'You are the ultimate Hero Pilot! ๐Ÿ†';
    document.getElementById('lcNext').style.display = 'block';
  }
  showEl('scrLevel');
}

function spendNow() { gs.bonus = false; hideEl('scrLevel'); advance(); }
function saveIt()   { gs.bonus = true;  say('Patience pays! +50% bonus next level! ๐ŸŒŸ', 4000); hideEl('scrLevel'); advance(); }
function nextLevel(){ hideEl('scrLevel'); advance(); }

function advance() {
  if (gs.level >= 4) {
    document.getElementById('winText').textContent = 'You helped everyone around the world! ๐ŸŒ Total Saved: $' + gs.savings + ' | Coins: ๐Ÿช™' + gs.coins;
    hideEl('scrLevel');
    showEl('scrWin');
    return;
  }
  gs.level++;
  loadLevel(gs.level);
}

// โ”€โ”€ SCROLL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function scrollUpdate() {
  ld.scrollX = clamp(car.x - W / 2, 0, ld.worldW - W);
}

// โ”€โ”€ DRAW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function draw() {
  var lv = LEVELS[gs.level];
  if (!lv || !ld) return;
  var sx = ld.scrollX;
  var gY = ld.gY;
  var s  = sc();
  var pl = PILOTS[pilotIdx];

  ctx.clearRect(0, 0, W, H);

  // Sky
  var skyG = ctx.createLinearGradient(0, 0, 0, H);
  skyG.addColorStop(0, lv.sky[0]);
  skyG.addColorStop(1, lv.sky[1]);
  ctx.fillStyle = skyG;
  ctx.fillRect(0, 0, W, H);

  // Stars (parallax 0.3)
  ld.stars.forEach(function(st) {
    var px = st.wx - sx * 0.3;
    if (px < -4 || px > W + 4) return;
    var t = 0.4 + 0.6 * Math.sin(fr * 0.045 + st.ph);
    ctx.fillStyle = 'rgba(255,255,255,' + t.toFixed(2) + ')';
    ctx.beginPath(); ctx.arc(px, st.wy, st.r, 0, Math.PI * 2); ctx.fill();
  });

  // Clouds (parallax 0.7)
  ld.clouds.forEach(function(c) {
    c.wx -= c.spd;
    if (c.wx < -200) c.wx = ld.worldW + 100;
    var px = c.wx - sx * 0.7;
    if (px < -200 || px > W + 200) return;
    ctx.fillStyle = 'rgba(255,255,255,0.82)';
    ctx.beginPath();
    ctx.arc(px,            c.wy,          c.r,       0, Math.PI*2);
    ctx.arc(px + c.r*.57, c.wy - c.r*.2, c.r*.66,   0, Math.PI*2);
    ctx.arc(px - c.r*.44, c.wy + c.r*.1, c.r*.54,   0, Math.PI*2);
    ctx.fill();
  });

  // Buildings
  ld.buildings.forEach(function(b) {
    var bx = b.wx - sx;
    if (bx < -90 || bx > W + 90) return;
    ctx.fillStyle = b.col;
    ctx.fillRect(bx, gY - b.bh, b.bw, b.bh);
    ctx.fillStyle = 'rgba(255,255,180,0.45)';
    for (var wy = gY - b.bh + 8; wy < gY - 8; wy += 13) {
      for (var bwx = bx + 4; bwx < bx + b.bw - 4; bwx += 10) {
        ctx.fillRect(bwx, wy, 5, 7);
      }
    }
  });

  // Ground
  ctx.fillStyle = lv.gFill; ctx.fillRect(0, gY, W, H - gY);
  ctx.fillStyle = lv.gTop;  ctx.fillRect(0, gY, W, 6);
  ctx.fillStyle = 'rgba(0,0,0,0.07)';
  for (var gx = (-(sx % 55)); gx < W; gx += 55) ctx.fillRect(gx, gY, 2, H - gY);

  // Destinations
  ld.dests.forEach(function(d) {
    var dx = d.wx - sx;
    if (dx < -140 || dx > W + 140) return;
    var pulse  = 0.55 + 0.45 * Math.sin(fr * 0.07);
    var padW   = 80 * s;
    var centerX = dx + 44;

    ctx.shadowColor = d.col; ctx.shadowBlur = 12 * pulse;
    ctx.fillStyle = d.col + 'bb';
    ctx.fillRect(centerX - padW/2, gY - 9, padW, 9);
    ctx.shadowBlur = 0;

    ctx.font = Math.round(22 * s) + 'px serif';
    ctx.textAlign = 'center';
    ctx.fillText(d.emoji, centerX, gY - 12);

    var lblSz = Math.round(12 * s);
    ctx.font = 'bold ' + lblSz + 'px Nunito,sans-serif';
    ctx.lineWidth = 3; ctx.strokeStyle = d.col;
    ctx.strokeText(d.label, centerX, gY - 50 * s);
    ctx.fillStyle = '#fff';
    ctx.fillText(d.label, centerX, gY - 50 * s);
    ctx.lineWidth = 1; ctx.textAlign = 'left';

    ctx.strokeStyle = d.col; ctx.lineWidth = 2 * pulse; ctx.globalAlpha = 0.4 * pulse;
    ctx.beginPath(); ctx.arc(centerX, gY - 66 * s, 18 * s + pulse * 7, 0, Math.PI*2); ctx.stroke();
    ctx.globalAlpha = 1; ctx.lineWidth = 1;
  });

  // Passengers waiting
  ld.pax.forEach(function(p) {
    if (p.picked || p.done) return;
    var px = p.wx - sx;
    if (px < -80 || px > W + 80) return;
    var bob = Math.sin(fr * 0.065 + p.id) * 3;
    var bubR = 15 * s;

    ctx.fillStyle = 'rgba(255,255,255,0.94)';
    ctx.beginPath(); ctx.arc(px + 14, p.wy - 33*s + bob, bubR, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = '#e74c3c';
    ctx.font = 'bold ' + Math.round(14*s) + 'px Nunito,sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('!', px + 14, p.wy - 28*s + bob);
    ctx.textAlign = 'left';

    ctx.font = Math.round(26*s) + 'px serif';
    ctx.fillText(p.emoji, px, p.wy + bob);

    // Destination hint
    var dm = null;
    for (var i = 0; i < ld.dests.length; i++) {
      if (ld.dests[i].type === p.dest) { dm = ld.dests[i]; break; }
    }
    if (dm) {
      ctx.font = Math.round(10*s) + 'px Nunito,sans-serif';
      ctx.fillStyle = 'rgba(0,0,0,0.62)';
      ctx.textAlign = 'center';
      ctx.fillText('โ†’' + dm.label, px + 14, p.wy + 28*s + bob);
      ctx.textAlign = 'left';
    }
  });

  // Coins
  ld.coins.forEach(function(coin) {
    if (coin.collected) return;
    var px = coin.wx - sx;
    if (px < -30 || px > W + 30) return;
    coin.ph += 0.06;
    var py = coin.wy + Math.sin(coin.ph) * 4;
    var r  = clamp(coin.r, 6, 18);

    var grd = ctx.createRadialGradient(px - r*.28, py - r*.28, 0, px, py, r);
    grd.addColorStop(0,   '#FFF176');
    grd.addColorStop(0.5, '#FFD700');
    grd.addColorStop(1,   '#F57F17');
    ctx.fillStyle = grd;
    ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI*2); ctx.fill();

    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    ctx.beginPath(); ctx.arc(px - r*.28, py - r*.28, r*.32, 0, Math.PI*2); ctx.fill();

    ctx.fillStyle = '#7B5800';
    ctx.font = 'bold ' + Math.round(r * 1.1) + 'px Nunito,sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('$', px, py);
    ctx.textBaseline = 'alphabetic';
    ctx.textAlign = 'left';
  });

  // Particles
  ld.parts.forEach(function(p) {
    ctx.save();
    ctx.globalAlpha = p.life;
    ctx.fillStyle = p.col;
    ctx.beginPath(); ctx.arc(p.x - sx, p.y, p.sz, 0, Math.PI*2); ctx.fill();
    ctx.restore();
  });

  // Car
  drawCar(car.x - sx, car.y, pl, s);

  // Wind indicator
  if (Math.abs(ld.wind) > 0.04) {
    ctx.fillStyle = 'rgba(255,255,255,0.65)';
    ctx.font = Math.round(12 * s) + 'px Nunito,sans-serif';
    ctx.fillText('Wind ' + (ld.wind > 0 ? '๐Ÿ’จโžก๏ธ' : 'โฌ…๏ธ๐Ÿ’จ'), 10, gY - 12);
  }

  minimap(s);
}

function drawCar(x, y, pl, s) {
  var cw = car.w, ch = car.h;
  ctx.save();
  ctx.translate(x + cw/2, y + ch/2);
  ctx.rotate(car.angle);

  // Drop shadow
  ctx.fillStyle = 'rgba(0,0,0,0.18)';
  ctx.beginPath(); ctx.ellipse(0, ch/2 + 4*s, cw*.44, 5*s, 0, 0, Math.PI*2); ctx.fill();

  // Body
  ctx.fillStyle = pl.carCol;
  ctx.beginPath(); ctx.roundRect(-cw/2, -ch/2, cw, ch, 8*s); ctx.fill();
  // Shine
  ctx.fillStyle = 'rgba(255,255,255,0.18)';
  ctx.beginPath(); ctx.roundRect(-cw/2, -ch/2, cw, ch*0.44, 8*s); ctx.fill();

  // Wings
  ctx.fillStyle = pl.wingCol;
  ctx.beginPath(); ctx.moveTo(-8*s,-ch/2); ctx.lineTo(-22*s,-ch/2-20*s); ctx.lineTo(12*s,-ch/2); ctx.closePath(); ctx.fill();
  ctx.beginPath(); ctx.moveTo(-8*s, ch/2); ctx.lineTo(-22*s, ch/2+20*s); ctx.lineTo(12*s, ch/2);  ctx.closePath(); ctx.fill();
  // Wing shine
  ctx.fillStyle = 'rgba(255,255,255,0.28)';
  ctx.beginPath(); ctx.moveTo(-6*s,-ch/2); ctx.lineTo(-12*s,-ch/2-10*s); ctx.lineTo(5*s,-ch/2); ctx.closePath(); ctx.fill();

  // Windows
  ctx.fillStyle = 'rgba(135,206,235,0.88)';
  ctx.fillRect(-cw/2 + 6,    -ch/2 + 4, 20*s, 11*s);
  ctx.fillRect(-cw/2 + 28*s, -ch/2 + 4, 13*s, 11*s);
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  ctx.fillRect(-cw/2 + 7,    -ch/2 + 5, 5*s,  4*s);

  // Pilot emoji + hat
  var eSize = Math.round(13*s);
  ctx.font = eSize + 'px serif';
  ctx.textAlign = 'center';
  ctx.fillText(pl.emoji, -cw/2 + 13*s, -ch/2 + eSize + 1);
  ctx.font = Math.round(10*s) + 'px serif';
  ctx.fillText(pl.hat,   -cw/2 + 13*s, -ch/2 - 3);

  // In-car passengers
  car.pax.forEach(function(p, i) {
    ctx.font = Math.round(10*s) + 'px serif';
    ctx.fillText(p.emoji, -cw/2 + 28*s + i*12*s, -ch/2 + eSize + 1);
  });

  // Engine flame
  var jyPush = joy.y < -0.15 || goU();
  if (jyPush) {
    var fl = 0.7 + 0.3 * Math.random();
    ctx.fillStyle = 'rgba(255,' + Math.round(70 + Math.random()*90) + ',0,' + fl + ')';
    ctx.beginPath(); ctx.arc(-cw/2 - 9*s, 0, 9*s*fl, 0, Math.PI*2); ctx.fill();
    ctx.fillStyle = 'rgba(255,220,0,' + fl + ')';
    ctx.beginPath(); ctx.arc(-cw/2 - 6*s, 0, 5*s*fl, 0, Math.PI*2); ctx.fill();
  }

  ctx.restore();
}

function minimap(s) {
  var mW = Math.round(clamp(W * 0.35, 110, 160));
  var mH = Math.round(clamp(H * 0.06, 28,  38));
  var mx = W - mW - 10;
  var my = ld.gY - mH - 8;

  ctx.fillStyle = 'rgba(0,0,0,0.48)';
  ctx.fillRect(mx, my, mW, mH);

  // Car dot
  ctx.fillStyle = PILOTS[pilotIdx].carCol;
  ctx.fillRect(mx + clamp((car.x / ld.worldW) * mW, 1, mW-5), my+2, 5, mH-4);

  // Passengers
  ld.pax.forEach(function(p) {
    if (p.done) return;
    ctx.fillStyle = p.picked ? '#3498db' : '#FFD700';
    ctx.fillRect(mx + clamp((p.wx / ld.worldW) * mW, 1, mW-3), my + mH - 8, 3, 6);
  });

  // Destinations
  ld.dests.forEach(function(d) {
    ctx.fillStyle = d.col;
    ctx.fillRect(mx + clamp((d.wx / ld.worldW) * mW, 1, mW-3), my + 2, 3, 8);
  });

  // Coins
  ld.coins.forEach(function(c) {
    if (c.collected) return;
    ctx.fillStyle = '#FFD700';
    ctx.fillRect(mx + clamp((c.wx / ld.worldW) * mW, 1, mW-3), my + mH/2 - 2, 3, 3);
  });

  ctx.strokeStyle = 'rgba(255,255,255,0.38)';
  ctx.strokeRect(mx, my, mW, mH);
  ctx.fillStyle = 'white';
  ctx.font = Math.round(9*s) + 'px Nunito,sans-serif';
  ctx.fillText('MAP', mx+3, my+10);
}

// โ”€โ”€ MAIN LOOP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function loop() {
  if (!gs.started) return;
  fr++;
  physics();
  interact();
  tickParts();
  scrollUpdate();

  if (spTimer    > 0) { spTimer    -= 16; if (spTimer    <= 0) document.getElementById('speech').classList.add('hide'); }
  if (comboTimer > 0) { comboTimer -= 16; if (comboTimer <= 0) document.getElementById('combo').classList.add('hide'); }

  draw();
  requestAnimationFrame(loop);
}

// โ”€โ”€ INIT KNOB SIZE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
window.addEventListener('load', function() { setTimeout(setKnobSize, 100); });
setTimeout(setKnobSize, 200);

})(); // end IIFE
</script>

</body>
</html>

Game Source: Hero Pilot

Creator: StealthWolf75

Libraries: none

Complexity: complex (1035 lines, 41.0 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: hero-pilot-stealthwolf75" to link back to the original. Then publish at arcadelab.ai/publish.