🎮ArcadeLab

Medieval Reforged — Rank Edition

by ElectricPirate55
620 lines47.1 KB
▶ Play
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Medieval Reforged — Rank Edition</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@600;800;900&family=Inter:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:#0a1510;font-family:Inter,system-ui,sans-serif;touch-action:none;user-select:none;-webkit-user-select:none}
.hidden{display:none!important}
button{font-family:inherit}
/* ===== MAIN MENU ===== */
#menuScreen{position:fixed;inset:0;z-index:100;background:radial-gradient(ellipse at 50% 0%, #2a4a2a 0%, #12271a 45%, #07120c 100%);overflow-y:auto;padding:18px 14px 28px;display:flex;justify-content:center}
#menuScreen:before{content:"";position:fixed;inset:0;background:radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,.55) 100%);pointer-events:none}
.menu-inner{position:relative;width:100%;max-width:860px}
.menu-top{text-align:center;padding:18px 6px 10px}
.menu-badge{display:inline-flex;gap:6px;align-items:center;background:rgba(251,191,36,.14);border:1px solid rgba(251,191,36,.45);color:#fde68a;font-size:11px;font-weight:800;letter-spacing:.18em;padding:6px 12px;border-radius:999px}
.menu-top h1{font-family:Cinzel,serif;font-weight:900;font-size:clamp(32px,7vw,58px);line-height:.95;color:#fff;text-shadow:0 4px 0 #7c2d12, 0 10px 30px rgba(0,0,0,.7);margin:14px 0 6px}
.menu-top h1 span{color:#fbbf24}
.menu-top p{color:#cbd5e1;font-size:13px;line-height:1.5;max-width:560px;margin:0 auto}
.menu-card{background:rgba(12,22,16,.92);border:1px solid rgba(255,255,255,.14);border-radius:20px;padding:18px;box-shadow:0 20px 60px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.08);margin-top:16px}
.menu-card h2{font-family:Cinzel,serif;color:#fde68a;font-size:16px;letter-spacing:.08em;margin-bottom:4px}
.menu-card p.sub{color:#94a3b8;font-size:12px;line-height:1.5;margin-bottom:12px}
label.lbl{font-size:11px;font-weight:800;letter-spacing:.12em;color:#e2e8f0}
.input-row{display:flex;gap:10px;margin-top:8px}
#usernameInput{flex:1;background:#0f172a;border:2px solid #334155;color:#fff;padding:14px 14px;border-radius:12px;font-size:16px;font-weight:700;outline:none}
#usernameInput:focus{border-color:#fbbf24;box-shadow:0 0 0 3px rgba(251,191,36,.25)}
#usernameInput::placeholder{color:#64748b;font-weight:600}
#startBtn{background:linear-gradient(180deg,#fbbf24,#d97706);color:#1f1300;border:0;padding:14px 22px;border-radius:12px;font-weight:900;letter-spacing:.06em;font-size:14px;cursor:pointer;box-shadow:0 6px 20px rgba(251,191,36,.35), inset 0 1px 0 rgba(255,255,255,.5);white-space:nowrap}
#startBtn:active{transform:scale(.97)}
#usernameError{color:#fca5a5;font-size:12px;font-weight:700;margin-top:8px;min-height:16px}
.username-hint{margin-top:10px;background:rgba(168,85,247,.12);border:1px dashed rgba(192,132,252,.5);border-radius:12px;padding:10px 12px;font-size:12px;line-height:1.5;color:#e9d5ff}
.username-hint b{color:#fde68a}
.username-hint code{background:#1e1b4b;padding:2px 7px;border-radius:6px;color:#fff;font-weight:800;margin:0 2px}
.rank-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:12px}
.rank-card{border-radius:14px;padding:12px 10px;text-align:center;border:1px solid rgba(255,255,255,.12);background:#111c14;position:relative;overflow:hidden}
.rank-card .r-icon{font-size:26px;line-height:1}
.rank-card .r-name{font-family:Cinzel,serif;font-weight:900;font-size:12px;letter-spacing:.06em;margin:7px 0 2px}
.rank-card .r-desc{font-size:11px;color:#94a3b8;line-height:1.4}
.rank-card .r-stat{margin-top:8px;display:flex;flex-direction:column;gap:3px;font-size:11px;font-weight:700}
.rank-card .r-stat span{background:rgba(255,255,255,.07);padding:4px 6px;border-radius:7px}
.rank-card.grand{background:linear-gradient(180deg,#3b0764,#1e1b4b);border-color:#c084fc;box-shadow:0 0 24px rgba(192,132,252,.35)}
.rank-card.grand .r-name{color:#e9d5ff}
.howto{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
.howto div{background:rgba(255,255,255,.06);border-radius:12px;padding:11px 12px;font-size:12px;line-height:1.5;color:#cbd5e1}
.howto b{color:#fff}
.menu-foot{text-align:center;color:#64748b;font-size:11px;margin-top:14px}
/* ===== GAME ===== */
#game-wrap{position:fixed;inset:0;background:#0e1f13;overflow:hidden}
#main-canvas{display:block;width:100%;height:100%;touch-action:none}
.vignette{position:fixed;inset:0;pointer-events:none;z-index:5;background:radial-gradient(ellipse at center, transparent 58%, rgba(0,0,0,.35) 92%, rgba(0,0,0,.72) 100%)}
.hud{position:fixed;top:12px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:flex-start;z-index:20;pointer-events:none;gap:10px}
.panel{background:rgba(15,23,42,.86);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.13);border-radius:14px;padding:10px 12px;color:white;box-shadow:0 8px 32px rgba(0,0,0,.45);pointer-events:auto}
.player-panel{display:flex;align-items:center;gap:11px;min-width:268px}
.avatar{width:46px;height:46px;border-radius:11px;background:radial-gradient(circle at 35% 30%, #60a5fa, #1e3a8a);display:flex;align-items:center;justify-content:center;font-size:22px;border:1px solid rgba(255,255,255,.2);flex-shrink:0}
.player-info{flex:1;min-width:0}
.player-name{font-weight:900;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.player-name small{display:block;font-size:10px;letter-spacing:.1em;color:#fde68a;font-weight:800;margin-top:1px}
.rank-badge{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:900;letter-spacing:.08em;padding:3px 8px;border-radius:999px;margin-top:5px}
.hp-bar-wrap{margin-top:6px}
.hp-bar{width:100%;height:13px;background:#1e1212;border-radius:999px;overflow:hidden;border:1px solid rgba(255,255,255,.1);position:relative}
.hp-fill{height:100%;background:linear-gradient(90deg,#ef4444,#f87171 30%,#22c55e 100%);transition:width .18s;position:relative}
.hp-text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;letter-spacing:.05em;text-shadow:0 1px 2px #000}
.rank-progress{margin-top:6px}
.rank-progress .rp-top{display:flex;justify-content:space-between;font-size:10px;font-weight:800;color:#cbd5e1}
.rank-progress .rp-bar{height:7px;background:rgba(255,255,255,.12);border-radius:999px;overflow:hidden;margin-top:3px}
.rank-progress .rp-fill{height:100%;background:linear-gradient(90deg,#fbbf24,#a855f7);transition:width .3s}
.stats-panel{min-width:150px;display:flex;flex-direction:column;gap:5px}
.stat-row{display:flex;justify-content:space-between;align-items:center;font-size:12px;gap:12px}
.stat-row span{opacity:.8;font-weight:600}
.stat-row b{font-family:Cinzel,serif;font-weight:800}
.stat-row.small{font-size:11px;margin-top:2px}
.stat-row.small span{background:rgba(255,255,255,.08);padding:2px 6px;border-radius:6px}
#menuBtn{margin-top:6px;width:100%;padding:7px;border-radius:8px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-size:11px;font-weight:800;cursor:pointer}
.minimap-wrap{position:fixed;top:152px;right:14px;width:118px;height:118px;border-radius:50%;overflow:hidden;border:2px solid rgba(255,255,255,.2);box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:18;background:#16291b;pointer-events:none}
.minimap-wrap canvas{width:100%;height:100%;display:block}
.controls{position:fixed;inset:0;pointer-events:none;z-index:25}
#joystick{position:absolute;left:22px;bottom:22px;width:138px;height:138px;border-radius:50%;background:radial-gradient(circle at 35% 35%, rgba(255,255,255,.14), rgba(0,0,0,.35));border:1.5px solid rgba(255,255,255,.22);box-shadow:0 10px 30px rgba(0,0,0,.5);pointer-events:auto;touch-action:none}
#stick{position:absolute;width:58px;height:58px;left:40px;top:40px;border-radius:50%;background:radial-gradient(circle at 32% 28%, #fff, #93c5fd);border:1.5px solid #fff;box-shadow:0 4px 14px rgba(0,0,0,.45)}
.right-stack{position:absolute;right:22px;bottom:22px;display:flex;flex-direction:column;align-items:center;gap:13px;pointer-events:auto}
#attackBtn{width:108px;height:108px;border-radius:50%;border:2px solid rgba(255,255,255,.75);background:radial-gradient(circle at 35% 30%, #ef4444, #991b1b);color:white;box-shadow:0 10px 28px rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;cursor:pointer;position:relative;overflow:hidden;touch-action:none}
#attackBtn:active{transform:scale(.92)}
#attackBtn span{font-size:38px}
#dashBtn{width:62px;height:62px;border-radius:50%;border:1.5px solid rgba(255,255,255,.55);background:radial-gradient(circle at 30% 30%, #38bdf8, #0c4a6e);color:white;font-size:22px;box-shadow:0 6px 18px rgba(0,0,0,.45);cursor:pointer;display:flex;align-items:center;justify-content:center}
#dashBtn.cooldown{opacity:.4;filter:grayscale(.6)}
#rankUpBanner{position:fixed;top:38%;left:50%;transform:translate(-50%,-50%) scale(.8);z-index:50;text-align:center;pointer-events:none;opacity:0;transition:.3s}
#rankUpBanner.show{opacity:1;transform:translate(-50%,-50%) scale(1)}
#rankUpBanner .ru-inner{background:linear-gradient(180deg, rgba(20,20,35,.96), rgba(10,10,20,.96));border:2px solid #fbbf24;border-radius:18px;padding:18px 28px;box-shadow:0 0 40px rgba(251,191,36,.5)}
#rankUpBanner h3{font-family:Cinzel,serif;font-size:12px;letter-spacing:.25em;color:#fde68a}
#rankUpBanner h2{font-family:Cinzel,serif;font-size:32px;color:#fff;line-height:1;margin:6px 0}
#rankUpBanner p{font-size:12px;color:#cbd5e1}
#toast{position:fixed;top:118px;left:50%;transform:translateX(-50%);z-index:40;background:rgba(0,0,0,.75);border:1px solid rgba(251,191,36,.4);color:#fde68a;padding:8px 14px;border-radius:999px;font-size:12px;font-weight:800;opacity:0;transition:.25s;pointer-events:none}
#toast.show{opacity:1}
.modal-backdrop{position:fixed;inset:0;background:rgba(5,10,20,.72);backdrop-filter:blur(14px);z-index:60;display:flex;align-items:center;justify-content:center;padding:18px}
.message-modal{background:linear-gradient(180deg, rgba(20,30,50,.96), rgba(10,16,30,.98));border:1.5px solid rgba(251,191,36,.35);border-radius:20px;padding:26px;text-align:center;max-width:400px;width:100%}
.message-modal h1{font-family:Cinzel,serif;font-weight:800;font-size:24px;color:#fde68a;margin-bottom:8px}
.message-modal p{color:#cbd5e1;font-size:13px;line-height:1.5;margin-bottom:14px}
.message-modal .end-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:16px}
.message-modal .end-stats div{background:rgba(255,255,255,.07);border-radius:10px;padding:9px 6px}
.message-modal .end-stats b{display:block;font-size:16px}
.message-modal .end-stats span{font-size:10px;letter-spacing:.1em;color:#94a3b8;font-weight:800}
.btn-row{display:flex;gap:10px;justify-content:center}
.btn-primary{padding:12px 20px;border:0;border-radius:10px;background:linear-gradient(180deg,#fbbf24,#d97706);color:#1f1300;font-weight:900;font-size:13px;cursor:pointer}
.btn-ghost{padding:12px 18px;border-radius:10px;border:1px solid rgba(255,255,255,.25);background:rgba(255,255,255,.08);color:#fff;font-weight:800;font-size:13px;cursor:pointer}
@media (max-width:640px){
  .rank-grid{grid-template-columns:repeat(2,1fr)}
  .rank-grid .rank-card:last-child{grid-column:span 2}
  .howto{grid-template-columns:1fr}
  .hud{top:8px;left:8px;right:8px}
  .player-panel{min-width:0;flex:1}
  .stats-panel{min-width:118px}
  .minimap-wrap{top:168px;right:8px;width:100px;height:100px}
  #joystick{left:14px;bottom:14px;width:122px;height:122px}
  #stick{width:50px;height:50px;left:36px;top:36px}
  #attackBtn{width:94px;height:94px}
  .right-stack{right:14px;bottom:14px}
  .input-row{flex-direction:column}
  #startBtn{width:100%}
}
</style>
</head>
<body>

<!-- ============ MAIN MENU ============ -->
<div id="menuScreen">
  <div class="menu-inner">
    <div class="menu-top">
      <div class="menu-badge">⚔️ MEDIEVAL REFORGED • RANK EDITION</div>
      <h1>MEDIEVAL<br><span>REFORGED</span></h1>
      <p>Defend the castle, slay enemies, and climb your rank from <b style="color:#fff">Bronze</b> to <b style="color:#e9d5ff">Grandmaster</b>. The higher your rank, the thicker your HP & the more painful your attacks!</p>
    </div>

    <div class="menu-card">
      <h2>👤 ENTER USERNAME</h2>
      <p class="sub">Username is required. Your username will appear above your character's head during gameplay.</p>
      <label class="lbl" for="usernameInput">YOUR USERNAME (3–12 LETTERS)</label>
      <div class="input-row">
        <input id="usernameInput" maxlength="12" placeholder="Virtue: Knight..." autocomplete="off" />
        <button id="startBtn">▶ START PLAYING</button>
      </div>
      <div id="usernameError"></div>
      <div class="username-hint">✨ <b>SECRET USERNAME:</b> Enter one of these to instantly become <b>GRANDMASTER</b> with 
 thick HP & painful damage: <code>NEXTUPDATE</code> <code>SUPER</code> <code>MODAPK</code> <code>UPG</code> <code>ADMIN</code> <code>DEV</code></div>
      <div style="margin-top:12px;font-size:12px;color:#94a3b8;line-height:1.6">Game Mode: <b style="color:#fff">MAIN MODE — Ranked</b> • Kill enemies to rank up • Lose if your HP / castle runs out • Win if 60 enemies fall.</div>
    </div>

    <div class="menu-card">
      <h2>🏆 RANK SYSTEM — MAIN MODE</h2>
      <p class="sub">Rank increases automatically every time you kill an enemy. Each rank up increases Max HP & multiplies damage. You also get a bonus heal on rank up!</p>
      <div class="rank-grid">
        <div class="rank-card" style="border-color:#cd7f32"><div class="r-icon">🥉</div><div class="r-name" style="color:#e8a87c">BRONZE</div><div class="r-desc">Bronze Knight<br>Start here</div><div class="r-stat"><span>🎯 0 Kills</span><span>❤️ HP 120</span><span>⚔️ Damage ×1.0</span></div></div>
        <div class="rank-card" style="border-color:#cbd5e1"><div class="r-icon">🥈</div><div class="r-name" style="color:#e2e8f0">SILVER</div><div class="r-desc">Silver Knight<br>8 Kills</div><div class="r-stat"><span>🎯 8 Kills</span><span>❤️ HP 170</span><span>⚔️ Damage ×1.4</span></div></div>
        <div class="rank-card" style="border-color:#facc15"><div class="r-icon">🥇</div><div class="r-name" style="color:#fde68a">GOLD</div><div class="r-desc">Gold Knight<br>18 Kills</div><div class="r-stat"><span>🎯 18 Kills</span><span>❤️ HP 230</span><span>⚔️ Damage ×1.85</span></div></div>
        <div class="rank-card" style="border-color:#22d3ee"><div class="r-icon">💎</div><div class="r-name" style="color:#a5f3fc">DIAMOND</div><div class="r-desc">Diamond Knight<br>32 Kills</div><div class="r-stat"><span>🎯 32 Kills</span><span>❤️ HP 300</span><span>⚔️ Damage ×2.4</span></div></div>
        <div class="rank-card grand"><div class="r-icon">👑</div><div class="r-name">GRANDMASTER</div><div class="r-desc">Legendary<br>50 Kills / Secret Username</div><div class="r-stat"><span>🎯 50 Kills</span><span>❤️ HP 400</span><span>⚔️ Damage ×3.3</span></div></div>
      </div>
      <div style="margin-top:10px;font-size:11px;color:#a78bfa">👑 <b style="color:#e9d5ff">Secret Username = Instant Grandmaster:</b> HP 520 • Damage ×4.5 • Special purple-gold aura on your character.</div>
    </div>

    <div class="menu-card">
      <h2>🎮 HOW TO PLAY</h2>
      <div class="howto">
        <div><b>🕹️ Move:</b><br>Left joystick (Mobile) or WASD / Arrow Keys (Keyboard).</div>
        <div><b>⚔️ Attack:</b><br>Red sword button / Space / J. 3-hit combo, the 3rd slash hits hardest.</div>
        <div><b>💨 Dash:</b><br>💨 Button / Shift / Double-tap joystick. Brief invulnerability & fast dash.</div>
        <div><b>🏰 Defend the Castle:</b><br>Enemies also attack the castle. If the castle falls, you lose. Blue allies will help you fight.</div>
      </div>
    </div>
    <div class="menu-foot">Medieval Reforged Edition • Made to play on Mobile & Laptop • v2 Rank Edition</div>
  </div>
</div>

<!-- ============ GAME ============ -->
<div id="game-wrap" class="hidden">
  <canvas id="main-canvas"></canvas>
  <div class="vignette"></div>

  <div class="hud">
    <div class="panel player-panel">
      <div class="avatar" id="avatarEl">⚔️</div>
      <div class="player-info">
        <div class="player-name"><span id="hudUsername">KNIGHT</span><small id="hudRankTitle">BRONZE KNIGHT</small></div>
        <div class="rank-badge" id="rankBadge">🥉 BRONZE</div>
        <div class="hp-bar-wrap"><div class="hp-bar"><div class="hp-fill" id="hpFill" style="width:100%"></div><div class="hp-text" id="hpText">120 / 120</div></div></div>
        <div class="rank-progress"><div class="rp-top"><span id="rpLabel">0 / 8 KILLSS TO SILVER</span><span id="rpPct">0%</span></div><div class="rp-bar"><div class="rp-fill" id="rpFill" style="width:0%"></div></div></div>
      </div>
    </div>
    <div class="panel stats-panel">
      <div class="stat-row"><span>WAVE</span><b id="hudWave" style="color:#fde68a">1 / 5</b></div>
      <div class="stat-row"><span>🏰 CASTLE</span><b id="hudCastle" style="color:#fbbf24">100%</b></div>
      <div class="stat-row small"><span>🛡️ <b id="hudAllies">0</b></span><span>☠️ <b id="hudEnemies">0</b></span><span>⚔️ <b id="hudKills">0</b>/60</span></div>
      <button id="menuBtn">🏠 MAIN MENU</button>
    </div>
  </div>

  <div class="minimap-wrap"><canvas id="miniCanvas" width="120" height="120"></canvas></div>

  <div class="controls">
    <div id="joystick"><div id="stick"></div></div>
    <div class="right-stack">
      <button id="dashBtn" aria-label="Dash">💨</button>
      <button id="attackBtn" aria-label="Attack"><span>⚔️</span></button>
    </div>
  </div>

  <div id="toast"></div>
  <div id="rankUpBanner"><div class="ru-inner"><h3>⭐ RANK UP! ⭐</h3><h2 id="ruTitle">SILVER</h2><p id="ruDesc">HP increased • Damage gets more painful!</p></div></div>

  <div id="endScreen" class="hidden"><div class="modal-backdrop"><div class="message-modal"><h1 id="endTitle">Victory!</h1><p id="endText"></p><div class="end-stats"><div><b id="endKills">0</b><span>KILLS</span></div><div><b id="endRank">BRONZE</b><span>FINAL RANK</span></div><div><b id="endWave">1</b><span>WAVE</span></div></div><div class="btn-row"><button class="btn-primary" id="playAgainBtn">▶ PLAY AGAIN</button><button class="btn-ghost" id="toMenuBtn">🏠 MAIN MENU</button></div></div></div></div>
</div>

<script>
const RANKS=[
  {name:"BRONZE", title:"BRONZE KNIGHT", minKills:0, maxHp:120, dmgMul:1.0, color:"#cd7f32", icon:"🥉"},
  {name:"SILVER", title:"SILVER KNIGHT", minKills:8, maxHp:170, dmgMul:1.4, color:"#e5e7eb", icon:"🥈"},
  {name:"GOLD", title:"GOLD KNIGHT", minKills:18, maxHp:230, dmgMul:1.85, color:"#facc15", icon:"🥇"},
  {name:"DIAMOND", title:"DIAMOND KNIGHT", minKills:32, maxHp:300, dmgMul:2.4, color:"#22d3ee", icon:"💎"},
  {name:"GRANDMASTER", title:"LEGENDARY GRANDMASTER", minKills:50, maxHp:400, dmgMul:3.3, color:"#c084fc", icon:"👑"},
];
const SPECIAL_NAMES=["upgrade","upg","super","modapk","nextupdate","admin","dev","electricp55","oldversion"];
const SPECIAL_BUFF={maxHp:520, dmgMul:4.5};

let username="KNIGHT", isSpecial=false, rankIndex=0, kills=0, wave=1, gameOver=false, gameStarted=false;
let player, castle, enemies=[], allies=[], particles=[], floatTexts=[], slashes=[], trees=[], rocks=[], camera={x:2500,y:2500}, keys={}, joy={x:0,y:0,active:false}, lastAttackTap=0;

const canvas=document.getElementById("main-canvas");
const ctx=canvas.getContext("2d",{alpha:false});
const mini=document.getElementById("miniCanvas");
const mctx=mini.getContext("2d");
let W=innerWidth, H=innerHeight;
function resize(){ W=innerWidth; H=innerHeight; const dpr=Math.max(1,devicePixelRatio||1); canvas.width=W*dpr; canvas.height=H*dpr; canvas.style.width=W+"px"; canvas.style.height=H+"px"; ctx.setTransform(dpr,0,0,dpr,0,0); }
addEventListener("resize",resize); resize();

const WORLD={w:5000,h:5000};
const rand=(a,b)=>Math.random()*(b-a)+a;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));

// Audio kecil
let actx=null, again=null;
function ensureAudio(){ if(!actx){ try{ actx=new (window.AudioContext||window.webkitAudioContext)(); again=actx.createGain(); again.gain.value=0.18; again.connect(actx.destination);}catch(e){} } if(actx&&actx.state==="suspended") actx.resume(); }
function beep(freq,dur,type="sine",vol=0.4,slideTo=null){ try{ ensureAudio(); if(!actx) return; const t=actx.currentTime, o=actx.createOscillator(), g=actx.createGain(); o.type=type; o.frequency.setValueAtTime(freq,t); if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,t+dur); g.gain.setValueAtTime(vol,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur); o.connect(g); g.connect(again); o.start(t); o.stop(t+dur+0.02);}catch(e){} }
function sfx(name){ if(name==="attack") beep(320,0.12,"sawtooth",0.35,70); else if(name==="hit") beep(160,0.15,"square",0.35,30); else if(name==="dash") beep(220,0.12,"sine",0.3,600); else if(name==="rankup"){ [523,659,784,1046].forEach((f,i)=>setTimeout(()=>beep(f,0.22,"sine",0.4), i*110)); } else if(name==="win"){ [261,329,392,523].forEach((f,i)=>setTimeout(()=>beep(f,0.4,"sine",0.4), i*130)); } else if(name==="lose") beep(120,0.7,"sawtooth",0.4,30); }

function currentRank(){ return RANKS[rankIndex]; }
function maxHpNow(){ return isSpecial ? SPECIAL_BUFF.maxHp : currentRank().maxHp; }
function dmgMulNow(){ return isSpecial ? SPECIAL_BUFF.dmgMul : currentRank().dmgMul; }
function rankForKills(k){ let idx=0; RANKS.forEach((r,i)=>{ if(k>=r.minKills) idx=i; }); return idx; }

function showToast(msg){ const t=document.getElementById("toast"); t.textContent=msg; t.classList.add("show"); setTimeout(()=>t.classList.remove("show"),2200); }
function showRankUp(newRank){
  const b=document.getElementById("rankUpBanner");
  document.getElementById("ruTitle").textContent = newRank.icon+" "+newRank.name;
  document.getElementById("ruDesc").textContent = `Max HP is now ${maxHpNow()} • Damage ×${dmgMulNow()} • +50 HP bonus!`;
  b.classList.add("show"); sfx("rankup");
  setTimeout(()=>b.classList.remove("show"),2600);
}
function checkRankUp(){
  if(isSpecial) return; // sudah mentok
  const nr=rankForKills(kills);
  if(nr>rankIndex){
    const oldMax=RANKS[rankIndex].maxHp;
    rankIndex=nr;
    const newMax=RANKS[rankIndex].maxHp;
    // HP increased: raise max, heal difference + 30 bonus
    player.maxHp=newMax;
    player.hp=Math.min(newMax, player.hp + (newMax-oldMax) + 30);
    showRankUp(RANKS[rankIndex]);
    updateHUD();
  }
}

function resetGame(){
  kills=0; wave=1; gameOver=false;
  // rank awal
  if(isSpecial){ rankIndex=4; }
  else { rankIndex=0; }
  castle={x:2500,y:1200,w:600,h:500,hp:300,maxHp:300};
  player={x:2500,y:2500,radius:22,speed:3.9,hp:maxHpNow(),maxHp:maxHpNow(),dir:0,attackCd:0,combo:0,comboTimer:0,dashCd:0,dashTime:0,isDashing:false,dashX:0,dashY:0};
  enemies=[]; allies=[]; particles=[]; floatTexts=[]; slashes=[];
  trees=[]; rocks=[];
  for(let i=0;i<300;i++){ let x=rand(80,WORLD.w-80), y=rand(80,WORLD.h-80); if(Math.hypot(x-castle.x,y-castle.y)<520) continue; if(Math.hypot(x-player.x,y-player.y)<380) continue; trees.push({x,y,s:rand(.75,1.35)}); }
  for(let i=0;i<150;i++) rocks.push({x:rand(40,WORLD.w-40),y:rand(40,WORLD.h-40),s:rand(.6,1.25)});
  trees.sort((a,b)=>a.y-b.y);
  for(let i=0;i<14;i++) spawnEnemy("bandit");
  for(let i=0;i<2;i++) spawnEnemy("brute");
  camera.x=player.x; camera.y=player.y;
  document.getElementById("hudUsername").textContent=username.toUpperCase();
  document.getElementById("endScreen").classList.add("hidden");
  updateHUD();
  if(isSpecial) setTimeout(()=>showToast("👑 Special username! Instant GRANDMASTER — HP 520 • Damage ×4.5"),600);
}

function spawnEnemy(force){
  if(enemies.length>=38) return;
  const ang=rand(0,Math.PI*2), dist=rand(850,1800);
  const x=clamp(player.x+Math.cos(ang)*dist,100,WORLD.w-100), y=clamp(player.y+Math.sin(ang)*dist,100,WORLD.h-100);
  const isBrute = force==="brute" ? true : force==="bandit" ? false : Math.random()<0.22+wave*0.04;
  if(isBrute) enemies.push({x,y,radius:26,hp:120,maxHp:120,speed:1.05+rand(0,.35),dir:ang,attackCd:rand(0,60),hitFlash:0,type:"brute"});
  else enemies.push({x,y,radius:20,hp:50,maxHp:50,speed:2.05+rand(0,.6),dir:ang,attackCd:rand(0,40),hitFlash:0,type:"bandit"});
}
function spawnAlly(x,y){ if(allies.length>=12) return; allies.push({x:x+rand(-20,20),y:y+rand(-20,20),radius:20,hp:55,maxHp:55,speed:1.9,dir:0,attackCd:rand(0,30),hitFlash:0}); }

function doAttack(){
  if(!gameStarted||gameOver||player.attackCd>0) return;
  ensureAudio();
  const base=[25,25,42][player.combo]||25;
  const dmg=Math.round(base*dmgMulNow());
  const range=[105,115,135][player.combo]||105;
  const arc=[120,140,180][player.combo]||120;
  player.attackCd = player.combo===2?28:18;
  slashes.push({x:player.x,y:player.y,dir:player.dir,life:1,combo:player.combo});
  sfx("attack");
  let hitCount=0;
  for(const e of enemies){
    const dx=e.x-player.x, dy=e.y-player.y, dist=Math.hypot(dx,dy);
    if(dist>range+e.radius) continue;
    let ang=Math.atan2(dy,dx), diff=Math.abs(ang-player.dir); diff=Math.atan2(Math.sin(diff),Math.cos(diff));
    if(Math.abs(diff)>arc*Math.PI/180/2) continue;
    e.hp-=dmg; e.hitFlash=9; hitCount++;
    const nx=dx/(dist||1), ny=dy/(dist||1);
    e.x+=nx*(26+player.combo*10); e.y+=ny*(26+player.combo*10);
    for(let i=0;i<10+player.combo*3;i++) particles.push({x:e.x,y:e.y-10,vx:nx*rand(.5,3)+rand(-1.5,1.5),vy:ny*rand(.5,2)+rand(-2,0),color:e.type==="brute"?"#7f1d1d":"#991b1b",size:rand(2,4.5),life:rand(.5,1),max:1,grav:.18});
    floatTexts.push({x:e.x,y:e.y-28,vy:-2.2,life:1,text:`-${dmg}`,color:player.combo===2?"#fde047":"#fff"});
    if(e.hp<=0){ e.dead=true; kills++; checkRankUp(); spawnAlly(e.x,e.y); for(let i=0;i<14;i++) particles.push({x:e.x,y:e.y,vx:rand(-3,3),vy:rand(-4,-.5),color:"rgba(90,20,20,.9)",size:rand(2,5),life:rand(.6,1.1),max:1,grav:.15}); }
  }
  enemies=enemies.filter(e=>!e.dead);
  if(hitCount>0) sfx("hit");
  player.combo=(player.combo+1)%3; player.comboTimer=42;
  if(hitCount===0) player.comboTimer=Math.min(player.comboTimer,18);
  updateHUD();
}
function doDash(){
  if(!gameStarted||gameOver||player.dashCd>0||player.isDashing) return;
  ensureAudio();
  let dx=joy.x, dy=joy.y, len=Math.hypot(dx,dy);
  if(len<0.2){ dx=Math.cos(player.dir); dy=Math.sin(player.dir); } else { dx/=len; dy/=len; }
  player.isDashing=true; player.dashTime=14; player.dashCd=95; player.dashX=dx*12; player.dashY=dy*12;
  for(let i=0;i<12;i++) particles.push({x:player.x,y:player.y+6,vx:-dx*rand(1,4)+rand(-1,1),vy:rand(-1,1),color:"rgba(180,200,255,.7)",size:rand(2,4),life:.7,max:.7,grav:.02});
  sfx("dash");
}

// Input
const stickEl=document.getElementById("stick");
const joyEl=document.getElementById("joystick");
function handleJoy(cx,cy){ const r=joyEl.getBoundingClientRect(); const cx0=r.left+r.width/2, cy0=r.top+r.height/2; let dx=cx-cx0, dy=cy-cy0; const max=52, len=Math.hypot(dx,dy); if(len>max){ dx=dx/len*max; dy=dy/len*max; } joy.x=dx/max; joy.y=dy/max; stickEl.style.transform=`translate(${dx}px, ${dy}px)`; joy.active=true; }
function endJoy(){ joy.x=0; joy.y=0; joy.active=false; stickEl.style.transform="translate(0px,0px)"; }
joyEl.addEventListener("pointerdown",e=>{ e.target.setPointerCapture(e.pointerId); handleJoy(e.clientX,e.clientY); const now=performance.now(); if(now-lastAttackTap<280) doDash(); lastAttackTap=now; });
joyEl.addEventListener("pointermove",e=>{ if(joy.active) handleJoy(e.clientX,e.clientY); });
joyEl.addEventListener("pointerup",endJoy); joyEl.addEventListener("pointercancel",endJoy);
document.getElementById("attackBtn").addEventListener("pointerdown",e=>{ e.preventDefault(); doAttack(); });
document.getElementById("dashBtn").addEventListener("pointerdown",e=>{ e.preventDefault(); doDash(); });
addEventListener("keydown",e=>{ keys[e.code]=true; if(e.code==="Space"){ e.preventDefault(); doAttack(); } if(e.code==="ShiftLeft"||e.code==="ShiftRight"){ e.preventDefault(); doDash(); } if(e.code==="KeyJ") doAttack(); if(e.code==="KeyK") doDash(); });
addEventListener("keyup",e=>{ keys[e.code]=false; });

// Menu
const usernameInput=document.getElementById("usernameInput");
const usernameError=document.getElementById("usernameError");
function startGame(){
  const val=usernameInput.value.trim();
  if(val.length<3){ usernameError.textContent="⚠️ Username must be at least 3 letters! Please fill it in."; usernameInput.style.borderColor="#ef4444"; usernameInput.focus(); return; }
  if(!/^[a-zA-Z0-9 _-]+$/.test(val)){ usernameError.textContent="⚠️ Username can only contain letters, numbers, spaces, _ and -."; return; }
  username=val; usernameError.textContent="";
  isSpecial=SPECIAL_NAMES.includes(val.toLowerCase());
  document.getElementById("menuScreen").classList.add("hidden");
  document.getElementById("game-wrap").classList.remove("hidden");
  gameStarted=true;
  resetGame();
  resize();
  showToast(`Welcome, ${username}! Rank: ${isSpecial?"👑 GRANDMASTER (Special)":"🥉 BRONZE"}`);
}
document.getElementById("startBtn").addEventListener("click",startGame);
usernameInput.addEventListener("keydown",e=>{ if(e.code==="Enter") startGame(); });
document.getElementById("menuBtn").addEventListener("click",()=>{ gameStarted=false; document.getElementById("game-wrap").classList.add("hidden"); document.getElementById("menuScreen").classList.remove("hidden"); });
document.getElementById("toMenuBtn").addEventListener("click",()=>{ document.getElementById("endScreen").classList.add("hidden"); document.getElementById("game-wrap").classList.add("hidden"); document.getElementById("menuScreen").classList.remove("hidden"); gameStarted=false; });
document.getElementById("playAgainBtn").addEventListener("click",()=>{ resetGame(); });
usernameInput.focus();

function endGame(win,title,text){
  gameOver=true;
  document.getElementById("endTitle").textContent=title;
  document.getElementById("endText").textContent=text;
  document.getElementById("endKills").textContent=kills;
  document.getElementById("endRank").textContent=(isSpecial?"👑 ":"")+currentRank().name;
  document.getElementById("endWave").textContent=wave;
  document.getElementById("endScreen").classList.remove("hidden");
  if(win) sfx("win"); else sfx("lose");
}

function updateHUD(){
  const r=currentRank();
  const pct=Math.round(clamp(player.hp/player.maxHp*100,0,100));
  document.getElementById("hpFill").style.width=pct+"%";
  document.getElementById("hpText").textContent=`${Math.max(0,Math.ceil(player.hp))} / ${player.maxHp}`;
  document.getElementById("hudWave").textContent=`${wave} / 5`;
  document.getElementById("hudCastle").textContent=Math.round(clamp(castle.hp/castle.maxHp*100,0,100))+"%";
  document.getElementById("hudCastle").style.color = castle.hp/castle.maxHp<0.3 ? "#ef4444" : "#fbbf24";
  document.getElementById("hudAllies").textContent=allies.length;
  document.getElementById("hudEnemies").textContent=enemies.length;
  document.getElementById("hudKills").textContent=kills;
  const badge=document.getElementById("rankBadge");
  badge.textContent=`${r.icon} ${r.name}${isSpecial?" • SPESIAL":""}`;
  badge.style.background=r.color+"22"; badge.style.border=`1px solid ${r.color}`; badge.style.color=r.color==="#e5e7eb"?"#fff":r.color;
  document.getElementById("hudRankTitle").textContent = isSpecial ? "GRANDMASTER • SPECIAL USERNAME" : r.title;
  document.getElementById("avatarEl").style.background = isSpecial ? "radial-gradient(circle at 35% 30%, #c084fc, #6b21a8)" : "radial-gradient(circle at 35% 30%, #60a5fa, #1e3a8a)";
  // progress ke rank berikutnya
  const next=RANKS[rankIndex+1];
  if(!next || isSpecial){
    document.getElementById("rpLabel").textContent="🏆 MAX RANK — GRANDMASTER";
    document.getElementById("rpFill").style.width="100%";
    document.getElementById("rpPct").textContent="MAX";
  } else {
    const need=next.minKills - RANKS[rankIndex].minKills;
    const have=kills - RANKS[rankIndex].minKills;
    const p=clamp(have/need*100,0,100);
    document.getElementById("rpLabel").textContent=`${kills} / ${next.minKills} KILLSS TO ${next.name}`;
    document.getElementById("rpFill").style.width=p+"%";
    document.getElementById("rpPct").textContent=Math.round(p)+"%";
  }
  document.getElementById("dashBtn").classList.toggle("cooldown", player.dashCd>0);
  // minimap
  mctx.clearRect(0,0,120,120);
  mctx.fillStyle="#1e3a24"; mctx.beginPath(); mctx.arc(60,60,58,0,Math.PI*2); mctx.fill();
  const mx=x=>x/WORLD.w*100+10, my=y=>y/WORLD.h*100+10;
  mctx.fillStyle="#9ca3af"; mctx.fillRect(mx(castle.x)-6,my(castle.y)-5,12,10);
  const dot=(x,y,c,s=2.2)=>{ const X=mx(x),Y=my(y); if(X<4||X>116||Y<4||Y>116) return; mctx.fillStyle=c; mctx.beginPath(); mctx.arc(X,Y,s,0,Math.PI*2); mctx.fill(); };
  enemies.forEach(e=>dot(e.x,e.y,e.type==="brute"?"#ef4444":"#f87171", e.type==="brute"?3.2:2.2));
  allies.forEach(a=>dot(a.x,a.y,"#60a5fa",2.4));
  dot(player.x,player.y,"#facc15",3.6);
}

// Drawing helpers
function drawGround(camX,camY){
  ctx.fillStyle="#1b3320"; ctx.fillRect(0,0,W,H);
  ctx.fillStyle="rgba(60,110,50,.35)";
  const grid=90;
  for(let gx=Math.floor((camX-W/2-200)/grid)*grid; gx<camX+W/2+200; gx+=grid){
    for(let gy=Math.floor((camY-H/2-200)/grid)*grid; gy<camY+H/2+200; gy+=grid){
      const v=Math.sin(gx*12.9898+gy*78.233)*43758.5453; const f=v-Math.floor(v);
      if(f>0.72){ ctx.beginPath(); ctx.arc(gx-camX+W/2, gy-camY+H/2, f*2.2,0,Math.PI*2); ctx.fill(); }
    }
  }
}
function drawCastle(){
  const x=castle.x-camera.x+W/2, y=castle.y-camera.y+H/2;
  if(x<-600||x>W+600||y<-600||y>H+600) return;
  ctx.save(); ctx.translate(x,y);
  ctx.fillStyle="rgba(0,0,0,.35)"; ctx.beginPath(); ctx.ellipse(0,castle.h/2+40,castle.w/2+30,28,0,0,Math.PI*2); ctx.fill();
  ctx.fillStyle="#3f4756"; ctx.fillRect(-castle.w/2,-castle.h/2,castle.w,castle.h);
  ctx.strokeStyle="rgba(0,0,0,.22)"; ctx.lineWidth=1;
  for(let yy=-castle.h/2; yy<castle.h/2; yy+=22){ ctx.beginPath(); ctx.moveTo(-castle.w/2,yy); ctx.lineTo(castle.w/2,yy); ctx.stroke(); }
  ctx.fillStyle="#2f3642"; for(let xx=-castle.w/2; xx<castle.w/2; xx+=28) ctx.fillRect(xx,-castle.h/2-18,16,18);
  [ -castle.w/2+12, castle.w/2-122 ].forEach(tx=>{ ctx.fillStyle="#374151"; ctx.fillRect(tx,-castle.h/2-105,110,230); ctx.fillStyle="#1e293b"; ctx.beginPath(); ctx.moveTo(tx-12,-castle.h/2-105); ctx.lineTo(tx+55,-castle.h/2-173); ctx.lineTo(tx+122,-castle.h/2-105); ctx.closePath(); ctx.fill(); ctx.fillStyle="#dc2626"; ctx.fillRect(tx+54,-castle.h/2-173,2,22); ctx.fillStyle="#fbbf24"; ctx.beginPath(); ctx.moveTo(tx+55,-castle.h/2-173); ctx.lineTo(tx+83,-castle.h/2-165); ctx.lineTo(tx+55,-castle.h/2-157); ctx.fill(); });
  ctx.fillStyle="#111827"; ctx.beginPath(); ctx.arc(0,castle.h/2-22,56,Math.PI,0); ctx.fill(); ctx.fillRect(-56,castle.h/2-22,112,46);
  ctx.fillStyle="#422006"; ctx.fillRect(-38,castle.h/2-10,76,34);
  ctx.fillStyle="#92400e"; for(let xx=-28;xx<=28;xx+=14) ctx.fillRect(xx,castle.h/2-10,3,34);
  ctx.fillStyle="rgba(0,0,0,.6)"; ctx.fillRect(-70,castle.h/2+32,140,8); ctx.fillStyle=castle.hp/castle.maxHp<0.3?"#ef4444":"#fbbf24"; ctx.fillRect(-70,castle.h/2+32,140*clamp(castle.hp/castle.maxHp,0,1),8);
  // Label kastil
  ctx.font="800 13px Inter"; ctx.textAlign="center"; ctx.fillStyle="rgba(0,0,0,.6)"; ctx.fillText("🏰 CASTLE",1, -castle.h/2-190+1); ctx.fillStyle="#fff"; ctx.fillText("🏰 CASTLE",0,-castle.h/2-190);
  ctx.restore();
}
function drawTree(x,y,s){ ctx.save(); ctx.translate(x,y); ctx.scale(s,s); ctx.fillStyle="rgba(0,0,0,.25)"; ctx.beginPath(); ctx.ellipse(0,34,22,9,0,0,Math.PI*2); ctx.fill(); ctx.fillStyle="#5a3a1e"; ctx.fillRect(-7,0,14,38); [{y:-18,r:34,col:"#162d14"},{y:-26,r:28,col:"#234a1e"},{y:-38,r:20,col:"#2f6a26"}].forEach(c=>{ ctx.fillStyle=c.col; ctx.beginPath(); ctx.arc(0,c.y,c.r,0,Math.PI*2); ctx.fill(); }); ctx.restore(); }
function drawRock(x,y,s){ ctx.save(); ctx.translate(x,y); ctx.scale(s,s); ctx.fillStyle="rgba(0,0,0,.2)"; ctx.beginPath(); ctx.ellipse(2,10,20,7,0,0,Math.PI*2); ctx.fill(); ctx.fillStyle="#4b5563"; ctx.beginPath(); ctx.ellipse(0,0,22,14,0,0,Math.PI*2); ctx.fill(); ctx.fillStyle="#9ca3af"; ctx.beginPath(); ctx.ellipse(-4,-5,3.5,1.8,-.3,0,Math.PI*2); ctx.fill(); ctx.restore(); }
function drawCharacter(x,y,dir,body,accent,isPlayer,weapon,attacking, isBrute=false){
  ctx.save(); ctx.translate(x,y); ctx.rotate(dir+Math.PI/2);
  // aura grandmaster / spesial
  if(isPlayer && (rankIndex===4 || isSpecial)){ ctx.save(); ctx.rotate(-dir-Math.PI/2); ctx.globalAlpha=0.35+Math.sin(Date.now()/300)*0.12; ctx.fillStyle=isSpecial?"#c084fc":"#fbbf24"; ctx.beginPath(); ctx.arc(0,0,34,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1; ctx.restore(); }
  ctx.fillStyle="rgba(0,0,0,.32)"; ctx.beginPath(); ctx.ellipse(0,16,16,6.5,0,0,Math.PI*2); ctx.fill();
  ctx.fillStyle="#1e1b18"; ctx.fillRect(-8,6,5,12); ctx.fillRect(3,6,5,12);
  ctx.fillStyle=body; if(ctx.roundRect){ ctx.beginPath(); ctx.roundRect(-11,-12,22,22,4); ctx.fill(); } else ctx.fillRect(-11,-12,22,22);
  ctx.fillStyle=accent; ctx.fillRect(-5,-12,10,22);
  ctx.fillStyle="#d1a37a"; ctx.beginPath(); ctx.arc(0,-18,10,0,Math.PI*2); ctx.fill();
  ctx.fillStyle="#78350f"; ctx.beginPath(); ctx.arc(0,-21,10,Math.PI,0); ctx.fill();
  // senjata
  ctx.fillStyle="#e5e7eb"; ctx.fillRect(10,-14,4,8); ctx.fillStyle="#374151"; ctx.fillRect(12,-12,2,14);
  if(weapon==="sword"){ ctx.fillStyle="#e5e7eb"; ctx.beginPath(); ctx.moveTo(11,-16); ctx.lineTo(15,-16); ctx.lineTo(14,-38); ctx.lineTo(12,-38); ctx.closePath(); ctx.fill(); }
  else if(weapon==="spear"){ ctx.fillStyle="#78350f"; ctx.fillRect(12,-35,3,50); ctx.fillStyle="#cbd5e1"; ctx.beginPath(); ctx.moveTo(11,-35); ctx.lineTo(15,-35); ctx.lineTo(13.5,-52); ctx.closePath(); ctx.fill(); }
  else if(weapon==="axe"){ ctx.fillStyle="#422006"; ctx.fillRect(12,-28,3,34); ctx.fillStyle="#9ca3af"; ctx.beginPath(); ctx.moveTo(12,-30); ctx.lineTo(12,-16); ctx.lineTo(26,-22); ctx.lineTo(24,-30); ctx.closePath(); ctx.fill(); }
  else { ctx.fillStyle="#d1d5db"; ctx.beginPath(); ctx.moveTo(10,-12); ctx.lineTo(14,-12); ctx.lineTo(12,-24); ctx.closePath(); ctx.fill(); }
  ctx.restore();
}

let lastTime=performance.now(), waveTimer=0, hudTimer=0, shake=0;
function loop(now){
  requestAnimationFrame(loop);
  const dt=Math.min(33, now-lastTime)/16.666; lastTime=now;
  if(!gameStarted){ return; }
  if(gameOver){ // tetap gambar scene diam
    drawScene(dt); return;
  }
  // Input gerak
  let mx=joy.x, my=joy.y;
  if(keys.KeyW||keys.ArrowUp) my-=1; if(keys.KeyS||keys.ArrowDown) my+=1;
  if(keys.KeyA||keys.ArrowLeft) mx-=1; if(keys.KeyD||keys.ArrowRight) mx+=1;
  const mlen=Math.hypot(mx,my); if(mlen>1){ mx/=mlen; my/=mlen; }
  if(player.isDashing){
    player.x+=player.dashX*dt*0.12; player.y+=player.dashY*dt*0.12;
    player.dashTime-=dt; if(player.dashTime<=0){ player.isDashing=false; player.dashX=0; player.dashY=0; }
    if(Math.random()<0.5) particles.push({x:player.x,y:player.y,vx:rand(-.5,.5),vy:rand(-.5,.5),color:"rgba(150,180,255,.25)",size:14,life:.45,max:.45,grav:0});
  } else if(Math.abs(mx)>0.05||Math.abs(my)>0.05){
    player.x+=mx*player.speed*dt; player.y+=my*player.speed*dt; player.dir=Math.atan2(my,mx);
    if(Math.random()<0.28) particles.push({x:player.x+rand(-4,4),y:player.y+10,vx:rand(-.4,.4),vy:rand(-.2,.6),color:"rgba(60,90,50,.5)",size:rand(1.5,2.8),life:.6,max:.6,grav:.05});
  }
  player.x=clamp(player.x,40,WORLD.w-40); player.y=clamp(player.y,40,WORLD.h-40);
  if(player.attackCd>0) player.attackCd-=dt;
  if(player.dashCd>0) player.dashCd-=dt;
  if(player.comboTimer>0){ player.comboTimer-=dt; if(player.comboTimer<=0) player.combo=0; }

  // Wave
  waveTimer+=dt;
  if(waveTimer>=840){ waveTimer=0; wave++; if(wave<=5){ spawnEnemy("brute"); for(let i=0;i<2+wave;i++) spawnEnemy("bandit"); showToast(`🌊 WAVE ${wave} INCOMING!`); } else { for(let i=0;i<3;i++) spawnEnemy(); } }
  if(enemies.length<6 && Math.random()<0.02) spawnEnemy();

  // Allies AI
  for(const a of allies){
    if(a.attackCd>0) a.attackCd-=dt;
    let target=null, best=1e9;
    for(const e of enemies){ const d=Math.hypot(e.x-a.x,e.y-a.y); if(d<best){ best=d; target=e; } }
    if(target){
      const dx=target.x-a.x, dy=target.y-a.y, d=Math.hypot(dx,dy)||1; a.dir=Math.atan2(dy,dx);
      if(d>52){ a.x+=dx/d*a.speed*dt; a.y+=dy/d*a.speed*dt; }
      else if(a.attackCd<=0){ target.hp-=16; target.hitFlash=6; a.attackCd=48; floatTexts.push({x:target.x,y:target.y-22,vy:-2.2,life:1,text:"-16",color:"#60a5fa"}); if(target.hp<=0){ target.dead=true; kills++; checkRankUp(); spawnAlly(target.x,target.y); } }
    } else {
      const tx = Math.hypot(player.x-castle.x, player.y-castle.y)<600 ? player : {x:castle.x,y:castle.y-80};
      const dx=tx.x-a.x, dy=tx.y-a.y, d=Math.hypot(dx,dy)||1;
      if(d>120){ a.dir=Math.atan2(dy,dx); a.x+=dx/d*a.speed*0.7*dt; a.y+=dy/d*a.speed*0.7*dt; }
    }
    a.x=clamp(a.x,30,WORLD.w-30); a.y=clamp(a.y,30,WORLD.h-30);
  }
  enemies=enemies.filter(e=>!e.dead);

  // Enemies AI
  for(const e of enemies){
    if(e.attackCd>0) e.attackCd-=dt;
    if(e.hitFlash>0) e.hitFlash-=dt;
    let tx=player.x, ty=player.y, best=Math.hypot(player.x-e.x, player.y-e.y);
    for(const a of allies){ const d=Math.hypot(a.x-e.x,a.y-e.y); if(d<best-20){ best=d; tx=a.x; ty=a.y; } }
    if(Math.hypot(castle.x-e.x, castle.y-e.y)<380){ tx=castle.x; ty=castle.y; }
    const dx=tx-e.x, dy=ty-e.y, d=Math.hypot(dx,dy)||1; e.dir=Math.atan2(dy,dx);
    if(d>(e.type==="brute"?58:46)){ e.x+=dx/d*e.speed*dt; e.y+=dy/d*e.speed*dt; }
    else if(e.attackCd<=0){
      if(tx===castle.x && ty===castle.y){
        castle.hp-= e.type==="brute"?18:7; e.attackCd=e.type==="brute"?90:60; shake=Math.max(shake, e.type==="brute"?6:3); sfx("hit");
        for(let i=0;i<8;i++) particles.push({x:castle.x+rand(-40,40),y:castle.y+rand(-20,20),vx:rand(-2,2),vy:rand(-3,-.5),color:"#6b7280",size:rand(2,4),life:.8,max:.8,grav:.12});
        floatTexts.push({x:castle.x,y:castle.y-40,vy:-2.2,life:1,text:`-${e.type==="brute"?18:7}`,color:"#fbbf24"});
      } else if(Math.hypot(player.x-e.x, player.y-e.y)<60){
        player.hp-= e.type==="brute"?18:8; e.attackCd=e.type==="brute"?85:52; shake=Math.max(shake, e.type==="brute"?8:4); sfx("hit");
        for(let i=0;i<10;i++) particles.push({x:player.x,y:player.y-10,vx:rand(-2.5,2.5),vy:rand(-2.5,1),color:"#ef4444",size:rand(2,4),life:.7,max:.7,grav:.12});
        floatTexts.push({x:player.x,y:player.y-30,vy:-2.2,life:1,text:`-${e.type==="brute"?18:8}`,color:"#ef4444"});
      } else {
        const a=allies.find(al=>Math.hypot(al.x-e.x, al.y-e.y)<55);
        if(a){ a.hp-= e.type==="brute"?22:12; if(a.hp<=0) allies=allies.filter(x=>x!==a); e.attackCd=e.type==="brute"?80:50; }
      }
    }
    e.x=clamp(e.x,20,WORLD.w-20); e.y=clamp(e.y,20,WORLD.h-20);
  }

  particles.forEach(p=>{ p.x+=p.vx; p.y+=p.vy; p.vy+=p.grav; p.life-=0.02; p.vx*=0.98; });
  particles=particles.filter(p=>p.life>0);
  floatTexts.forEach(f=>{ f.y+=f.vy; f.vy+=0.06; f.life-=0.018; });
  floatTexts=floatTexts.filter(f=>f.life>0);
  slashes.forEach(s=>s.life-=0.09); slashes=slashes.filter(s=>s.life>0);

  camera.x+=(player.x-camera.x)*0.12; camera.y+=(player.y-camera.y)*0.12;
  if(shake>0) shake-=dt*0.45;

  // Cek menang/kalah
  if(player.hp<=0){ endGame(false,"Fallen in Battle",`You fell in Wave ${wave} with ${kills} kills as ${currentRank().name}.. The kingdom remembers your bravery, ${username}!`); }
  else if(castle.hp<=0){ endGame(false,"Castle Has Fallen",`The castle fell after you killed ${kills} enemies. Defend the gate better next time!`); }
  else if(kills>=60 || (wave>=5 && enemies.length===0 && kills>=35)){ endGame(true,"Victory Reforged!",`You & ${allies.length} allies defended the kingdom! ${kills} enemies fell in ${wave} waves. Final Rank: ${currentRank().name}.`); }

  drawScene(dt);
  hudTimer+=dt; if(hudTimer>6){ hudTimer=0; updateHUD(); }
}

function drawScene(dt){
  const shX = shake>0 ? rand(-shake,shake) : 0;
  const shY = shake>0 ? rand(-shake,shake) : 0;
  const camX=camera.x+shX, camY=camera.y+shY;
  drawGround(camX,camY);
  drawCastle();
  for(const t of trees){ const sx=t.x-camX+W/2, sy=t.y-camY+H/2; if(sx<-120||sx>W+120||sy<-120||sy>H+120) continue; drawTree(sx,sy,t.s); }
  for(const r of rocks){ const sx=r.x-camX+W/2, sy=r.y-camY+H/2; if(sx<-120||sx>W+120||sy<-120||sy>H+120) continue; drawRock(sx,sy,r.s); }
  // particles kecil
  for(const p of particles){ if(p.size>8) continue; const sx=p.x-camX+W/2, sy=p.y-camY+H/2; if(sx<-20||sx>W+20||sy<-20||sy>H+20) continue; ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(sx,sy,p.size,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1; }
  // allies
  for(const a of allies){ const sx=a.x-camX+W/2, sy=a.y-camY+H/2; drawCharacter(sx,sy,a.dir,"#14532d","#22c55e",false,"spear",a.attackCd>30); if(a.hp<a.maxHp){ ctx.fillStyle="rgba(0,0,0,.6)"; ctx.fillRect(sx-16,sy-38,32,4); ctx.fillStyle="#22c55e"; ctx.fillRect(sx-16,sy-38,32*(a.hp/a.maxHp),4); } }
  // enemies
  for(const e of enemies){ const sx=e.x-camX+W/2, sy=e.y-camY+H/2; drawCharacter(sx,sy,e.dir, e.hitFlash>0?"#ffffff": e.type==="brute"?"#3f0a0a":"#450a0a", e.type==="brute"?"#7f1d1d":"#b91c1c", false, e.type==="brute"?"axe":"dagger", e.attackCd>35, e.type==="brute"); ctx.fillStyle="rgba(0,0,0,.6)"; ctx.fillRect(sx-16,sy-38,32,4); ctx.fillStyle="#ef4444"; ctx.fillRect(sx-16,sy-38,32*clamp(e.hp/e.maxHp,0,1),4); }
  // player + username di atas kepala
  const px=player.x-camX+W/2, py=player.y-camY+H/2;
  if(player.isDashing){ ctx.globalAlpha=0.28; for(let i=1;i<=3;i++) drawCharacter(px-Math.cos(player.dir)*i*12, py-Math.sin(player.dir)*i*12, player.dir, "#1e3a8a","#3b82f6",false,"sword",false); ctx.globalAlpha=1; }
  drawCharacter(px,py,player.dir,"#1e3a8a","#3b82f6",true,"sword",player.attackCd>9);
  // === USERNAME DI ATAS KEPALA ===
  const r=currentRank();
  ctx.font="800 13px Inter, sans-serif"; ctx.textAlign="center";
  const nameText = (isSpecial?"👑 ":"") + username.toUpperCase();
  const tw=ctx.measureText(nameText).width;
  ctx.fillStyle="rgba(0,0,0,.68)"; ctx.beginPath(); ctx.roundRect(px-tw/2-8, py-62, tw+16, 22, 11); ctx.fill();
  ctx.strokeStyle= isSpecial ? "#c084fc" : r.color; ctx.lineWidth=1.2; ctx.beginPath(); ctx.roundRect(px-tw/2-8, py-62, tw+16, 22, 11); ctx.stroke();
  ctx.fillStyle="#fff"; ctx.fillText(nameText, px, py-46);
  ctx.font="700 9px Inter"; ctx.fillStyle= isSpecial ? "#e9d5ff" : "#fde68a"; ctx.fillText(r.icon+" "+r.name, px, py-68);
  // slashes
  for(const s of slashes){ const sx=s.x-camX+W/2, sy=s.y-camY+H/2; ctx.save(); ctx.translate(sx,sy); ctx.rotate(s.dir); ctx.globalAlpha=s.life*0.85; ctx.strokeStyle=s.combo===2?"#fde047":"#ffffff"; ctx.lineWidth=s.combo===2?4.5:3; ctx.lineCap="round"; const arc=s.combo===0?1:s.combo===1?1.35:1.9, rad=s.combo===2?62:48; ctx.beginPath(); ctx.arc(12,-4,rad,-arc/2,arc/2); ctx.stroke(); ctx.globalAlpha=s.life*0.28; ctx.lineWidth=12; ctx.beginPath(); ctx.arc(12,-4,rad,-arc/2,arc/2); ctx.stroke(); ctx.restore(); }
  // particles besar
  for(const p of particles){ if(p.size<=8) continue; const sx=p.x-camX+W/2, sy=p.y-camY+H/2; ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=p.color; ctx.beginPath(); ctx.arc(sx,sy,p.size,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1; }
  // floating texts
  ctx.font="700 16px Inter"; ctx.textAlign="center";
  for(const f of floatTexts){ const sx=f.x-camX+W/2, sy=f.y-camY+H/2; ctx.globalAlpha=f.life; ctx.fillStyle="rgba(0,0,0,.6)"; ctx.fillText(f.text,sx+1,sy+1); ctx.fillStyle=f.color; ctx.fillText(f.text,sx,sy); ctx.globalAlpha=1; }
  // HP bar player di bawah karakter (opsional)
  ctx.fillStyle="rgba(0,0,0,.6)"; ctx.fillRect(px-22,py+28,44,5); ctx.fillStyle=player.hp/player.maxHp<0.3?"#ef4444":"#22c55e"; ctx.fillRect(px-22,py+28,44*clamp(player.hp/player.maxHp,0,1),5);
}

resetGame();
gameStarted=false; // tunggu menu
requestAnimationFrame(loop);
</script>
</body>
</html>

Game Source: Medieval Reforged — Rank Edition

Creator: ElectricPirate55

Libraries: none

Complexity: complex (620 lines, 47.1 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: medieval-reforged-rank-edition-electricpirate55" to link back to the original. Then publish at arcadelab.ai/publish.