// Pulau Petualangan Nusantara
// Game edukasi HTML5, CSS3, dan Vanilla JavaScript untuk siswa SD kelas 5.

const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));

const screens = {
  loading: $("#loadingScreen"),
  menu: $("#menuScreen"),
  character: $("#characterScreen"),
  world: $("#worldScreen"),
  inventory: $("#inventoryScreen"),
  achievement: $("#achievementScreen"),
  map: $("#mapScreen"),
  gameOver: $("#gameOverScreen"),
  victory: $("#victoryScreen")
};

const ui = {
  loadingProgress: $("#loadingProgress"),
  menuHighScore: $("#menuHighScore"),
  playerLevel: $("#playerLevel"),
  playerXP: $("#playerXP"),
  coinCount: $("#coinCount"),
  gemCount: $("#gemCount"),
  activeAreaName: $("#activeAreaName"),
  activeQuestTitle: $("#activeQuestTitle"),
  activeQuestText: $("#activeQuestText"),
  heartContainer: $("#heartContainer"),
  objectLayer: $("#objectLayer"),
  particleLayer: $("#particleLayer"),
  worldMap: $("#worldMap"),
  worldViewport: $("#worldViewport"),
  player: $("#playerAvatar"),
  questOverlay: $("#questOverlay"),
  questAreaLabel: $("#questAreaLabel"),
  questTitle: $("#questTitle"),
  questDescription: $("#questDescription"),
  questRewards: $("#questRewards"),
  miniGameOverlay: $("#miniGameOverlay"),
  miniGameSubject: $("#miniGameSubject"),
  miniGameTitle: $("#miniGameTitle"),
  miniGameContent: $("#miniGameContent"),
  miniGameActions: $("#miniGameActions"),
  toastLayer: $("#toastLayer"),
  achievementList: $("#achievementList"),
  areaList: $("#areaList"),
  inventoryCoins: $("#inventoryCoins"),
  inventoryGems: $("#inventoryGems"),
  inventoryBooks: $("#inventoryBooks"),
  inventoryBadges: $("#inventoryBadges"),
  gameOverLevel: $("#gameOverLevel"),
  gameOverXP: $("#gameOverXP"),
  gameOverQuests: $("#gameOverQuests"),
  victoryLevel: $("#victoryLevel"),
  victoryXP: $("#victoryXP"),
  victoryBadge: $("#victoryBadge"),
  clickMarker: $("#clickMarker")
};

const buttons = {
  start: $("#startButton"),
  character: $("#characterButton"),
  inventory: $("#inventoryButton"),
  achievement: $("#achievementButton"),
  map: $("#mapButton"),
  backChar: $("#backToMenuFromChar"),
  backInventory: $("#backToMenuFromInventory"),
  backAchievement: $("#backToMenuFromAchievement"),
  backMap: $("#backToMenuFromMap"),
  pause: $("#pauseButton"),
  worldInventory: $("#worldInventoryButton"),
  worldAchievement: $("#worldAchievementButton"),
  worldMap: $("#worldMapButton"),
  acceptQuest: $("#acceptQuestButton"),
  declineQuest: $("#declineQuestButton"),
  retry: $("#retryGameButton"),
  gameOverMenu: $("#backToMenuFromGameOver"),
  victoryReplay: $("#victoryReplayButton"),
  victoryMenu: $("#victoryMenuButton")
};

const STORE_KEY = "ppn-high-score";
const WORLD = { width: 2400, height: 1600 };
const BASE_SPEED = 235;
const RUN_MULTIPLIER = 1.55;
const LEVEL_TARGET = 100;
const MAX_HEARTS = 3;

const areas = [
  { id: "village", name: "Desa Harmoni", x: 430, y: 1190, unlockLevel: 1, theme: "Kehidupan sehari-hari" },
  { id: "forest", name: "Hutan Pengetahuan", x: 1760, y: 410, unlockLevel: 1, theme: "IPA dan alam" },
  { id: "mountain", name: "Gunung Matematika", x: 430, y: 280, unlockLevel: 1, theme: "Hitung dan logika" },
  { id: "lake", name: "Danau Bahasa", x: 1220, y: 1190, unlockLevel: 2, theme: "Bahasa Indonesia" },
  { id: "beach", name: "Pantai Sains", x: 1990, y: 1180, unlockLevel: 2, theme: "Air, cahaya, dan energi" },
  { id: "city", name: "Kota Budaya", x: 1230, y: 660, unlockLevel: 3, theme: "IPS dan budaya daerah" }
];

const quests = [
  { id: "mango", area: "village", x: 430, y: 1190, icon: "🥭", npc: "Ibu Lani", title: "Carikan aku 5 buah mangga", description: "Ibu Lani membutuhkan 5 mangga untuk pasar Desa Harmoni. Ambil 5 koin mangga yang tersebar di sekitar desa.", type: "collect", target: "mango", amount: 5, rewards: { coins: 30, xp: 40, badge: "Sahabat Desa" } },
  { id: "market", area: "village", x: 690, y: 1210, icon: "🧺", npc: "Bima", title: "Belanja Pasar Pintar", description: "Bantu Bima menghitung total belanja sederhana di pasar desa.", type: "money", rewards: { coins: 35, xp: 45, badge: "Kasir Cilik" } },
  { id: "bridge", area: "mountain", x: 430, y: 280, icon: "🧱", npc: "Pak Wira", title: "Bangun Jembatan Balok", description: "Hitung jumlah balok yang diperlukan agar jembatan Gunung Matematika bisa dilewati.", type: "math", rewards: { gems: 1, xp: 45, badge: "Arsitek Cilik" } },
  { id: "fraction", area: "mountain", x: 645, y: 480, icon: "🍕", npc: "Tara", title: "Bagi Bekal Pendaki", description: "Pilih pecahan yang sesuai supaya bekal dibagi adil.", type: "fraction", rewards: { coins: 30, xp: 45, badge: "Jago Pecahan" } },
  { id: "plant", area: "forest", x: 1760, y: 410, icon: "🌱", npc: "Kak Rimba", title: "Tanam Tanaman Sehat", description: "Bantu tanaman tumbuh dengan memilih kebutuhan fotosintesis yang benar.", type: "science", rewards: { books: 1, xp: 45, badge: "Penjaga Hutan" } },
  { id: "foodchain", area: "forest", x: 1530, y: 595, icon: "☀️", npc: "Maya", title: "Rantai Makanan Hutan", description: "Susun urutan energi pada rantai makanan sederhana.", type: "ecosystem", rewards: { books: 1, coins: 20, xp: 50, badge: "Ahli Ekosistem" } },
  { id: "word", area: "lake", x: 1220, y: 1190, icon: "🔤", npc: "Nara", title: "Susun Kata di Danau Bahasa", description: "Seret huruf ke kotak jawaban untuk membentuk kata Bahasa Indonesia yang benar.", type: "language", rewards: { books: 1, coins: 25, xp: 50, badge: "Ahli Kata" } },
  { id: "sentence", area: "lake", x: 1050, y: 1015, icon: "✍️", npc: "Seno", title: "Rapikan Kalimat", description: "Pilih susunan kalimat yang paling benar dan mudah dipahami.", type: "sentence", rewards: { books: 1, xp: 45, badge: "Penyusun Kalimat" } },
  { id: "science", area: "beach", x: 1990, y: 1180, icon: "🪐", npc: "Dino", title: "Bantu Menemukan Planet", description: "Jawab tantangan sains tentang tata surya di Pantai Sains.", type: "space", rewards: { gems: 1, xp: 50, badge: "Penjelajah Planet" } },
  { id: "water", area: "beach", x: 2155, y: 975, icon: "💧", npc: "Ayu", title: "Siklus Air Pantai", description: "Pilih proses siklus air yang benar berdasarkan petunjuk visual.", type: "watercycle", rewards: { gems: 1, coins: 20, xp: 50, badge: "Sahabat Air" } },
  { id: "culture", area: "city", x: 1230, y: 660, icon: "🎭", npc: "Bu Ratih", title: "Cocokkan Budaya Daerah", description: "Pasangkan budaya daerah dengan wilayah asalnya di Kota Budaya.", type: "culture", rewards: { gems: 2, xp: 55, badge: "Penjaga Budaya" } },
  { id: "mapculture", area: "city", x: 1425, y: 790, icon: "🏠", npc: "Reno", title: "Rumah Adat Nusantara", description: "Kenali rumah adat dan daerah asalnya.", type: "houseculture", rewards: { gems: 1, books: 1, xp: 55, badge: "Arsitek Budaya" } },
  { id: "adaptation", area: "forest", x: 1970, y: 350, icon: "🌵", npc: "Kiki", title: "Adaptasi Makhluk Hidup", description: "Bantu Kiki mengelompokkan adaptasi hewan dan tumbuhan terhadap lingkungannya.", type: "adaptation", rewards: { coins: 30, xp: 50, badge: "Pecinta Ekologi" } },
  { id: "geometry", area: "mountain", x: 655, y: 235, icon: "📐", npc: "Susi", title: "Taman Geometri Susi", description: "Hitung luas dan keliling bangun datar untuk membuat taman bunga.", type: "geometry", rewards: { gems: 1, xp: 50, badge: "Master Geometri" } },
  { id: "energy", area: "beach", x: 2215, y: 1330, icon: "🔌", npc: "Rian", title: "Eksperimen Energi Rian", description: "Bantu Rian mengidentifikasi perubahan bentuk energi di sekitar pantai.", type: "energy", rewards: { coins: 30, xp: 50, badge: "Penyelamat Energi" } },
  { id: "civics", area: "village", x: 260, y: 1035, icon: "🇮🇩", npc: "Pak RT", title: "Musyawarah Warga Desa", description: "Jawab pertanyaan seputar penerapan nilai-nilai Pancasila dalam kehidupan sehari-hari.", type: "civics", rewards: { coins: 40, xp: 45, badge: "Warga Teladan" } },
  { id: "history", area: "city", x: 1070, y: 585, icon: "📜", npc: "Opa Hasan", title: "Museum Kemerdekaan", description: "Jelajahi sejarah perjuangan bangsa Indonesia dan jawab tantangan Opa Hasan.", type: "history", rewards: { gems: 1, books: 1, xp: 50, badge: "Sejarawan Muda" } },
  { id: "proverb", area: "lake", x: 1370, y: 1075, icon: "🗣️", npc: "Dewi", title: "Teka-teki Peribahasa", description: "Uji pemahamanmu tentang peribahasa bahasa Indonesia di tepi danau.", type: "proverb", rewards: { books: 1, xp: 45, badge: "Pujangga Danau" } },
  { id: "scale", area: "mountain", x: 280, y: 410, icon: "🗺️", npc: "Budi", title: "Peta Harta Karun Budi", description: "Hitung jarak sebenarnya menggunakan skala peta yang digambar Budi.", type: "scale", rewards: { coins: 25, xp: 50, badge: "Kartografer Cilik" } },
  { id: "synonym", area: "lake", x: 1390, y: 1295, icon: "📖", npc: "Lia", title: "Padanan Kata Danau", description: "Cari persamaan kata (sinonim) atau lawan kata (antonim) dengan tepat.", type: "synonym", rewards: { books: 1, coins: 20, xp: 45, badge: "Kamus Berjalan" } }
];

const achievements = [
  { id: "firstQuest", title: "Quest Pertama", text: "Selesaikan misi pertama.", unlocked: false },
  { id: "collector", title: "Kolektor Pulau", text: "Kumpulkan 40 koin.", unlocked: false },
  { id: "bookworm", title: "Pembaca Cerdas", text: "Dapatkan 2 buku pengetahuan.", unlocked: false },
  { id: "levelUp", title: "Naik Level", text: "Capai level 2.", unlocked: false },
  { id: "halfway", title: "Separuh Pulau", text: "Selesaikan 6 quest.", unlocked: false },
  { id: "culture", title: "Cinta Nusantara", text: "Selesaikan misi budaya.", unlocked: false },
  { id: "victory", title: "Petualang Cendekia", text: "Selesaikan semua wilayah.", unlocked: false }
];

const miniGames = {
  math: [
    { prompt: "Jembatan punya 4 ruas. Tiap ruas perlu 3 balok. Berapa balok seluruhnya?", answer: "12", options: ["7", "12", "16"], blocks: 12, visual: "🧱🧱🧱 × 4" },
    { prompt: "Sudah ada 8 balok. Jembatan perlu 15 balok. Berapa balok tambahan?", answer: "7", options: ["6", "7", "8"], blocks: 7, visual: "15 - 8 = ?" },
    { prompt: "2 pekerja membawa masing-masing 6 balok. Total baloknya?", answer: "12", options: ["10", "12", "14"], blocks: 12, visual: "👷 6 + 👷 6" }
  ],
  money: [
    { prompt: "Satu mangga Rp2.000. Raka membeli 3 mangga. Total harganya?", answer: "Rp6.000", options: ["Rp5.000", "Rp6.000", "Rp8.000"], visual: "🥭🥭🥭" },
    { prompt: "Sari punya Rp10.000. Ia membeli buku Rp7.000. Sisa uangnya?", answer: "Rp3.000", options: ["Rp2.000", "Rp3.000", "Rp4.000"], visual: "💵 10.000 - 📘 7.000" },
    { prompt: "Bayu membeli 2 pensil, tiap pensil Rp1.500. Total bayar?", answer: "Rp3.000", options: ["Rp2.500", "Rp3.000", "Rp4.000"], visual: "✏️ + ✏️" }
  ],
  fraction: [
    { prompt: "Satu pizza dibagi 4 bagian sama besar. Dua bagian dimakan. Pecahan yang dimakan adalah...", answer: "2/4", options: ["1/4", "2/4", "4/2"], visual: "🍕🍕🍽️🍽️" },
    { prompt: "Dari 8 kue, 4 kue dibagikan. Pecahannya adalah...", answer: "4/8", options: ["4/8", "8/4", "3/8"], visual: "🍪🍪🍪🍪🍽️🍽️🍽️🍽️" },
    { prompt: "Setengah sama artinya dengan...", answer: "1/2", options: ["1/2", "1/3", "2/1"], visual: "🌗" }
  ],
  science: [
    { prompt: "Agar tanaman membuat makanan sendiri, kombinasi yang paling tepat adalah...", answer: "Air, cahaya matahari, dan karbon dioksida", options: ["Air, cahaya matahari, dan karbon dioksida", "Pasir, minyak, dan garam", "Batu, asap, dan es"], visual: "💧 + ☀️ + 🌬️ = 🌱" },
    { prompt: "Bagian tanaman yang paling banyak menangkap cahaya adalah...", answer: "Daun", options: ["Akar", "Daun", "Batang"], visual: "🌿" },
    { prompt: "Jika tanaman kurang air, yang mungkin terjadi adalah...", answer: "Layu", options: ["Layu", "Langsung berbuah", "Berubah menjadi batu"], visual: "🥀" }
  ],
  ecosystem: [
    { prompt: "Urutan rantai makanan yang benar adalah...", answer: "Rumput → belalang → katak", options: ["Rumput → belalang → katak", "Katak → rumput → belalang", "Belalang → katak → rumput"], visual: "🌿 → 🦗 → 🐸" },
    { prompt: "Sumber energi utama bagi tumbuhan adalah...", answer: "Matahari", options: ["Matahari", "Batu", "Bulan"], visual: "☀️" },
    { prompt: "Hewan pemakan tumbuhan disebut...", answer: "Herbivor", options: ["Herbivor", "Karnivor", "Omnivor"], visual: "🦌 + 🌿" }
  ],
  space: [
    { prompt: "Planet ketiga dari Matahari adalah...", answer: "Bumi", options: ["Mars", "Bumi", "Venus"], visual: "☀️ 1 2 🌍" },
    { prompt: "Benda langit yang menjadi pusat tata surya adalah...", answer: "Matahari", options: ["Matahari", "Bulan", "Komet"], visual: "☀️" },
    { prompt: "Planet merah adalah sebutan untuk...", answer: "Mars", options: ["Saturnus", "Mars", "Neptunus"], visual: "🔴 🪐" }
  ],
  watercycle: [
    { prompt: "Air laut menguap karena panas matahari. Proses ini disebut...", answer: "Evaporasi", options: ["Evaporasi", "Erosi", "Rotasi"], visual: "🌊 + ☀️ = ☁️" },
    { prompt: "Awan berubah menjadi hujan. Proses turunnya air disebut...", answer: "Presipitasi", options: ["Presipitasi", "Fotosintesis", "Gravitasi"], visual: "☁️ → 🌧️" },
    { prompt: "Air hujan masuk ke tanah dan menjadi cadangan air. Proses ini disebut...", answer: "Infiltrasi", options: ["Infiltrasi", "Migrasi", "Respirasi"], visual: "🌧️ → 🟫" }
  ],
  language: [
    { prompt: "Susun huruf menjadi kata benda yang ada di sekolah.", word: "BUKU", letters: ["U", "B", "K", "U"] },
    { prompt: "Susun huruf menjadi kata kegiatan yang baik.", word: "BELAJAR", letters: ["A", "B", "R", "E", "J", "L", "A"] },
    { prompt: "Susun huruf menjadi kata tentang alam.", word: "TANAMAN", letters: ["A", "T", "N", "A", "M", "A", "N"] },
    { prompt: "Susun huruf menjadi nama tempat membaca.", word: "PERPUS", letters: ["P", "E", "R", "P", "U", "S"] }
  ],
  sentence: [
    { prompt: "Pilih kalimat yang benar.", answer: "Ibu memasak nasi di dapur.", options: ["Ibu memasak nasi di dapur.", "Dapur nasi memasak ibu.", "Memasak ibu di nasi dapur."], visual: "👩‍🍳 🍚" },
    { prompt: "Pilih kalimat dengan huruf kapital yang benar.", answer: "Raka pergi ke Danau Bahasa.", options: ["raka pergi ke danau bahasa.", "Raka pergi ke Danau Bahasa.", "Raka pergi ke danau bahasa"], visual: "A B C" },
    { prompt: "Pilih kalimat tanya yang benar.", answer: "Di mana buku Sari?", options: ["Di mana buku Sari?", "Di mana buku Sari.", "Buku di mana Sari"], visual: "❓📘" }
  ],
  culture: [
    { prompt: "Pilih pasangan budaya yang benar.", answer: "Tari Saman - Aceh", options: ["Tari Saman - Aceh", "Honai - Bali", "Gamelan - Papua"], visual: "💃 + Aceh" },
    { prompt: "Rumah adat Honai berasal dari...", answer: "Papua", options: ["Papua", "Sumatra Barat", "Kalimantan Selatan"], visual: "🛖" },
    { prompt: "Alat musik angklung berasal dari...", answer: "Jawa Barat", options: ["Jawa Barat", "Maluku", "Nusa Tenggara Timur"], visual: "🎵" }
  ],
  houseculture: [
    { prompt: "Rumah Gadang berasal dari daerah...", answer: "Sumatra Barat", options: ["Sumatra Barat", "Bali", "Sulawesi Utara"], visual: "🏠 Minangkabau" },
    { prompt: "Rumah Tongkonan berasal dari...", answer: "Sulawesi Selatan", options: ["Sulawesi Selatan", "Aceh", "Papua"], visual: "🏘️ Toraja" },
    { prompt: "Rumah Joglo dikenal sebagai rumah adat dari...", answer: "Jawa Tengah", options: ["Jawa Tengah", "Kalimantan Barat", "Maluku"], visual: "🏡 Joglo" }
  ],
  adaptation: [
    { prompt: "Kaktus memiliki daun berbentuk duri. Apa tujuan utama adaptasi ini?", answer: "Mengurangi penguapan air", options: ["Mengurangi penguapan air", "Menangkap serangga", "Mempercepat fotosintesis"], visual: "🌵" },
    { prompt: "Bunglon mengubah warna kulitnya sesuai tempat ia berada. Kemampuan ini disebut...", answer: "Mimikri", options: ["Mimikri", "Kamuflase", "Autotomi"], visual: "🦎" }
  ],
  geometry: [
    { prompt: "Sebuah taman berbentuk persegi dengan panjang sisi 6 meter. Berapa luas taman tersebut?", answer: "36 m²", options: ["24 m²", "36 m²", "12 m²"], shape: "square", size: 6, visual: "📏 Sisi = 6m" },
    { prompt: "Kolam ikan berbentuk persegi panjang dengan panjang 8m dan lebar 5m. Keliling kolam adalah...", answer: "26 m", options: ["13 m", "40 m", "26 m"], shape: "rectangle", width: 8, height: 5, visual: "📏 8m × 5m" }
  ],
  energy: [
    { prompt: "Saat kita menyalakan kipas angin, terjadi perubahan energi...", answer: "Listrik menjadi gerak", options: ["Listrik menjadi gerak", "Gerak menjadi panas", "Listrik menjadi suara"], visual: "🔌 ➡️ 🌀" },
    { prompt: "Panel surya dipasang untuk menangkap sinar matahari. Energi cahaya diubah menjadi energi...", answer: "Listrik", options: ["Listrik", "Kimia", "Gerak"], visual: "☀️ ➡️ 🔋" }
  ],
  civics: [
    { prompt: "Kegiatan 'Gotong Royong' membersihkan selokan desa merupakan pengamalan Pancasila sila ke...", answer: "3", options: ["2", "3", "5"], visual: "🤝 Gotong Royong" },
    { prompt: "Menghormati teman yang sedang beribadah sesuai agamanya adalah wujud penerapan sila...", answer: "Pertama", options: ["Pertama", "Kedua", "Ketiga"], visual: "🕌 ⛪ 🕍" }
  ],
  history: [
    { prompt: "Siapakah tokoh yang membacakan teks Proklamasi Kemerdekaan Indonesia?", answer: "Ir. Soekarno", options: ["Ir. Soekarno", "Drs. Moh. Hatta", "Sayuti Melik"], visual: "🎤 17 Agustus 1945" },
    { prompt: "Di kota manakah teks proklamasi kemerdekaan Indonesia dibacakan?", answer: "Jakarta", options: ["Jakarta", "Bandung", "Surabaya"], visual: "📍 Jl. Pegangsaan Timur" }
  ],
  proverb: [
    { prompt: "Apa arti dari peribahasa 'Tong kosong nyaring bunyinya'?", answer: "Orang yang bodoh biasanya banyak bicara", options: ["Orang yang bodoh biasanya banyak bicara", "Orang pintar suka mengalah", "Tempat kosong bunyinya keras"], visual: "🛢️ 🗣️" },
    { prompt: "Lengkapilah peribahasa ini: 'Ada udang di balik...'", answer: "batu", options: ["daun", "batu", "air"], visual: "🦐 ➡️ ❓" }
  ],
  scale: [
    { prompt: "Skala peta 1 : 100.000. Jarak pada peta 3 cm. Berapa jarak sebenarnya?", answer: "3 km", options: ["300 m", "3 km", "30 km"], visual: "🗺️ 1:100.000" },
    { prompt: "Skala peta adalah 1 : 50. Jarak sebenarnya 10 meter. Jarak pada peta adalah...", answer: "20 cm", options: ["2 cm", "10 cm", "20 cm"], visual: "🗺️ Jarak Sebenarnya = 10m" }
  ],
  synonym: [
    { prompt: "Persamaan kata (sinonim) dari kata 'Bahagia' adalah...", answer: "Gembira", options: ["Gembira", "Sedih", "Duka"], visual: "😊 = ?" },
    { prompt: "Lawan kata (antonim) dari kata 'Rajin' adalah...", answer: "Malas", options: ["Pintar", "Malas", "Sombong"], visual: "📚 ✖️ 💤" }
  ]
};

const state = {
  screen: "loading",
  highScore: Number(localStorage.getItem(STORE_KEY) || 0),
  character: "raka",
  level: 1,
  xp: 0,
  coins: 0,
  gems: 0,
  books: 0,
  questItems: { mango: 0 },
  badges: [],
  hearts: MAX_HEARTS,
  completedQuests: new Set(),
  activeQuest: null,
  acceptedQuest: null,
  visitedAreas: new Set(),
  objects: [],
  collectibles: [],
  position: { x: 220, y: 1190 },
  joystick: { x: 0, y: 0 },
  targetPosition: null,
  keys: { up: false, down: false, left: false, right: false, run: false },
  lastFrame: 0,
  running: false,
  audioReady: false,
  audioContext: null,
  musicTimer: null,
  wordAnswer: []
};

function init() {
  detectTouchDevice();
  bindButtons();
  bindCharacterCards();
  bindKeyboard();
  bindJoystick();
  bindAdditionalControls();
  renderMapInfo();
  updateHud();
  loadingSequence();
}

function loadingSequence() {
  let progress = 0;
  const timer = setInterval(() => {
    progress += Math.floor(Math.random() * 13) + 10;
    ui.loadingProgress.style.width = `${Math.min(progress, 100)}%`;
    if (progress >= 100) {
      clearInterval(timer);
      showScreen("menu");
    }
  }, 170);
}

function showScreen(name) {
  state.screen = name;
  Object.values(screens).forEach((screen) => screen.classList.remove("active"));
  screens[name].classList.add("active");
  if (name === "inventory") renderInventory();
  if (name === "achievement") renderAchievements();
  if (name === "map") renderMapInfo();
}

function bindButtons() {
  buttons.start.addEventListener("click", startGame);
  buttons.character.addEventListener("click", () => showScreen("character"));
  buttons.inventory.addEventListener("click", () => showScreen("inventory"));
  buttons.achievement.addEventListener("click", () => showScreen("achievement"));
  buttons.map.addEventListener("click", () => showScreen("map"));
  buttons.backChar.addEventListener("click", () => showScreen("menu"));
  buttons.backInventory.addEventListener("click", () => showScreen(state.running ? "world" : "menu"));
  buttons.backAchievement.addEventListener("click", () => showScreen(state.running ? "world" : "menu"));
  buttons.backMap.addEventListener("click", () => showScreen(state.running ? "world" : "menu"));
  buttons.pause.addEventListener("click", () => showScreen("menu"));
  buttons.worldInventory.addEventListener("click", () => showScreen("inventory"));
  buttons.worldAchievement.addEventListener("click", () => showScreen("achievement"));
  buttons.worldMap.addEventListener("click", () => showScreen("map"));
  buttons.acceptQuest.addEventListener("click", acceptQuest);
  buttons.declineQuest.addEventListener("click", closeQuest);
  buttons.retry.addEventListener("click", startGame);
  buttons.gameOverMenu.addEventListener("click", () => showScreen("menu"));
  buttons.victoryReplay.addEventListener("click", startGame);
  buttons.victoryMenu.addEventListener("click", () => showScreen("menu"));
}

function bindCharacterCards() {
  $$("#characterGrid .character-card").forEach((card) => {
    card.addEventListener("click", () => {
      $$("#characterGrid .character-card").forEach((item) => item.classList.remove("selected"));
      card.classList.add("selected");
      state.character = card.dataset.character;
      ui.player.className = `player avatar-${state.character}`;
      playTone("select");
    });
  });
}

function bindKeyboard() {
  window.addEventListener("keydown", (event) => {
    setKey(event.key, true);
    if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(event.key)) event.preventDefault();
  });
  window.addEventListener("keyup", (event) => setKey(event.key, false));
}

function setKey(key, isDown) {
  const lower = key.toLowerCase();
  if (lower === "w" || key === "ArrowUp") state.keys.up = isDown;
  if (lower === "s" || key === "ArrowDown") state.keys.down = isDown;
  if (lower === "a" || key === "ArrowLeft") state.keys.left = isDown;
  if (lower === "d" || key === "ArrowRight") state.keys.right = isDown;
  if (key === "Shift") state.keys.run = isDown;
}

function bindJoystick() {
  const base = $("#virtualJoystick");
  const knob = $("#joystickKnob");
  if (!base || !knob) return;

  let dragStart = null;
  const maxRadius = 40; // Pixels knob can move

  const handleStart = (event) => {
    event.preventDefault();
    ensureAudio();
    const touch = event.touches ? event.touches[0] : event;
    dragStart = { x: touch.clientX, y: touch.clientY };
    knob.style.transition = "none";
    state.targetPosition = null;
    hideClickMarker();
  };

  const handleMove = (event) => {
    if (!dragStart) return;
    event.preventDefault();
    const touch = event.touches ? event.touches[0] : event;
    let dx = touch.clientX - dragStart.x;
    let dy = touch.clientY - dragStart.y;
    const distance = Math.hypot(dx, dy);

    if (distance > maxRadius) {
      dx = (dx / distance) * maxRadius;
      dy = (dy / distance) * maxRadius;
    }

    knob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;

    state.joystick.x = dx / maxRadius;
    state.joystick.y = dy / maxRadius;
  };

  const handleEnd = () => {
    dragStart = null;
    knob.style.transition = "transform 0.15s ease-out";
    knob.style.transform = `translate(-50%, -50%)`;
    state.joystick.x = 0;
    state.joystick.y = 0;
  };

  knob.addEventListener("pointerdown", handleStart);
  window.addEventListener("pointermove", handleMove);
  window.addEventListener("pointerup", handleEnd);
  window.addEventListener("pointercancel", handleEnd);
}

function bindAdditionalControls() {
  // Mobile Run Toggle
  const runBtn = $("#mobileRunButton");
  if (runBtn) {
    runBtn.addEventListener("click", () => {
      state.keys.run = !state.keys.run;
      runBtn.classList.toggle("active", state.keys.run);
      playTone("select");
    });
  }

  // Tap-to-move on worldViewport
  ui.worldViewport.addEventListener("click", (event) => {
    // Ignore clicks on HUD, Joystick, Quests, or Modals
    if (event.target.closest(".hud-top") || 
        event.target.closest(".quick-nav") || 
        event.target.closest("#virtualJoystick") || 
        event.target.closest("#mobileRunButton") || 
        event.target.closest(".quest-panel") ||
        event.target.closest(".modal")) {
      return;
    }

    ensureAudio();
    const rect = ui.worldViewport.getBoundingClientRect();
    const touchX = event.clientX - rect.left;
    const touchY = event.clientY - rect.top;

    const scale = getWorldScale();
    const mapX = rect.width / 2 - state.position.x * scale;
    const mapY = rect.height / 2 - state.position.y * scale;

    const mapTouchX = (touchX - mapX) / scale;
    const mapTouchY = (touchY - mapY) / scale;

    // Check bounds
    if (mapTouchX >= 52 && mapTouchX <= WORLD.width - 52 &&
        mapTouchY >= 58 && mapTouchY <= WORLD.height - 58) {
      state.targetPosition = { x: mapTouchX, y: mapTouchY };
      showClickMarker(mapTouchX, mapTouchY);
    }
  });
}

function showClickMarker(x, y) {
  const marker = $("#clickMarker");
  if (marker) {
    marker.style.left = `${x}px`;
    marker.style.top = `${y}px`;
    marker.classList.remove("hidden");
  }
}

function hideClickMarker() {
  const marker = $("#clickMarker");
  if (marker) {
    marker.classList.add("hidden");
  }
}

function detectTouchDevice() {
  const hasTouch = (
    "ontouchstart" in window ||
    navigator.maxTouchPoints > 0 ||
    navigator.msMaxTouchPoints > 0
  );
  if (hasTouch) {
    document.body.classList.add("has-touch");
  }
  // Also detect touch at first interaction (e.g. touchscreen laptops that may
  // not report ontouchstart until actual touch is made)
  window.addEventListener("touchstart", () => {
    document.body.classList.add("has-touch");
  }, { once: true, passive: true });
}

function startGame() {
  ensureAudio();
  resetGame();
  buildWorld();
  state.running = true;
  state.lastFrame = performance.now();
  showScreen("world");
  requestAnimationFrame(gameLoop);
  toast("Selamat datang di Pulau Nusantara. Dekati NPC bertanda misi.");
}

function resetGame() {
  state.level = 1;
  state.xp = 0;
  state.coins = 0;
  state.gems = 0;
  state.books = 0;
  state.questItems = { mango: 0 };
  state.badges = [];
  state.hearts = MAX_HEARTS;
  state.completedQuests = new Set();
  state.activeQuest = null;
  state.acceptedQuest = null;
  state.visitedAreas = new Set();
  state.objects = [];
  state.collectibles = [];
  state.position = { x: 220, y: 1190 };
  achievements.forEach((item) => (item.unlocked = false));
  ui.objectLayer.innerHTML = "";
  ui.particleLayer.innerHTML = "";
  updateHud();
}

function buildWorld() {
  addDecorations();
  quests.forEach((quest) => createNPC(quest));
  [
    [320, 1090, "mango"], [480, 1355, "mango"], [675, 1170, "mango"], [345, 1385, "mango"], [590, 1015, "mango"],
    [770, 880, "coin"], [1425, 985, "coin"], [1875, 945, "coin"], [1630, 300, "book"], [1095, 1045, "book"],
    [620, 420, "gem"], [2000, 1325, "coin"], [2090, 1045, "gem"], [1350, 555, "coin"],
    [1715, 585, "coin"], [1790, 225, "book"], [1155, 1345, "coin"], [1320, 1065, "book"],
    [2045, 840, "coin"], [1370, 690, "gem"], [825, 430, "coin"],
    [2200, 1100, "coin"], [1600, 500, "gem"], [350, 700, "coin"], [900, 800, "coin"],
    [500, 1300, "mango"], [1600, 900, "book"], [1100, 450, "coin"], [250, 1450, "mango"]
  ].forEach(([x, y, type]) => createCollectible(x, y, type));

  // Ambient butterflies
  [
    [320, 1090], [1600, 390], [1800, 490], [1150, 1000], [750, 600], [450, 900]
  ].forEach(([x, y]) => {
    const bf = document.createElement("div");
    bf.className = "butterfly";
    bf.style.left = `${x}px`;
    bf.style.top = `${y}px`;
    ui.objectLayer.appendChild(bf);
  });

  // Ambient waves
  [
    [1900, 875], [2155, 1050], [2050, 1255]
  ].forEach(([x, y]) => {
    const wave = document.createElement("div");
    wave.className = "wave-line";
    wave.style.left = `${x}px`;
    wave.style.top = `${y}px`;
    ui.objectLayer.appendChild(wave);
  });

  positionPlayer();
}

function addDecorations() {
  const decorations = [
    // Village houses
    ["house", 260, 1315], ["house", 655, 1335], ["house", 550, 1255], ["house", 350, 1455],
    // Forest trees (top-right dense)
    ["tree", 1480, 320], ["tree", 1650, 505], ["tree", 1915, 485], ["tree", 1380, 475],
    ["tree", 1800, 600], ["tree", 2000, 480], ["tree", 1700, 280], ["tree", 2100, 380],
    // Scattered trees across map
    ["tree", 945, 945], ["tree", 860, 1130], ["tree", 225, 885], ["tree", 790, 660],
    ["tree", 600, 300], ["tree", 1100, 200], ["tree", 1600, 1000], ["tree", 900, 1300],
    ["tree", 500, 750], ["tree", 1300, 400], ["tree", 750, 450], ["tree", 1050, 700],
    // Mountain rocks (top-left)
    ["rock", 320, 360], ["rock", 580, 225], ["rock", 470, 505], ["rock", 1095, 525],
    ["rock", 1500, 200], ["rock", 350, 500], ["rock", 200, 200], ["rock", 160, 420]
  ];
  decorations.forEach(([type, x, y]) => createObject(type, x, y));
}

function createObject(type, x, y) {
  const element = document.createElement("div");
  element.className = `map-object ${type}`;
  element.style.left = `${x}px`;
  element.style.top = `${y}px`;
  ui.objectLayer.appendChild(element);
  state.objects.push({ type, x, y, radius: type === "house" ? 58 : 38, element });
}

function createNPC(quest) {
  const area = areas.find((item) => item.id === quest.area);
  const element = document.createElement("div");
  element.className = `map-object npc npc-${quest.type}`;
  element.style.left = `${quest.x || area.x}px`;
  element.style.top = `${quest.y || area.y}px`;
  element.innerHTML = `<div class="npc-body"><span class="npc-icon">${quest.icon || "🧑"}</span></div><span class="label-bubble"><strong>${quest.npc}</strong><br><small>${area.name}</small></span>`;
  ui.objectLayer.appendChild(element);
  state.objects.push({ type: "npc", areaId: area.id, questId: quest.id, x: quest.x || area.x, y: quest.y || area.y, radius: 70, element });
}

function createCollectible(x, y, type) {
  const element = document.createElement("div");
  element.className = `map-object collectible ${type === "mango" ? "coin mango" : type}`;
  element.style.left = `${x}px`;
  element.style.top = `${y}px`;
  element.title = type;
  element.textContent = { mango: "🥭", coin: "🪙", gem: "💎", book: "📘" }[type] || "✨";
  ui.objectLayer.appendChild(element);
  state.collectibles.push({ x, y, type, radius: 36, element });
}

function gameLoop(now) {
  if (!state.running) return;
  const delta = Math.min((now - state.lastFrame) / 1000, 0.05);
  state.lastFrame = now;
  if (state.screen === "world") {
    updatePlayer(delta);
    checkInteractions();
  }
  requestAnimationFrame(gameLoop);
}

function updatePlayer(delta) {
  let dx = 0;
  let dy = 0;

  // 1. Keyboard Input
  if (state.keys.left) dx -= 1;
  if (state.keys.right) dx += 1;
  if (state.keys.up) dy -= 1;
  if (state.keys.down) dy += 1;

  // Normalize keyboard vector
  if (dx !== 0 && dy !== 0) {
    dx *= 0.707;
    dy *= 0.707;
  }

  // 2. Analog Joystick Input (override keyboard if joystick is active)
  const joystickActive = state.joystick.x !== 0 || state.joystick.y !== 0;
  if (dx === 0 && dy === 0 && joystickActive) {
    dx = state.joystick.x;
    dy = state.joystick.y;
    // Cancel tap-to-move
    state.targetPosition = null;
    hideClickMarker();
  }

  // If any keyboard input, cancel tap-to-move
  if (state.keys.left || state.keys.right || state.keys.up || state.keys.down) {
    state.targetPosition = null;
    hideClickMarker();
  }

  let moving = dx !== 0 || dy !== 0;

  // 3. Tap-to-move logic
  if (!moving && state.targetPosition) {
    const dist = getDistance(state.position, state.targetPosition);
    if (dist > 5) {
      dx = (state.targetPosition.x - state.position.x) / dist;
      dy = (state.targetPosition.y - state.position.y) / dist;
      moving = true;
    } else {
      state.targetPosition = null;
      hideClickMarker();
    }
  }

  const isRunning = state.keys.run;
  const speed = BASE_SPEED * (isRunning ? RUN_MULTIPLIER : 1) * (1 + (state.level - 1) * 0.06);

  if (moving) {
    const nextX = clamp(state.position.x + dx * speed * delta, 52, WORLD.width - 52);
    const nextY = clamp(state.position.y + dy * speed * delta, 58, WORLD.height - 58);

    // Obstacle collision cancel for tap-to-move
    if (state.targetPosition) {
      const actualDist = getDistance(state.position, { x: nextX, y: nextY });
      const expectedDist = speed * delta;
      // If we move less than 20% of expected distance, assume we are blocked and stop
      if (actualDist < expectedDist * 0.2) {
        state.targetPosition = null;
        hideClickMarker();
      }
    }

    state.position.x = nextX;
    state.position.y = nextY;
  }

  ui.player.classList.toggle("walk", moving);
  ui.player.classList.toggle("run", moving && isRunning);

  // Idle check for emote
  if (!state.idleTime) state.idleTime = 0;
  if (moving) {
    state.idleTime = 0;
  } else {
    state.idleTime += delta;
    if (state.idleTime > 6) {
      triggerEmote(ui.player, pick(["ðŸ’¤", "ðŸ’­", "ðŸ‘€", "ðŸ§ ", "ðŸŽ’"]));
      state.idleTime = 0;
    }
  }

  positionPlayer();
}

function positionPlayer() {
  ui.player.style.left = `${state.position.x}px`;
  ui.player.style.top = `${state.position.y}px`;
  const viewport = ui.worldViewport.getBoundingClientRect();
  const scale = getWorldScale();
  const mapX = viewport.width / 2 - state.position.x * scale;
  const mapY = viewport.height / 2 - state.position.y * scale;
  ui.worldMap.style.transform = `translate(${mapX}px, ${mapY}px) scale(${scale})`;
  updateAreaName();
}

function getWorldScale() {
  const viewport = ui.worldViewport.getBoundingClientRect();
  return Math.max(viewport.width / WORLD.width, viewport.height / WORLD.height, 0.52);
}

function updateAreaName() {
  const nearest = areas.reduce((best, area) => {
    const distance = getDistance(state.position, area);
    return distance < best.distance ? { area, distance } : best;
  }, { area: areas[0], distance: Infinity });
  if (nearest.distance < 180) {
    state.visitedAreas.add(nearest.area.id);
    ui.activeAreaName.textContent = nearest.area.name;
  }
}

function checkInteractions() {
  for (const item of [...state.collectibles]) {
    if (getDistance(state.position, item) < item.radius) collectItem(item);
  }
  let nearQuestGiver = false;
  for (const object of state.objects) {
    if (object.type !== "npc") continue;
    
    // Proximity label logic (show if < 180px)
    const dist = getDistance(state.position, object);
    object.element.classList.toggle("show", dist < 180);

    const nearNpc = dist < object.radius;
    if (nearNpc) {
      nearQuestGiver = true;
      const quest = quests.find((item) => item.id === object.questId);
      if (quest && !state.completedQuests.has(quest.id) && state.activeQuest?.id !== quest.id) {
        openQuest(quest);
        triggerEmote(object.element, "💬");
      }
    }
  }
  if (!nearQuestGiver && !state.acceptedQuest && ui.questOverlay.classList.contains("hidden")) {
    state.activeQuest = null;
  }
}

function collectItem(item) {
  item.element.remove();
  state.collectibles = state.collectibles.filter((entry) => entry !== item);
  if (item.type === "coin" || item.type === "mango") state.coins += item.type === "mango" ? 1 : 5;
  if (item.type === "mango") state.questItems.mango += 1;
  if (item.type === "gem") state.gems += 1;
  if (item.type === "book") state.books += 1;
  playTone("coin");
  spawnSparkles(item.x, item.y, item.type === "gem" ? "#49c6dc" : "#ffd15f");
  updateHud();
  checkAcceptedQuestProgress();
  checkAchievements();
}

function openQuest(quest) {
  state.activeQuest = quest;
  const area = areas.find((item) => item.id === quest.area);
  if (state.level < area.unlockLevel) {
    const now = performance.now();
    if (!state.lockToastAt || now - state.lockToastAt > 1800) {
      toast(`${area.name} terbuka mulai level ${area.unlockLevel}. Selesaikan quest lain dulu.`);
      state.lockToastAt = now;
    }
    return;
  }
  ui.questAreaLabel.textContent = `${area.name} - ${quest.npc}`;
  ui.questTitle.textContent = quest.title;
  ui.questDescription.textContent = quest.description;
  ui.questRewards.innerHTML = rewardText(quest.rewards).map((text) => `<span>${text}</span>`).join("");
  ui.questOverlay.classList.remove("hidden");
  updateQuestHud(quest);
}

function acceptQuest() {
  if (!state.activeQuest) return;
  state.acceptedQuest = state.activeQuest;
  closeQuest();
  if (state.acceptedQuest.type === "collect") {
    toast("Quest diterima. Kumpulkan 5 koin mangga di Desa Harmoni.");
    checkAcceptedQuestProgress();
    return;
  }
  openMiniGame(state.acceptedQuest);
}

function closeQuest() {
  ui.questOverlay.classList.add("hidden");
}

function checkAcceptedQuestProgress() {
  const quest = state.acceptedQuest;
  if (!quest || quest.type !== "collect") return;
  const collected = Math.min(state.questItems[quest.target] || 0, quest.amount);
  ui.activeQuestText.textContent = `Mangga terkumpul: ${collected}/${quest.amount}.`;
  if ((state.questItems[quest.target] || 0) >= quest.amount) completeQuest(quest);
}

function openMiniGame(quest) {
  ui.miniGameOverlay.classList.remove("hidden");
  ui.miniGameSubject.textContent = subjectName(quest.type);
  ui.miniGameTitle.textContent = quest.title;
  ui.miniGameContent.innerHTML = "";
  ui.miniGameActions.innerHTML = "";
  if (quest.type === "language") renderLanguageGame(quest);
  else if (quest.type === "culture") renderChoiceGame(quest, "culture");
  else renderChoiceGame(quest, quest.type);
}

function renderFractionVisual(fractionStr) {
  let num = 1;
  let den = 2;
  if (fractionStr && fractionStr.includes("/")) {
    const parts = fractionStr.split("/");
    num = parseInt(parts[0]) || 1;
    den = parseInt(parts[1]) || 2;
  }
  
  const radius = 45;
  const cx = 50;
  const cy = 50;
  let paths = "";
  
  for (let i = 0; i < den; i++) {
    const startAngle = (i * 360) / den - 90;
    const endAngle = ((i + 1) * 360) / den - 90;
    
    const x1 = cx + radius * Math.cos((startAngle * Math.PI) / 180);
    const y1 = cy + radius * Math.sin((startAngle * Math.PI) / 180);
    const x2 = cx + radius * Math.cos((endAngle * Math.PI) / 180);
    const y2 = cy + radius * Math.sin((endAngle * Math.PI) / 180);
    
    const isLargeArc = 0;
    const d = `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${isLargeArc} 1 ${x2} ${y2} Z`;
    
    const isFilled = i < num;
    const fill = isFilled ? "var(--yellow)" : "rgba(255, 255, 255, 0.4)";
    paths += `<path d="${d}" fill="${fill}" stroke="var(--ink)" stroke-width="2.5" />`;
  }
  
  return `
    <div class="fraction-visual-container">
      <svg width="100" height="100" viewBox="0 0 100 100">
        ${paths}
      </svg>
    </div>
  `;
}

function renderGeometryVisual(shape, w, h, size) {
  if (shape === "square") {
    return `
      <div class="geometry-visual-container">
        <div class="visual-shape square-shape" style="width: 75px; height: 75px;">
          <span class="dim-top">${size} m</span>
          <span class="dim-left">${size} m</span>
        </div>
      </div>
    `;
  } else if (shape === "rectangle") {
    return `
      <div class="geometry-visual-container">
        <div class="visual-shape rectangle-shape" style="width: 100px; height: 60px;">
          <span class="dim-top">${w} m</span>
          <span class="dim-left">${h} m</span>
        </div>
      </div>
    `;
  }
  return "";
}

function renderChoiceGame(quest, type) {
  const data = pick(miniGames[type]);
  const stageClass = type === "math" ? "bridge-stage" : type === "science" ? "plant-stage" : "mini-stage";
  
  let visualContent = "";
  if (type === "math") {
    visualContent = renderBridge(data.blocks);
  } else if (type === "science") {
    visualContent = '<span class="sun"></span><span class="sprout"></span>';
  } else if (type === "fraction") {
    visualContent = renderFractionVisual(data.answer);
  } else if (type === "geometry") {
    visualContent = renderGeometryVisual(data.shape, data.width, data.height, data.size);
  } else {
    visualContent = `<div class="mini-visual">${data.visual || "✨"}</div>`;
  }

  ui.miniGameContent.innerHTML = `
    <p>${data.prompt}</p>
    <div class="${stageClass}">
      ${visualContent}
    </div>
    <div class="${type.includes("culture") ? "culture-grid" : "choice-grid"}">
      ${data.options.map((option) => `<button class="btn ghost answer-button" data-answer="${option}">${option}</button>`).join("")}
    </div>
  `;

  $$(".answer-button").forEach((button) => {
    button.addEventListener("click", () => {
      const selected = button.dataset.answer;
      const correct = selected === data.answer;
      
      // Disable buttons and show correctness colors
      $$(".answer-button").forEach((btn) => {
        btn.disabled = true;
        if (btn.dataset.answer === data.answer) {
          btn.classList.add("correct");
        } else if (btn.dataset.answer === selected && !correct) {
          btn.classList.add("wrong");
        }
      });
      
      // Add explanation text
      const exp = document.createElement("div");
      exp.className = "explanation-text";
      const defaultExplanation = correct ? "Pilihanmu sangat tepat!" : `Jawaban yang benar adalah: ${data.answer}.`;
      const explanationText = data.explanation || defaultExplanation;
      exp.innerHTML = correct ? `<strong>🎉 Benar!</strong> ${explanationText}` : `<strong>❌ Kurang Tepat.</strong> ${explanationText}`;
      ui.miniGameContent.appendChild(exp);
      
      // Replace actions with continue button
      ui.miniGameActions.innerHTML = "";
      const continueBtn = document.createElement("button");
      continueBtn.className = "btn primary";
      continueBtn.textContent = "Lanjutkan";
      continueBtn.addEventListener("click", () => {
        evaluateAnswer(correct, quest);
      });
      ui.miniGameActions.appendChild(continueBtn);
    });
  });

  ui.miniGameActions.innerHTML = "";
  ui.miniGameActions.appendChild(cancelMiniGameButton());
}

function renderBridge(count) {
  return Array.from({ length: Math.min(count, 16) }, (_, index) => `<span class="block" style="height:${45 + (index % 4) * 18}px; animation-delay:${index * 0.03}s"></span>`).join("");
}

function renderLanguageGame(quest) {
  const data = pick(miniGames.language);
  state.wordAnswer = Array(data.word.length).fill("");
  ui.miniGameContent.innerHTML = `
    <p>${data.prompt}</p>
    <div class="mini-stage">
      <div class="word-drop">
        ${state.wordAnswer.map((_, index) => `<div class="drop-slot" data-slot="${index}"></div>`).join("")}
      </div>
      <div class="letter-bank">
        ${shuffle(data.letters).map((letter, index) => `<button class="letter-tile" draggable="true" data-letter="${letter}" data-index="${index}">${letter}</button>`).join("")}
      </div>
    </div>
  `;
  bindLetterTiles();
  const submit = document.createElement("button");
  submit.className = "btn primary";
  submit.textContent = "Kirim Jawaban";
  submit.addEventListener("click", () => evaluateAnswer(state.wordAnswer.join("") === data.word, quest));
  const reset = document.createElement("button");
  reset.className = "btn ghost";
  reset.textContent = "Ulangi Huruf";
  reset.addEventListener("click", () => renderLanguageGame(quest));
  ui.miniGameActions.append(submit, reset, cancelMiniGameButton());
}

function bindLetterTiles() {
  let selectedTile = null;
  $$(".letter-tile").forEach((tile) => {
    tile.addEventListener("dragstart", (event) => {
      event.dataTransfer.setData("text/plain", tile.dataset.index);
    });
    tile.addEventListener("click", () => {
      selectedTile = tile;
      $$(".letter-tile").forEach((item) => item.classList.remove("selected"));
      tile.classList.add("selected");
    });
  });
  $$(".drop-slot").forEach((slot) => {
    slot.addEventListener("dragover", (event) => event.preventDefault());
    slot.addEventListener("drop", (event) => {
      event.preventDefault();
      const tile = $(`.letter-tile[data-index="${event.dataTransfer.getData("text/plain")}"]`);
      placeLetter(tile, slot);
    });
    slot.addEventListener("click", () => {
      if (selectedTile) placeLetter(selectedTile, slot);
    });
  });
}

function placeLetter(tile, slot) {
  if (!tile || tile.classList.contains("used")) return;
  const index = Number(slot.dataset.slot);
  state.wordAnswer[index] = tile.dataset.letter;
  slot.textContent = tile.dataset.letter;
  tile.classList.add("used");
  playTone("select");
}

function cancelMiniGameButton() {
  const button = document.createElement("button");
  button.className = "btn ghost";
  button.textContent = "Tutup";
  button.addEventListener("click", () => {
    ui.miniGameOverlay.classList.add("hidden");
    state.acceptedQuest = null;
    updateQuestHud(null);
  });
  return button;
}

function evaluateAnswer(correct, quest) {
  ui.miniGameOverlay.classList.add("hidden");
  if (correct) {
    completeQuest(quest);
  } else {
    state.hearts -= 1;
    triggerEmote(ui.player, "💔");
    state.acceptedQuest = null;
    state.activeQuest = null;
    playTone("bad");
    toast("Jawaban belum tepat. Hati berkurang satu.");
    spawnConfetti("#ef5d5d");
    if (state.hearts <= 0) return gameOver();
    updateHud();
  }
}

function completeQuest(quest) {
  if (state.completedQuests.has(quest.id)) return;
  state.completedQuests.add(quest.id);
  state.activeQuest = null;
  state.acceptedQuest = null;
  applyRewards(quest.rewards);
  triggerEmote(ui.player, "🎉");
  if (quest.id === "culture") unlockAchievement("culture");
  unlockAchievement("firstQuest");
  if (state.completedQuests.size >= 6) unlockAchievement("halfway");
  ui.player.classList.add("celebrate");
  setTimeout(() => ui.player.classList.remove("celebrate"), 1300);
  playTone("success");
  spawnConfetti("#ffd15f");
  toast(`Quest selesai: ${quest.title}`);
  updateQuestHud(null);
  checkAchievements();
  if (state.completedQuests.size >= quests.length) victory();
}

function applyRewards(rewards) {
  state.coins += rewards.coins || 0;
  state.gems += rewards.gems || 0;
  state.books += rewards.books || 0;
  if (rewards.badge && !state.badges.includes(rewards.badge)) state.badges.push(rewards.badge);
  addXP(rewards.xp || 0);
  updateHud();
}

function addXP(amount) {
  state.xp += amount;
  while (state.xp >= LEVEL_TARGET) {
    state.xp -= LEVEL_TARGET;
    state.level += 1;
    unlockAchievement("levelUp");
    toast(`Level up! Area baru mungkin terbuka. Level ${state.level}.`);
    spawnConfetti("#49c6dc");
    playTone("level");
    triggerEmote(ui.player, "🌟");
  }
}

function updateHud() {
  ui.menuHighScore.textContent = state.highScore;
  ui.playerLevel.textContent = state.level;
  ui.playerXP.textContent = `${state.xp}/${LEVEL_TARGET}`;
  ui.coinCount.textContent = state.coins;
  ui.gemCount.textContent = state.gems;
  renderHearts();
  renderInventory();
}

function updateQuestHud(quest) {
  if (!quest) {
    ui.activeQuestTitle.textContent = "Temui NPC di pulau";
    ui.activeQuestText.textContent = "Dekati karakter dengan tanda misi untuk memulai petualangan belajar.";
    return;
  }
  ui.activeQuestTitle.textContent = quest.title;
  ui.activeQuestText.textContent = quest.description;
}

function renderHearts() {
  ui.heartContainer.innerHTML = "";
  for (let index = 0; index < state.hearts; index += 1) {
    const heart = document.createElement("span");
    heart.className = "heart";
    ui.heartContainer.appendChild(heart);
  }
}

function renderInventory() {
  ui.inventoryCoins.textContent = state.coins;
  ui.inventoryGems.textContent = state.gems;
  ui.inventoryBooks.textContent = state.books;
  ui.inventoryBadges.textContent = state.badges.length;
}

function renderAchievements() {
  ui.achievementList.innerHTML = achievements.map((item) => `
    <article class="achievement-card ${item.unlocked ? "unlocked" : "locked"}">
      <p class="eyebrow">${item.unlocked ? "Terbuka" : "Terkunci"}</p>
      <strong>${item.title}</strong>
      <p>${item.text}</p>
    </article>
  `).join("");
}

function renderMapInfo() {
  ui.areaList.innerHTML = areas.map((area) => {
    const unlocked = state.level >= area.unlockLevel;
    const visited = state.visitedAreas.has(area.id);
    return `
      <article class="area-card ${unlocked ? "unlocked" : ""}">
        <p class="eyebrow">${unlocked ? "Terbuka" : `Level ${area.unlockLevel}`}</p>
        <strong>${area.name}</strong>
        <p>${area.theme}${visited ? " - sudah dikunjungi." : ""}</p>
      </article>
    `;
  }).join("");
}

function checkAchievements() {
  if (state.coins >= 40) unlockAchievement("collector");
  if (state.books >= 2) unlockAchievement("bookworm");
}

function unlockAchievement(id) {
  const achievement = achievements.find((item) => item.id === id);
  if (!achievement || achievement.unlocked) return;
  achievement.unlocked = true;
  playTone("achievement");
  toast(`Achievement: ${achievement.title}`);
}

function gameOver() {
  state.running = false;
  updateHighScore();
  ui.gameOverLevel.textContent = state.level;
  ui.gameOverXP.textContent = state.xp;
  ui.gameOverQuests.textContent = state.completedQuests.size;
  showScreen("gameOver");
}

function victory() {
  state.running = false;
  unlockAchievement("victory");
  updateHighScore();
  ui.victoryLevel.textContent = state.level;
  ui.victoryXP.textContent = state.xp;
  ui.victoryBadge.textContent = state.badges.length;
  playTone("victory");
  showScreen("victory");
}

function updateHighScore() {
  const score = state.level * 100 + state.coins + state.gems * 25 + state.books * 20 + state.completedQuests.size * 50;
  state.highScore = Math.max(state.highScore, score);
  localStorage.setItem(STORE_KEY, String(state.highScore));
  ui.menuHighScore.textContent = state.highScore;
}

function ensureAudio() {
  if (state.audioReady) return;
  const AudioContext = window.AudioContext || window.webkitAudioContext;
  if (!AudioContext) return;
  state.audioContext = new AudioContext();
  state.audioReady = true;
  startMusic();
}

function startMusic() {
  if (state.musicTimer || !state.audioContext) return;
  const notes = [392, 440, 523, 494, 440, 392, 330, 392];
  let index = 0;
  state.musicTimer = setInterval(() => {
    playOsc(notes[index % notes.length], 0.05, 0.045, "sine");
    index += 1;
  }, 520);
}

function playTone(type) {
  ensureAudio();
  if (!state.audioContext) return;
  const map = {
    coin: [880, 0.07, 0.12, "triangle"],
    select: [520, 0.04, 0.08, "sine"],
    success: [660, 0.12, 0.16, "triangle"],
    achievement: [990, 0.13, 0.12, "square"],
    level: [760, 0.14, 0.16, "triangle"],
    victory: [1040, 0.25, 0.12, "triangle"],
    bad: [170, 0.16, 0.12, "sawtooth"]
  };
  const [frequency, duration, volume, wave] = map[type] || map.select;
  playOsc(frequency, duration, volume, wave);
  if (type === "success" || type === "victory") setTimeout(() => playOsc(frequency * 1.25, duration, volume, wave), 120);
}

function playOsc(frequency, duration, volume, wave) {
  const context = state.audioContext;
  const oscillator = context.createOscillator();
  const gain = context.createGain();
  oscillator.type = wave;
  oscillator.frequency.value = frequency;
  gain.gain.value = volume;
  gain.gain.exponentialRampToValueAtTime(0.001, context.currentTime + duration);
  oscillator.connect(gain).connect(context.destination);
  oscillator.start();
  oscillator.stop(context.currentTime + duration);
}

function showClickMarker(x, y) {
  if (!ui.clickMarker) return;
  const scale = getWorldScale();
  const px = x * scale;
  const py = y * scale;
  ui.clickMarker.style.left = `${px}px`;
  ui.clickMarker.style.top = `${py}px`;
  ui.clickMarker.classList.remove("hidden");
  // Force reflow for animation restart
  void ui.clickMarker.offsetWidth;
  ui.clickMarker.classList.add("pulse-marker");
}

function hideClickMarker() {
  if (!ui.clickMarker) return;
  ui.clickMarker.classList.add("hidden");
  ui.clickMarker.classList.remove("pulse-marker");
}

function spawnSparkles(x, y, color) {
  for (let index = 0; index < 12; index += 1) {
    const particle = document.createElement("span");
    particle.className = "sparkle";
    particle.style.left = `${x + random(-24, 24)}px`;
    particle.style.top = `${y + random(-12, 18)}px`;
    particle.style.background = color;
    particle.style.animationDelay = `${index * 0.025}s`;
    ui.particleLayer.appendChild(particle);
    setTimeout(() => particle.remove(), 1100);
  }
}

function spawnConfetti(color) {
  for (let index = 0; index < 30; index += 1) {
    const particle = document.createElement("span");
    particle.className = "confetti";
    particle.style.left = `${state.position.x + random(-130, 130)}px`;
    particle.style.top = `${state.position.y + random(-70, 40)}px`;
    particle.style.background = index % 3 === 0 ? color : index % 3 === 1 ? "#49c6dc" : "#ef6fa8";
    particle.style.animationDelay = `${index * 0.018}s`;
    ui.particleLayer.appendChild(particle);
    setTimeout(() => particle.remove(), 1400);
  }
}

function toast(message) {
  const element = document.createElement("div");
  element.className = "toast";
  element.textContent = message;
  ui.toastLayer.appendChild(element);
  setTimeout(() => element.remove(), 3200);
}

function rewardText(rewards) {
  const result = [];
  if (rewards.coins) result.push(`+${rewards.coins} koin`);
  if (rewards.gems) result.push(`+${rewards.gems} permata`);
  if (rewards.books) result.push(`+${rewards.books} buku`);
  if (rewards.xp) result.push(`+${rewards.xp} XP`);
  if (rewards.badge) result.push(`Badge: ${rewards.badge}`);
  return result;
}

function subjectName(type) {
  return {
    math: "Matematika",
    money: "Matematika - Uang",
    fraction: "Matematika - Pecahan",
    science: "IPA",
    ecosystem: "IPA - Ekosistem",
    language: "Bahasa Indonesia",
    sentence: "Bahasa Indonesia - Kalimat",
    culture: "IPS",
    houseculture: "IPS - Rumah Adat",
    watercycle: "IPA - Siklus Air",
    space: "IPA - Tata Surya",
    adaptation: "IPA - Adaptasi",
    geometry: "Matematika - Geometri",
    energy: "IPA - Energi",
    civics: "PPKn - Pancasila",
    history: "IPS - Sejarah",
    proverb: "Bahasa Indonesia - Peribahasa",
    scale: "IPS/Matematika - Skala",
    synonym: "Bahasa Indonesia - Padanan Kata"
  }[type] || "Mini Game";
}

function pick(list) {
  return list[Math.floor(Math.random() * list.length)];
}

function shuffle(list) {
  return [...list].sort(() => Math.random() - 0.5);
}

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

function random(min, max) {
  return Math.random() * (max - min) + min;
}

function getDistance(a, b) {
  return Math.hypot(a.x - b.x, a.y - b.y);
}

function triggerEmote(targetElement, emoji) {
  if (!targetElement) return;
  let emote = targetElement.querySelector(".player-emote");
  if (!emote) {
    emote = document.createElement("span");
    emote.className = "player-emote";
    targetElement.appendChild(emote);
  }
  emote.textContent = emoji;
  emote.classList.remove("hidden");
  // Force restart animation
  emote.style.animation = "none";
  void emote.offsetHeight; // force reflow
  emote.style.animation = "";
  
  if (targetElement.emoteTimer) clearTimeout(targetElement.emoteTimer);
  targetElement.emoteTimer = setTimeout(() => {
    emote.classList.add("hidden");
  }, 1200);
}

window.addEventListener("resize", positionPlayer);
window.addEventListener("load", init);
