Battle Realms – ממלכת הקרב
by FrozenOtter311200 lines50.6 KB
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Battle Realms – ממלכת הקרב</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;background:#0a0a1a;color:#fff;overflow:hidden;touch-action:none;width:100vw;height:100vh;user-select:none}
#game-container{width:100%;height:100%;position:relative}
#lang-screen{position:absolute;inset:0;background:linear-gradient(180deg,#0f0f2e,#1a1a3e,#0a0a1a);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:1000}
#lang-screen h1{font-size:28px;margin-bottom:8px;color:#fbbf24;text-shadow:0 0 20px rgba(251,191,36,0.4)}
#lang-screen p{font-size:16px;color:#94a3b8;margin-bottom:40px}
.lang-btn{width:220px;padding:16px 24px;margin:8px 0;font-size:20px;font-weight:bold;border:2px solid #475569;border-radius:16px;background:rgba(30,41,59,0.8);color:#fff;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:12px}
.lang-btn:hover,.lang-btn.active{border-color:#fbbf24;background:rgba(251,191,36,0.15);transform:scale(1.05)}
.lang-flag{font-size:28px}
#lang-continue{margin-top:30px;padding:14px 48px;font-size:18px;font-weight:bold;background:linear-gradient(135deg,#f59e0b,#d97706);border:none;border-radius:12px;color:#fff;cursor:pointer;opacity:.5;pointer-events:none;transition:all .2s}
#lang-continue.active{opacity:1;pointer-events:all}
#main-menu{position:absolute;inset:0;background:linear-gradient(180deg,#0f0f2e,#1e1b4b 40%,#0a0a1a);display:none;flex-direction:column;align-items:center;justify-content:center;z-index:900}
.menu-logo{font-size:42px;font-weight:900;color:#fbbf24;text-shadow:0 0 30px rgba(251,191,36,0.5),0 4px 8px rgba(0,0,0,0.5);margin-bottom:4px;letter-spacing:2px}
.menu-subtitle{font-size:14px;color:#94a3b8;margin-bottom:40px;letter-spacing:4px}
.menu-btn{width:260px;padding:16px;margin:6px 0;font-size:18px;font-weight:700;border:none;border-radius:14px;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:10px}
.menu-btn-primary{background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff;font-size:22px;padding:20px;box-shadow:0 4px 20px rgba(245,158,11,0.4)}
.menu-btn-primary:active{transform:scale(0.95)}
.menu-btn-secondary{background:rgba(30,41,59,0.8);color:#e2e8f0;border:2px solid #475569}
.menu-btn-secondary:active{background:rgba(51,65,85,0.9);transform:scale(0.97)}
#battle-screen{position:absolute;inset:0;display:none;flex-direction:column;background:#0f172a}
#battle-canvas{flex:1;width:100%;display:block}
#top-bar{position:absolute;top:0;left:0;right:0;height:56px;background:linear-gradient(180deg,rgba(0,0,0,0.7),transparent);display:flex;align-items:center;justify-content:space-between;padding:0 12px;z-index:50;pointer-events:none}
.tower-hp-bar{width:40px;height:6px;background:#334155;border-radius:3px;overflow:hidden;margin-top:2px}
.tower-hp-fill{height:100%;background:#ef4444;border-radius:3px;transition:width .3s}
.tower-hp-fill.friendly{background:#3b82f6}
.enemy-info,.player-info{display:flex;flex-direction:column;align-items:center;font-size:11px;font-weight:bold}
.enemy-info{color:#f87171}
.player-info{color:#60a5fa}
#battle-timer{font-size:20px;font-weight:900;color:#fbbf24;background:rgba(0,0,0,0.5);padding:4px 14px;border-radius:20px;font-variant-numeric:tabular-nums}
#bottom-ui{position:absolute;bottom:0;left:0;right:0;height:130px;background:linear-gradient(0deg,rgba(0,0,0,0.85),rgba(0,0,0,0.4) 60%,transparent);display:flex;flex-direction:column;align-items:center;padding:8px 8px 16px;z-index:50}
#energy-bar-container{width:100%;max-width:400px;margin-bottom:8px}
#energy-label{display:flex;justify-content:space-between;font-size:11px;color:#94a3b8;margin-bottom:3px}
#energy-bar{width:100%;height:10px;background:#1e293b;border-radius:5px;overflow:hidden;border:1px solid #475569}
#energy-fill{height:100%;background:linear-gradient(90deg,#fbbf24,#f59e0b);border-radius:5px;transition:width .2s;box-shadow:0 0 10px rgba(251,191,36,0.3)}
#cards-row{display:flex;gap:8px;justify-content:center;width:100%}
.card-slot{width:72px;height:90px;background:linear-gradient(180deg,#1e293b,#0f172a);border:2px solid #475569;border-radius:12px;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:all .15s;position:relative;touch-action:none}
.card-slot.selected{border-color:#fbbf24;transform:translateY(-8px) scale(1.08);box-shadow:0 8px 25px rgba(251,191,36,0.3)}
.card-slot.disabled{opacity:.4;pointer-events:none}
.card-emoji{font-size:28px;margin-bottom:2px}
.card-name{font-size:9px;font-weight:bold;color:#e2e8f0;text-align:center;line-height:1.1}
.card-cost{position:absolute;top:-8px;left:-8px;width:24px;height:24px;border-radius:50%;background:linear-gradient(135deg,#fbbf24,#d97706);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:900;color:#fff;border:2px solid #0f172a;box-shadow:0 2px 6px rgba(0,0,0,0.4)}
.card-rarity-common{border-color:#94a3b8}
.card-rarity-rare{border-color:#3b82f6}
.card-rarity-epic{border-color:#a855f7}
.card-rarity-legendary{border-color:#fbbf24;box-shadow:0 0 8px rgba(251,191,36,0.2)}
#next-card{width:50px;height:65px;background:linear-gradient(180deg,#1e293b,#0f172a);border:1px dashed #475569;border-radius:8px;display:flex;flex-direction:column;align-items:center;justify-content:center;opacity:.6}
#next-card .card-emoji{font-size:20px}
#next-card-label{font-size:8px;color:#64748b;margin-top:2px}
#drag-ghost{position:fixed;pointer-events:none;z-index:9999;width:60px;height:60px;border-radius:50%;display:none;align-items:center;justify-content:center;font-size:36px;background:rgba(0,0,0,0.5);border:3px solid #fbbf24;box-shadow:0 0 20px rgba(251,191,36,0.5)}
#result-overlay{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,0.75);z-index:200;backdrop-filter:blur(4px)}
#result-title{font-size:52px;font-weight:900;margin-bottom:16px;text-shadow:0 0 40px}
#result-title.victory{color:#fbbf24;text-shadow:0 0 40px rgba(251,191,36,0.5)}
#result-title.defeat{color:#ef4444;text-shadow:0 0 40px rgba(239,68,68,0.5)}
.result-stats{background:rgba(30,41,59,0.9);border-radius:16px;padding:20px 32px;margin-bottom:24px;display:flex;flex-direction:column;gap:8px;min-width:220px}
.result-stat{display:flex;justify-content:space-between;font-size:16px}
.result-stat .label{color:#94a3b8}
.result-stat .value{font-weight:bold;color:#fbbf24}
.result-btn{padding:14px 40px;font-size:18px;font-weight:bold;border:none;border-radius:12px;cursor:pointer;margin:4px;transition:all .15s}
.result-btn-primary{background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff}
.result-btn-secondary{background:#334155;color:#e2e8f0}
.damage-number{position:absolute;pointer-events:none;font-size:16px;font-weight:900;text-shadow:0 2px 4px rgba(0,0,0,0.8);animation:dmgFloat 1s ease-out forwards;z-index:60}
@keyframes dmgFloat{0%{transform:translateY(0) scale(1);opacity:1}50%{transform:translateY(-30px) scale(1.3);opacity:1}100%{transform:translateY(-50px) scale(0.8);opacity:0}}
.combo-text{position:absolute;pointer-events:none;font-size:28px;font-weight:900;color:#fbbf24;text-shadow:0 0 20px rgba(251,191,36,0.6);animation:comboPop 1.2s ease-out forwards;z-index:70}
@keyframes comboPop{0%{transform:scale(0) rotate(-10deg);opacity:0}20%{transform:scale(1.4) rotate(5deg);opacity:1}40%{transform:scale(1) rotate(0deg)}100%{transform:translateY(-40px) scale(0.9);opacity:0}}
#transition-overlay{position:absolute;inset:0;background:#0a0a1a;z-index:500;display:none;align-items:center;justify-content:center;flex-direction:column}
#transition-text{font-size:24px;color:#fbbf24;margin-bottom:16px}
.loading-dots::after{content:'';animation:dots 1.5s infinite}
@keyframes dots{0%{content:'.'}33%{content:'..'}66%{content:'...'}}
#settings-modal{position:absolute;inset:0;background:rgba(0,0,0,0.8);z-index:300;display:none;align-items:center;justify-content:center}
#settings-panel{background:linear-gradient(180deg,#1e293b,#0f172a);border:2px solid #475569;border-radius:20px;padding:24px;width:90%;max-width:360px}
#settings-panel h2{text-align:center;margin-bottom:20px;color:#fbbf24}
.setting-row{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-bottom:1px solid #334155}
.setting-row:last-child{border:none}
.toggle-btn{width:48px;height:26px;border-radius:13px;background:#475569;border:none;cursor:pointer;position:relative;transition:background .2s}
.toggle-btn.on{background:#22c55e}
.toggle-btn::after{content:'';position:absolute;top:2px;left:2px;width:22px;height:22px;border-radius:50%;background:#fff;transition:left .2s}
.toggle-btn.on::after{left:24px}
#collection-screen{position:absolute;inset:0;background:linear-gradient(180deg,#0f0f2e,#1a1a3e);display:none;flex-direction:column;z-index:800}
#collection-header{padding:16px;display:flex;justify-content:space-between;align-items:center;background:rgba(0,0,0,0.3)}
#collection-header h2{color:#fbbf24;font-size:22px}
.back-btn{padding:8px 16px;background:#334155;border:none;border-radius:10px;color:#fff;font-size:14px;cursor:pointer}
#collection-grid{flex:1;overflow-y:auto;padding:12px;display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.collection-card{background:linear-gradient(180deg,#1e293b,#0f172a);border:2px solid #475569;border-radius:14px;padding:12px 8px;text-align:center;cursor:pointer;transition:all .15s}
.collection-card:hover{transform:scale(1.03);border-color:#64748b}
.collection-card .coll-emoji{font-size:32px;margin-bottom:4px}
.collection-card .coll-name{font-size:11px;font-weight:bold;margin-bottom:2px}
.collection-card .coll-rarity{font-size:9px}
.collection-card .coll-level{font-size:10px;color:#fbbf24;margin-top:4px}
@media(max-width:360px){.card-slot{width:62px;height:80px}.card-emoji{font-size:22px}.card-name{font-size:8px}.menu-logo{font-size:32px}}
@media(min-width:600px){.card-slot{width:85px;height:105px}.card-emoji{font-size:34px}.card-name{font-size:11px}#bottom-ui{height:150px}}
</style>
</head>
<body>
<div id="game-container">
<div id="lang-screen">
<h1 id="lang-title">Choose Your Language</h1>
<p id="lang-subtitle">בחר את השפה שלך</p>
<button class="lang-btn" data-lang="he" onclick="selectLang('he')"><span class="lang-flag">🇮🇱</span><span>עברית</span></button>
<button class="lang-btn" data-lang="en" onclick="selectLang('en')"><span class="lang-flag">🇺🇸</span><span>English</span></button>
<button id="lang-continue" onclick="confirmLang()">Continue / המשך</button>
</div>
<div id="main-menu">
<div class="menu-logo">BATTLE REALMS</div>
<div class="menu-subtitle" id="menu-subtitle">Enter the Battle</div>
<button class="menu-btn menu-btn-primary" id="btn-play" onclick="startMatchmaking()">⚔️ <span id="txt-play">PLAY</span></button>
<button class="menu-btn menu-btn-secondary" id="btn-cards" onclick="openCollection()">🃏 <span id="txt-cards">CARDS</span></button>
<button class="menu-btn menu-btn-secondary" id="btn-settings" onclick="openSettings()">⚙️ <span id="txt-settings">SETTINGS</span></button>
</div>
<div id="collection-screen">
<div id="collection-header">
<button class="back-btn" onclick="closeCollection()">← <span id="txt-back">Back</span></button>
<h2 id="txt-collection-title">COLLECTION</h2>
<div></div>
</div>
<div id="collection-grid"></div>
</div>
<div id="settings-modal" onclick="if(event.target===this)closeSettings()">
<div id="settings-panel">
<h2 id="txt-settings-title">SETTINGS</h2>
<div class="setting-row"><span id="txt-music">Music</span><button class="toggle-btn on" id="toggle-music" onclick="toggleSetting('music')"></button></div>
<div class="setting-row"><span id="txt-sfx">Sound Effects</span><button class="toggle-btn on" id="toggle-sfx" onclick="toggleSetting('sfx')"></button></div>
<div class="setting-row"><span id="txt-vibration">Vibration</span><button class="toggle-btn on" id="toggle-vibration" onclick="toggleSetting('vibration')"></button></div>
<div class="setting-row"><span id="txt-language">Language</span><button class="back-btn" onclick="showLangSelect()" style="font-size:12px">🌐</button></div>
</div>
</div>
<div id="transition-overlay"><div id="transition-text">Searching for opponent<span class="loading-dots"></span></div></div>
<div id="battle-screen">
<canvas id="battle-canvas"></canvas>
<div id="top-bar">
<div class="enemy-info">
<span id="enemy-name">Enemy</span>
<div style="display:flex;gap:4px;margin-top:2px">
<div><div style="font-size:9px;color:#fbbf24">🏆</div><div class="tower-hp-bar"><div class="tower-hp-fill" id="enemy-tower-l" style="width:100%"></div></div></div>
<div><div style="font-size:9px;color:#fbbf24">👑</div><div class="tower-hp-bar"><div class="tower-hp-fill" id="enemy-tower-c" style="width:100%"></div></div></div>
<div><div style="font-size:9px;color:#fbbf24">🏆</div><div class="tower-hp-bar"><div class="tower-hp-fill" id="enemy-tower-r" style="width:100%"></div></div></div>
</div>
</div>
<div id="battle-timer">03:00</div>
<div class="player-info">
<span id="player-name">You</span>
<div style="display:flex;gap:4px;margin-top:2px">
<div><div style="font-size:9px;color:#fbbf24">🏆</div><div class="tower-hp-bar"><div class="tower-hp-fill friendly" id="player-tower-l" style="width:100%"></div></div></div>
<div><div style="font-size:9px;color:#fbbf24">👑</div><div class="tower-hp-bar"><div class="tower-hp-fill friendly" id="player-tower-c" style="width:100%"></div></div></div>
<div><div style="font-size:9px;color:#fbbf24">🏆</div><div class="tower-hp-bar"><div class="tower-hp-fill friendly" id="player-tower-r" style="width:100%"></div></div></div>
</div>
</div>
</div>
<div id="bottom-ui">
<div id="energy-bar-container">
<div id="energy-label"><span id="txt-energy">Energy</span><span id="energy-value">0 / 10</span></div>
<div id="energy-bar"><div id="energy-fill" style="width:0%"></div></div>
</div>
<div id="cards-row">
<div class="card-slot" id="card-0" onclick="selectCard(0)" ontouchstart="handleTouchStart(event,0)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)"><div class="card-cost">4</div><div class="card-emoji">⚔️</div><div class="card-name">Flame Knight</div></div>
<div class="card-slot" id="card-1" onclick="selectCard(1)" ontouchstart="handleTouchStart(event,1)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)"><div class="card-cost">3</div><div class="card-emoji">🗡️</div><div class="card-name">Shadow</div></div>
<div class="card-slot" id="card-2" onclick="selectCard(2)" ontouchstart="handleTouchStart(event,2)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)"><div class="card-cost">6</div><div class="card-emoji">🗿</div><div class="card-name">Stone Golem</div></div>
<div class="card-slot" id="card-3" onclick="selectCard(3)" ontouchstart="handleTouchStart(event,3)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)"><div class="card-cost">5</div><div class="card-emoji">⚡</div><div class="card-name">Storm Mage</div></div>
<div id="next-card"><div class="card-emoji" id="next-emoji">🏹</div><div id="next-card-label">Next</div></div>
</div>
</div>
<div id="result-overlay">
<div id="result-title" class="victory">VICTORY</div>
<div class="result-stats">
<div class="result-stat"><span class="label" id="res-trophies-label">Trophies</span><span class="value" id="res-trophies">+30</span></div>
<div class="result-stat"><span class="label" id="res-coins-label">Coins</span><span class="value" id="res-coins">+150</span></div>
<div class="result-stat"><span class="label" id="res-time-label">Time</span><span class="value" id="res-time">02:45</span></div>
</div>
<div>
<button class="result-btn result-btn-primary" id="btn-play-again" onclick="playAgain()">PLAY AGAIN</button>
<button class="result-btn result-btn-secondary" id="btn-home" onclick="goHome()">HOME</button>
</div>
</div>
</div>
<div id="drag-ghost"></div>
</div>
<script>
const i18n={he:{chooseLanguage:"בחר את השפה שלך",continue:"המשך",play:"שחק",cards:"קלפים",settings:"הגדרות",collection:"אוסף",back:"חזור",music:"מוזיקה",sfx:"אפקטים",vibration:"רטט",language:"שפה",energy:"אנרגיה",next:"הבא",victory:"ניצחון!",defeat:"הפסד",trophies:"גביעים",coins:"מטבעות",time:"זמן",playAgain:"שחק שוב",home:"תפריט",searching:"מחפש יריב",enterBattle:"היכנס לקרב",towerDestroyed:"מגדל הושמד!",combo:"קומבו!",suddenDeath:"מוות פתאומי!",rotatePhone:"סובב את הטלפון",flameKnight:"אביר הלהבה",shadowAssassin:"מתנקש הצללים",stoneGolem:"גולם האבן",stormMage:"קוסם הסערה",forestArcher:"קשתית היער",goblinSquad:"חוליית הגובלינים",frostWitch:"מכשפת הקרח",ironBeast:"החיה המשוריינת",spiritHealer:"מרפאת הרוחות",voidDragon:"דרקון הריק",common:"נפוץ",rare:"נדיר",epic:"אפי",legendary:"אגדי"},en:{chooseLanguage:"Choose Your Language",continue:"Continue",play:"PLAY",cards:"CARDS",settings:"SETTINGS",collection:"COLLECTION",back:"Back",music:"Music",sfx:"Sound Effects",vibration:"Vibration",language:"Language",energy:"Energy",next:"Next",victory:"VICTORY!",defeat:"DEFEAT",trophies:"Trophies",coins:"Coins",time:"Time",playAgain:"PLAY AGAIN",home:"HOME",searching:"Searching for opponent",enterBattle:"Enter the Battle",towerDestroyed:"TOWER DESTROYED!",combo:"COMBO!",suddenDeath:"SUDDEN DEATH!",rotatePhone:"Rotate your phone",flameKnight:"Flame Knight",shadowAssassin:"Shadow Assassin",stoneGolem:"Stone Golem",stormMage:"Storm Mage",forestArcher:"Forest Archer",goblinSquad:"Goblin Squad",frostWitch:"Frost Witch",ironBeast:"Iron Beast",spiritHealer:"Spirit Healer",voidDragon:"Void Dragon",common:"Common",rare:"Rare",epic:"Epic",legendary:"Legendary"}};
let currentLang='he',selectedLang=null;
let settings={music:true,sfx:true,vibration:true};
function selectLang(lang){selectedLang=lang;document.querySelectorAll('.lang-btn').forEach(b=>b.classList.remove('active'));document.querySelector('.lang-btn[data-lang="'+lang+'"]').classList.add('active');document.getElementById('lang-continue').classList.add('active');}
function confirmLang(){if(!selectedLang)return;currentLang=selectedLang;applyLanguage();document.getElementById('lang-screen').style.display='none';document.getElementById('main-menu').style.display='flex';}
function showLangSelect(){closeSettings();document.getElementById('lang-screen').style.display='flex';document.getElementById('main-menu').style.display='none';document.getElementById('collection-screen').style.display='none';}
function applyLanguage(){
const t=i18n[currentLang];
document.documentElement.lang=currentLang;
document.documentElement.dir=currentLang==='he'?'rtl':'ltr';
document.getElementById('lang-title').textContent=t.chooseLanguage;
document.getElementById('lang-subtitle').textContent=currentLang==='he'?'Choose Your Language':'בחר את השפה שלך';
document.getElementById('lang-continue').textContent=t.continue;
document.getElementById('menu-subtitle').textContent=t.enterBattle;
document.getElementById('txt-play').textContent=t.play;
document.getElementById('txt-cards').textContent=t.cards;
document.getElementById('txt-settings').textContent=t.settings;
document.getElementById('txt-collection-title').textContent=t.collection;
document.getElementById('txt-back').textContent=t.back;
document.getElementById('txt-settings-title').textContent=t.settings;
document.getElementById('txt-music').textContent=t.music;
document.getElementById('txt-sfx').textContent=t.sfx;
document.getElementById('txt-vibration').textContent=t.vibration;
document.getElementById('txt-language').textContent=t.language;
document.getElementById('txt-energy').textContent=t.energy;
document.getElementById('next-card-label').textContent=t.next;
document.getElementById('btn-play-again').textContent=t.playAgain;
document.getElementById('btn-home').textContent=t.home;
document.getElementById('res-trophies-label').textContent=t.trophies;
document.getElementById('res-coins-label').textContent=t.coins;
document.getElementById('res-time-label').textContent=t.time;
updateCardNames();renderCollection();
}
function getCharName(charId){
const t=i18n[currentLang];
const names=[t.flameKnight,t.shadowAssassin,t.stoneGolem,t.stormMage,t.forestArcher,t.goblinSquad,t.frostWitch,t.ironBeast,t.spiritHealer,t.voidDragon];
return names[charId]||CHARACTERS[charId].name;
}
function getRarityName(r){return i18n[currentLang][r]||r;}
function updateCardNames(){
for(let i=0;i<4;i++){
const slot=document.getElementById('card-'+i);
if(slot&&hand[i]){const name=getCharName(hand[i].id);const short=name.split(' ').slice(0,2).join(' ');slot.querySelector('.card-name').textContent=short;}
}
const next=document.getElementById('next-emoji');
if(next&&nextCard)next.textContent=CHARACTERS[nextCard.id].emoji;
}
const AudioCtx=window.AudioContext||window.webkitAudioContext;
let audioCtx=null;
function initAudio(){if(!audioCtx)audioCtx=new AudioCtx();}
function playTone(freq,type,duration,vol){
if(!settings.sfx||!audioCtx)return;
const osc=audioCtx.createOscillator();
const gain=audioCtx.createGain();
osc.type=type;osc.frequency.value=freq;
gain.gain.setValueAtTime(vol||0.1,audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+duration);
osc.connect(gain);gain.connect(audioCtx.destination);
osc.start();osc.stop(audioCtx.currentTime+duration);
}
function playSound(name){
initAudio();
switch(name){
case'spawn':playTone(440,'sine',0.15,0.08);setTimeout(()=>playTone(660,'sine',0.1,0.06),80);break;
case'attack':playTone(200,'square',0.08,0.05);break;
case'hit':playTone(150,'sawtooth',0.1,0.06);break;
case'death':playTone(100,'sawtooth',0.3,0.08);break;
case'tower_destroy':playTone(80,'sawtooth',0.5,0.12);setTimeout(()=>playTone(60,'sawtooth',0.4,0.1),150);break;
case'victory':[523,659,784,1047].forEach((f,i)=>setTimeout(()=>playTone(f,'sine',0.3,0.1),i*120));break;
case'defeat':[400,350,300,200].forEach((f,i)=>setTimeout(()=>playTone(f,'sine',0.4,0.1),i*150));break;
case'select':playTone(880,'sine',0.08,0.05);break;
case'error':playTone(200,'square',0.15,0.06);break;
case'combo':playTone(600,'square',0.1,0.08);setTimeout(()=>playTone(900,'square',0.15,0.08),100);break;
case'sudden_death':playTone(300,'sawtooth',0.4,0.1);break;
}
}
function vibrate(pattern){if(settings.vibration&&navigator.vibrate)navigator.vibrate(pattern);}
const CHARACTERS=[
{id:0,name:'Flame Knight',nameHe:'אביר הלהבה',emoji:'⚔️',cost:4,hp:280,dmg:45,speed:1.2,range:35,atkSpeed:1.0,rarity:'rare',type:'melee',target:'nearest',ability:'berserk',abilityDesc:'Below 40% HP, damage increases by 50%'},
{id:1,name:'Shadow Assassin',nameHe:'מתנקש הצללים',emoji:'🗡️',cost:3,hp:120,dmg:60,speed:2.2,range:40,atkSpeed:0.7,rarity:'epic',type:'melee',target:'lowhp',ability:'stealth',abilityDesc:'Becomes invisible for 2s on spawn, then strikes'},
{id:2,name:'Stone Golem',nameHe:'גולם האבן',emoji:'🗿',cost:6,hp:600,dmg:70,speed:0.6,range:45,atkSpeed:1.5,rarity:'epic',type:'melee',target:'building',ability:'split',abilityDesc:'Splits into 2 Stone Shards on death'},
{id:3,name:'Storm Mage',nameHe:'קוסם הסערה',emoji:'⚡',cost:5,hp:150,dmg:35,speed:1.0,range:140,atkSpeed:1.2,rarity:'rare',type:'ranged',target:'nearest',ability:'chain',abilityDesc:'Every 3rd attack chains to nearby enemies'},
{id:4,name:'Forest Archer',nameHe:'קשתית היער',emoji:'🏹',cost:3,hp:110,dmg:30,speed:1.4,range:130,atkSpeed:0.9,rarity:'common',type:'ranged',target:'nearest',ability:'heal_arrow',abilityDesc:'Every 5th arrow heals lowest HP ally'},
{id:5,name:'Goblin Squad',nameHe:'חוליית הגובלינים',emoji:'👺',cost:2,hp:60,dmg:18,speed:2.0,range:30,atkSpeed:0.6,rarity:'common',type:'melee',target:'nearest',ability:'swarm',abilityDesc:'Spawns 3 goblins',count:3},
{id:6,name:'Frost Witch',nameHe:'מכשפת הקרח',emoji:'❄️',cost:4,hp:140,dmg:25,speed:1.1,range:120,atkSpeed:1.1,rarity:'rare',type:'ranged',target:'nearest',ability:'freeze',abilityDesc:'Slows enemies, chance to freeze for 1.5s'},
{id:7,name:'Iron Beast',nameHe:'החיה המשוריינת',emoji:'🦏',cost:7,hp:500,dmg:90,speed:0.9,range:50,atkSpeed:1.3,rarity:'legendary',type:'melee',target:'building',ability:'charge',abilityDesc:'Charges forward dealing massive damage'},
{id:8,name:'Spirit Healer',nameHe:'מרפאת הרוחות',emoji:'✨',cost:4,hp:130,dmg:10,speed:1.0,range:100,atkSpeed:1.5,rarity:'rare',type:'ranged',target:'nearest',ability:'heal_zone',abilityDesc:'Creates healing zone for allies'},
{id:9,name:'Void Dragon',nameHe:'דרקון הריק',emoji:'🐉',cost:8,hp:450,dmg:55,speed:1.3,range:150,atkSpeed:1.4,rarity:'legendary',type:'flying',target:'highest',ability:'void_breath',abilityDesc:'Void breath deals massive area damage'}
];
const RARITY_COLORS={common:'#94a3b8',rare:'#3b82f6',epic:'#a855f7',legendary:'#fbbf24'};
let gameState='menu',canvas,ctx,W,H;
let units=[],particles=[],projectiles=[],towers={};
let energy=3,maxEnergy=10,energyRate=0.25;
let battleTime=180,suddenDeath=false;
let hand=[],nextCard=null,selectedCardIndex=-1;
let deck=[0,1,2,3,4,5,6,7],deckIndex=0;
let playerTowersDestroyed=0,enemyTowersDestroyed=0;
let cameraShake=0,slowMo=0,battleTimer=null;
let lastTime=0,aiTimer=0,comboCount=0,comboTimer=0;
let gameOver=false,battleStartTime=0,dragIndex=-1;
function initCanvas(){
canvas=document.getElementById('battle-canvas');
ctx=canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize',resizeCanvas);
}
function resizeCanvas(){
const rect=canvas.parentElement.getBoundingClientRect();
const dpr=window.devicePixelRatio||1;
canvas.width=rect.width*dpr;
canvas.height=rect.height*dpr;
W=canvas.width/dpr;H=canvas.height/dpr;
ctx.setTransform(dpr,0,0,dpr,0,0);
}
function drawBattlefield(){
const g=ctx;
const grd=g.createLinearGradient(0,0,0,H);
grd.addColorStop(0,'#1a0f2e');grd.addColorStop(0.45,'#0f172a');grd.addColorStop(0.55,'#0f172a');grd.addColorStop(1,'#0a1f0a');
g.fillStyle=grd;g.fillRect(0,0,W,H);
const riverY=H*0.48,riverH=H*0.04;
g.fillStyle='rgba(30,58,138,0.6)';g.fillRect(0,riverY,W,riverH);
const shimmer=Math.sin(Date.now()*0.003)*0.1+0.1;
g.fillStyle='rgba(96,165,250,'+shimmer+')';g.fillRect(0,riverY+riverH*0.3,W,riverH*0.4);
const bridgeW=W*0.12,bx1=W*0.25-bridgeW/2,bx2=W*0.75-bridgeW/2;
g.fillStyle='#57534e';g.fillRect(bx1,riverY-4,bridgeW,riverH+8);g.fillRect(bx2,riverY-4,bridgeW,riverH+8);
g.fillStyle='#78716c';
for(let i=0;i<5;i++){g.fillRect(bx1+i*(bridgeW/5)+2,riverY-2,bridgeW/5-4,riverH+4);g.fillRect(bx2+i*(bridgeW/5)+2,riverY-2,bridgeW/5-4,riverH+4);}
g.fillStyle='rgba(251,191,36,0.04)';g.fillRect(W*0.2,0,W*0.3,H);g.fillRect(W*0.5,0,W*0.3,H);
g.strokeStyle='rgba(251,191,36,0.12)';g.lineWidth=2;g.setLineDash([8,8]);
g.beginPath();g.moveTo(W*0.35,0);g.lineTo(W*0.35,H);g.moveTo(W*0.65,0);g.lineTo(W*0.65,H);g.stroke();g.setLineDash([]);
drawTree(g,W*0.08,H*0.15,0.7);drawTree(g,W*0.92,H*0.12,0.6);drawTree(g,W*0.05,H*0.75,0.8);drawTree(g,W*0.95,H*0.78,0.7);
drawRock(g,W*0.15,H*0.35,12);drawRock(g,W*0.85,H*0.38,10);drawRock(g,W*0.12,H*0.6,8);drawRock(g,W*0.88,H*0.62,9);
drawTorch(g,W*0.22,H*0.25);drawTorch(g,W*0.78,H*0.25);drawTorch(g,W*0.22,H*0.75);drawTorch(g,W*0.78,H*0.75);
if(selectedCardIndex>=0){
g.fillStyle='rgba(34,197,94,0.06)';g.fillRect(0,H*0.55,W,H*0.35);
g.strokeStyle='rgba(34,197,94,0.25)';g.lineWidth=2;g.setLineDash([6,6]);
g.strokeRect(4,H*0.55+4,W-8,H*0.35-8);g.setLineDash([]);
}
}
function drawTree(g,x,y,scale){
g.save();g.translate(x,y);g.scale(scale,scale);
g.fillStyle='#3f2e18';g.fillRect(-4,0,8,25);
g.fillStyle='#166534';g.beginPath();g.arc(0,-10,18,0,Math.PI*2);g.fill();
g.fillStyle='#15803d';g.beginPath();g.arc(-6,-14,12,0,Math.PI*2);g.fill();
g.fillStyle='#14532d';g.beginPath();g.arc(6,-12,10,0,Math.PI*2);g.fill();
g.restore();
}
function drawRock(g,x,y,r){
g.save();g.translate(x,y);
g.fillStyle='#4b5563';g.beginPath();g.ellipse(0,0,r,r*0.7,0,0,Math.PI*2);g.fill();
g.fillStyle='#6b7280';g.beginPath();g.ellipse(-r*0.3,-r*0.2,r*0.5,r*0.4,-0.3,0,Math.PI*2);g.fill();
g.restore();
}
function drawTorch(g,x,y){
g.save();g.translate(x,y);
g.fillStyle='#57534e';g.fillRect(-2,0,4,20);
const flicker=Math.sin(Date.now()*0.01)*2;
g.fillStyle='#fbbf24';
g.beginPath();g.moveTo(0,-5);g.quadraticCurveTo(-6+flicker,-15,0,-25+flicker);g.quadraticCurveTo(6+flicker,-15,0,-5);g.fill();
g.fillStyle='#ef4444';
g.beginPath();g.moveTo(0,-5);g.quadraticCurveTo(-3+flicker,-12,0,-18+flicker);g.quadraticCurveTo(3+flicker,-12,0,-5);g.fill();
g.fillStyle='rgba(251,191,36,0.15)';g.beginPath();g.arc(0,-15,20+flicker,0,Math.PI*2);g.fill();
g.restore();
}
function initTowers(){
const th=50,tw=40;
towers={
pLeft:{x:W*0.25,y:H-70,hp:1200,maxHp:1200,w:tw,h:th,team:'player',type:'side',alive:true,dmg:40,range:120,atkSpeed:0.8,atkTimer:0},
pCenter:{x:W*0.5,y:H-60,hp:2400,maxHp:2400,w:tw+10,h:th+15,team:'player',type:'main',alive:true,dmg:60,range:140,atkSpeed:0.7,atkTimer:0},
pRight:{x:W*0.75,y:H-70,hp:1200,maxHp:1200,w:tw,h:th,team:'player',type:'side',alive:true,dmg:40,range:120,atkSpeed:0.8,atkTimer:0},
eLeft:{x:W*0.25,y:70,hp:1200,maxHp:1200,w:tw,h:th,team:'enemy',type:'side',alive:true,dmg:40,range:120,atkSpeed:0.8,atkTimer:0},
eCenter:{x:W*0.5,y:60,hp:2400,maxHp:2400,w:tw+10,h:th+15,team:'enemy',type:'main',alive:true,dmg:60,range:140,atkSpeed:0.7,atkTimer:0},
eRight:{x:W*0.75,y:70,hp:1200,maxHp:1200,w:tw,h:th,team:'enemy',type:'side',alive:true,dmg:40,range:120,atkSpeed:0.8,atkTimer:0}
};
}
function drawTowers(){
for(const key in towers){
const t=towers[key];
if(!t.alive)continue;
drawTower(ctx,t);
}
}
function drawTower(g,t){
g.save();g.translate(t.x,t.y);
g.fillStyle='rgba(0,0,0,0.3)';
g.beginPath();g.ellipse(0,t.h/2+5,t.w/2+5,8,0,0,Math.PI*2);g.fill();
const isMain=t.type==='main';
const color=t.team==='player'?'#1e40af':'#991b1b';
const lightColor=t.team==='player'?'#3b82f6':'#ef4444';
g.fillStyle=color;g.fillRect(-t.w/2,-t.h/2,t.w,t.h);
g.fillStyle=lightColor;
g.beginPath();g.moveTo(-t.w/2-5,-t.h/2);g.lineTo(0,-t.h/2-(isMain?20:12));g.lineTo(t.w/2+5,-t.h/2);g.closePath();g.fill();
g.fillStyle='rgba(255,255,255,0.15)';g.fillRect(-t.w/2+4,-t.h/2+4,t.w-8,t.h-8);
const hpPct=t.hp/t.maxHp;
const barW=t.w+10,barH=5;
g.fillStyle='#1f2937';g.fillRect(-barW/2,-t.h/2-18,barW,barH);
g.fillStyle=hpPct>0.5?'#22c55e':hpPct>0.25?'#fbbf24':'#ef4444';
g.fillRect(-barW/2,-t.h/2-18,barW*hpPct,barH);
if(isMain){g.fillStyle='#fbbf24';g.font='14px serif';g.textAlign='center';g.fillText('👑',0,-t.h/2-22);}
g.restore();
}
function spawnUnit(charId,x,y,team){
const char=CHARACTERS[charId];
const base={
id:charId,team:team,x:x,y:y,
hp:char.hp,maxHp:char.hp,dmg:char.dmg,speed:char.speed,
range:char.range,atkSpeed:char.atkSpeed,atkTimer:0,
state:'walk',stateTimer:0,target:null,
stealthTimer:char.ability==='stealth'?2:0,
abilityTimer:0,attackCount:0,frozenTimer:0,slowTimer:0,
chargeReady:char.ability==='charge',chargeTimer:0,healZoneTimer:0,
voidBreathTimer:0,scale:1,opacity:char.ability==='stealth'?0.3:1,
spawnAnim:0.5,facing:team==='player'?-1:1,
emoji:char.emoji,name:char.name,type:char.type,ability:char.ability,targetType:char.target,
berserkActive:false
};
if(char.ability==='swarm'){
for(let i=0;i<3;i++){
const u=Object.assign({},base);
u.x=x+(Math.random()-0.5)*50;
u.y=y+(Math.random()-0.5)*40;
u.hp=char.hp;u.maxHp=char.hp;
u.emoji='👺';
units.push(u);
}
playSound('spawn');vibrate(30);
spawnParticles(x,y,team==='player'?'#3b82f6':'#ef4444',8);
}else{
units.push(base);
playSound('spawn');vibrate(char.cost>=6?50:30);
const pColor=char.ability==='split'?'#78716c':char.ability==='stealth'?'#a855f7':team==='player'?'#3b82f6':'#ef4444';
spawnParticles(x,y,pColor,10);
if(char.ability==='void_breath'){cameraShake=8;slowMo=0.3;setTimeout(()=>slowMo=0,400);}
}
}
function updateUnits(dt){
for(let i=units.length-1;i>=0;i--){
const u=units[i];
if(u.spawnAnim>0){u.spawnAnim-=dt;continue;}
if(u.frozenTimer>0){u.frozenTimer-=dt;continue;}
let spd=u.speed*40;
if(u.slowTimer>0){spd*=0.5;u.slowTimer-=dt;}
if(u.stealthTimer>0){
u.stealthTimer-=dt;
u.opacity=0.3+Math.sin(Date.now()*0.01)*0.1;
if(u.stealthTimer<=0)u.opacity=1;
}
if(u.ability==='berserk'&&!u.berserkActive&&u.hp/u.maxHp<0.4){
u.berserkActive=true;
u.dmg=Math.floor(u.dmg*1.5);
spawnParticles(u.x,u.y,'#ef4444',15);
}
if(u.ability==='charge'&&u.chargeReady){
u.chargeTimer+=dt;
if(u.chargeTimer>2){
u.chargeReady=false;u.chargeTimer=0;
spd*=4;cameraShake=4;
spawnParticles(u.x,u.y,'#fbbf24',12);
setTimeout(()=>{u.chargeReady=true;u.chargeTimer=0;},6);
}
}
if(!u.target||!u.target.alive||(u.target.hp!==undefined&&u.target.hp<=0)){
u.target=findTarget(u);
}
if(u.target){
const dx=u.target.x-u.x;
const dy=u.target.y-u.y;
const dist=Math.sqrt(dx*dx+dy*dy);
if(dist>u.range){
u.x+=(dx/dist)*spd*dt;
u.y+=(dy/dist)*spd*dt;
u.state='walk';
u.facing=dx>0?1:-1;
}else{
u.state='attack';
u.atkTimer+=dt;
if(u.atkTimer>=u.atkSpeed){
u.atkTimer=0;
performAttack(u);
}
}
}else{
const targetY=u.team==='player'?60:H-60;
const targetX=W*0.5;
const dx=targetX-u.x;
const dy=targetY-u.y;
const dist=Math.sqrt(dx*dx+dy*dy);
if(dist>5){
u.x+=(dx/dist)*spd*dt;
u.y+=(dy/dist)*spd*dt;
u.state='walk';
}
}
if(u.ability==='heal_zone'){
u.healZoneTimer+=dt;
if(u.healZoneTimer>3){
u.healZoneTimer=0;
for(const ally of units){
if(ally.team===u.team&&ally!==u){
const d=Math.hypot(ally.x-u.x,ally.y-u.y);
if(d<100){ally.hp=Math.min(ally.maxHp,ally.hp+40);spawnParticles(ally.x,ally.y,'#fbbf24',3);}
}
}
}
}
if(u.ability==='void_breath'){
u.voidBreathTimer+=dt;
if(u.voidBreathTimer>4){
u.voidBreathTimer=0;
cameraShake=6;slowMo=0.2;
setTimeout(()=>slowMo=0,500);
for(const enemy of units){
if(enemy.team!==u.team){
const d=Math.hypot(enemy.x-u.x,enemy.y-u.y);
if(d<180){
enemy.hp-=u.dmg*2;
showDamage(enemy.x,enemy.y,u.dmg*2,'#a855f7');
enemy.frozenTimer=0.5;
spawnParticles(enemy.x,enemy.y,'#a855f7',8);
}
}
}
playSound('attack');
}
}
if(u.hp<=0){
handleUnitDeath(u,i);
}
}
}
function findTarget(u){
let best=null,bestScore=Infinity;
if(u.targetType==='building'){
for(const key in towers){
const t=towers[key];
if(!t.alive||t.team===u.team)continue;
const d=Math.hypot(t.x-u.x,t.y-u.y);
if(d<bestScore){bestScore=d;best=t;}
}
if(best)return best;
}
for(const other of units){
if(other.team===u.team||other===u||other.hp<=0)continue;
const d=Math.hypot(other.x-u.x,other.y-u.y);
let score=d;
if(u.targetType==='lowhp')score=other.hp;
else if(u.targetType==='highest')score=-other.dmg;
else if(u.targetType==='nearest')score=d;
if(score<bestScore){bestScore=score;best=other;}
}
if(!best&&u.targetType!=='building'){
for(const key in towers){
const t=towers[key];
if(!t.alive||t.team===u.team)continue;
const d=Math.hypot(t.x-u.x,t.y-u.y);
if(d<bestScore){bestScore=d;best=t;}
}
}
return best;
}
function performAttack(u){
if(!u.target||u.target.hp<=0)return;
u.attackCount++;
let dmg=u.dmg;
if(u.ability==='chain'&&u.attackCount%3===0){
let chained=0;
for(const other of units){
if(other.team!==u.team&&other!==u.target&&other.hp>0){
const d=Math.hypot(other.x-u.target.x,other.y-u.target.y);
if(d<100&&chained<2){
other.hp-=Math.floor(dmg*0.6);
showDamage(other.x,other.y,Math.floor(dmg*0.6),'#3b82f6');
spawnParticles(other.x,other.y,'#60a5fa',6);
projectiles.push({x1:u.target.x,y1:u.target.y,x2:other.x,y2:other.y,life:0.3,color:'#60a5fa'});
chained++;
}
}
}
spawnParticles(u.x,u.y,'#fbbf24',8);
}
if(u.ability==='heal_arrow'&&u.attackCount%5===0){
let lowest=null,lowestHp=Infinity;
for(const ally of units){
if(ally.team===u.team&&ally!==u&&ally.hp>0&&ally.hp<lowestHp){
lowestHp=ally.hp;lowest=ally;
}
}
if(lowest){
lowest.hp=Math.min(lowest.maxHp,lowest.hp+30);
showDamage(lowest.x,lowest.y,30,'#22c55e');
spawnParticles(lowest.x,lowest.y,'#22c55e',5);
}
}
if(u.ability==='freeze'&&u.target.hp>0){
u.target.slowTimer=2;
if(Math.random()<0.25){
u.target.frozenTimer=1.5;
spawnParticles(u.target.x,u.target.y,'#93c5fd',10);
}
}
if(u.target.hp!==undefined){
u.target.hp-=dmg;
showDamage(u.target.x,u.target.y,dmg,u.team==='player'?'#fbbf24':'#ef4444');
spawnParticles(u.target.x,u.target.y,u.team==='player'?'#fbbf24':'#ef4444',4);
if(u.team==='player'){
comboCount++;comboTimer=2;
if(comboCount>=3){
showFloatingText(u.target.x,u.target.y-30,i18n[currentLang].combo,'combo-text');
playSound('combo');comboCount=0;
}
}
if(u.target.hp<=0&&u.target.alive!==undefined){
u.target.alive=false;
handleTowerDestroyed(u.target);
}
}
playSound('attack');
if(u.type==='ranged'||u.type==='flying'){
projectiles.push({
x:u.x,y:u.y-10,
tx:u.target.x,ty:u.target.y-10,
speed:400,color:u.ability==='freeze'?'#93c5fd':u.ability==='chain'?'#fbbf24':'#fff',
life:1
});
}
}
function handleUnitDeath(u,index){
playSound('death');
spawnParticles(u.x,u.y,'#78716c',8);
if(u.ability==='split'){
for(let i=0;i<2;i++){
const shard=Object.assign({},u);
shard.hp=150;shard.maxHp=150;shard.dmg=25;
shard.speed=1.0;shard.x+=(Math.random()-0.5)*30;
shard.y+=(Math.random()-0.5)*30;shard.ability='none';
shard.emoji='🪨';shard.scale=0.6;
units.push(shard);
}
cameraShake=5;
spawnParticles(u.x,u.y,'#78716c',20);
}
units.splice(index,1);
}
function handleTowerDestroyed(tower){
playSound('tower_destroy');
cameraShake=10;
vibrate([50,30,50]);
spawnParticles(tower.x,tower.y,'#fbbf24',30);
spawnParticles(tower.x,tower.y,'#ef4444',20);
showFloatingText(tower.x,tower.y-40,i18n[currentLang].towerDestroyed,'combo-text');
if(tower.team==='enemy')enemyTowersDestroyed++;
else playerTowersDestroyed++;
updateTowerUI();
if(tower.type==='main'){
endBattle(tower.team==='enemy'?'victory':'defeat');
}else if(enemyTowersDestroyed>=3){
endBattle('victory');
}else if(playerTowersDestroyed>=3){
endBattle('defeat');
}
}
function updateTowerUI(){
for(const key in towers){
const t=towers[key];
if(!t.alive)continue;
const el=document.getElementById((t.team==='enemy'?'enemy':'player')+'-tower-'+(key.includes('Left')?'l':key.includes('Right')?'r':'c'));
if(el)el.style.width=(t.hp/t.maxHp*100)+'%';
}
}
function drawUnits(){
const sorted=[...units].sort((a,b)=>a.y-b.y);
for(const u of sorted){
if(u.hp<=0)continue;
drawUnit(ctx,u);
}
}
function drawUnit(g,u){
g.save();
g.globalAlpha=u.opacity;
g.translate(u.x,u.y);
const scale=u.scale*(u.spawnAnim>0?0.5+(0.5-u.spawnAnim):1);
g.scale(scale,scale);
g.fillStyle='rgba(0,0,0,0.3)';
g.beginPath();g.ellipse(0,14,14,5,0,0,Math.PI*2);g.fill();
if(selectedCardIndex>=0&&hand[selectedCardIndex]&&hand[selectedCardIndex].id===u.id&&u.team==='player'){
g.strokeStyle='rgba(251,191,36,0.5)';g.lineWidth=2;
g.beginPath();g.arc(0,0,22,0,Math.PI*2);g.stroke();
}
if(u.frozenTimer>0){
g.fillStyle='rgba(147,197,253,0.4)';
g.beginPath();g.arc(0,0,18,0,Math.PI*2);g.fill();
}
if(u.berserkActive){
g.fillStyle='rgba(239,68,68,0.2)';
g.beginPath();g.arc(0,0,20,0,Math.PI*2);g.fill();
}
g.font='28px serif';g.textAlign='center';g.textBaseline='middle';
if(u.facing<0){g.scale(-1,1);g.fillText(u.emoji,0,-2);g.scale(-1,1);}
else g.fillText(u.emoji,0,-2);
const hpPct=Math.max(0,u.hp/u.maxHp);
const barW=30,barH=4;
g.fillStyle='#1f2937';g.fillRect(-barW/2,-22,barW,barH);
g.fillStyle=hpPct>0.5?'#22c55e':hpPct>0.25?'#fbbf24':'#ef4444';
g.fillRect(-barW/2,-22,barW*hpPct,barH);
if(u.ability==='berserk'&&u.berserkActive){
g.fillStyle='#ef4444';
g.beginPath();g.arc((Math.random()-0.5)*20,(Math.random()-0.5)*20,2,0,Math.PI*2);g.fill();
}
if(u.ability==='freeze'){
g.fillStyle='#93c5fd';
g.beginPath();g.arc((Math.random()-0.5)*16,(Math.random()-0.5)*16,1.5,0,Math.PI*2);g.fill();
}
g.restore();
}
function spawnParticles(x,y,color,count){
for(let i=0;i<count;i++){
particles.push({
x:x,y:y,
vx:(Math.random()-0.5)*120,
vy:(Math.random()-0.5)*120-30,
life:0.5+Math.random()*0.5,
maxLife:0.5+Math.random()*0.5,
size:2+Math.random()*4,
color:color,
gravity:80
});
}
}
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.x+=p.vx*dt;
p.y+=p.vy*dt;
p.vy+=p.gravity*dt;
p.life-=dt;
if(p.life<=0)particles.splice(i,1);
}
}
function drawParticles(g){
for(const p of particles){
const alpha=p.life/p.maxLife;
g.globalAlpha=alpha;
g.fillStyle=p.color;
g.beginPath();
g.arc(p.x,p.y,p.size*alpha,0,Math.PI*2);
g.fill();
}
g.globalAlpha=1;
}
function updateProjectiles(dt){
for(let i=projectiles.length-1;i>=0;i--){
const p=projectiles[i];
if(p.life!==undefined&&p.life<=1){
const dx=p.tx-p.x,dy=p.ty-p.y;
const dist=Math.hypot(dx,dy);
if(dist<10||p.life<=0){projectiles.splice(i,1);continue;}
p.x+=(dx/dist)*p.speed*dt;
p.y+=(dy/dist)*p.speed*dt;
p.life-=dt;
}else{
p.life-=dt;
if(p.life<=0)projectiles.splice(i,1);
}
}
}
function drawProjectiles(g){
for(const p of projectiles){
if(p.life>1)continue;
g.strokeStyle=p.color||'#fff';
g.lineWidth=2;
g.globalAlpha=0.8;
g.beginPath();
g.moveTo(p.x,p.y);
const dx=p.tx-p.x,dy=p.ty-p.y;
const dist=Math.hypot(dx,dy);
g.lineTo(p.x-(dx/dist)*10,p.y-(dy/dist)*10);
g.stroke();
g.globalAlpha=1;
}
for(const p of projectiles){
if(p.life<=1)continue;
g.strokeStyle=p.color;
g.lineWidth=2+Math.random()*2;
g.globalAlpha=p.life*2;
g.beginPath();
let lx=p.x1,ly=p.y1;
g.moveTo(lx,ly);
const steps=6;
for(let s=1;s<=steps;s++){
const t=s/steps;
const tx=p.x1+(p.x2-p.x1)*t;
const ty=p.y1+(p.y2-p.y1)*t;
lx=tx+(Math.random()-0.5)*15;
ly=ty+(Math.random()-0.5)*15;
g.lineTo(lx,ly);
}
g.stroke();
}
g.globalAlpha=1;
}
function updateTowers(dt){
for(const key in towers){
const t=towers[key];
if(!t.alive)continue;
t.atkTimer+=dt;
if(t.atkTimer>=t.atkSpeed){
let nearest=null,nearestDist=t.range;
for(const u of units){
if(u.team!==t.team&&u.hp>0){
const d=Math.hypot(u.x-t.x,u.y-t.y);
if(d<nearestDist){nearestDist=d;nearest=u;}
}
}
if(nearest){
t.atkTimer=0;
nearest.hp-=t.dmg;
showDamage(nearest.x,nearest.y,t.dmg,t.team==='player'?'#fbbf24':'#ef4444');
projectiles.push({x:t.x,y:t.y-15,tx:nearest.x,ty:nearest.y-10,speed:500,color:'#fbbf24',life:1});
spawnParticles(nearest.x,nearest.y,'#fbbf24',4);
}
}
}
}
function showDamage(x,y,amount,color){
const el=document.createElement('div');
el.className='damage-number';
el.textContent=Math.floor(amount);
el.style.left=x+'px';
el.style.top=y+'px';
el.style.color=color;
document.getElementById('game-container').appendChild(el);
setTimeout(()=>el.remove(),1000);
}
function showFloatingText(x,y,text,className){
const el=document.createElement('div');
el.className=className||'combo-text';
el.textContent=text;
el.style.left=x+'px';
el.style.top=y+'px';
document.getElementById('game-container').appendChild(el);
setTimeout(()=>el.remove(),1200);
}
function updateAI(dt){
aiTimer+=dt;
if(aiTimer<1.5)return;
aiTimer=0;
const aiEnergy=Math.min(maxEnergy,energy+2);
const available=deck.filter(id=>CHARACTERS[id].cost<=aiEnergy);
if(available.length===0)return;
const charId=available[Math.floor(Math.random()*available.length)];
const playerUnits=units.filter(u=>u.team==='player'&&u.hp>0);
let laneX=W*0.5;
if(playerUnits.length>0){
const threat=playerUnits.sort((a,b)=>a.y-b.y)[0];
laneX=threat.x+(Math.random()-0.5)*60;
}else{
laneX=W*(0.25+Math.random()*0.5);
}
const spawnY=H*0.15+Math.random()*40;
spawnUnit(charId,Math.max(40,Math.min(W-40,laneX)),spawnY,'enemy');
}
function initDeck(){
deck=[0,1,2,3,4,5,6,7];
shuffleDeck();
hand=[];
for(let i=0;i<4;i++)hand.push(drawCard());
nextCard=drawCard();
updateHandUI();
}
function shuffleDeck(){
for(let i=deck.length-1;i>0;i--){
const j=Math.floor(Math.random()*(i+1));
[deck[i],deck[j]]=[deck[j],deck[i]];
}
}
function drawCard(){
if(deckIndex>=deck.length){deckIndex=0;shuffleDeck();}
return{id:deck[deckIndex++],level:1};
}
function updateHandUI(){
for(let i=0;i<4;i++){
const slot=document.getElementById('card-'+i);
const char=CHARACTERS[hand[i].id];
slot.querySelector('.card-emoji').textContent=char.emoji;
slot.querySelector('.card-name').textContent=getCharName(hand[i].id).split(' ').slice(0,2).join(' ');
slot.querySelector('.card-cost').textContent=char.cost;
slot.className='card-slot card-rarity-'+char.rarity;
if(char.cost>energy)slot.classList.add('disabled');
if(i===selectedCardIndex)slot.classList.add('selected');
}
if(nextCard){
document.getElementById('next-emoji').textContent=CHARACTERS[nextCard.id].emoji;
}
document.getElementById('energy-value').textContent=Math.floor(energy)+' / '+maxEnergy;
document.getElementById('energy-fill').style.width=(energy/maxEnergy*100)+'%';
}
function selectCard(index){
initAudio();
if(!hand[index])return;
const char=CHARACTERS[hand[index].id];
if(char.cost>energy){playSound('error');return;}
if(selectedCardIndex===index){
selectedCardIndex=-1;
}else{
selectedCardIndex=index;
playSound('select');
}
updateHandUI();
}
function handleTouchStart(e,index){
e.preventDefault();
if(!hand[index])return;
const char=CHARACTERS[hand[index].id];
if(char.cost>energy)return;
dragIndex=index;
const ghost=document.getElementById('drag-ghost');
ghost.textContent=char.emoji;
ghost.style.display='flex';
ghost.style.left=(e.touches[0].clientX-30)+'px';
ghost.style.top=(e.touches[0].clientY-30)+'px';
}
function handleTouchMove(e){
if(dragIndex<0)return;
e.preventDefault();
const ghost=document.getElementById('drag-ghost');
ghost.style.left=(e.touches[0].clientX-30)+'px';
ghost.style.top=(e.touches[0].clientY-30)+'px';
}
function handleTouchEnd(e){
if(dragIndex<0)return;
e.preventDefault();
const ghost=document.getElementById('drag-ghost');
ghost.style.display='none';
const touch=e.changedTouches[0];
const rect=canvas.getBoundingClientRect();
const x=touch.clientX-rect.left;
const y=touch.clientY-rect.top;
if(y>H*0.55&&y<H-130&&x>0&&x<W){
spawnFromCard(dragIndex,x,y);
}
dragIndex=-1;
}
document.addEventListener('mousedown',(e)=>{
if(gameState!=='battle')return;
const rect=canvas.getBoundingClientRect();
const x=e.clientX-rect.left;
const y=e.clientY-rect.top;
if(y>H*0.55&&y<H-130&&x>0&&x<W&&selectedCardIndex>=0){
spawnFromCard(selectedCardIndex,x,y);
}
});
function spawnFromCard(index,x,y){
if(!hand[index])return;
const card=hand[index];
const char=CHARACTERS[card.id];
if(char.cost>energy){playSound('error');return;}
energy-=char.cost;
spawnUnit(card.id,x,y,'player');
hand[index]=nextCard;
nextCard=drawCard();
selectedCardIndex=-1;
updateHandUI();
}
function startBattle(){
gameState='battle';
gameOver=false;
units=[];
particles=[];
projectiles=[];
energy=3;
battleTime=180;
suddenDeath=false;
playerTowersDestroyed=0;
enemyTowersDestroyed=0;
comboCount=0;
aiTimer=0;
battleStartTime=Date.now();
initCanvas();
initTowers();
initDeck();
updateTowerUI();
document.getElementById('main-menu').style.display='none';
document.getElementById('battle-screen').style.display='flex';
document.getElementById('result-overlay').style.display='none';
document.getElementById('battle-timer').textContent='03:00';
document.getElementById('battle-timer').style.color='#fbbf24';
lastTime=performance.now();
requestAnimationFrame(gameLoop);
battleTimer=setInterval(()=>{
if(gameOver)return;
battleTime--;
const m=Math.floor(battleTime/60).toString().padStart(2,'0');
const s=(battleTime%60).toString().padStart(2,'0');
document.getElementById('battle-timer').textContent=m+':'+s;
if(battleTime<=0){
if(!suddenDeath){
suddenDeath=true;
battleTime=60;
energyRate=0.5;
showFloatingText(W/2,H/2,i18n[currentLang].suddenDeath,'combo-text');
playSound('sudden_death');
cameraShake=5;
}else{
if(enemyTowersDestroyed>playerTowersDestroyed)endBattle('victory');
else if(playerTowersDestroyed>enemyTowersDestroyed)endBattle('defeat');
else{battleTime=30;}
}
}
if(battleTime===30&&!suddenDeath){
document.getElementById('battle-timer').style.color='#ef4444';
}
},1000);
}
function gameLoop(now){
if(gameState!=='battle'||gameOver)return;
const rawDt=(now-lastTime)/1000;
lastTime=now;
const dt=Math.min(rawDt,0.05)*(slowMo>0?0.3:1);
if(slowMo>0)slowMo-=rawDt;
energy=Math.min(maxEnergy,energy+energyRate*rawDt);
if(comboTimer>0){
comboTimer-=rawDt;
if(comboTimer<=0)comboCount=0;
}
updateUnits(dt);
updateParticles(dt);
updateProjectiles(dt);
updateTowers(dt);
updateAI(dt);
updateHandUI();
updateTowerUI();
if(cameraShake>0)cameraShake*=0.9;
if(cameraShake<0.5)cameraShake=0;
ctx.save();
if(cameraShake>0){
ctx.translate((Math.random()-0.5)*cameraShake,(Math.random()-0.5)*cameraShake);
}
drawBattlefield();
drawTowers();
drawUnits();
drawParticles(ctx);
drawProjectiles(ctx);
ctx.restore();
requestAnimationFrame(gameLoop);
}
function endBattle(result){
gameOver=true;
gameState=result;
clearInterval(battleTimer);
const t=i18n[currentLang];
const overlay=document.getElementById('result-overlay');
const title=document.getElementById('result-title');
if(result==='victory'){
title.textContent=t.victory;
title.className='victory';
document.getElementById('res-trophies').textContent='+30';
document.getElementById('res-coins').textContent='+150';
playSound('victory');
vibrate([30,50,30,50,100]);
}else{
title.textContent=t.defeat;
title.className='defeat';
document.getElementById('res-trophies').textContent='-15';
document.getElementById('res-coins').textContent='+50';
playSound('defeat');
vibrate([100,50]);
}
const elapsed=Math.floor((Date.now()-battleStartTime)/1000);
const m=Math.floor(elapsed/60).toString().padStart(2,'0');
const s=(elapsed%60).toString().padStart(2,'0');
document.getElementById('res-time').textContent=m+':'+s;
setTimeout(()=>{overlay.style.display='flex';},500);
}
function startMatchmaking(){
initAudio();
const t=i18n[currentLang];
document.getElementById('transition-text').innerHTML=t.searching+'<span class="loading-dots"></span>';
document.getElementById('transition-overlay').style.display='flex';
setTimeout(()=>{
document.getElementById('transition-overlay').style.display='none';
startBattle();
},2000);
}
function playAgain(){
document.getElementById('result-overlay').style.display='none';
startBattle();
}
function goHome(){
gameState='menu';
gameOver=false;
document.getElementById('battle-screen').style.display='none';
document.getElementById('result-overlay').style.display='none';
document.getElementById('main-menu').style.display='flex';
clearInterval(battleTimer);
}
function openSettings(){
initAudio();
document.getElementById('settings-modal').style.display='flex';
}
function closeSettings(){
document.getElementById('settings-modal').style.display='none';
}
function toggleSetting(key){
settings[key]=!settings[key];
document.getElementById('toggle-'+key).classList.toggle('on',settings[key]);
}
function openCollection(){
initAudio();
document.getElementById('main-menu').style.display='none';
document.getElementById('collection-screen').style.display='flex';
renderCollection();
}
function closeCollection(){
document.getElementById('collection-screen').style.display='none';
document.getElementById('main-menu').style.display='flex';
}
function renderCollection(){
const grid=document.getElementById('collection-grid');
grid.innerHTML='';
for(const char of CHARACTERS){
const card=document.createElement('div');
card.className='collection-card';
card.style.borderColor=RARITY_COLORS[char.rarity];
card.innerHTML='<div class="coll-emoji">'+char.emoji+'</div><div class="coll-name">'+getCharName(char.id)+'</div><div class="coll-rarity" style="color:'+RARITY_COLORS[char.rarity]+'">'+getRarityName(char.rarity)+'</div><div class="coll-level">⭐ Lv.1</div>';
grid.appendChild(card);
}
}
window.onload=function(){
applyLanguage();
};
</script>
</body>
</html>
Game Source: Battle Realms – ממלכת הקרב
Creator: FrozenOtter31
Libraries: none
Complexity: complex (1200 lines, 50.6 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: battle-realms-frozenotter31" to link back to the original. Then publish at arcadelab.ai/publish.