Skip to content

thientnl10/Test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 

Repository files navigation

<!doctype html>

<title>Mobile RPG UI - Inventory (Dropdown)</title> <style> :root{--gap:10px;--nav-height:64px;--accent:#6b5ce7} *{box-sizing:border-box;font-family:Inter,system-ui,Arial} html,body{height:100%;margin:0;background:#0f172a;color:#e6eef8} .app{min-height:100vh;display:flex;flex-direction:column} .header{padding:12px 14px;background:linear-gradient(180deg,#081028 0%,#071224 100%);display:flex;align-items:center;justify-content:space-between} .header h1{font-size:16px;margin:0} .view{flex:1;overflow:auto;padding:12px} .row{display:flex;gap:var(--gap)} .left{flex:1;min-width:220px} .right{width:40%;min-width:200px} .char-wrap{position:relative;background:linear-gradient(180deg,#0b1220,#071025);padding:8px;border-radius:12px} canvas#charCanvas{width:100%;height:320px;border-radius:8px;background:#08121b;display:block} .equip-slot{position:absolute;width:48px;height:48px;border-radius:6px;background:rgba(255,255,255,0.03);display:flex;align-items:center;justify-content:center;border:1px dashed rgba(255,255,255,0.04);font-size:20px;cursor:pointer} .equip-slot {opacity: 0.6;border: 1px dashed rgba(255,255,255,0.15);transition: all 0.2s ease;} .equip-slot.equipped {opacity: 1;border: 0.5px solid gold;box-shadow: 0 0 6px gold;} .panel{background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);padding:10px;border-radius:10px;margin-bottom:10px} .panel h3{margin:0 0 8px 0;font-size:13px} .list-item{padding:8px;border-radius:8px;background:rgba(0,0,0,0.15);margin-bottom:6px;font-size:13px} .nav{position:fixed;left:0;right:0;bottom:0;height:var(--nav-height);display:flex;background:linear-gradient(180deg,#061021,#04101a);border-top:1px solid rgba(255,255,255,0.03);gap:6px;padding:8px; z-index: 10} .nav button{flex:1;border-radius:10px;border:0;padding:10px;font-size:14px;cursor:pointer}/* inventory grid: responsive square cells */ .training-panel, .modal {position: fixed;top: 0;left: 0;right: 0;bottom: 0;display: none;background: rgba(0,0,0,0.6);z-index: 100;} .inv-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;align-items:start} .inv-slot{background:linear-gradient(180deg,#071024,#061220);border-radius:8px;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:10px;cursor:pointer;aspect-ratio:1/1;overflow:hidden;text-align:center} .inv-slot .icon{font-size:28px;line-height:1} .inv-slot .name{font-size:13px;margin-top:8px;line-height:1.1;color:inherit;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} .inv-slot .qty{font-size:12px;color:rgba(255,255,255,0.75);margin-top:6px} .header-toggle {display: inline-block;border: 1px solid rgba(255,255,255,0.2);border-radius: 6px;padding: 4px 10px;background: rgba(0,0,0,0.25);margin-bottom: 10px;} .header-toggle h3 {margin: 0;font-size: 14px;font-weight: normal;}

/* dropdown filter */ .filter-container{position:relative;display:inline-block} .filter-btn{background:rgba(255,255,255,0.03);border:0;color:inherit;padding:8px 10px;border-radius:8px;cursor:pointer} .filter-options{display:none;position:absolute;top:110%;left:0;background:linear-gradient(180deg,#071025,#061224);border:1px solid rgba(255,255,255,0.03);border-radius:8px;padding:6px;z-index:30;min-width:160px} .filter-options button{display:block;width:100%;background:none;border:0;color:inherit;text-align:left;padding:8px;border-radius:6px;cursor:pointer} .filter-options button:hover{background:rgba(255,255,255,0.03)}

.tag{font-size:14px;padding:4px 6px;border-radius:6px;background:rgba(255,255,255,0.03);display:inline-block}
.modal{position:fixed;left:0;top:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.6);padding:12px}
.modal .card {background:#071025;padding:14px;border-radius:10px;max-width:420px;width:100%;max-height:80vh;overflow-y:auto;}

.skills-list{display:flex;flex-direction:column;gap:8px;}
.skill{padding:10px;border-radius:8px;background:linear-gradient(180deg,#061226,#04101a);display:flex;justify-content:space-between;align-items:center}
.skill-slots {display: grid;grid-template-columns: repeat(2, 60px);grid-template-rows: repeat(5, 60px);gap: 8px;}
.skill-slot {width: 60px;height: 60px;border-radius: 6px;border: 1px dashed rgba(255,255,255,0.2);display: flex;align-items: center;justify-content: center;font-size: 12px;text-align: center;cursor: pointer;}
.skill-slot.active {border: 1px solid gold;box-shadow: 0 0 6px gold;}
.skills-container {display:flex;gap:12px;max-height:400px; /* để kéo */overflow-y:auto; /* scroll được */}
.equip-slots {display:grid;grid-template-columns: repeat(2, 60px);grid-template-rows: repeat(5, 60px);gap:8px;}
.slot {width:60px; height:60px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;display:flex; align-items:center; justify-content:center;font-size:12px;opacity:0.3;}
.slot.active {opacity:1;}
.learned-skills {flex:1;display:flex;flex-direction:column;gap:8px;}
.learned-skills .skill-item {padding:8px;border-radius:6px;background:rgba(0,0,0,0.2);cursor:pointer;}
#view-skills .panel {max-height: 400px;overflow-y: auto;}
#skillsList {max-height: 400px;overflow-y: auto;}
#invGrid {max-height: calc(100vh - 180px);overflow-y: auto;scrollbar-width: thin;scrollbar-color: #6b5ce7 #08121b;}
#invGrid::-webkit-scrollbar {width: 6px;}
#invGrid::-webkit-scrollbar-thumb {background: #6b5ce7;border-radius: 6px;}

#worldMapCanvas {position: fixed;top: 0;left: 0;width: 100vw;height: 100vh;display: block;background: black;z-index: 1;}
#mapControls {position: fixed;top: 12px;left: 12px;z-index: 9999;display: flex;gap: 8px;flex-wrap: wrap;}
#mapControls button {background: rgba(255, 255, 255, 0.08);border: 1px solid rgba(255, 255, 255, 0.2);color: #fff;padding: 6px 12px;border-radius: 8px;cursor: pointer;font-size: 13px;backdrop-filter: blur(4px);transition: all 0.2s ease;}
#mapControls button:hover {background: rgba(107, 92, 231, 0.4); /* tím accent giống UI */border-color: #6b5ce7;box-shadow: 0 0 6px #6b5ce7;}

#dungeonExitBtn button{ min-width:130px; padding:8px 12px; border-radius:10px; border:0; cursor:pointer; background: rgba(255,255,255,0.06); } #dungeonExitBtn button:hover{ background: rgba(255,255,255,0.12); }

.map-wrap{background:linear-gradient(180deg,#061122,#04111a);padding:10px;border-radius:10px}
.world-map{width:100%;height:320px;border-radius:8px;background-image:radial-gradient(circle at 30% 20%, rgba(255,255,255,0.02), transparent 10%), linear-gradient(180deg,#091524,#04111a)}
.stat-btn{background:transparent;border:0;color:inherit;font:inherit;cursor:pointer;padding:4px 6px;border-radius:6px}
.stat-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.panel:last-of-type{margin-bottom: calc(var(--nav-height) + 0px)}
@media (max-width:720px){.row{flex-direction:column}.right{width:auto}canvas#charCanvas{height:260px}.inv-grid{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}}

@keyframes fadeOut { from { opacity: 1; transform: scale(1) translate(-50%, -50%); } to { opacity: 0; transform: scale(2) translate(-50%, -50%); } }

/* progress bars */ .bar-wrap { margin:6px 0; text-align:left; } .bar-label { font-size:12px; margin-bottom:2px; display:block; } .bar { width:100%; height:16px; border-radius:6px; background:rgba(255,255,255,0.08); overflow:hidden; position:relative; } .bar-fill { height:100%; width:50%; background:#6b5ce7; transition:width .3s ease; } .bar-text { position:absolute; top:0;left:0;right:0;bottom:0; display:flex;align-items:center;justify-content:center; font-size:11px; color:#fff; } .bar.hp .bar-fill {background:#e74c3c;} .bar.mp .bar-fill {background:#3498db;} .bar.stamina .bar-fill {background:#27ae60;}

</style>

RPG Mobile UI (Demo)

Level: 1 • XP: 0/100 Reset +10 Lv -10%
👑
🛡️
🖐️
🦵
⚔️
🗡️
📿
📿
📿
💍
💍
💍
💍
💍
HP
MP
Sức Bền

Trạng thái

Bình thường

Buff / Debuff

Không có

Danh hiệu

Tân thủ

Đẳng cấp

Võ giả

Chỉ số

Đang tải...
Số lượng: 6/50
Phân loại: Tất cả
Tất cả Công pháp Trang bị Đan dược Khác

Công pháp đã học

<!-- Dungeon canvas -->
<canvas id="dungeonCanvas" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:100;background:black"></canvas>

<!-- Container điều khiển map -->
<div id="mapControls"></div>
🏠 Khởi đầu 🎒 Túi đồ 📖 Công pháp 🗺️ Bản đồ
<script> // --- Dữ liệu mặc định --- const playerDefault = { level:1, xp:0, xpToNext:100, stats:{ HP:100, MaxHP:100, Mana:50, MaxMana:50, SucBen:20, MaxSucBen:20, TheChat:10, TriLuc:9, NhanhNhen:7, MayMan:10, Khang:10, CanCot:10, NgoTinh:10 }, inventory:[ {id:'1001', qty:3}, {id:'1004', qty:1}, {id:'1003', qty:1}, {id:'1002', qty:1}, {id:'1005', qty:1}, {id:'2002', qty:1}, {id:'2001', qty:1}, {id:'1006', qty:1} ], invMax: 50, techniques:[], // công pháp đã học skills:[], // kỹ năng đã mở equipment:{ head:null, body:null, hand:null, leg:null, necklace:null, bracelet1:null, bracelet2:null, ring1:null, ring2:null, ring3:null, ring4:null, ring5:null, "main-weapon":null,"sub-weapon":null } };
const gameData = {
  "items": [
    {"id":"1001","type":"potion","name":"Đan Dược Hồi Phục","rarity":"Phổ","effect":{"HP":50},"description":"Khôi phục 50 HP ngay lập tức."},
    {"id":"1002","type":"weapon","name":"Kiếm Huyết Ảnh","slot":"main-weapon","rarity":"Huyền","stats":{"attack":15,"critRate":0.1},"durability":200,"effects":[{"type":"lifesteal","value":0.05},{"type":"statusBoost","status":"bloodlust","value":0.1}],"description":"Thanh kiếm đỏ như máu, tăng sức mạnh khi HP thấp."},
    {"id":"1003","type":"armor","name":"Giáp Hàn Băng","slot":"body","rarity":"Địa","stats":{"defense":20,"resistance":{"ice":0.2}},"durability":300,"effects":[{"type":"statusResistance","status":"freeze","value":0.5}],"description":"Giáp băng giá, giảm sát thương băng và kháng đóng băng."},
    {"id":"1004","type":"potion","name":"Đan Dược Hồi Phục Trung Cấp","rarity":"Phổ","effect":{"HP":200},"description":"Khôi phục 200 HP ngay lập tức."},
    {"id":"1005","type":"ring","name":"Nhẫn Vận Mệnh","slot":"ring","rarity":"Thiên","stats":{"MayMan":10},"durability":100,"effects":[{"type":"critBoost","value":0.15},{"type":"dropRateBoost","value":0.2}],"description":"Nhẫn cổ truyền, làm gia tăng may mắn và cơ hội nhặt đồ hiếm."},
    {"id":"1006","type":"ring","name":"Nhẫn Không Gian","slot":"ring","effects":[{"type":"invBonus","value":10}]},
   ],

// === Công pháp (Techniques) === "techniques": [ { id: "2001", type: "techniques", name: "Bát Chuyển Huyền Công", // Tên công pháp summary: "Cường hóa thân thể, mở huyệt đạo", // Tổng cương rarity: "Địa", // Đẳng cấp: Hoàng/Huyền/Địa/Thiên/Thần/Nguyên Sơ rules: [ // Quy tắc lĩnh ngộ { level: 3, effect: "Ngộ Quy Tắc Lực – +10% sát thương vật lý" }, { level: 6, effect: "Ngộ Quy Tắc Kháng – giảm 10% sát thương nhận vào" } ], hiddenLine: [ // Dòng ẩn (kích hoạt khi đủ điều kiện) { condition: "Căn Cốt ≥ 25 và đã học Cuồng Quyền", effect: "Thể Huyết Chi Lực – HP +20%" } ], description: "Công pháp rèn luyện huyết nhục, gia tăng thể chất.", // Mô tả levelDetails:[ {level:1, bonus:{MaxHP:50,MaxSucBen:5}, expRequired: 100, description:"Tăng cường thể chất ban đầu"}, {level:2, bonus:{MaxHP:100,MaxSucBen:10}, expRequired: 150, description:"Gia tăng sinh lực đáng kể"}, {level:3, bonus:{MaxHP:150,MaxSucBen:15}, expRequired: 200, description:"Thân thể đạt độ bền vững cao"} ], skillsUnlocked: ["3001"], // Kỹ năng nhận được khi học công pháp skillLevelUpRules: [ // 🌟 Thêm phần này { skillId: "3001", techLv: 1, skillLv: 1 }, { skillId: "3001", techLv: 3, skillLv: 2 }, { skillId: "3001", techLv: 6, skillLv: 3 } ] }, { id: "2002", type: "techniques", name: "Cửu Chuyển Linh Khí", summary: "Dẫn dắt linh khí thiên địa", rarity: "Thiên", rules: [ { level: 5, effect: "Ngộ Quy Tắc Linh Khí – Mana phục hồi gấp đôi" } ], hiddenLine: [ { condition: "Ngộ tính ≥ 30", effect: "Linh Hồn Chi Hỏa – kỹ năng hệ hỏa +20%" } ], description: "Tăng cường nội lực, đại幅提升 Mana.", levelDetails: [ { level: 1, bonus: { MaxMana: 50, TriLuc: 5 }, expRequired: 150 }, { level: 2, bonus: { MaxMana: 120, TriLuc: 10 }, expRequired: 300 } ], skillsUnlocked: ["3002"], skillLevelUpRules: [ { skillId: "3002", techLv: 1, skillLv: 1 }, { skillId: "3002", techLv: 2, skillLv: 2 }, { skillId: "3002", techLv: 5, skillLv: 3 } ] } ],

// === Kỹ năng (Skills) === "skills": [ { id: "3001", name: "Cuồng Quyền", // Tên kỹ năng level: 1, // Lv kỹ năng description: "Ra đòn liên hoàn cực mạnh.", // Giới thiệu cooldown: 5, // Cooldown manaCost: 10, // Mana Cost damage: { physical: 20 } // Sát thương }, { id: "3002", name: "Linh Khí Bạo Tạc", level: 1, description: "Giải phóng linh khí trong cơ thể gây sát thương diện rộng.", cooldown: 8, manaCost: 20, damage: { magic: 40 } } ] };

// helper DOM

function $(id){return document.getElementById(id);}

// --- Lưu / tải ---

function findSkillName(id){ const it = gameData.items.find(x=>String(x.id)===String(id)); return it ? it.name : ('Kỹ năng ' + id); }

function saveGame(){

try { // đảm bảo luôn là mảng 10 phần tử if (!Array.isArray(equippedSkills) || equippedSkills.length !== 10) { equippedSkills = Array(10).fill(null).map((v, i) => equippedSkills[i] || null); }

const saveData = {
  player: player,
  equippedSkills: equippedSkills
};

localStorage.setItem('rpgSave', JSON.stringify(saveData));

} catch(e){ console.error('save failed', e); } } function loadGame(){ const data = localStorage.getItem('rpgSave'); if(data){ try { const parsed = JSON.parse(data) || null;

  // lấy player
  player = parsed.player || null;
  if(!player || typeof player !== 'object') throw new Error('Invalid save data');

  if(!player.stats) player.stats = JSON.parse(JSON.stringify(playerDefault.stats));
  if(!player.inventory) player.inventory = JSON.parse(JSON.stringify(playerDefault.inventory));
  if(!player.techniques) player.techniques = [];
  if(!player.skills) player.skills = [];

  // chuẩn hóa công pháp
  player.techniques = (player.techniques||[]).map(t=>{
    const base = gameData.techniques.find(x=>x.id===t.id) || {};
    return { 
      id: t.id, 
      name: t.name || base.name || ('Công pháp ' + t.id), 
      lvl: t.lvl || 1, 
      exp: t.exp || 0 
    };
  });

  // chuẩn hóa kỹ năng
  player.skills = (player.skills||[]).map(s=>{
    const base = gameData.skills.find(x=>x.id===s.id) || {};
    return { 
      id: s.id, 
      name: s.name || base.name || ('Kỹ năng ' + s.id), 
      lvl: s.lvl || base.level || 1, 
      exp: s.exp || 0 
    };
  });

  // lấy equippedSkills (nếu có), mặc định 10 slot
  equippedSkills = parsed.equippedSkills || Array(10).fill(null);

} catch(e){
  console.error('Load error', e);
  player = JSON.parse(JSON.stringify(playerDefault));
  equippedSkills = Array(10).fill(null);
  saveGame();
}

} else { player = JSON.parse(JSON.stringify(playerDefault)); equippedSkills = Array(10).fill(null); saveGame(); } }

function add10Levels(){ if(!player) return; for(let i=0;i<10;i++){ player.level += 1; const delta = getLevelUpDeltaForLevel(player.level); // Cộng chỉ số theo quy tắc Object.keys(player.stats).forEach(k=>{ player.stats[k] = (player.stats[k] || 0) + delta; }); // tăng XP yêu cầu cho cấp tiếp theo player.xpToNext = Math.floor(player.xpToNext * 1.25); } saveGame(); updateHeader(); renderStats(); updateBars(); $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Tăng cấp nhanh</h3> <p>Nhân vật đã tăng thêm 10 cấp! Hiện tại: Lv ${player.level}</p> <div style="display:flex;gap:8px;margin-top:8px"> <button onclick="closeModal()">Đóng</button> </div>; }

// --- Công thức (placeholder) ---

function calcFinalStat(base, key) { let val = Math.floor(Number(base) || 0);

// Cộng từ trang bị if (player && player.equipment) { Object.values(player.equipment).forEach(eid => { const item = findItemById(eid); if (item && item.stats && key in item.stats) { val += Number(item.stats[key]) || 0; } }); }

// Cộng từ công pháp (techniques) (player.techniques || []).forEach(t => { const tech = gameData.techniques.find(x => x.id === t.id); if (tech && tech.levelDetails) { const current = tech.levelDetails.find(ld => ld.level === t.lvl); if (current && current.bonus && key in current.bonus) { val += current.bonus[key]; } } });

return val; }

function getLevelUpDeltaForLevel(level) { // ❖ Giữ lại ý tưởng cũ: mỗi 10 cấp thưởng thêm // ❖ Nhưng thêm ảnh hưởng của CanCot và NgoTinh

const base = (level % 10 === 0) ? 5 : 1;

const canCot = calcFinalStat(player.stats.CanCot, "CanCot"); const ngoTinh = calcFinalStat(player.stats.NgoTinh, "NgoTinh");

// CanCốt cao → cần ít exp hơn, nên thưởng ít hơn // Ngộ tính cao → thưởng thêm exp để đồng bộ hóa tốc độ tiến bộ const factor = 1 + (ngoTinh * 0.01) - (canCot * 0.005);

return Math.max(1, Math.round(base * factor)); }

// --- Inventory filter state ---
let currentInvFilter = 'all'; // all, skill, equip, potion, other

// Biến dùng cho animation nhân vật let animFrame = 0; let blinkToggle = false;

function mapItemToCategory(item){ if(!item || !item.type) return 'other'; const t = item.type; if(t === 'techniques') return 'technique'; // Công pháp if(t === 'potion') return 'potion'; if(t === 'weapon' || t === 'armor' || t === 'ring') return 'equip'; return 'other'; }

function showView(name){

const views = ['start','inv','skills','map']; views.forEach(v=>{ const el = document.getElementById('view-'+v); if(el) el.style.display = (v === name ? 'block' : 'none'); });

// nếu rời map -> xóa luôn controls if (name !== 'map') { removeDungeonControls(); } else { // vào map: nếu đang trong dungeon thì đảm bảo controls tồn tại if (currentMapLevel === 'dungeon') { // tránh tạo đôi if (!document.getElementById('dungeonControls')) setupDungeonControls(); } else { // đang ở region/world/cosmos trong view map -> không hiển thị controls removeDungeonControls(); } }

updateHeader(); if(name === 'inv') renderInventory(); if(name === 'skills') renderSkills(); renderStats(); updateBars(); saveGame(); }

function getClassByLevel(level){ if(level <= 9) return "Phàm nhân - không năng lực siêu phàm"; if(level <= 39){ if(level < 20) return "Khai Linh sơ kỳ"; if(level < 30) return "Khai Linh trung kỳ"; return "Khai Linh hậu kỳ"; } if(level <= 69){ if(level < 50) return "Lột Xác sơ kỳ"; if(level < 60) return "Lột Xác trung kỳ"; return "Lột Xác hậu kỳ"; } if(level <= 98){ if(level < 80) return "Hóa Thần sơ kỳ"; if(level < 90) return "Hóa Thần trung kỳ"; return "Hóa Thần hậu kỳ"; } if(level === 99) return "Bán Thần"; if(level >= 100){ if(player.specialClass === "nguyenso") return "Nguyên Sơ - Thế Giới chi chủ"; return "Đăng Thần"; // mặc định giữ nguyên } return "Phàm nhân"; }

function updateHeader(){

if(!player) return; $('playerLevel').innerText = player.level; $('playerXP').innerText = player.xp; $('xpToNext').innerText = player.xpToNext; $('statusText').innerText = "Bình thường";

const hpNow = Number(player.stats.HP) || 0; const hpMax = calcFinalStat(player.stats.MaxHP, "MaxHP"); const hpPct = Math.max(0, Math.min(100, Math.round((hpNow/hpMax)*100))); const hpBar = $('hpBar'); if(hpBar) hpBar.style.width = hpPct + "%"; const hpText = $('hpText'); if(hpText) hpText.innerText = ${hpNow}/${hpMax};

const mpNow = Number(player.stats.Mana) || 0; const mpMax = calcFinalStat(player.stats.MaxMana, "MaxMana"); const mpPct = Math.max(0, Math.min(100, Math.round((mpNow/mpMax)*100))); const mpBar = $('mpBar'); if(mpBar) mpBar.style.width = mpPct + "%"; const mpText = $('mpText'); if(mpText) mpText.innerText = ${mpNow}/${mpMax};

const stNow = Number(player.stats.SucBen) || 0; const stMax = calcFinalStat(player.stats.MaxSucBen, "MaxSucBen"); const stPct = Math.max(0, Math.min(100, Math.round((stNow/stMax)*100))); const stBar = $('staminaBar'); if(stBar) stBar.style.width = stPct + "%"; const stText = $('staminaText'); if(stText) stText.innerText = ${stNow}/${stMax};

const classPanel = $('panel-class'); if(classPanel){ const el = classPanel.querySelector('.list-item'); if(el) el.innerText = getClassByLevel(player.level); } }

function updateBars() { if(!player) return;

const hpNow = Number(player.stats.HP) || 0; const hpMax = calcFinalStat(player.stats.MaxHP, "MaxHP"); $('hpBar').style.width = Math.max(0, Math.min(100, Math.round((hpNow/hpMax)*100))) + "%"; $('hpText').innerText = ${hpNow}/${hpMax};

const mpNow = Number(player.stats.Mana) || 0; const mpMax = calcFinalStat(player.stats.MaxMana, "MaxMana"); $('mpBar').style.width = Math.max(0, Math.min(100, Math.round((mpNow/mpMax)*100))) + "%"; $('mpText').innerText = ${mpNow}/${mpMax};

const stNow = Number(player.stats.SucBen) || 0; const stMax = calcFinalStat(player.stats.MaxSucBen, "MaxSucBen"); $('staminaBar').style.width = Math.max(0, Math.min(100, Math.round((stNow/stMax)*100))) + "%"; $('staminaText').innerText = ${stNow}/${stMax}; }

let showingTechniques = true;

$('skillsHeader').onclick = function(){ showingTechniques = !showingTechniques; if(showingTechniques){ $('skillsHeader').innerText = "Công pháp đã học"; $('skillsList').style.display = "block"; $('skillsContainer').style.display = "none"; renderTechniques(); } else { $('skillsHeader').innerText = "Kỹ năng đã học"; $('skillsList').style.display = "none"; $('skillsContainer').style.display = "flex"; renderLearnedSkills(); } };

function renderLearnedSkills(){ const list = $('learnedSkillsList'); list.innerHTML = '';

if(player.skills.length === 0){ list.innerHTML = '

Chưa học kỹ năng nào
'; return; }

player.skills.forEach(s=>{ const el = document.createElement('div'); el.className = 'skill-item'; el.innerHTML = <strong>${s.name}</strong> <span style="font-size:12px">Lv ${s.lvl}</span>; el.onclick = ()=>viewSkill(s.id, "learned"); // ✅ chỉ chỗ này cho phép trang bị list.appendChild(el); });

renderSkillSlots(); }

let equippedSkills = Array(10).fill(null);

function equipSkill(skillId){ // kiểm tra kỹ năng đã trang bị chưa const alreadyIndex = equippedSkills.findIndex(s => s === skillId); if(alreadyIndex !== -1){ $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Thông báo</h3> <p>Kỹ năng này đã được trang bị ở ô ${alreadyIndex+1}.</p> <div style="margin-top:8px;display:flex;gap:8px"> <button onclick="closeModal()">Đóng</button> </div>; return; }

// tìm slot trống const index = equippedSkills.findIndex(s => s === null); if(index === -1){ $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Thông báo</h3> <p>Tất cả ô kỹ năng đã đầy!</p> <div style="margin-top:8px;display:flex;gap:8px"> <button onclick="closeModal()">Đóng</button> </div>; return; }

equippedSkills[index] = skillId; saveGame(); closeModal(); renderSkillSlots(); }

function unequipSkill(index){ equippedSkills[index] = null; saveGame(); renderSkillSlots(); }

let draggedIndex = null;

function renderSkillSlots(){ const slots = document.querySelectorAll('#skillSlots .skill-slot'); slots.forEach((slot, i)=>{ const skillId = equippedSkills[i];

// reset ô
slot.innerHTML = '';
slot.classList.remove('active');

if(skillId){
  const skill = player.skills.find(s=>s.id === skillId);
  const data = gameData.skills.find(s=>s.id === skillId);
  if(skill && data){
    slot.classList.add('active');
    slot.innerHTML = `
      <div style="text-align:center">
        <div style="font-weight:bold">${data.name}</div>
        <div style="font-size:11px;opacity:0.8">Lv ${skill.lvl}</div>
      </div>`;
  }
}

// click → gỡ kỹ năng khỏi slot
slot.onclick = ()=>unequipSkill(i);

// drag & drop
slot.setAttribute("draggable", "true");

slot.ondragstart = ()=>{
  draggedIndex = i;
};

slot.ondragover = (e)=>{
  e.preventDefault(); // cho phép drop
};

slot.ondrop = (e)=>{
  e.preventDefault();
  if(draggedIndex !== null && draggedIndex !== i){
    // hoán đổi skill giữa 2 slot
    [equippedSkills[draggedIndex], equippedSkills[i]] = [equippedSkills[i], equippedSkills[draggedIndex]];
    saveGame();
    renderSkillSlots();
  }
  draggedIndex = null;
};

}); }

// === RENDER TÚI ĐỒ (có lọc) ===
function renderInventory(){

const grid = $('invGrid'); if(!grid) return; grid.innerHTML = ''; if(!player || !player.inventory || player.inventory.length===0){ grid.innerHTML = '

Túi đồ rỗng
'; $('invCount').innerText = 0; return; }

// TÍNH TỔNG SỐ LƯỢNG TOÀN BỘ let totalAll = 0; player.inventory.forEach(entry=>{ totalAll += (entry.qty || 1); }); $('invCount').innerText = totalAll; $('invMax').innerText = getInventoryLimit();

// Lọc theo phân loại để hiển thị const filtered = player.inventory.filter(entry=>{ const data = findItemById(entry.id); if(!data) return false; const cat = mapItemToCategory(data); if(currentInvFilter === 'all') return true; return cat === currentInvFilter; });

filtered.forEach(entry=>{ const data = findItemById(entry.id); if(!data) return; const el = document.createElement('div'); el.className='inv-slot'; el.innerHTML = <div class="icon">${getItemIcon(data)}</div><div class="name">${data.name}</div><div class="qty">x${entry.qty||1}</div>; el.onclick = ()=> openItem(entry.id); grid.appendChild(el); });

// update visible filter label const labelMap = { all: 'Tất cả', technique: 'Công pháp', skill: 'Kỹ năng', equip: 'Trang bị', potion: 'Đan dược', other: 'Khác' }; const filt = $('invFilter'); if(filt) filt.innerText = labelMap[currentInvFilter] || currentInvFilter;

if(filtered.length === 0){ grid.innerHTML = '

Không tìm thấy vật phẩm theo phân loại này.
'; } }

function getInventoryLimit(){ let limit = player.invMax || 50;

// ví dụ: nhẫn Vận Mệnh tăng 10 ô if(player.equipment){ Object.values(player.equipment).forEach(eid=>{ const it = findItemById(eid); if(it && it.effects){ it.effects.forEach(e=>{ if(e.type === "invBonus"){ // 👈 thêm type mới limit += e.value || 0; } }); } }); } return limit; } function addItem(id, qty=1){ const totalAll = player.inventory.reduce((a,b)=>a+(b.qty||1),0); const limit = getInventoryLimit(); if(totalAll + qty > limit){ $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Thông báo</h3> <p>Túi đồ đã đầy (${totalAll}/${limit}). Không thể thêm ${qty} vật phẩm.</p> <div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>; return false; }

let entry = player.inventory.find(x=>x.id===id); if(entry) entry.qty += qty; else player.inventory.push({id:id, qty:qty});

saveGame(); renderInventory(); return true; }

function getItemIcon(it){

switch(it.type){ case 'techniques': return '📜'; // công pháp case 'skill': return '✨'; // kỹ năng case 'weapon': return '⚔️'; case 'potion': return '⚗️'; case 'armor': return '🛡️'; case 'ring': return '💍'; default: return 'ℹ️'; } }

function findItemById(id){

let it = null; if(gameData.items) it = gameData.items.find(x => String(x.id) === String(id)); if(!it && gameData.techniques) it = gameData.techniques.find(x => String(x.id) === String(id)); if(!it && gameData.skills) it = gameData.skills.find(x => String(x.id) === String(id)); return it || null; }

// --- Hiển thị chi tiết item ---

function getItemDetailsHTML(data, inv){ if(!data) return '

Dữ liệu vật phẩm không tìm thấy.
';

let html = <div style="font-size:18px;margin-bottom:6px">${getItemIcon(data)} <strong>${data.name}</strong> <span class="small">${data.rarity||''}</span></div>;

// Công pháp if(data.type === 'techniques' || data.type === 'technique'){ html += <div><strong>Tổng cương:</strong> ${data.summary||''}</div>; html += <div><strong>Đẳng cấp:</strong> ${data.rarity||''}</div>; if(data.description) html += <div><strong>Mô tả:</strong> ${data.description}</div>;

// Chi tiết tất cả cấp độ
if(Array.isArray(data.levelDetails) && data.levelDetails.length){
  html += `<div style="margin-top:8px"><strong>Chi tiết:</strong>`;
  data.levelDetails.forEach(ld=>{
    let bonusText = Object.entries(ld.bonus)
      .map(([k,v])=>`+${v} ${translateStatName(k)}`)
      .join(", ");
    html += `<div class="small">Lv ${ld.level}: ${ld.description||''}</div>`;

html += <div class="small" style="margin-left:12px">${bonusText}</div>; }); html += </div>; }

// Quy tắc lĩnh ngộ
if(Array.isArray(data.rules) && data.rules.length){
  html += `<div style="margin-top:8px"><strong>Quy tắc lĩnh ngộ:</strong>`;
  data.rules.forEach(r=>{
    html += `<div class="small">Lv ${r.level}: ${r.effect}</div>`;
  });
  html += `</div>`;
}

// Dòng ẩn
if(Array.isArray(data.hiddenLine) && data.hiddenLine.length){
  html += `<div style="margin-top:8px"><strong>Dòng ẩn:</strong>`;
  data.hiddenLine.forEach(r=>{
    html += `<div class="small">• ${r.condition} → ${r.effect}</div>`;
  });
  html += `</div>`;
}

// Kỹ năng học được
if(Array.isArray(data.skillsUnlocked) && data.skillsUnlocked.length && gameData && Array.isArray(gameData.skills)){
  html += `<div style="margin-top:8px"><strong>Kỹ năng học được:</strong>`;
  data.skillsUnlocked.forEach(sid=>{
    const skill = gameData.skills.find(x=>x.id===sid);
    if(skill){
      html += `<div class="small">✨ ${skill.name} (Lv ${skill.level}) - ${skill.description}</div>`;
    }
  });
  html += `</div>`;
}

if(inv) html += `<div class="small" style="margin-top:8px">Số lượng: ${inv.qty||1}</div>`;

} // Vật phẩm khác (trang bị, đan dược…) else{ if(data.stats){ for(const k in data.stats){ html += <div class="small">${k}: ${JSON.stringify(data.stats[k])}</div>; } } if(data.effect){ for(const k in data.effect){ html += <div class="small">+${data.effect[k]} ${k}</div>; } } if(Array.isArray(data.effects)){ data.effects.forEach(e=> html += <div class="small">* ${e.type}: ${e.value}</div>); } if(data.description) html += <div style="margin-top:8px;font-style:italic;color:rgba(255,255,255,0.8)">${data.description}</div>; if(inv) html += <div class="small" style="margin-top:8px">Số lượng: ${inv.qty||1}</div>; } return html; }

// === MODAL ITEM ===
function openItem(id){

const inv = player.inventory.find(x=>String(x.id)===String(id)); const data = findItemById(id); if(!inv || !data){ console.warn('openItem: not found', id); return; } const details = getItemDetailsHTML(data, inv);

let actionBtns = <button onclick="closeModal()">Đóng</button>; if(data.type === 'potion'){ actionBtns = <button id="useBtn">Sử dụng</button> + actionBtns; } // Nếu là trang bị -> thêm nút "Trang bị" if(data.type === 'weapon' || data.type === 'armor' || data.type === 'ring' || data.slot === 'necklace'){ actionBtns = <button onclick="autoEquipItem('${id}')">Trang bị</button> + actionBtns; } if(data.type === 'techniques'){ actionBtns = <button onclick="learnTechnique('${id}')">Học</button> + actionBtns; }

$('modal').style.display = 'flex'; $('modalCard').innerHTML = <div id="itemDetail">${details}</div><div style="display:flex;gap:8px;margin-top:8px">${actionBtns}</div>;

const useBtn = $('useBtn'); if(data.type === 'potion' && useBtn){ useBtn.onclick = ()=> useItem(id); } }

function closeModal() {

// 🚫 Ngăn đóng khi đang tu luyện if (trainingInProgress) { return; }

const m = $('modal'); if (m) { // 🩵 Thêm hiệu ứng mờ dần khi đóng m.style.transition = "opacity 0.2s ease"; m.style.opacity = "0"; setTimeout(() => { m.style.display = "none"; m.style.opacity = ""; m.style.zIndex = ""; // 🔹 reset z-index để tránh ảnh hưởng giao diện khác }, 200); }

// Gỡ sự kiện dùng vật phẩm (nếu có) const useBtn = $('useBtn'); if (useBtn) useBtn.onclick = null; }

// === SỬ DỤNG VẬT PHẨM ===
function useItem(itemId){

const idx = player.inventory.findIndex(x=>String(x.id)===String(itemId)); if(idx === -1){ alert('Item không tồn tại'); return; }

const inv = player.inventory[idx]; const data = findItemById(itemId); if(!data){ alert('Dữ liệu vật phẩm không hợp lệ'); return; }

if(data.type === 'potion' && data.effect){ if(data.effect.HP) player.stats.HP = Math.min(player.stats.MaxHP || 99999, (player.stats.HP || 0) + Number(data.effect.HP)); if(data.effect.Mana) player.stats.Mana = Math.min(player.stats.MaxMana || 99999, (player.stats.Mana || 0) + Number(data.effect.Mana));

// hiện bảng kết quả (không dùng alert)
$('modal').style.display = 'flex';
$('modalCard').innerHTML = `
  <h3>Kết quả</h3>
  <p>Sử dụng ${data.name}: ${data.effect.hp ? `+${data.effect.hp} HP` : ''} ${data.effect.mana ? `+${data.effect.mana} Mana` : ''}</p>
  <div style="display:flex;gap:8px;margin-top:8px">
    <button onclick="afterUseItem('${itemId}')">Đóng</button>
  </div>`;

}

// giảm số lượng & lưu inv.qty = (inv.qty||1) - 1; if(inv.qty <= 0) player.inventory.splice(idx, 1);

saveGame(); renderInventory(); updateHeader(); updateBars(); // KHÔNG closeModal ở đây! }

function afterUseItem(itemId){ const inv = player.inventory.find(x=>String(x.id)===String(itemId)); if(inv && inv.qty > 0){ // còn vật phẩm → quay lại chi tiết openItem(itemId); } else { // hết vật phẩm → về túi đồ closeModal(); renderInventory(); } updateHeader(); updateBars(); }

function learnTechnique(id) { const tech = gameData.techniques.find(t => t.id === id); if (!tech) return;

if (player.techniques.some(t => t.id === id)) { $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Thông báo</h3> <p>Bạn đã học công pháp này rồi: ${tech.name}</p> <div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>; return; }

// thêm công pháp player.techniques.push({ id: tech.id, name: tech.name, lvl: 1, exp: 0 });

// mở kỹ năng từ công pháp if (Array.isArray(tech.skillsUnlocked)) { tech.skillsUnlocked.forEach(sid=>{ const sk = gameData.skills.find(x=>x.id===sid); if (sk && !player.skills.some(s=>s.id===sid)) { player.skills.push({ id: sk.id, name: sk.name, lvl: sk.level || 1, exp: 0 }); } }); }

// xóa khỏi túi đồ const idx = player.inventory.findIndex(x=>String(x.id)===String(id)); if(idx !== -1) player.inventory.splice(idx,1);

saveGame(); renderInventory(); renderTechniques(); renderSkills(); renderStats(); // ← để cập nhật danh sách chỉ số updateHeader(); // ← để cập nhật HP/MP bars nếu cần updateBars();

// hiện thông báo $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Đã học công pháp</h3> <p>${tech.name} đã được thêm vào danh sách công pháp của bạn.</p> <div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>; }

function renderTechniques(){ const list = $('skillsList'); if(!list) return; list.innerHTML = ''; if(player.techniques.length===0){ list.innerHTML = '

Chưa học công pháp nào
'; return; } player.techniques.forEach(t=>{ const el = document.createElement('div'); el.className='skill'; el.innerHTML = <div><strong>${t.name}</strong> <div style="font-size:12px">Lv ${t.lvl} • Exp ${t.exp}</div></div> <div style="display:flex;gap:6px"> <button onclick="viewTechnique('${t.id}')">Xem</button> <button onclick="trainTechnique('${t.id}')">🧘 Tu luyện</button> </div>; list.appendChild(el); }); }

function renderSkills(){ const container = $('panel-skills'); // bạn cần thêm 1 panel riêng trong HTML if(!container) return; container.innerHTML = ''; if(player.skills.length===0){ container.innerHTML = '

Chưa có kỹ năng nào
'; return; } player.skills.forEach(s=>{ const el = document.createElement('div'); el.className='list-item'; el.innerHTML = ✨ ${s.name} (Lv ${s.lvl}); container.appendChild(el); }); }

function viewTechnique(id){ const learned = player.techniques.find(x=>x.id===id); const data = gameData.techniques.find(t=>t.id===id); if(!learned || !data) return;

$('modal').style.display = 'flex';

let html = <h3>${getItemIcon(data)} ${data.name} (Lv ${learned.lvl})</h3>; if(data.summary) html += <div><strong>Tổng cương:</strong> ${data.summary}</div>; html += <div><strong>Đẳng cấp:</strong> ${data.rarity||''}</div>; html += <div><strong>Lv hiện tại:</strong> ${learned.lvl}</div>; if(data.description) html += <div><strong>Mô tả:</strong> ${data.description}</div>;

// Chi tiết: chỉ các Lv đã đạt if(Array.isArray(data.levelDetails)){ html += <div style="margin-top:8px"><strong>Chi tiết:</strong>; data.levelDetails.forEach(ld=>{ if(ld.level <= learned.lvl){ let bonusText = ""; if(ld.bonus) bonusText = Object.entries(ld.bonus) .map(([k,v])=>+${v} ${translateStatName(k)}) .join(", "); html += <div class="small">Lv ${ld.level}: ${ld.description||''}</div>; html += <div class="small" style="margin-left:12px">${bonusText}</div>; } }); html += </div>; }

// Kỹ năng học được => clickable -> showSkillDetail if(Array.isArray(data.skillsUnlocked) && data.skillsUnlocked.length && Array.isArray(gameData.skills)){ html += <div style="margin-top:8px"><strong>Kỹ năng học được:</strong>; data.skillsUnlocked.forEach(sid=>{ const sk = gameData.skills.find(x=>x.id===sid); if(sk){ html += <div class="small">✨ <span style="color:#ffd700;cursor:pointer" onclick="showSkillDetail('${sid}')">${sk.name} (Lv ${sk.level||1})</span> - ${sk.description}</div>; } }); html += </div>; }

// nút chuyển sang giao diện Kỹ năng đã học (modal chứa danh sách skills + 10 slot) html += `

Đóng

`; $('modalCard').innerHTML = html; }

// ==== Training globals (thay thế let trainingInterval = null;) let trainingInterval = null; // interval tăng exp let trainingAnimationRaf = null; // requestAnimationFrame id cho animation canvas let trainingInProgress = false; // cờ báo đang tu luyện (chặn click ngoài) let currentTrainingTechId = null; // id công pháp đang tu luyện (nếu có)

function trainTechnique(id){ // ensure player.skillSlots exists player.skillSlots = player.skillSlots || Array(10).fill(null);

const t = player.techniques.find(x=>x.id===id); const data = gameData.techniques.find(tt=>tt.id===id); if(!t || !data) return;

// mark current training currentTrainingTechId = id; trainingInProgress = true;

// Lưu trạng thái bắt đầu training để dùng cho offline resume const trainingState = { techId: id, start: Date.now(), initialExp: t.exp || 0, techLvl: t.lvl || 1 }; try{ localStorage.setItem('rpg_training', JSON.stringify(trainingState)); }catch(e){}

// Show modal (nút Dừng to & đặt giữa) $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Đang tu luyện: ${t.name} (Lv ${t.lvl})</h3> <canvas id="trainCanvas" width="300" height="200" style="background:#111;border-radius:8px;margin:8px auto;display:block"></canvas> <div class="bar exp" style="margin-top:8px"> <div class="bar-fill" id="expBar"></div> <div class="bar-text" id="expText"></div> </div> <div style="display:flex;justify-content:center;margin-top:12px"> <button id="stopTrainingBtn" style="font-size:16px;padding:10px 18px;border-radius:8px;cursor:pointer">Dừng</button> </div>;

// stop handler document.getElementById('stopTrainingBtn').onclick = ()=> stopTraining();

// Canvas animation (bắt đầu ngay) const canvas = document.getElementById("trainCanvas"); if(!canvas) return; const ctx = canvas.getContext("2d"); let frame = 0; function drawTrainingChar(f){ ctx.clearRect(0,0,canvas.width,canvas.height); ctx.strokeStyle = "#0f0"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(150,60,20,0,Math.PI2); ctx.stroke(); // đầu ctx.beginPath(); ctx.moveTo(150,80); ctx.lineTo(150,140); ctx.stroke(); // thân ctx.beginPath(); ctx.moveTo(150,100); ctx.lineTo(120 + Math.sin(f0.2)20,120); ctx.moveTo(150,100); ctx.lineTo(180 - Math.sin(f0.2)20,120); ctx.stroke(); // tay ctx.beginPath(); ctx.moveTo(150,140); ctx.lineTo(130 + Math.cos(f0.2)20,180); ctx.moveTo(150,140); ctx.lineTo(170 - Math.cos(f0.2)*20,180); ctx.stroke(); // chân } function animate(){ if(!trainingInProgress) return; frame++; drawTrainingChar(frame); trainingAnimationRaf = requestAnimationFrame(animate); } // start animation immediately if(trainingAnimationRaf) cancelAnimationFrame(trainingAnimationRaf); animate();

// helper: update UI from current t function updateExpUI(){ const currentLevelData = (data.levelDetails||[]).find(ld => ld.level === t.lvl) || {}; const expMax = currentLevelData.expRequired || 100; const pct = Math.min(100, Math.round(((t.exp||0)/expMax)*100)); const bar = $('expBar'); if(bar) bar.style.width = pct + "%"; const txt = $('expText'); if(txt) txt.innerText = ${t.exp||0}/${expMax}; }

// if any previous interval running, clear if(trainingInterval){ clearInterval(trainingInterval); trainingInterval = null; }

// Main tick: 1s per exp trainingInterval = setInterval(()=>{ // re-fetch data (in case leveled up earlier) const techData = gameData.techniques.find(tt=>tt.id===id); if(!techData){ stopTraining(); return; }

const currentLevelData = (techData.levelDetails||[]).find(ld => ld.level === t.lvl) || {};
const expMax = currentLevelData.expRequired || 100;

// +1 exp per second
t.exp = (t.exp||0) + 1;
player.xp = (player.xp||0) + 1;

// check level-up for technique, with cap: technique lvl cannot exceed player.level + 5
while((t.exp || 0) >= expMax){
  if(t.lvl + 1 > (player.level || 1) + 5){
    // hit cap: don't increase lvl, clamp exp to expMax-1 and stop adding further exp for leveling
    t.exp = Math.min(t.exp, expMax - 1);
    break;
  }
  t.exp -= expMax;
  t.lvl = (t.lvl || 1) + 1;
  // after leveling up technique, recompute expMax for new level
  const nextLevelData = (techData.levelDetails||[]).find(ld => ld.level === t.lvl) || {};
  if(!nextLevelData) break;
  // optionally: show short notice (keeps modal open)
  $('modalCard').querySelector('h3').innerText = `Đang tu luyện: ${t.name} (Lv ${t.lvl})`;
}

// Giới hạn cấp nhân vật: max nhân vật không vượt quá highest technique level
const highestTechLv = Math.max(1, ...(player.techniques||[]).map(x=>x.lvl||1));
let leveledPlayer = false;
while((player.xp || 0) >= (player.xpToNext || 100) && (player.level || 1) < highestTechLv){
  player.xp -= player.xpToNext;
  player.level = (player.level || 1) + 1;
  player.xpToNext = Math.floor((player.xpToNext || 100) * 1.25);
  const delta = getLevelUpDeltaForLevel(player.level);
  Object.keys(player.stats).forEach(k=>{
    player.stats[k] = (player.stats[k] || 0) + delta;
  });
  leveledPlayer = true;
}
if(player.level >= highestTechLv){
  player.xp = Math.min(player.xp, (player.xpToNext || 100) - 1);
}

// save state each tick (also update training record to count elapsed correctly)
const nowState = {
  techId: id,
  start: Date.now(),
  initialExp: t.exp || 0,
  techLvl: t.lvl || 1
};
try{ localStorage.setItem('rpg_training', JSON.stringify(nowState)); }catch(e){}

updateExpUI();
renderTechniques();
renderSkills();
updateHeader();
renderStats();
saveGame();
updateBars();

// if player leveled up -> show short notice (modal remains)
if(leveledPlayer){
  // small non-blocking notice: change header text only
  $('modalCard').querySelector('h3').innerText = `Đang tu luyện: ${t.name} (Lv ${t.lvl}) — Nhân vật Lv ${player.level}`;
}

}, 1000);

// init ui immediately updateExpUI(); }

function stopTraining(){ // clear interval if(trainingInterval){ clearInterval(trainingInterval); trainingInterval = null; } // cancel animation if(trainingAnimationRaf){ cancelAnimationFrame(trainingAnimationRaf); trainingAnimationRaf = null; } // clear flags & storage trainingInProgress = false; currentTrainingTechId = null; try{ localStorage.removeItem('rpg_training'); }catch(e){} // close modal const m = $('modal'); if(m) m.style.display = 'none'; // save final state saveGame(); }

// Hiện chi tiết kỹ năng (từ gameData.skills) function showSkillDetail(skillId){ const sk = gameData.skills.find(s=>s.id===skillId); const learned = player.skills.find(s=>s.id===skillId); if(!sk) return; $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>✨ ${sk.name} ${learned ?(Lv ${learned.lvl}): ''}</h3> <div><strong>Mô tả:</strong> ${sk.description || "-"}</div> ${sk.cooldown ?

⏱️ Hồi chiêu: ${sk.cooldown}s
: ''} ${sk.manaCost ?
🔹 Tiêu hao Mana: ${sk.manaCost}
: ''} ${sk.damage ?
Sát thương:${Object.entries(sk.damage).map(([k,v])=><div class="small">- ${k}: ${v}</div>).join('')}
: ''} <div style="display:flex;gap:8px;margin-top:8px"> <button onclick="closeModal()">Đóng</button> </div>; }

// Resume training if there's a saved training state (offline progress) function resumeTrainingIfAny(){ try{ const raw = localStorage.getItem('rpg_training'); if(!raw) return; const st = JSON.parse(raw); if(!st || !st.techId) return; // find technique in player const t = player.techniques.find(x=>x.id === st.techId); const data = gameData.techniques.find(tt=>tt.id === st.techId); if(!t || !data) { localStorage.removeItem('rpg_training'); return; }

// calculate elapsed seconds since stored start
const elapsedMs = Date.now() - (st.start || Date.now());
const elapsedSec = Math.max(0, Math.floor(elapsedMs / 1000));

// apply offline exp: initialExp + elapsedSec
t.exp = (st.initialExp || 0) + elapsedSec;

// handle level-ups (with cap tech lvl <= player.level + 5)
let loopGuard = 0;
while(loopGuard < 100){
  loopGuard++;
  const levelData = (data.levelDetails||[]).find(ld => ld.level === t.lvl) || {};
  const expMax = levelData.expRequired || 100;
  if((t.exp || 0) >= expMax){
    if(t.lvl + 1 > (player.level || 1) + 5){
      t.exp = Math.min(t.exp, expMax - 1);
      break;
    }
    t.exp -= expMax;
    t.lvl = (t.lvl || 1) + 1;
  } else break;
}

saveGame();

// reopen modal and continue training from now (so animation + interval continue)
// set new training state start at now with initialExp = current t.exp
currentTrainingTechId = st.techId;
const newState = { techId: st.techId, start: Date.now(), initialExp: t.exp||0, techLvl: t.lvl||1 };
try{ localStorage.setItem('rpg_training', JSON.stringify(newState)); }catch(e){}
// open the modal and call trainTechnique to start intervals/animation
trainTechnique(st.techId);

}catch(e){ console.error('resumeTrainingIfAny failed', e); } }

function viewSkill(id, source){ const learned = player.skills.find(x=>x.id===id); const data = gameData.skills.find(s=>s.id===id); if(!learned || !data) return;

$('modal').style.display = 'flex';

let html = <h3>✨ ${data.name} (Lv ${learned.lvl})</h3>; html += <div><strong>Mô tả:</strong> ${data.description}</div>; if(data.cooldown) html += <div>⏱️ Hồi chiêu: ${data.cooldown}s</div>; if(data.manaCost) html += <div>🔹 Tiêu hao Mana: ${data.manaCost}</div>;

// Hiện sát thương if(data.damage){ html += <div><strong>Sát thương:</strong>; Object.entries(data.damage).forEach(([type,val])=>{ html += <div class="small">- ${type}: ${val}</div>; }); html += </div>; }

// Chỉ hiển thị nút Trang bị khi mở từ "Kỹ năng đã học" if(source === "learned"){ html += <div style="margin-top:8px;display:flex;gap:8px"> <button onclick="equipSkill('${id}')">Trang bị</button> <button onclick="closeModal()">Đóng</button> </div>; } else { html += <div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>; }

$('modalCard').innerHTML = html; }

function trainSkill(skillId){ const skill = player.skills.find(s=>s.id===skillId); const skillData = gameData.skills.find(s=>s.id===skillId); if(!skill || !skillData) return;

// tìm công pháp đã mở kỹ năng này const tech = player.techniques.find(t=>{ const base = gameData.techniques.find(tt=>tt.id===t.id); return base && base.skillsUnlocked && base.skillsUnlocked.includes(skillId); }); if(!tech){ showModal("Thông báo", "Kỹ năng này chưa liên kết công pháp nào."); return; }

// lấy quy tắc nâng cấp từ công pháp const techData = gameData.techniques.find(tt=>tt.id===tech.id); const rules = techData.skillLevelUpRules?.filter(r=>r.skillId===skillId) || [];

// xác định cấp kỹ năng cao nhất mà công pháp hiện tại cho phép let allowedLv = 1; rules.forEach(r=>{ if(tech.lvl >= r.techLv && r.skillLv > allowedLv){ allowedLv = r.skillLv; } });

if(allowedLv > skill.lvl){ skill.lvl = allowedLv; saveGame(); renderSkills();

showModal("Nâng cấp kỹ năng",
  `${skillData.name} đã tăng lên Lv ${skill.lvl} (nhờ công pháp ${techData.name} Lv ${tech.lvl}).`
);

} else { showModal("Tu luyện kỹ năng", Công pháp ${techData.name} (Lv ${tech.lvl}) chưa đạt mốc để nâng cấp kỹ năng này. ); } }

const statDescriptions = {
  MaxHP:"Sinh mệnh tổng thể. Có tốc độ hồi phục riêng.",
  MaxMana:"Nguồn năng lượng. Dùng cho kỹ năng, ma pháp.",
  MaxSucBen:"Ảnh hưởng tấn công vật lý, chịu đòn.",
  TheChat:"Duy trì hoạt động liên tục, dùng khi vận động kéo dài.",
  TriLuc:"Tốc độ học tập, thi triển kỹ năng trí tuệ.",
  NhanhNhen:"Phản xạ, tốc độ đánh, né tránh.",
  MayMan:"Tỷ lệ phát hiện vật phẩm, bạo kích, duyên số.",
  Khang:"Kháng trạng thái như cháy, đóng băng, độc, chảy máu,...",
  CanCot:"Cần để học công pháp.",
  NgoTinh:"Ngộ tính ảnh hưởng các tương tác học tập/kinh nghiệm."
};

function translateStatName(k){
  const map = { MaxHP: 'HP', MaxMana: 'Mana', MaxSucBen: 'Sức Bền', TheChat: 'Thể Chất', TriLuc: 'Trí Lực', NhanhNhen: 'Nhanh Nhẹn', MayMan: 'May Mắn', Khang: 'Kháng', CanCot: 'Căn Cốt', NgoTinh: 'Ngộ Tính' };
  return map[k] || k;
}

function getStatSources(statKey){

let sources = []; // chỉ số gốc const base = playerDefault.stats[statKey] || 0; sources.push(Gốc: ${base}); // trang bị let equipVal = 0; Object.values(player.equipment).forEach(eid=>{ const it = findItemById(eid); if(it && it.stats && statKey in it.stats) equipVal += it.stats[statKey]; }); if(equipVal) sources.push(Trang bị: +${equipVal}); // công pháp // công pháp (nguồn từ các công pháp đã học) (player.techniques||[]).forEach(t => { const tech = gameData.techniques.find(td => td.id === t.id); if (!tech || !Array.isArray(tech.levelDetails)) return; const current = tech.levelDetails.find(ld => ld.level === t.lvl); if (current && current.bonus && statKey in current.bonus) { sources.push(Công pháp (${tech.name} Lv${t.lvl}): +${current.bonus[statKey]}); } });

// danh hiệu (ví dụ) if(player.titleBonuses && statKey in player.titleBonuses){ sources.push(Danh hiệu: +${player.titleBonuses[statKey]}); } // buff/debuff if(player.tempBonuses && statKey in player.tempBonuses){ sources.push(Buff/Debuff: ${player.tempBonuses[statKey]}); } return sources.join("
"); }

function renderStats(){ const container = $('statsList'); if(!container) return; container.innerHTML = ''; if(!player) { container.innerHTML = '

Dữ liệu nhân vật trống
'; return; }

const stats = Object.assign({}, playerDefault.stats || {}, player.stats || {}); const wrapper = document.createElement('div'); wrapper.className='stat-row'; const hidden = new Set(['HP','Mana','SucBen']); const displayOrder = ['MaxHP','MaxMana','MaxSucBen','TheChat','TriLuc','NhanhNhen','MayMan','Khang','CanCot','NgoTinh'];

displayOrder.forEach(k=>{ if(hidden.has(k)) return; if(!(k in stats)) return; const val = calcFinalStat(stats[k], k); const btn = document.createElement('button'); btn.className = 'stat-btn'; btn.innerText = ${translateStatName(k)}: ${val}; btn.onclick = ()=>{ $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>${translateStatName(k)}</h3> <p>${statDescriptions[k]||'Không có mô tả.'}</p> <div style="margin-top:8px;font-size:12px;opacity:0.8"> <strong>Nguồn chỉ số:</strong><br>${getStatSources(k)} </div> <div style="display:flex;gap:8px;margin-top:8px"> <button onclick="closeModal()">Đóng</button> </div>;

// 🧩 Đảm bảo modal nổi trên giao diện 👤 const modal = $('modal'); if (modal) modal.style.zIndex = "13050"; // cao hơn charPopup (12000) };

wrapper.appendChild(btn);

});

if(wrapper.children.length === 0){ container.innerHTML = '

Không có chỉ số để hiển thị
'; } else container.appendChild(wrapper); }

function renderEquipmentSlots(){ document.querySelectorAll('.equip-slot').forEach(el=>{ const slot = el.dataset.slot; const eid = player.equipment[slot]; if(eid){ const item = findItemById(eid); el.innerText = item ? getItemIcon(item) : "✔️"; el.classList.add("equipped"); } else { if(slot.includes("ring")) el.innerText="💍"; else if(slot==="head") el.innerText="👑"; else if(slot==="body") el.innerText="🛡️"; else if(slot==="hand") el.innerText="🖐️"; else if(slot==="leg") el.innerText="🦵"; else if(slot==="main-weapon") el.innerText="⚔️"; else if(slot==="sub-weapon") el.innerText="🗡️"; else el.innerText="📿"; el.classList.remove("equipped"); } }); } // === Auto trang bị vào slot hợp lệ === function autoEquipItem(itemId){ const data = findItemById(itemId); if(!data) return;

let targetSlot = null; if(data.type === 'weapon') targetSlot = player.equipment["main-weapon"] ? "sub-weapon" : "main-weapon"; else if(data.type === 'armor') targetSlot = "body"; else if(data.type === 'ring'){ const ringSlots = ["ring1","ring2","ring3","ring4","ring5"]; targetSlot = ringSlots.find(s=>!player.equipment[s]) || "ring1"; } else if(data.slot === "necklace") targetSlot = "necklace";

if(targetSlot) equipItem(itemId, targetSlot); }

function equipItem(itemId,slot){ const idx=player.inventory.findIndex(x=>String(x.id)===String(itemId)); if(idx===-1) return; if(player.equipment[slot]) player.inventory.push({id:player.equipment[slot],qty:1}); player.equipment[slot]=itemId; player.inventory.splice(idx,1); saveGame(); renderInventory(); renderStats(); renderEquipmentSlots(); updateHeader(); closeModal(); updateBars(); } function unequipItem(slot){ if(!player.equipment[slot]) return; player.inventory.push({id:player.equipment[slot],qty:1}); player.equipment[slot]=null; saveGame(); renderInventory(); renderStats(); renderEquipmentSlots(); updateHeader(); closeModal(); updateBars(); }

function openEquipSlot(slot){

$('modal').style.display='flex'; let html=<h3>Slot: ${slot}</h3>; if(player.equipment[slot]){ const item=findItemById(player.equipment[slot]); html+=<p>Đang trang bị: ${item?item.name:"Item"}</p><button onclick="unequipItem('${slot}')">Tháo ra</button>; } else { const equips=player.inventory.filter(it=>{ const d=findItemById(it.id); if(!d) return false; if(d.type==="weapon"&&(slot==="main-weapon"||slot==="sub-weapon")) return true; if(d.type==="armor"&&slot==="body") return true; if(d.type==="ring"&&slot.startsWith("ring")) return true; if(d.slot==="necklace"&&slot==="necklace") return true; return false; }); if(equips.length===0) html+="

Không có trang bị phù hợp

"; else equips.forEach(inv=>{ const d=findItemById(inv.id); html+=<div class="list-item"><span>${d.name}</span><button onclick="equipItem('${inv.id}','${slot}')">Trang bị</button></div>; }); } $('modalCard').innerHTML=html+<div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>; }

document.addEventListener('click', function globalClickHandler(e){

if(e.target && e.target.id === 'modal'){ // nếu đang tu luyện thì không cho đóng modal bằng click ngoài if(trainingInProgress){ // optional: bạn có thể hiện tooltip nhỏ thông báo "Đang tu luyện, hãy bấm Dừng để thoát" return; } closeModal(); } });
// === Định nghĩa 16 tư thế stickman === const poses = [ // Pose 0: đứng thẳng { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:170,y:160}, elbowR:{x:230,y:160}, handL:{x:170,y:200}, handR:{x:230,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 1: khoanh tay (trái chéo) { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:190,y:150}, elbowR:{x:210,y:150}, handL:{x:210,y:160}, handR:{x:190,y:160}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 2: khoanh tay (phải chéo) { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:190,y:150}, elbowR:{x:210,y:150}, handL:{x:210,y:165}, handR:{x:190,y:165}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 3: tay chống hông phải { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:170,y:150}, elbowR:{x:220,y:160}, handL:{x:170,y:180}, handR:{x:215,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 4: tay ôm bụng { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:190,y:150}, elbowR:{x:210,y:150}, handL:{x:195,y:190}, handR:{x:205,y:190}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 5: tay trong túi { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:185,y:160}, elbowR:{x:215,y:160}, handL:{x:185,y:200}, handR:{x:215,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 6: gãi đầu trái { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:160,y:100}, elbowR:{x:230,y:160}, handL:{x:180,y:70}, handR:{x:230,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 7: hai tay sau đầu { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:160,y:100}, elbowR:{x:240,y:100}, handL:{x:170,y:70}, handR:{x:230,y:70}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 8: hai tay chống hông { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:180,y:150}, elbowR:{x:220,y:150}, handL:{x:185,y:200}, handR:{x:215,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 9: hai tay trong túi { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:185,y:160}, elbowR:{x:215,y:160}, handL:{x:185,y:200}, handR:{x:215,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 10: đứng vắt chéo chân trái { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:170,y:160}, elbowR:{x:230,y:160}, handL:{x:170,y:200}, handR:{x:230,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:195,y:230}, kneeR:{x:215,y:240}, footL:{x:205,y:280}, footR:{x:215,y:280} }, // Pose 11: đứng vắt chéo chân phải { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:170,y:160}, elbowR:{x:230,y:160}, handL:{x:170,y:200}, handR:{x:230,y:200}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:205,y:230}, footL:{x:185,y:280}, footR:{x:195,y:280} }, // Pose 12: dựa nghiêng trái { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:160,y:150}, elbowR:{x:230,y:150}, handL:{x:150,y:180}, handR:{x:230,y:200}, hipL:{x:180,y:200}, hipR:{x:210,y:200}, kneeL:{x:175,y:240}, kneeR:{x:210,y:240}, footL:{x:170,y:280}, footR:{x:210,y:280} }, // Pose 13: dựa nghiêng phải { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:170,y:150}, elbowR:{x:240,y:150}, handL:{x:170,y:200}, handR:{x:250,y:180}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:225,y:240}, footL:{x:185,y:280}, footR:{x:225,y:280} }, // Pose 14: ngả lưng thư giãn { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:160,y:100}, elbowR:{x:240,y:100}, handL:{x:170,y:70}, handR:{x:230,y:70}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} }, // Pose 15: chỉ tay sang phải { head:{x:200,y:80}, neck:{x:200,y:120}, shoulderL:{x:190,y:120}, shoulderR:{x:210,y:120}, elbowL:{x:170,y:160}, elbowR:{x:250,y:120}, handL:{x:170,y:200}, handR:{x:280,y:120}, hipL:{x:190,y:210}, hipR:{x:210,y:210}, kneeL:{x:185,y:240}, kneeR:{x:215,y:240}, footL:{x:185,y:280}, footR:{x:215,y:280} } ];

// scale & offset để phóng to nhân vật const scale = 1.3; const offsetX = 200; const offsetY = 10;

// === Quản lý chọn pose === let currentPose = JSON.parse(JSON.stringify(poses[0])); let targetPose = poses[0]; let tPose = 0; let poseHold = 0;

function pickNextPose(){ let i = pickPose(); // dùng logic random có trọng số targetPose = poses[i];

// ---- cập nhật trọng số ---- if(i > 0){ // bỏ qua pose 0 (tư thế gốc) poseWeights[i] = Math.max(1, poseWeights[i]-0.5); for(let j=1;j<poseWeights.length;j++){ if(j!==i) poseWeights[j]+=0.2; } }

return i; } let poseWeights = new Array(poses.length).fill(1); poseWeights[0] = 0; // tư thế gốc không random

function pickPose(){ let total = poseWeights.reduce((a,b)=>a+b,0); let r = Math.random() * total; let sum = 0; for(let i=0;i<poseWeights.length;i++){ sum += poseWeights[i]; if(r <= sum) return i; } }

function applyScale(joint){ return { x: offsetX + (joint.x-200) * scale, y: offsetY + (joint.y-60) * scale }; }

// easing function easeInOut(t){ return t<0.5 ? 2tt : -1+(4-2*t)*t; }

// --- Blink (nhấp nháy khi HP ≤ 10%) — khởi tạo 1 lần --- (function initCharBlink(){ if (typeof window.blinkToggle === "undefined") { window.blinkToggle = true; } if (typeof window.blinkTimerId === "undefined") { window.blinkTimerId = setInterval(function(){ window.blinkToggle = !window.blinkToggle; }, 300); } })();

function drawCharacterBase(){ const c = document.getElementById("charCanvas"); if(!c) return; const ctx = c.getContext("2d"); ctx.clearRect(0,0,c.width,c.height);

if (typeof player === "undefined" || !player.stats) return;

const hpNow = Number(player.stats.HP) || 0; const hpMax = Number(player.stats.MaxHP) || 1; const hpPct = (hpNow / hpMax) * 100;

// Màu let strokeColor = "#fff"; ctx.globalAlpha = 1.0; if (hpNow <= 0) { strokeColor = "gray"; ctx.globalAlpha = 0.4; } else { if (hpPct <= 10) { strokeColor = window.blinkToggle ? "red" : "rgba(255,0,0,0.6)"; } else if (hpPct <= 20) { strokeColor = "red"; } else if (hpPct <= 50) { strokeColor = "yellow"; } else if (hpPct <= 80) { strokeColor = "lime"; } else { strokeColor = "white"; } }

// Thở gấp let breathSpeed = 0.002, breathAmp = 2; if(hpPct <= 80){ breathSpeed = 0.004; breathAmp = 4; } if(hpPct <= 50){ breathSpeed = 0.006; breathAmp = 6; } if(hpPct <= 20){ breathSpeed = 0.008; breathAmp = 8; } if(hpPct <= 10){ breathSpeed = 0.012; breathAmp = 10; } const breathing = hpNow>0 ? Math.sin(Date.now()*breathSpeed)*breathAmp : 0;

let joints = {}; let tt = easeInOut(tPose); for (let k in currentPose){ joints[k] = { x: currentPose[k].x + (targetPose[k].x - currentPose[k].x) * tt, y: currentPose[k].y + (targetPose[k].y - currentPose[k].y) * tt }; joints[k] = applyScale(joints[k]); }

// áp breathing vào cổ & ngực if (joints.head) joints.head.y += breathing * 0.3; if (joints.neck) joints.neck.y += breathing * 0.6; if (joints.hipL && joints.hipR) { joints.hipL.y += breathing * 0.4; joints.hipR.y += breathing * 0.4; }

// --- Vẽ ---

// Nếu joints không đầy đủ -> bail out if(!joints || !joints.head || !joints.neck || !joints.shoulderL) return;

// compute mid-hip (để vẽ thân chính) const midHip = { x: ( (joints.hipL?.x || joints.hip?.x || 200) + (joints.hipR?.x || joints.hip?.x || 200) ) / 2, y: ( (joints.hipL?.y || joints.hip?.y || 200) + (joints.hipR?.y || joints.hip?.y || 200) ) / 2 };

// stroke/fill style (strokeColor phải tồn tại ở scope trên) ctx.strokeStyle = (typeof strokeColor !== 'undefined') ? strokeColor : "#fff"; ctx.fillStyle = ctx.strokeStyle; ctx.lineCap = "round"; ctx.lineJoin = "round";

// Vẽ thân (cổ → midHip) dày ctx.lineWidth = 35; ctx.beginPath(); ctx.moveTo(joints.neck.x, joints.neck.y); ctx.lineTo(midHip.x, midHip.y); ctx.stroke();

// Vẽ đầu ctx.beginPath(); ctx.arc(joints.head.x, joints.head.y, 30, 0, Math.PI*2); ctx.fill();

// Mắt ctx.fillStyle = "#000"; ctx.beginPath(); ctx.arc(joints.head.x - 10, joints.head.y - 8, 5, 0, Math.PI2); ctx.arc(joints.head.x + 10, joints.head.y - 8, 5, 0, Math.PI2); ctx.fill(); ctx.fillStyle = ctx.strokeStyle;

// Vẽ tay: vai -> khuỷu -> tay ctx.lineWidth = 12; ctx.beginPath(); ctx.moveTo(joints.shoulderL.x, joints.shoulderL.y); ctx.lineTo(joints.elbowL.x, joints.elbowL.y); ctx.lineTo(joints.handL.x, joints.handL.y); ctx.moveTo(joints.shoulderR.x, joints.shoulderR.y); ctx.lineTo(joints.elbowR.x, joints.elbowR.y); ctx.lineTo(joints.handR.x, joints.handR.y); ctx.stroke();

// Vẽ chân: hipL -> kneeL -> footL & hipR -> kneeR -> footR ctx.lineWidth = 14; ctx.beginPath(); ctx.moveTo(joints.hipL.x, joints.hipL.y); ctx.lineTo(joints.kneeL.x, joints.kneeL.y); ctx.lineTo(joints.footL.x, joints.footL.y); ctx.moveTo(joints.hipR.x, joints.hipR.y); ctx.lineTo(joints.kneeR.x, joints.kneeR.y); ctx.lineTo(joints.footR.x, joints.footR.y); ctx.stroke();

// Vẽ các "khớp" nhỏ để nhìn rõ ctx.fillStyle = ctx.strokeStyle; if (player.stats.HP > 0 && player.stats.HP / player.statsMaxHP > 0.1) { // HP > 10% → luôn vẽ khớp for (let k in joints) { if (joints[k]) { ctx.beginPath(); ctx.arc(joints[k].x, joints[k].y, 6, 0, Math.PI2); ctx.fill(); } } } else if (player.stats.HP > 0 && player.stats.HP / player.stats.MaxHP <= 0.1) { // HP ≤ 10% → chỉ vẽ khớp khi blinkToggle = true if (window.blinkToggle) { for (let k in joints) { if (joints[k]) { ctx.beginPath(); ctx.arc(joints[k].x, joints[k].y, 6, 0, Math.PI2); ctx.fill(); } } } } // HP = 0 → không vẽ khớp

}

function animateChar() { let hpNow = player.statsHP; // hoặc player.stats.HP nếu bạn dùng chữ hoa

if (hpNow > 0) { // tăng nội suy giữa currentPose và targetPose tPose += 0.01; if (tPose >= 1) { if (poseHold < 2400) { // giữ pose lâu hơn poseHold++; tPose = 1; } else { // kết thúc pose hiện tại, chuẩn bị pose tiếp theo currentPose = JSON.parse(JSON.stringify(targetPose)); let nextIndex = (targetPose === poses[0]) ? pickNextPose() : 0; targetPose = poses[nextIndex]; tPose = 0; poseHold = 0; } } } else { // HP = 0 → nhân vật về tư thế gốc targetPose = poses[0]; currentPose = poses[0]; }

// tạo joints nội suy giữa currentPose và targetPose let joints = {}; let tt = easeInOut(tPose); for (let k in currentPose) { joints[k] = { x: currentPose[k].x + (targetPose[k].x - currentPose[k].x) * tt, y: currentPose[k].y + (targetPose[k].y - currentPose[k].y) * tt }; joints[k] = applyScale(joints[k]); }

// hiệu ứng lắc tay nhẹ nếu đang ở tư thế gốc if (targetPose === poses[0]) { let sway = Math.sin(Date.now() / 400) * 2; if (joints.handL) joints.handL.x += sway; if (joints.handR) joints.handR.x -= sway; }

return joints; // để drawCharacterBase vẽ }

function gameLoop() { ctx.clearRect(0, 0, canvas.width, canvas.height);

// cập nhật nhân vật let joints = animateChar();

// vẽ nhân vật drawCharacterBase(ctx, joints);

// cập nhật nhấp nháy updateBlinking();

requestAnimationFrame(gameLoop); } // --- FULLSCREEN MAP MODULE (thay thế phần map cũ) --- // 🌍 Các loại khu vực địa hình const REGION_TYPES = { desert: { name: "Sa mạc", color: "#e1c16e", nodeColor: "#c2a04e" }, ice: { name: "Hàn băng", color: "#aeeaff", nodeColor: "#88ccee" }, cave: { name: "Hang động", color: "#555555", nodeColor: "#777777" }, mountain: { name: "Đồi núi", color: "#556b2f", nodeColor: "#6b8e23" }, wasteland: { name: "Đất hoang", color: "#8b4513", nodeColor: "#a0522d" }, sea: { name: "Biển", color: "#006699", nodeColor: "#3399cc" }, deepsea: { name: "Lòng biển", color: "#001f3f", nodeColor: "#003366" }, island: { name: "Đảo", color: "#228b22", nodeColor: "#2e8b57" }, volcano: { name: "Núi lửa", color: "#cc3300", nodeColor: "#ff4500" }, jungle: { name: "Rừng rậm", color: "#0b6623", nodeColor: "#228b22" }, plain: { name: "Đồng bằng", color: "#b4e197", nodeColor: "#9cd67d" }, swamp: { name: "Đầm lầy", color: "#3b5323", nodeColor: "#556b2f" }, tundra: { name: "Tuyết phủ", color: "#dfefff", nodeColor: "#b0d8e5" }, grassland: { name: "Thảo nguyên", color: "#66aa33", nodeColor: "#77cc44" }, ruins: { name: "Tàn tích", color: "#7a6c5d", nodeColor: "#a3917a" }, }; let currentMapLevel = "cosmos"; // "cosmos" | "world" | "region" let selectedWorld = null; let selectedRegion = null;

let worlds = []; // trong cosmos let regions = []; // trong world let maps = []; // trong region

window.addEventListener('load', () => { const canvas = document.getElementById("worldMapCanvas"); if(!canvas) return; const ctx = canvas.getContext("2d");

// cho phép thay đổi khi resize let W = 0, H = 0, CX = 0, CY = 0; let t = 0;

// khai báo tất cả biến trước (tránh lỗi TDZ) let stars = [], asteroids = [], spiralDust = [], planets = [];

// tinh vân tĩnh (cố định cấu trúc) const nebulae = [ { xOffset:-200, yOffset:-100, r: 400, baseColors:["128,0,128","75,0,130"] }, { xOffset:150, yOffset:50, r: 300, baseColors:["0,128,255","135,206,250"] }, { xOffset:0, yOffset:-200, r: 350, baseColors:["255,105,180","255,182,193"] } ]; const rotatingNebula = { rRatio: 0.6, colors: ["rgba(100,150,255,0.06)","rgba(200,100,255,0.04)","rgba(0,0,0,0)"], angle:0, speed:0.0005 };

canvas.addEventListener("click", e=>{ const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top;

if(currentMapLevel === "cosmos") { // chọn world const dx = x - CX; const dy = y - CY; const dist = Math.sqrt(dxdx + dydy); const base = Math.min(W, H);

const innerThreshold = base * 0.27; // Đại thiên
const midThreshold   = base * 0.38; // Trung thiên
const outerThreshold = base * 0.55; // Tiểu thiên

if(dist < innerThreshold) {
  console.log("Bạn đã vào thế giới: Đại thiên");
  enterWorld({ type: "dai" });
}
else if(dist < midThreshold) {
  console.log("Bạn đã vào thế giới: Trung thiên");
  enterWorld({ type: "trung" });
}
else if(dist < outerThreshold) {
  console.log("Bạn đã vào thế giới: Tiểu thiên");
  enterWorld({ type: "tieu" });
} else {
  console.log("Click ngoài các vòng thế giới");
}

} else if(currentMapLevel === "world") { for (let r of regions) { if(r.z > 0 && pointInRegion(ctx, x, y, r)) { enterRegion(r); break; } } } else if(currentMapLevel === "region") { // chọn map trong region for (let m of maps) { const dx = x - m.x; const dy = y - m.y; if(Math.sqrt(dxdx + dydy) <= m.r) { console.log("Bạn đã chọn map chi tiết")
enterDungeon(selectedRegion); // mở dungeon break; } } } });

// --- Thêm biến cache toàn cục (ngay sau khi khai báo currentMapLevel, selectedWorld...) --- let worldCache = {}; // cache regions theo world.type let regionCache = {}; // cache maps theo region.id

// --- Sửa hàm vào World --- function enterWorld(world){ currentMapLevel = "world"; selectedWorld = world;

// ensure quota: dùng world.id nếu có, fallback world.type const wk = world.id || world.type || world.name || world.type; ensureWorldQuota(wk, world.type || "tieu");

if(worldCache[world.type]){ regions = worldCache[world.type]; } else { regions = []; generateRegions(); worldCache[world.type] = regions; } updateMapButtons(); }

// --- Sửa hàm vào Region --- function enterRegion(region) { currentMapLevel = "region"; selectedRegion = region;

if (regionCache[region.id]) { continents = regionCache[region.id].continents; maps = regionCache[region.id].maps; } else { generateContinents(region); // có thể tuỳ biến theo loại generateMaps(region); // sinh map riêng theo region.type regionCache[region.id] = { continents: continents, maps: maps }; }

// 🔹 Khởi tạo quota phòng cho region này (theo loại thế giới) // Nếu bạn có selectedWorld, ta dựa theo world.type; nếu không, lấy region.type. const worldType = (selectedWorld && selectedWorld.type) ? selectedWorld.type : (region.type || "tieu"); const worldKey = (selectedWorld && selectedWorld.id) ? selectedWorld.id : (region.id || region.name || worldType);

// đảm bảo quota cho worldKey này đã tồn tại ensureWorldQuota(worldKey, worldType);

console.log(🌍 Đã vào region: ${region.name || region.id} (${worldType}) — Quota phòng còn lại: ${window.worldRoomQuota[worldKey]});

updateMapButtons(); }

// --- Sửa hàm quay lại World --- function backToWorld(){ currentMapLevel = "world"; selectedRegion = null; maps = []; removeDungeonControls(); // << thêm updateMapButtons(); resetAllQuotas(); }

function backToCosmos(){ currentMapLevel = "cosmos"; selectedWorld = null; regions = []; maps = []; worldCache = {}; regionCache = {}; initPlanets(); removeDungeonControls(); // << thêm updateMapButtons(); }

window.backToCosmos = backToCosmos; window.backToWorld = backToWorld; window.exitDungeon = exitDungeon; function updateMapButtons(){ const ctrl = document.getElementById("mapControls"); if(!ctrl) return; ctrl.innerHTML = "";

// Map Thế giới → có nút quay lại Vũ trụ if (currentMapLevel === "world") { ctrl.innerHTML = <button onclick="backToCosmos()">⬅️ Quay lại Vũ trụ</button>; }

// Map Vùng (Region) → chỉ có nút quay lại Thế giới else if (currentMapLevel === "region") { ctrl.innerHTML = <button onclick="backToWorld()">⬅️ Quay lại Thế giới</button>; }

// Map Dungeon → chỉ có nút thoát dungeon else if (currentMapLevel === "dungeon") { ctrl.innerHTML = <button onclick="exitDungeon()">🚪 Thoát Dungeon</button>; }

// Các cấp khác (cosmos, vũ trụ, vv) thì không hiện gì }

// tiện ích màu function randomColor() { const colors = ["#ff4444","#44ff44","#4444ff","#ffaa00","#00ffaa","#ff00aa"]; return colors[Math.floor(Math.random()*colors.length)]; }

// khởi tạo kích thước + trả về lại các đối tượng function resizeCanvas() { // đặt canvas full viewport (không set CSS ở đây, chỉ kích thước bitmap) canvas.style.position = 'fixed'; canvas.style.left = '0'; canvas.style.top = '0'; canvas.style.width = '100vw'; canvas.style.height = '100vh'; canvas.style.zIndex = '1';

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

W = canvas.width;
H = canvas.height;
CX = W/2;
CY = H/2;

initStars();
initAsteroids();
initSpiralDust();
initPlanets(); // tạo planets dựa trên kích thước mới

}

// tạo sao nền function initStars(count=200) { stars = []; for (let i = 0; i < count; i++) { stars.push({ x: Math.random()*W, y: Math.random()*H, r: Math.random()*1.6, tw: Math.random()*1000 }); } }

// thiên thạch vòng function initAsteroids(count=250) { asteroids = []; const minWH = Math.min(W,H); for (let i = 0; i < count; i++) { asteroids.push({ r: (minWH0.08) + Math.random()(minWH*0.4), // dựa theo kích thước màn hình size: Math.random()*2, speed: 0.001 + Math.random()*0.004, angle: Math.random()Math.PI2 }); } }

// spiral dust function initSpiralDust(count=2000) { spiralDust = []; const minWH = Math.min(W,H); for (let i=0;i<count;i++){ const angle = Math.random()Math.PI2; const radius = (minWH0.12) + Math.random()(minWH*0.45); spiralDust.push({ angle, r: radius, speed: 0.00005 + Math.random()*0.0004, size: 10 + Math.random()*18 }); } }

// planets (dùng kiểm tra khoảng cách để không chồng khi spawn) function initPlanets() { planets = []; const minWH = Math.min(W,H);

function placePlanet(rMin, rMax, size, count, minDist, type) {
  let placed = 0, tries = 0;
  while(placed < count && tries < count*80) {
    tries++;
    const r = rMin + Math.random()*(rMax-rMin);
    const angle = Math.random()*Math.PI*2;
    const x = CX + Math.cos(angle)*r;
    const y = CY + Math.sin(angle)*r*0.6;
    // kiểm tra khoảng cách
    let ok = planets.every(p=>{
      const dx = x - p._x;
      const dy = y - p._y;
      return Math.sqrt(dx*dx + dy*dy) > (size + p.size + minDist);
    });
    if(ok) {
      planets.push({
        r, angle, size,
        speed: (0.0002 + Math.random()*0.0008),
        color: randomColor(),
        _x: x, _y: y,
        zPhase: Math.random()*Math.PI*2,
        zSpeed: 0.001 + Math.random()*0.003,
        type: type
      });
      placed++;
    } else {
      // nếu không ok, thử đẩy r ra/đổi góc
      // (dời r chút để tránh deadlock)
    }
  }
}

// các tầng (tỉ lệ theo minWH)
const base = Math.min(W,H);
// Đại thiên: gần tâm
placePlanet(base*0.19, base*0.20, Math.max(12, Math.round(base*0.02)), 10, 17, "dai");

// Trung thiên: giữa placePlanet(base0.20, base0.37, Math.max(8, Math.round(base0.015)), 8, 30, "trung"); // Tiểu thiên: rìa placePlanet(base0.43, base0.53, Math.max(6, Math.round(base0.01)), 6, 50, "tieu"); } let usedLabels = []; // reset mỗi lần vẽ frame

function drawWorldRings() { const base = Math.min(W,H); const rings = [ {r: base0.26, label: "Đại Thiên Thế Giới", angle: -Math.PI/2}, // trên {r: base0.39, label: "Trung Thiên Thế Giới", angle: Math.PI}, // trái {r: base*0.55, label: "Tiểu Thiên Thế Giới", angle: Math.PI/2}, // dưới ];

usedLabels = [];

ctx.save(); ctx.strokeStyle = "rgba(200,255,200,0.25)"; ctx.lineWidth = 2;

rings.forEach(r=>{ // ellipse ctx.beginPath(); ctx.ellipse(CX, CY, r.r, r.r0.65, 0, 0, Math.PI2); ctx.stroke();

// nhãn
drawRingLabel(r.label, r.r, r.angle);

});

ctx.restore(); } function drawRingLabel(text, radius, baseAngle) { ctx.font = "18px sans-serif"; const metrics = ctx.measureText(text); const textW = metrics.width; const textH = 22;

let angle = baseAngle; let x = CX + Math.cos(angle) * radius * 1.3; let y = CY + Math.sin(angle) * radius * 1.0;

const box = {x: x, y: y - textH/2, w: textW+12, h: textH};

// Nếu box vượt ngoài màn hình hoặc chồng nhãn → dịch góc một chút for (let tries=0; tries<8; tries++) { if (box.x>=0 && box.x+box.w<=W && box.y>=0 && box.y+box.h<=H && !usedLabels.some(b => !(box.x+box.w < b.x || b.x+b.w < box.x || box.y+box.h < b.y || b.y+b.h < box.y))) { break; } angle += Math.PI/16; // xoay nhẹ nếu trùng x = CX + Math.cos(angle) * radius * 1.3; y = CY + Math.sin(angle) * radius * 1.0; box.x = x; box.y = y - textH/2; }

usedLabels.push(box);

// gạch nối từ ellipse ra gạch chân chữ const lineStartX = x + 6; const lineY = y + textH/2 + 4; const lineEndX = lineStartX + textW + 10;

ctx.strokeStyle = "white"; ctx.beginPath(); ctx.moveTo(CX + Math.cos(angle) * radius * 0.95, CY + Math.sin(angle) * radius * 0.65); ctx.lineTo(lineStartX, lineY); ctx.stroke();

// nền chữ ctx.fillStyle = "rgba(0,0,0,0.6)"; ctx.fillRect(x+6, y-textH/2, textW+12, textH);

// chữ ctx.fillStyle = "white"; ctx.textAlign = "left"; ctx.fillText(text, x+12, y+6);

// gạch dưới chữ ctx.beginPath(); ctx.moveTo(lineStartX, lineY); ctx.lineTo(lineEndX, lineY); ctx.stroke(); }

// Vẽ background (stars + nebulae + rotating nebula) function drawBackground() { ctx.fillStyle = "black"; ctx.fillRect(0,0,W,H);

// sao nền lấp lánh
stars.forEach(s=>{
  const tw = (Math.sin((t+s.tw)*0.02)+1)/2;
  ctx.fillStyle = `rgba(255,255,255,${0.25 + 0.7*tw})`;
  ctx.beginPath();
  ctx.arc(s.x, s.y, s.r, 0, Math.PI*2);
  ctx.fill();
});

// tinh vân tĩnh (vị trí dựa trên CX,CY)
nebulae.forEach((n, idx)=>{
  const x = CX + n.xOffset;
  const y = CY + n.yOffset;
  const pulse = (Math.sin(t*0.01 + idx*2)+1)/2;
  const c1 = n.baseColors[0];
  const c2 = n.baseColors[1];
  const colA = `rgba(${c1},${0.08 + 0.22*pulse})`;
  const colB = `rgba(${c2},0)`;
  const g = ctx.createRadialGradient(x,y,0,x,y,n.r);
  g.addColorStop(0,colA);
  g.addColorStop(1,colB);
  ctx.fillStyle = g;
  ctx.beginPath();
  ctx.arc(x,y,n.r,0,Math.PI*2);
  ctx.fill();
});

// rotating nebula: scale theo min(W,H)
ctx.save();
ctx.translate(CX, CY);
ctx.rotate(rotatingNebula.angle);
const rotR = Math.min(W,H) * rotatingNebula.rRatio;
const g = ctx.createRadialGradient(0,0,0,0,0,rotR);
g.addColorStop(0, rotatingNebula.colors[0]);
g.addColorStop(0.5, rotatingNebula.colors[1]);
g.addColorStop(1, rotatingNebula.colors[2]);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(0,0,rotR,0,Math.PI*2);
ctx.fill();
ctx.restore();

rotatingNebula.angle += rotatingNebula.speed;

}

function drawSpiralNebulaBackground() { ctx.save(); ctx.translate(CX, CY); ctx.scale(1,0.6); spiralDust.forEach(d=>{ // update angle d.angle += d.speed; const x = Math.cos(d.angle)*d.r; const y = Math.sin(d.angle)d.r; ctx.fillStyle = rgba(200,150,100,0.06); ctx.beginPath(); ctx.arc(x,y,d.size,0,Math.PI2); ctx.fill(); }); ctx.restore(); }

// Vẽ các hành tinh: update vị trí, dao động r, z, alpha; sort theo y trước khi vẽ function drawPlanets() { // cập nhật vị trí (tính _x,_y trước) planets.forEach(p=>{ p.angle += p.speed; const rOffset = 6 * Math.sin(t*0.001 + p.zPhase); // trôi ra/vào nhẹ const rDynamic = p.r + rOffset; const baseX = CX + Math.cos(p.angle)*rDynamic; const baseY = CY + Math.sin(p.angle)rDynamic0.6;

  // z dao động
  const z = Math.sin(t*p.zSpeed + p.zPhase); // -1..1
  const zHeight = z * (Math.min(W,H)*0.06); // biên độ phụ thuộc màn hình
  const wobble = Math.sin(t*0.002 + p.zPhase*3) * (Math.min(W,H)*0.02);

  p._x = baseX + wobble*0.4;
  p._y = baseY + zHeight + wobble*0.6;
  p._depth = (z+1)/2; // 0..1
  p._size = p.size * (0.8 + 0.4*p._depth);
  p._zval = z;
});

// sort theo y để vẽ đúng lớp trước/sau
planets.sort((a,b) => (a._y - b._y));

// vẽ
planets.forEach(p=>{
  const size = p._size;
  // ánh sáng trung tâm (gần tâm sáng hơn)
  const dx = p._x - CX, dy = p._y - CY;
  const dist = Math.sqrt(dx*dx + dy*dy);
  const lightFactor = Math.max(0, 1 - dist / (Math.min(W,H)*0.5));
  let alpha = (0.45 + 0.55*lightFactor) * (0.45 + 0.55*p._depth);

  // chìm sâu -> mờ thêm
  if(p._zval < -0.6) alpha *= Math.max(0, 1 + p._zval + 0.6);

  // gradient hành tinh
  const grad = ctx.createRadialGradient(
    p._x - size*0.5, p._y - size*0.5, 1,
    p._x, p._y, size
  );
  grad.addColorStop(0, `rgba(255,255,255,${0.45*alpha})`);
  grad.addColorStop(0.45, p.color);
  grad.addColorStop(1, `rgba(0,0,0,0.8)`);

  ctx.beginPath();
  ctx.fillStyle = grad;
  ctx.arc(p._x, p._y, size, 0, Math.PI*2);
  ctx.fill();

  // khi chìm (z < 0) phủ fog cục bộ (phần dấu hiệu chìm)
  if(p._zval < 0) {
    ctx.beginPath();
    ctx.fillStyle = `rgba(200,180,150,${0.08 * Math.abs(p._zval)})`;
    ctx.arc(p._x, p._y, size*1.15, 0, Math.PI*2);
    ctx.fill();
  }
});

}

// foreground fog (che phía trên hành tinh) function drawNebulaForeground() { ctx.save(); ctx.translate(CX, CY); ctx.scale(1,0.6); // Chọn một phần spiralDust để vẽ foreground, và dùng alpha động for (let i=0;i<spiralDust.length;i+=3){ const d = spiralDust[i]; // vị trí cập nhật đã trong drawSpiralNebulaBackground, dùng d.angle const x = Math.cos(d.angle)d.r; const y = Math.sin(d.angle)d.r; // alpha dao động nhịp nhàng để foreground động const a = 0.06 + 0.06 * Math.sin(t0.003 + i); ctx.fillStyle = rgba(200,150,100,${a}); ctx.beginPath(); ctx.arc(x,y,d.size1.2,0,Math.PI*2); ctx.fill(); } ctx.restore(); } // --- VẼ thiên thạch vòng (asteroids) --- function drawAsteroids() { if(!asteroids || asteroids.length === 0) return; asteroids.forEach(a=>{ a.angle += a.speed; // vị trí theo tâm, giữ tỉ lệ theo chiều cao để có hiệu ứng ellipse const x = CX + Math.cos(a.angle) * a.r; const y = CY + Math.sin(a.angle) * a.r * 0.5; // tính xem đang "ở phía sau" hay "phía trước" (dùng sin để phân lớp) const behind = Math.sin(a.angle) > 0;

ctx.beginPath();
// đảm bảo radius tối thiểu để nhìn rõ trên màn nhỏ
const drawSize = Math.max(0.6, a.size * (behind ? 0.6 : 1));
ctx.arc(x, y, drawSize, 0, Math.PI * 2);

// tone mờ phía sau, sáng phía trước
ctx.fillStyle = behind ? "rgba(180,180,180,0.28)" : "rgba(220,220,220,0.9)";
ctx.fill();

}); }

// --- VẼ lõi trung tâm (core) với gradient thích ứng màn hình --- function drawCore(t) { const coreR = Math.max(7, Math.min(W,H)0.07); const pulse = 1 + 0.08Math.sin(t*0.05);

// quả cầu ctx.save(); const gSphere = ctx.createRadialGradient(CX, CY, 0, CX, CY, coreR); gSphere.addColorStop(0, "rgba(80,40,20,1)"); // lõi tối gSphere.addColorStop(1, "rgba(20,10,5,1)"); ctx.fillStyle = gSphere; ctx.beginPath(); ctx.arc(CX, CY, coreR, 0, Math.PI*2); ctx.fill();

// quầng sáng const glowR = coreR3pulse; const gGlow = ctx.createRadialGradient(CX, CY, coreR0.8, CX, CY, glowR); gGlow.addColorStop(0, "rgba(255,220,120,0.9)"); gGlow.addColorStop(0.3, "rgba(255,150,50,0.6)"); gGlow.addColorStop(0.6, "rgba(255,100,20,0.25)"); gGlow.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = gGlow; ctx.beginPath(); ctx.arc(CX, CY, glowR, 0, Math.PI2); ctx.fill();

ctx.restore(); }

// ====================== WORLD MAP ====================== // --- thêm 1 biến toàn cục ở đầu module map (cùng scope với W,H,CX,CY,regions) let mapLonOffset = 0;

// --- Hàm tạo đa giác (1 lần) --- function generatePolygon(r, irregularity=0.5, points=12) { const shape = []; for (let i=0; i<points; i++) { const angle = (i/points)Math.PI2; const radius = r * (0.7 + Math.random()*irregularity); shape.push({ angle, radius }); } return shape; }

// --- Thay thế generateRegions() ---

function generateRegions() { regions = []; const base = Math.min(W, H); const R = Math.min(W, H) / 3; let targetCount; if (selectedWorld.type === "dai") targetCount = 8 + Math.floor(Math.random()*5); if (selectedWorld.type === "trung") targetCount = 7 + Math.floor(Math.random()*3); if (selectedWorld.type === "tieu") targetCount = 5 + Math.floor(Math.random()*3);

const minPx = Math.round(base * 0.05); const maxPx = Math.round(base * 0.10);

mapLonOffset = Math.random() * Math.PI * 2;

function sampleLatLon() { const u = Math.random(); const lat = Math.asin(2*u - 1); const lon = Math.random() * Math.PI * 2; return { lat, lon }; }

function angularDistance(lat1, lon1, lat2, lon2) { const dlon = lon1 - lon2; const cosAng = Math.sin(lat1)*Math.sin(lat2) + Math.cos(lat1)*Math.cos(lat2)*Math.cos(dlon); return Math.acos(Math.max(-1, Math.min(1, cosAng))); }

let attempts = 0; while (regions.length < targetCount && attempts < targetCount * 800) { attempts++; const { lat, lon } = sampleLatLon(); const pxRadius = minPx + Math.random() * (maxPx - minPx); const angRadius = pxRadius / R;

const rlon = lon + mapLonOffset;
const X = R * Math.cos(lat) * Math.cos(rlon);
const Y = R * Math.sin(lat);
const Z = R * Math.cos(lat) * Math.sin(rlon);
if (Z <= 0) continue;

const distFromCenter = Math.sqrt(X*X + Y*Y);
if (distFromCenter + pxRadius > (R - 2)) continue;

let ok = true;
for (const e of regions) {
  const ang = angularDistance(lat, lon, e.lat, e.lon);
  if (ang < (angRadius + e.angRadius + 0.25)) { ok = false; break; }
  const dx = (CX + X) - e.x;
  const dy = (CY + Y) - e.y;
  const dist2D = Math.sqrt(dx*dx + dy*dy);
  if (dist2D < (pxRadius + e.r) * 1.1) { ok = false; break; }
}
if (!ok) continue;

// 👉 chọn loại region ngẫu nhiên từ REGION_TYPES
const typeKeys = Object.keys(REGION_TYPES);
const type = typeKeys[Math.floor(Math.random()*typeKeys.length)];
const cfg = REGION_TYPES[type];

regions.push({
  id: selectedWorld.type + "_" + type + "_" + regions.length + "_" + Date.now(), // 👈 id duy nhất
  lat, lon, r: pxRadius, angRadius,
  x: CX + X, y: CY + Y, z: Z,
  type, name: cfg.name,
  shape: generatePolygon(pxRadius, 0.5, 14)
});

}

if (regions.length < targetCount) { console.warn(⚠️ Chỉ sinh được ${regions.length}/${targetCount} lục địa (hạn chế chồng lấn).); } }

// --- Hàm vẽ lục địa đã có shape sẵn --- function drawContinent(ctx, cx, cy, shape, type) { ctx.beginPath(); shape.forEach((p,i)=>{ const x = cx + Math.cos(p.angle)*p.radius; const y = cy + Math.sin(p.angle)*p.radius; if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.closePath();

const cfg = REGION_TYPES[type] || {color:"gray"}; ctx.fillStyle = cfg.color; // màu nền theo loại khu vực ctx.strokeStyle = "white"; // viền trắng ctx.lineWidth = 2; ctx.fill(); ctx.stroke(); }

// --- Render regions (bỏ qua mặt sau) --- function renderRegions(ctx) { regions.forEach(r=>{ if(r.z <= 0) return; drawContinent(ctx, r.x, r.y, r.shape, r.type); // 👈 thêm r.type }); }

// --- Hàm kiểm tra click chính xác trong shape --- function pointInRegion(ctx, x, y, region) { const path = new Path2D(); region.shape.forEach((p,i)=>{ const px = region.x + Math.cos(p.angle)*p.radius; const py = region.y + Math.sin(p.angle)*p.radius; if(i===0) path.moveTo(px, py); else path.lineTo(px, py); }); path.closePath(); return ctx.isPointInPath(path, x, y); }

// biến toàn cục lưu sao let worldStars = null;

function drawWorld() { // ===== Nền trời ===== ctx.fillStyle = "black"; ctx.fillRect(0,0,W,H);

// ===== Sinh sao 1 lần duy nhất ===== if(!worldStars){ worldStars = []; const starCount = 150; for (let i=0; i<starCount; i++){ worldStars.push({ x: Math.random()*W, y: Math.random()*H, r: Math.random()*1.5 + 0.5, alpha: Math.random()*0.5 + 0.5, speed: (Math.random()*0.02 + 0.01) }); } }

// ===== Vẽ sao (nhấp nháy nhẹ) ===== worldStars.forEach(s=>{ s.alpha += (Math.random()>0.5 ? 1 : -1) * s.speed; if(s.alpha < 0.3) s.alpha = 0.3; if(s.alpha > 1) s.alpha = 1;

ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI*2);
ctx.fillStyle = `rgba(255,255,255,${s.alpha})`;
ctx.fill();

});

// ===== Quả cầu ===== const R = Math.min(W,H)/3; const g = ctx.createRadialGradient(CX, CY, R0.1, CX, CY, R); g.addColorStop(0, "rgba(100,200,255,1)"); g.addColorStop(1, "rgba(0,0,60,1)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(CX,CY,R,0,Math.PI2); ctx.fill();

// ===== Region ===== const visibleRegions = regions.filter(r=>r.z > 0).sort((a,b)=> a.z - b.z); visibleRegions.forEach(r => drawContinent(ctx, r.x, r.y, r.shape, r.type));

// ===== Tên lục địa ===== ctx.font = "bold 14px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; visibleRegions.forEach(r => { let minY = Infinity, avgX = 0; r.shape.forEach(p => { const px = r.x + Math.cos(p.angle) * p.radius; const py = r.y + Math.sin(p.angle) * p.radius; avgX += px; if (py < minY) minY = py; }); avgX /= r.shape.length;

const labelX = avgX;
const labelY = minY - 5;

ctx.lineWidth = 4;
ctx.strokeStyle = "black";
ctx.strokeText(r.name, labelX, labelY);

ctx.fillStyle = "white";
ctx.fillText(r.name, labelX, labelY);

}); } // ====================== REGION MAP ======================

let continents = []; // mảng các polygon

function generateContinents() { continents = []; let n = 2 + Math.floor(Math.random()*3); // 2–4 châu lục let cols = n; // chia ngang let cellW = W / cols; // chiều rộng 1 ô

for (let i=0; i<n; i++) { // tâm lục địa nằm trong ô thứ i let cx = (i+0.5)cellW + (Math.random()cellW0.4 - cellW0.2); let cy = H/2 + (Math.random()*200 - 100);

let pts = [];
let sides = 6; // 6 đỉnh cho tự nhiên hơn
let radius = 100 + Math.random()*60;
for (let a=0; a<Math.PI*2; a+= (Math.PI*2)/sides) {
  let r = radius * (0.7 + Math.random()*0.6);
  pts.push({
    x: cx + Math.cos(a)*r,
    y: cy + Math.sin(a)*r
  });
}
continents.push(pts);

} }

// kiểm tra 1 điểm có nằm trong polygon không (thuật toán ray-casting) function pointInPolygon(x, y, poly) { let inside = false; for (let i=0, j=poly.length-1; i<poly.length; j=i++) { const xi = poly[i].x, yi = poly[i].y; const xj = poly[j].x, yj = poly[j].y; const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; }

function generateMaps(region) { maps = [];

// --- số node cơ bản dựa trên world.type --- let baseCount = 0; if(selectedWorld.type === "dai") baseCount = 9 + Math.floor(Math.random()*4); if(selectedWorld.type === "trung") baseCount = 5 + Math.floor(Math.random()*3); if(selectedWorld.type === "tieu") baseCount = 3 + Math.floor(Math.random()*3);

// --- modifier theo region.type --- let modifier = 0; switch(region.type){ case "cave": modifier = +3; break; // hang động nhiều ngõ nhỏ case "jungle": modifier = +6; break; // rừng rậm → nhiều node hơn case "volcano": modifier = -2; break; // núi lửa → ít node hơn case "mountain": modifier = 0; break; case "ice": modifier = +2; break; case "desert": modifier = -1; break; case "sea": modifier = -2; break; case "island": modifier = +1; break; case "wasteland": modifier = -3; break; default: modifier = 0; break; }

const count = Math.max(2, baseCount + modifier); // không để < 2

for (let i=0;i<count;i++) { let node, tries=0; do { node = { x: 50 + Math.random()(W-100), y: 50 + Math.random()(H-100), r: 20 + Math.random()10, name: "Map " + (i+1), color: REGION_TYPES[region.type]?.nodeColor || "#999" }; tries++; } while ( (tries<100) && ( !continents.some(poly => pointInPolygon(node.x, node.y, poly)) || maps.some(m=>{ let dx=m.x-node.x, dy=m.y-node.y; return Math.sqrt(dxdx + dy*dy) < m.r+m.r+10; }) ) ); maps.push(node); } }

function drawRegion() { // nền biển chung ctx.fillStyle = "#002b55"; ctx.fillRect(0,0,W,H);

// chọn kiểu region hiện tại const cfg = REGION_TYPES[selectedRegion.type] || REGION_TYPES.jungle;

// vẽ châu lục ctx.fillStyle = cfg.color; continents.forEach(poly=>{ ctx.beginPath(); ctx.moveTo(poly[0].x, poly[0].y); for(let k=1;k<poly.length;k++) ctx.lineTo(poly[k].x, poly[k].y); ctx.closePath(); ctx.fill(); });

// vẽ các map node maps.forEach(m=>{ ctx.beginPath(); ctx.fillStyle = cfg.nodeColor; ctx.arc(m.x, m.y, m.r, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.stroke();

ctx.fillStyle = "white";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.fillText(m.name, m.x, m.y - m.r - 6);

}); }

// vẽ map nodes maps.forEach(m=>{ ctx.beginPath(); ctx.fillStyle = "rgba(255,215,0,0.9)"; ctx.arc(m.x, m.y, m.r, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.stroke();

ctx.fillStyle = "white";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.fillText(m.name, m.x, m.y - m.r - 6);

});

// ==================== DUNGEON MODULE (V9.5 – đầy đủ: loop, resize, controls hiển thị, boss) ==================== let dungeon = null; let playerPos = { x: 1, y: 1 }; let tileSize = 32; let lightRadius = 8; let dungeonCtx = null; let dungeonBlink = 0; let inBossRoom = false; let dungeonLoopId = null;

// ======== HỆ THỐNG TẦNG DUNGEON ======== window.currentFloor = 1; // bắt đầu tầng 1

function floorDifficultyMultiplier(floor) { // hệ số tăng theo tầng: 1.0, 1.2, 1.5, 1.8, ... return 1 + (floor - 1) * 0.2; } // ⚙️ Sinh đặc điểm dungeon dựa trên loại vùng // === Cấu hình đặc điểm Dungeon theo từng loại khu vực === const DUNGEON_TRAITS = Object.fromEntries(Object.entries(REGION_TYPES).map(([key, region]) => { let base = { floorColor: region.color, wallColor: region.nodeColor, chestChance: 0.1, enemyChance: 0.15, enemyType: "default", lavaChance: 0, slippery: false, poisonous: false, trapChance: 0, pitChance: 0, fog: null, wallSymbol: null, };

switch (key) { case "desert": Object.assign(base, { enemyType: "sandWorm", chestChance: 0.08, fog: "light", trapChance: 0.03, wallSymbol: "⛰️", wallColor: "#c2a04e", floorColor: "#e1c16e", }); break;

case "ice":
case "tundra":
  Object.assign(base, {
    enemyType: "iceSpirit",
    chestChance: 0.12,
    fog: "frost",
    slippery: true,
    trapChance: 0.02,
    wallSymbol: "❄️",
    wallColor: "#b0d8e5",
    floorColor: "#dfefff",
  });
  break;

case "cave":
  Object.assign(base, {
    enemyType: "bat",
    darkness: true,
    torchChance: 0.25,
    pitChance: 0.03,
    fog: "dense",
    wallSymbol: "🪨",
    wallColor: "#3b3b3b",
    floorColor: "#1e1e1e",
  });
  break;

case "mountain":
  Object.assign(base, {
    enemyType: "rockGolem",
    heightEffect: true,
    pitChance: 0.04,
    wallSymbol: "⛰️",
    wallColor: "#505050",
    floorColor: "#707070",
  });
  break;

case "volcano":
  Object.assign(base, {
    enemyType: "fireDemon",
    lavaChance: 0.15,
    fog: "heat",
    trapChance: 0.02,
    wallSymbol: "🌋",
    wallColor: "#7a1f00",
    floorColor: "#3a0d00",
  });
  break;

case "jungle":
  Object.assign(base, {
    enemyType: "giantSpider",
    vineEffect: true,
    chestChance: 0.1,
    fog: "dense",
    poisonous: true,
    trapChance: 0.02,
    wallSymbol: "🌳",
    wallColor: "#144015",
    floorColor: "#1e5620",
  });
  break;

case "swamp":
  Object.assign(base, {
    enemyType: "slime",
    slowEffect: true,
    chestChance: 0.05,
    poisonous: true,
    fog: "dense",
    wallSymbol: "🌫️",
    wallColor: "#2f3b2d",
    floorColor: "#3b4f35",
  });
  break;

case "sea":
  Object.assign(base, {
    enemyType: "seaSerpent",
    underwater: true,
    fog: "water",
    chestChance: 0.07,
    pitChance: 0.03,
    wallSymbol: "🌊",
    wallColor: "#006699",
    floorColor: "#3399cc",
  });
  break;

case "deepsea":
  Object.assign(base, {
    enemyType: "deepSerpent",
    underwater: true,
    fog: "water",
    chestChance: 0.06,
    pitChance: 0.04,
    poisonous: true,
    wallSymbol: "🪸",
    wallColor: "#002b4a",
    floorColor: "#003b66",
  });
  break;

case "island":
  Object.assign(base, {
    enemyType: "boar",
    chestChance: 0.1,
    fog: "light",
    trapChance: 0.02,
    wallSymbol: "🌴",
    wallColor: "#2a6b37",
    floorColor: "#3b8044",
  });
  break;

case "wasteland":
  Object.assign(base, {
    enemyType: "skeleton",
    darkness: true,
    chestChance: 0.07,
    poisonous: true,
    wallSymbol: "💀",
    wallColor: "#4b3621",
    floorColor: "#7a5230",
  });
  break;

case "ruins":
  Object.assign(base, {
    enemyType: "ancientGuardian",
    darkness: true,
    torchChance: 0.2,
    trapChance: 0.05,
    wallSymbol: "🧱",
    wallColor: "#6b5c4a",
    floorColor: "#84715a",
  });
  break;

case "deepforest":
  Object.assign(base, {
    enemyType: "shadowWolf",
    poisonous: true,
    fog: "dense",
    chestChance: 0.08,
    wallSymbol: "🌲",
    wallColor: "#0b4f1c",
    floorColor: "#1a6f2d",
  });
  break;

case "grassland":
  Object.assign(base, {
    enemyType: "boar",
    chestChance: 0.1,
    fog: "light",
    wallSymbol: "🌿",
    wallColor: "#3b7a2a",
    floorColor: "#4e8c3a",
  });
  break;

case "plain":
  Object.assign(base, {
    enemyType: "boar",
    chestChance: 0.12,
    fog: "light",
    wallSymbol: "🌾",
    wallColor: "#7aa84f",
    floorColor: "#9dd465",
  });
  break;

default:
  Object.assign(base, {
    wallSymbol: "⬜",
    wallColor: region.nodeColor || "#777",
    floorColor: region.color || "#555",
  });
  break;

}

return [key, base]; }));

// === WORLD ROOM QUOTA (giữ tổng số phòng theo loại thế giới) === window.worldRoomQuota = window.worldRoomQuota || {}; // { worldKey: remainingCount }

// trả về số ban đầu theo worldType function initialQuotaForWorld(worldType) { if (worldType === "dai") return 10; if (worldType === "trung") return 7; return 4; // tieu }

// khởi tạo quota cho 1 thế giới (gọi khi vào world lần đầu) function ensureWorldQuota(worldKey, worldType) { if (!window.worldRoomQuota[worldKey]) { window.worldRoomQuota[worldKey] = initialQuotaForWorld(worldType); } }

// tiêu 1 room nếu còn quota, trả về true nếu thành công function consumeRoomQuota(worldKey) { if (!window.worldRoomQuota[worldKey]) return false; if (window.worldRoomQuota[worldKey] <= 0) return false; window.worldRoomQuota[worldKey]--; return true; }

// reset quota (nếu cần) — gọi khi quay lại Cosmos hoặc khi muốn reset toàn bộ function resetAllQuotas(preserveContext=true) { const backup = preserveContext ? window.currentWorldType : null; window.worldRoomQuota = {}; if (preserveContext && backup) window.currentWorldType = backup; }

// --- Gọi khi click region --- // --- Vào dungeon --- function enterDungeon(region) { currentMapLevel = "dungeon"; inBossRoom = false;

// 🧭 Xác định loại thế giới hiện tại let worldType = "tieu"; if (selectedWorld && selectedWorld.type) { worldType = selectedWorld.type; } window.currentWorldType = worldType; // 🔹 lưu global để movePlayer dùng window.lastWorldContext = { type: worldType, worldId: selectedWorld?.id || worldType, worldName: selectedWorld?.name || worldType, regionId: region.id || null }; // 🪐 LOG PHÂN LOẠI DUNGEON let worldLabel = "Tiểu Thiên Thế Giới"; if (worldType === "trung") worldLabel = "Trung Thiên Thế Giới"; else if (worldType === "dai") worldLabel = "Đại Thiên Thế Giới";

console.log(🌌 Vào Dungeon thuộc ${worldLabel}); console.log(🗺️ Khu vực: ${region.name || region.id || "(Không rõ)"} | Chủ đề: ${region.type || "cave"}); console.log(🏷️ worldType = ${worldType}, worldId = ${selectedWorld?.id || "?"});

// 🔹 Lưu chủ đề gốc của dungeon để giữ nguyên qua các tầng window.dungeonBaseTheme = region.type || "cave";

// ♻️ Reset flag để dungeon mới không kế thừa boss cũ window.bossRoomCreated = false;

// 🧩 Tạo dungeon theo loại thế giới dungeon = generateDungeon(region.type || "cave", 25, 18, worldType); playerPos = { ...dungeon.playerStart };

setupDungeonCanvas(); setupDungeonControls(); startDungeonLoop(); setupDungeonHUD();

// --- QUOTA: kiểm tra và tiêu cho phòng đầu tiên --- const worldKey = selectedWorld ? (selectedWorld.id || selectedWorld.type) : worldType; ensureWorldQuota(worldKey, worldType);

const consumed = consumeRoomQuota(worldKey); dungeon._canSpawnChildren = consumed;

console.log("Đã vào dungeon:", region.type, "thuộc thế giới:", worldType); updateMapButtons && updateMapButtons();

// Mini-map setup initRoomMiniMap(); markRoomVisited(dungeon); drawRoomMiniMap(); setupMiniMapToggle();

// 🚫 Khóa các nút điều hướng khi đang trong dungeon const navBtns = ["btnHome", "btnInv", "btnSkills", "btnMap"]; navBtns.forEach(id => { const el = document.getElementById(id); if (el) { el.disabled = true; el.style.opacity = "0.4"; el.style.pointerEvents = "none"; el.style.filter = "grayscale(0.6)"; el.style.transition = "all 0.3s ease"; } }); }

// --- Thoát dungeon --- function exitDungeon() { console.log("🚪 Thoát dungeon..."); window.currentFloor = 1; // reset tầng resetAllQuotas(); // reset quota tất cả thế giới console.log("🔁 Reset toàn bộ room quota khi thoát dungeon");

// 1️⃣ Chuyển trạng thái map currentMapLevel = "region";

// 2️⃣ Dừng vòng lặp dungeon stopDungeonLoop();

// 3️⃣ Xóa listener & D-pad document.removeEventListener("keydown", handleDungeonKey); removeDungeonControls(); removeDungeonHUD();

// 4️⃣ Xóa canvas dungeon khỏi giao diện (nếu có) const c = document.getElementById("worldMapCanvas"); if (c) { const ctx = c.getContext("2d"); ctx && ctx.clearRect(0, 0, c.width, c.height); }

// 5️⃣ Làm sạch dữ liệu tạm dungeon (nếu cần) window.dungeon = null; window.dungeonGraph = {}; if (window.lastWorldContext) { window.currentWorldType = window.lastWorldContext.type; if (!selectedWorld) { selectedWorld = { id: window.lastWorldContext.worldId, type: window.lastWorldContext.type, name: window.lastWorldContext.worldName }; } }

// 6️⃣ Cập nhật lại nút giao diện if (typeof updateMapButtons === "function") updateMapButtons();

// 7️⃣ Gọi lại hàm hiển thị bản đồ region (nếu có) if (typeof showRegionMap === "function") { showRegionMap(); } else { console.warn("⚠️ Không tìm thấy hàm showRegionMap() để hiển thị lại bản đồ vùng."); }

// 8️⃣ Dọn canvas mini-map const mm = document.getElementById("roomMiniMap"); if (mm) mm.remove(); const toggleBtn = document.getElementById("toggleMiniMapBtn"); if (toggleBtn) toggleBtn.remove();

// 9️⃣ 🔹 Reset dữ liệu mini-map (rất quan trọng!) roomMapVisited = {}; roomMapPositions = {}; roomMapCanvas = null; window.roomMapCenter = null;

// ✅ Mở khóa lại nút điều hướng sau khi thoát dungeon const navBtns = ["btnHome", "btnInv", "btnSkills", "btnMap"]; navBtns.forEach(id => { const el = document.getElementById(id); if (el) { el.disabled = false; el.style.opacity = "1"; el.style.pointerEvents = "auto"; el.style.filter = "none"; } });

console.log("🧹 Đã xoá dữ liệu mini-map phòng sau khi thoát dungeon"); try { resizeCanvas?.(); // đảm bảo kích thước canvas đúng startLoop?.(); // khởi động lại vòng lặp chính console.log("🌍 Quay lại bản đồ region thành công!"); } catch (err) { console.error("❌ Lỗi khi khởi động lại region loop:", err); } }

// === Sinh dungeon (phòng + hành lang + cửa) === function generateDungeon(theme = "cave", w = 25, h = 18, worldType = "tieu") { // 🏷️ Lưu chủ đề gốc toàn cục (để các tầng sau giữ nguyên) window.dungeonBaseTheme = window.dungeonBaseTheme || theme;

const map = Array.from({ length: h }, () => Array(w).fill(1)); // 1 = tường const trait = DUNGEON_TRAITS[theme] || {}; // đặc điểm theo vùng

// --- thiết lập số lượng phòng theo loại thế giới --- let roomCount; if (worldType === "dai") roomCount = 10; else if (worldType === "trung") roomCount = 7; else roomCount = 4; // tiểu

// --- tạo phòng --- for (let i = 0; i < roomCount; i++) { const rw = 3 + Math.floor(Math.random() * 4); const rh = 3 + Math.floor(Math.random() * 3); const rx = 1 + Math.floor(Math.random() * (w - rw - 2)); const ry = 1 + Math.floor(Math.random() * (h - rh - 2)); for (let y = ry; y < ry + rh; y++) { for (let x = rx; x < rx + rw; x++) { map[y][x] = 0; } } }

// --- hành lang ngẫu nhiên --- for (let i = 0; i < roomCount - 1; i++) { const x1 = 2 + Math.floor(Math.random() * (w - 4)); const y1 = 2 + Math.floor(Math.random() * (h - 4)); const x2 = 2 + Math.floor(Math.random() * (w - 4)); const y2 = 2 + Math.floor(Math.random() * (h - 4)); for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) map[y1][x] = 0; for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) map[y][x2] = 0; }

// --- tường ngoài --- for (let x = 0; x < w; x++) { map[0][x] = 1; map[h - 1][x] = 1; } for (let y = 0; y < h; y++) { map[y][0] = 1; map[y][w - 1] = 1; }

// --- spawn rương & quái --- const chestChance = trait.chestChance ?? 0.1; const enemyChance = trait.enemyChance ?? 0.15;

const floorCells = []; for (let y = 1; y < h - 1; y++) for (let x = 1; x < w - 1; x++) if (map[y][x] === 0) floorCells.push([x, y]);

for (const [x, y] of floorCells) { const rnd = Math.random(); if (rnd < chestChance) map[y][x] = "C"; else if (rnd < chestChance + enemyChance) map[y][x] = "E"; else if (Math.random() < 0.02) map[y][x] = "?"; }

// --- hiệu ứng môi trường (tùy theme) --- if (trait.lavaChance) for (let i = 0; i < w * h * trait.lavaChance; i++) { const x = Math.floor(Math.random() * w); const y = Math.floor(Math.random() * h); if (map[y]?.[x] === 0) map[y][x] = "🔥"; } if (trait.slippery) for (let i = 0; i < w * h * 0.02; i++) { const x = Math.floor(Math.random() * w); const y = Math.floor(Math.random() * h); if (map[y]?.[x] === 0) map[y][x] = "❄️"; } if (trait.poisonous) for (let i = 0; i < w * h * 0.02; i++) { const x = Math.floor(Math.random() * w); const y = Math.floor(Math.random() * h); if (map[y]?.[x] === 0) map[y][x] = "☠️"; } if (trait.trapChance) for (let i = 0; i < w * h * trait.trapChance; i++) { const x = Math.floor(Math.random() * w); const y = Math.floor(Math.random() * h); if (map[y]?.[x] === 0) map[y][x] = "🧨"; } if (trait.pitChance) for (let i = 0; i < w * h * trait.pitChance; i++) { const x = Math.floor(Math.random() * w); const y = Math.floor(Math.random() * h); if (map[y]?.[x] === 0) map[y][x] = "🕳️"; } if (["swamp", "ruins", "deepsea"].includes(theme)) { const fogDensity = theme === "swamp" ? 0.05 : 0.02; for (let i = 0; i < w * h * fogDensity; i++) { const x = Math.floor(Math.random() * w); const y = Math.floor(Math.random() * h); if (map[y]?.[x] === 0) map[y][x] = "🌫️"; } }

// 🌳🌋🪨 Thêm tường đặc trưng và biến thể địa hình const wallSymbol = trait.wallSymbol || null; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { // xen kẽ biểu tượng tường đặc trưng if (map[y][x] === 1 && wallSymbol && Math.random() < 0.2) { map[y][x] = wallSymbol; }

  // dòng dung nham trong vùng núi lửa
  if (theme === "volcano" && map[y][x] === 0 && Math.random() < 0.05)
    map[y][x] = "🔥";

  // đầm lầy / rừng rậm: bãi độc
  if (["swamp", "jungle"].includes(theme) && map[y][x] === 0 && Math.random() < 0.03)
    map[y][x] = "☠️";
}

}

// --- cửa & boss --- const doors = []; const wallPositions = []; for (let x = 1; x < w - 1; x++) { if (map[1][x] === 1 && map[2][x] === 0) wallPositions.push({ x, y: 1, openTo: { x, y: 2 } }); if (map[h - 2][x] === 0 && map[h - 1][x] === 1) wallPositions.push({ x, y: h - 1, openTo: { x, y: h - 2 } }); } for (let y = 1; y < h - 1; y++) { if (map[y][1] === 1 && map[y][2] === 0) wallPositions.push({ x: 1, y, openTo: { x: 2, y } }); if (map[y][w - 2] === 0 && map[y][w - 1] === 1) wallPositions.push({ x: w - 1, y, openTo: { x: w - 2, y } }); }

const makeDoor = (obj, special = false) => ({ id: 'door_' + Math.random().toString(36).slice(2, 9), x: obj.x, y: obj.y, openTo: obj.openTo, special, });

if (wallPositions.length) { const doorCount = Math.min(3, 1 + Math.floor(Math.random() * 3)); for (let i = 0; i < doorCount; i++) { const pos = wallPositions[Math.floor(Math.random() * wallPositions.length)]; map[pos.y][pos.x] = "D"; map[pos.openTo.y][pos.openTo.x] = 0; doors.push(makeDoor(pos)); } }

if (wallPositions.length > 1 && !window.bossRoomCreated) { window.bossRoomCreated = true; const candidate = wallPositions[Math.floor(Math.random() * wallPositions.length)]; map[candidate.y][candidate.x] = "G"; map[candidate.openTo.y][candidate.openTo.x] = 0; doors.push(makeDoor(candidate, true)); }

// --- hoàn thiện dungeon --- const dungeonObj = { map, w, h, theme, doors, trait };

connectIsolatedAreas(dungeonObj); dungeonObj.playerStart = findSafeSpawn(dungeonObj);

// 🌈 Hiệu ứng nền riêng theo theme dungeonObj.visual = { background: (() => { switch (theme) { case "desert": return "#d4b483"; case "ice": return "#b5d8f0"; case "jungle": return "#2e7d32"; case "volcano": return "#2b0d0d"; case "cave": return "#1a1a1a"; case "swamp": return "#3e4035"; case "ruins": return "#6b6659"; case "deepsea": return "#0b1d3a"; case "mountain": return "#5b5b5b"; case "wasteland": return "#5a4b3f"; default: return "#222"; } })(), lightColor: (() => { switch (theme) { case "desert": return "rgba(255,240,180,0.4)"; case "ice": return "rgba(180,220,255,0.3)"; case "jungle": return "rgba(80,255,120,0.2)"; case "volcano": return "rgba(255,80,0,0.3)"; case "swamp": return "rgba(100,160,80,0.2)"; default: return "rgba(255,255,255,0.2)"; } })() };

return dungeonObj; }

function findClosestWallToPlayer(dungeon) { const { map, w, h } = dungeon; const px = Math.floor(dungeon.playerStart.x); const py = Math.floor(dungeon.playerStart.y); const dirs = [ { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 } ]; for (const d of dirs) { const tx = px + d.x; const ty = py + d.y; if (map[ty] && map[ty][tx] === 1) { return { x: tx, y: ty, openTo: { x: px, y: py } }; } } // fallback: chọn tường ngẫu nhiên for (let y = 1; y < h - 1; y++) { for (let x = 1; x < w - 1; x++) { if (map[y][x] === 1 && map[y + 1]?.[x] === 0) return { x, y, openTo: { x, y: y + 1 } }; } } return null; } // === Phòng Boss riêng biệt === function generateBossRoom() { const w = 25, h = 18; const map = Array.from({ length: h }, () => Array(w).fill(1));

for (let y = 2; y < h - 2; y++) for (let x = 2; x < w - 2; x++) map[y][x] = 0;

const centerX = Math.floor(w / 2); const centerY = Math.floor(h / 2);

map[centerY][centerX] = "👹";

return { map, w, h, doors: [], theme: "boss", playerStart: { x: centerX, y: centerY + 3 }, isBossRoom: true, visual: { background: "#200000", lightColor: "rgba(255, 80, 0, 0.35)", gradient: ["#220000", "#5a0000", "#9c1a00"], particle: "🔥" } }; }

// --- nối các vùng tách rời bằng đường hầm --- function connectIsolatedAreas(dungeonObj) { const { w, h, map } = dungeonObj; const visited = Array.from({ length: h }, () => Array(w).fill(false)); const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];

function floodFill(sx, sy) { const stack = [[sx, sy]]; const cells = []; while (stack.length) { const [cx, cy] = stack.pop(); if (cx < 0 || cy < 0 || cx >= w || cy >= h) continue; if (visited[cy][cx]) continue; if (map[cy][cx] !== 0) continue; visited[cy][cx] = true; cells.push([cx, cy]); for (const [dx, dy] of dirs) stack.push([cx + dx, cy + dy]); } return cells; }

const regions = []; for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (!visited[y][x] && map[y][x] === 0) regions.push(floodFill(x, y)); if (regions.length <= 1) return;

for (let i = 1; i < regions.length; i++) { const rA = regions[0][Math.floor(Math.random() * regions[0].length)]; const rB = regions[i][Math.floor(Math.random() * regions[i].length)]; let [cx, cy] = rA, [x2, y2] = rB; while (cx !== x2 || cy !== y2) { map[cy][cx] = 0; const dx = Math.sign(x2 - cx); const dy = Math.sign(y2 - cy); if (Math.random() < 0.6) { if (Math.abs(x2 - cx) > 0) cx += dx; else cy += dy; } else { if (Math.abs(y2 - cy) > 0) cy += dy; else cx += dx; } } } }

// === Tìm spawn an toàn (ô 0) === function findSafeSpawn(dungeonObj) { const { map, w, h } = dungeonObj; for (let tries = 0; tries < 1000; tries++) { const x = Math.floor(Math.random() * (w - 2)) + 1; const y = Math.floor(Math.random() * (h - 2)) + 1; if (map[y][x] === 0) return { x, y }; } return { x: 1, y: 1 }; }

// === Setup canvas & resize handler === function setupDungeonCanvas() { const c = document.getElementById("worldMapCanvas"); if (!c) { console.warn("Canvas #worldMapCanvas không tồn tại"); return; } // canvas full viewport c.style.position = "fixed"; c.style.left = "0"; c.style.top = "0"; c.style.width = "100vw"; c.style.height = "100vh"; c.width = window.innerWidth; c.height = window.innerHeight; c.style.zIndex = "1";

dungeonCtx = c.getContext("2d"); dungeonCtx.imageSmoothingEnabled = false;

window.addEventListener("resize", onDungeonResize); document.addEventListener("keydown", handleDungeonKey); }

// ==================== PATCH HUD + POPUP NHÂN VẬT ====================

// ==================== HUD + Popup nâng cao (ghi đè các hàm cũ) ====================

// REMOVE HUD (gọi khi thoát dungeon / backToWorld / backToCosmos) function removeDungeonHUD() { // close popup nếu mở if (charPopup) { // trả các node đã move về vị trí cũ try { toggleCharacterPopup(); } catch(e){} } const el = document.getElementById("dungeonHUD"); if (el) el.remove();

// cũng đảm bảo mini-map toggle bị xóa (tuỳ ý) const tbtn = document.getElementById("toggleMiniMapBtn"); if (tbtn) tbtn.remove(); }

window.updateTinyBars = safeUpdateBars; // SETUP HUD: avatar + 3 thanh nhỏ nằm ngang bên phải avatar function setupDungeonHUD() { removeDungeonHUD(); // sạch trước khi tạo

const hud = document.createElement("div"); hud.id = "dungeonHUD"; Object.assign(hud.style, { position: "fixed", left: "12px", zIndex: "10001", pointerEvents: "auto", display: "flex", alignItems: "center", gap: "10px" });

// đặt HUD dưới #mapControls nếu có (tránh che 'Thoát Dungeon') const mapCtrl = document.getElementById("mapControls"); if (mapCtrl) { const rect = mapCtrl.getBoundingClientRect(); hud.style.top = (rect.bottom && rect.bottom > 0 ? rect.bottom + 8 : 10) + "px"; } else { hud.style.top = "10px"; }

// Avatar button const charBtn = document.createElement("button"); charBtn.id = "dungeonCharBtn"; charBtn.innerText = "👤"; Object.assign(charBtn.style, { width: "44px", height: "44px", fontSize: "22px", borderRadius: "10px", border: "1px solid rgba(255,255,255,0.18)", background: "rgba(0,0,0,0.36)", color: "#fff", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }); charBtn.onclick = toggleCharacterPopup; hud.appendChild(charBtn);

// small bars container (to the right of avatar) const bars = document.createElement("div"); bars.id = "dungeonSmallBars"; Object.assign(bars.style, { display: "flex", flexDirection: "column", gap: "6px", width: "160px", padding: "8px", borderRadius: "8px", background: "rgba(0,0,0,0.36)", border: "1px solid rgba(255,255,255,0.04)" });

// each bar: just a colored fill (no numbers) function mkBar(id, color) { const wrap = document.createElement("div"); wrap.style.width = "100%"; wrap.style.height = "8px"; wrap.style.background = "#2a2a2a"; wrap.style.borderRadius = "6px"; wrap.style.overflow = "hidden"; const fill = document.createElement("div"); fill.id = id; fill.style.height = "100%"; fill.style.width = "0%"; fill.style.background = color; wrap.appendChild(fill); return wrap; }

bars.appendChild(mkBar("hpBarTiny", "#ff6161")); bars.appendChild(mkBar("mpBarTiny", "#5fb0ff")); bars.appendChild(mkBar("stBarTiny", "#79ffb5"));

hud.appendChild(bars); document.body.appendChild(hud);

// Tạo nút mini-map nhưng ẩn (setupMiniMapToggle() có thể được gọi sau) setupMiniMapToggle && setupMiniMapToggle();

// cập nhật giá trị nhỏ ngay safeUpdateBars(); }

// SAFE update: cập nhật cả thanh nhỏ từ các nguồn chính function safeUpdateBars(){ // gọi updateHeader/updateBars nếu có (để cập nhật các id chính trong #view-start) try { if (typeof updateHeader === "function") updateHeader(); if (typeof updateBars === "function") updateBars(); } catch(e){ /* ignore */ }

// if primary (view-start) bars exist, copy values -> tiny bars const copyFromPrimary = function() { const pHpBar = document.getElementById("hpBar"); const pMpBar = document.getElementById("mpBar"); const pStBar = document.getElementById("staminaBar");

const tHp = document.getElementById("hpBarTiny");
const tMp = document.getElementById("mpBarTiny");
const tSt = document.getElementById("stBarTiny");

if (pHpBar && tHp) tHp.style.width = pHpBar.style.width || "0%";
if (pMpBar && tMp) tMp.style.width = pMpBar.style.width || "0%";
if (pStBar && tSt) tSt.style.width = pStBar.style.width || "0%";

// also copy text if you want tiny text (we skip per request)

};

copyFromPrimary();

// fallback: nếu chưa có primary hoặc player stats thay đổi, compute from player if ((!document.getElementById("hpBar") || !document.getElementById("mpBar")) && typeof player === "object" && player.stats) { const hpNow = Number(player.stats.HP) || 0; const hpMax = calcFinalStat(player.stats.MaxHP, "MaxHP") || 1; const mpNow = Number(player.stats.Mana) || 0; const mpMax = calcFinalStat(player.stats.MaxMana, "MaxMana") || 1; const stNow = Number(player.stats.SucBen) || 0; const stMax = calcFinalStat(player.stats.MaxSucBen, "MaxSucBen") || 1;

const per = (a,b)=> Math.max(0,Math.min(100,Math.round((a/b)*100))) + "%";

const tHp = document.getElementById("hpBarTiny");
const tMp = document.getElementById("mpBarTiny");
const tSt = document.getElementById("stBarTiny");
if (tHp) tHp.style.width = per(hpNow,hpMax);
if (tMp) tMp.style.width = per(mpNow,mpMax);
if (tSt) tSt.style.width = per(stNow,stMax);

} }

// ==================== Toggle Character POPUP (move panels for realtime) ====================

let charPopup = null; let movedNodes = [];

// === Mở / đóng giao diện nhân vật === function toggleCharacterPopup() { // --- Nếu đang mở thì đóng --- if (charPopup) { // Trả lại các node về vị trí gốc for (let info of movedNodes) { try { if (info.originalNextSibling && info.originalNextSibling.parentNode === info.originalParent) { info.originalParent.insertBefore(info.node, info.originalNextSibling); } else { info.originalParent.appendChild(info.node); } } catch (e) { try { info.originalParent.appendChild(info.node); } catch(e){} } } movedNodes = [];

if (charPopup) charPopup.remove();
charPopup = null;

const overlay = document.getElementById("charOverlay");
if (overlay) overlay.remove();

// 🧹 Dọn toàn bộ popup/phụ trợ khi đóng giao diện 👤
const extraPopups = [
  "equipTooltip",
  "equipManagePopup",
  "itemDetailPopup"
];
for (const id of extraPopups) {
  const el = document.getElementById(id);
  if (el) el.remove();
}

const dc = document.getElementById("dungeonControls");
if (dc) { dc.style.visibility = ""; dc.style.pointerEvents = ""; }

const statDetail = document.getElementById("statDetail");
if (statDetail) statDetail.style.zIndex = "15000";
return;

}

// --- Khi mở --- const dc = document.getElementById("dungeonControls"); if (dc) { dc.style.visibility = "hidden"; dc.style.pointerEvents = "none"; }

// Overlay nền mờ const overlay = document.createElement("div"); overlay.id = "charOverlay"; Object.assign(overlay.style, { position: "fixed", inset: "0", background: "rgba(0,0,0,0.5)", backdropFilter: "blur(4px)", zIndex: "11999" }); overlay.onclick = (e) => { if (e.target === overlay) toggleCharacterPopup(); }; document.body.appendChild(overlay);

// Popup chính charPopup = document.createElement("div"); charPopup.id = "charPopup"; Object.assign(charPopup.style, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%,-50%)", width: "min(92%, 440px)", maxHeight: "82vh", background: "rgba(25,25,25,0.9)", color: "#fff", border: "1px solid rgba(255,255,255,0.12)", borderRadius: "12px", padding: "12px", overflowY: "auto", zIndex: "12000", boxShadow: "0 0 18px rgba(0,0,0,0.45)" });

// Nút đóng const close = document.createElement("button"); close.innerText = "✖"; Object.assign(close.style, { position: "absolute", top: "8px", right: "8px", background: "rgba(255,255,255,0.08)", border: "none", color: "#fff", borderRadius: "6px", cursor: "pointer" }); close.onclick = () => toggleCharacterPopup(); charPopup.appendChild(close);

// === TRANG BỊ === const equipHeader = document.createElement("h3"); equipHeader.innerText = "Trang bị"; equipHeader.style.marginTop = "4px"; equipHeader.style.borderBottom = "1px solid rgba(255,255,255,0.1)"; equipHeader.style.paddingBottom = "4px"; charPopup.appendChild(equipHeader);

// Grid ô trang bị const equipGrid = document.createElement("div"); Object.assign(equipGrid.style, { display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: "6px", marginBottom: "10px", marginTop: "6px" });

const slots = [ ["head","👑"],["body","🛡️"],["hand","🖐️"], ["main-weapon","⚔️"],["sub-weapon","🗡️"], ["necklace","📿"],["bracelet1","📿"],["bracelet2","📿"], ["ring1","💍"],["ring2","💍"],["ring3","💍"],["ring4","💍"],["ring5","💍"] ];

// === Tạo từng ô trang bị === for (const [slot, icon] of slots) { const slotDiv = document.createElement("div"); slotDiv.className = "equip-slot"; // ⚡ Dùng chung class với giao diện khởi đầu slotDiv.dataset.slot = slot; Object.assign(slotDiv.style, { width: "50px", height: "50px", borderRadius: "8px", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "22px", background: "rgba(255,255,255,0.05)", cursor: "pointer", position: "relative", transition: "box-shadow 0.2s, border 0.2s" }); slotDiv.innerText = icon; slotDiv.title = slot;

// --- Click và double click ---
let lastClickTime = 0;
slotDiv.addEventListener("click", (e) => {
  e.stopPropagation();
  const now = performance.now();

  // Dọn tooltip trước khi hiển thị mới
  const oldTip = document.getElementById("equipTooltip");
  if (oldTip) oldTip.remove();

  if (now - lastClickTime < 250) {
    // Double click → mở popup chi tiết
    showEquipManagePopup(slot);
  } else {
    // Single click → tooltip thông tin cơ bản
    showEquipTooltip(slot, slotDiv);
  }
  lastClickTime = now;
});

equipGrid.appendChild(slotDiv);

}

charPopup.appendChild(equipGrid);

// --- Chèn các panel thông tin --- const idsToMove = [ "char-stats", "panel-status", "panel-buffs", "panel-title", "panel-class", "panel-stats" ]; for (const id of idsToMove) { const node = document.getElementById(id); if (!node) continue; movedNodes.push({ node: node, originalParent: node.parentNode, originalNextSibling: node.nextSibling }); charPopup.appendChild(node); }

if (movedNodes.length === 0) { const fallback = document.createElement("div"); fallback.innerHTML = <h3>Nhân vật</h3><div>Không tìm thấy các panel.</div>; charPopup.appendChild(fallback); }

document.body.appendChild(charPopup);

// ✅ Sau khi charPopup gắn vào DOM mới render requestAnimationFrame(() => { renderEquipmentSlots(); // cập nhật trạng thái trang bị });

// Đảm bảo z-index cho chi tiết chỉ số const statDetail = document.getElementById("statDetail"); if (statDetail) statDetail.style.zIndex = "13000";

safeUpdateBars(); }

window.showItemDetailInPopup = function(itemId) { const data = findItemById(itemId); const inv = player.inventory.find(x => String(x.id) === String(itemId));

const old = document.getElementById("itemDetailPopup"); if (old) old.remove();

const popup = document.createElement("div"); popup.id = "itemDetailPopup"; Object.assign(popup.style, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "rgba(25,25,25,0.97)", color: "#fff", border: "1px solid rgba(255,255,255,0.2)", borderRadius: "12px", boxShadow: "0 0 12px rgba(0,0,0,0.5)", padding: "12px", width: "min(90%, 420px)", maxHeight: "80vh", overflowY: "auto", zIndex: "200000", animation: "fadeIn 0.15s ease-out" });

popup.innerHTML = <div style="display:flex;justify-content:space-between;align-items:center; border-bottom:1px solid rgba(255,255,255,0.1);padding-bottom:6px;margin-bottom:8px;"> <strong>Chi tiết vật phẩm</strong> <button onclick="document.getElementById('itemDetailPopup').remove()" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;">✖</button> </div> <div>${getItemDetailsHTML(data, inv)}</div>;

document.body.appendChild(popup); };

// === Tooltip trang bị === function showEquipTooltip(slot, target) { const old = document.getElementById("equipTooltip"); if (old) old.remove();

const tip = document.createElement("div"); tip.id = "equipTooltip"; Object.assign(tip.style, { position: "fixed", background: "rgba(0,0,0,0.9)", color: "#fff", border: "1px solid rgba(255,255,255,0.2)", borderRadius: "8px", padding: "8px 10px", fontSize: "13px", zIndex: "99999", maxWidth: "260px", lineHeight: "1.4em", pointerEvents: "none", });

const itemId = player?.equipment?.[slot]; if (!itemId) { tip.innerHTML = <b>${slot}</b><br><i>Chưa trang bị</i>; } else { const item = findItemById(itemId); if (!item) { tip.innerHTML = <b>${slot}</b><br><i>Không tìm thấy dữ liệu vật phẩm</i>; } else { tip.innerHTML = <b>${item.name}</b><br> <span style="color:gold;">Phẩm chất: ${item.rarity || "Thường"}</span><br> ${item.description || item.desc || "<i>Không có mô tả</i>"}; } }

document.body.appendChild(tip);

// --- Tính vị trí an toàn --- const rect = target.getBoundingClientRect(); const tipW = tip.offsetWidth; const tipH = tip.offsetHeight; let left = rect.right + 10; let top = rect.top;

if (left + tipW > window.innerWidth) left = rect.left - tipW - 10; if (top + tipH > window.innerHeight) top = window.innerHeight - tipH - 8; if (top < 8) top = 8;

tip.style.left = ${left}px; tip.style.top = ${top}px;

const hideTip = (e) => { if (!tip.contains(e.target)) { tip.remove(); document.removeEventListener("click", hideTip); } }; document.addEventListener("click", hideTip); }

function showEquipManagePopup(slot) { const old = document.getElementById("equipManagePopup"); if (old) old.remove();

// --- Khung chính --- const popup = document.createElement("div"); popup.id = "equipManagePopup"; Object.assign(popup.style, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%,-50%)", background: "rgba(25,25,25,0.97)", color: "#fff", border: "1px solid rgba(255,255,255,0.2)", borderRadius: "12px", boxShadow: "0 0 12px rgba(0,0,0,0.5)", width: "min(90%,420px)", maxHeight: "82vh", zIndex: "100000", display: "flex", flexDirection: "column", overflow: "hidden" });

// --- Tiêu đề & nút đóng --- popup.innerHTML = <div style="padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.1); display:flex;justify-content:space-between;align-items:center;"> <strong>Trang bị: ${slot}</strong> <button style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;" onclick="document.getElementById('equipManagePopup').remove()">✖</button> </div>;

// --- Phần trên: Trang bị hiện tại --- const currentContainer = document.createElement("div"); Object.assign(currentContainer.style, { padding: "10px", flex: "0 0 auto", borderBottom: "1px solid rgba(255,255,255,0.08)", });

const currentId = player?.equipment?.[slot]; const currentItem = currentId ? findItemById(currentId) : null;

if (currentItem) { currentContainer.innerHTML = <div style="font-size:14px;margin-bottom:4px;"> ${getItemIcon(currentItem)} <b style="cursor:pointer;" onclick="showItemDetailInPopup('${currentItem.id}')"> ${currentItem.name} </b> <span style="color:gold;">(${currentItem.rarity || "Thường"})</span> </div> <div style="font-size:12px;color:rgba(255,255,255,0.85);margin-bottom:6px;"> ${currentItem.description || currentItem.desc || ""} </div> <button style="padding:4px 8px;border-radius:6px;border:none;background:#c0392b;color:#fff;cursor:pointer;" onclick="unequipItem('${slot}');document.getElementById('equipManagePopup').remove();updateEquipSlotBorders();"> Tháo ra </button>; } else { currentContainer.innerHTML = <div style="font-size:13px;color:rgba(255,255,255,0.6)">Hiện chưa trang bị</div>; }

popup.appendChild(currentContainer);

// --- Phần dưới: Các trang bị khác --- const otherContainer = document.createElement("div"); Object.assign(otherContainer.style, { flex: "1 1 auto", overflowY: "auto", padding: "8px 10px", });

const equips = player.inventory.filter(it => { const d = findItemById(it.id); if (!d) return false; if (d.slot === "necklace" && slot === "necklace") return true; if (d.type === "ring" && slot.startsWith("ring")) return true; if (d.type === "armor" && slot === "body") return true; if (d.type === "weapon" && (slot === "main-weapon" || slot === "sub-weapon")) return true; return false; });

if (equips.length === 0) { otherContainer.innerHTML = <p style="font-size:13px;color:rgba(255,255,255,0.6);text-align:center;"> Không có trang bị phù hợp </p>; } else { equips.forEach(inv => { const d = findItemById(inv.id); const itemHTML = <div style="margin-bottom:8px;padding:6px;border-radius:8px; background:rgba(255,255,255,0.05);"> <div style="font-size:13px;display:flex;justify-content:space-between;align-items:center;"> <div style="cursor:pointer;" onclick="showItemDetailInPopup('${inv.id}')"> ${getItemIcon(d)} <b>${d.name}</b> <span style="color:gold;">(${d.rarity || "Thường"})</span> </div> <button style="padding:3px 6px;border:none;border-radius:6px; background:#2980b9;color:#fff;cursor:pointer;font-size:12px;" onclick="equipItem('${inv.id}','${slot}');document.getElementById('equipManagePopup').remove();updateEquipSlotBorders();"> Trang bị </button> </div> <div style="font-size:12px;color:rgba(255,255,255,0.75);margin-top:3px;"> ${d.description || d.desc || ""} </div> </div>; otherContainer.insertAdjacentHTML("beforeend", itemHTML); }); }

popup.appendChild(otherContainer); document.body.appendChild(popup); }

function showItemDetailInPopup(itemId) { const data = findItemById(itemId); const inv = player.inventory.find(x => String(x.id) === String(itemId));

// Xóa popup cũ (nếu có) const old = document.getElementById("itemDetailPopup"); if (old) old.remove();

// Tạo popup mới const popup = document.createElement("div"); popup.id = "itemDetailPopup"; Object.assign(popup.style, { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "rgba(25,25,25,0.97)", color: "#fff", border: "1px solid rgba(255,255,255,0.2)", borderRadius: "12px", boxShadow: "0 0 12px rgba(0,0,0,0.5)", padding: "12px", width: "min(90%, 420px)", maxHeight: "80vh", overflowY: "auto", zIndex: "200000" });

popup.innerHTML = <div style="display:flex;justify-content:space-between;align-items:center; border-bottom:1px solid rgba(255,255,255,0.1);padding-bottom:6px;margin-bottom:8px;"> <strong>Chi tiết vật phẩm</strong> <button onclick="document.getElementById('itemDetailPopup').remove()" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;">✖</button> </div> <div>${getItemDetailsHTML(data, inv)}</div>;

document.body.appendChild(popup); }

// --- Gọi mỗi khi vào dungeon --- const oldEnterDungeon = enterDungeon; enterDungeon = function(region){ oldEnterDungeon(region); setupDungeonHUD(); };

// --- Cập nhật mỗi frame --- const oldDrawDungeon = drawDungeon; drawDungeon = function(ctx){ oldDrawDungeon(ctx); safeUpdateBars(); };

function onDungeonResize() { const c = document.getElementById("worldMapCanvas"); if (!c) return; c.width = window.innerWidth; c.height = window.innerHeight; }

// === Controls (D-Pad) với style chắc chắn hiển thị --- function setupDungeonControls() { removeDungeonControls(); const controls = document.createElement("div"); controls.id = "dungeonControls";

// style cố định + z-index cao để nổi trên canvas Object.assign(controls.style, { position: "fixed", bottom: "26px", left: "50%", transform: "translateX(-50%)", display: "grid", gridTemplateColumns: "repeat(3, 64px)", gridTemplateRows: "repeat(3, 64px)", gap: "8px", placeItems: "center", zIndex: "99999", pointerEvents: "auto", touchAction: "none", userSelect: "none", WebkitUserSelect: "none", background: "transparent", });

controls.innerHTML = <div></div> <button class="dpad" data-dir="up">⬆️</button> <div></div> <button class="dpad" data-dir="left">⬅️</button> <button class="dpad" data-dir="down">⬇️</button> <button class="dpad" data-dir="right">➡️</button> <div></div>;

document.body.appendChild(controls);

controls.querySelectorAll(".dpad").forEach(btn => { Object.assign(btn.style, { width: "64px", height: "64px", fontSize: "26px", borderRadius: "12px", border: "1px solid rgba(255,255,255,0.16)", background: "rgba(0,0,0,0.28)", color: "#fff", backdropFilter: "blur(6px)" }); btn.onpointerdown = (ev) => { ev.preventDefault(); const d = btn.dataset.dir; if (d === "up") movePlayer(0, -1); if (d === "down") movePlayer(0, 1); if (d === "left") movePlayer(-1, 0); if (d === "right") movePlayer(1, 0); }; }); }

function removeDungeonControls() { const el = document.getElementById("dungeonControls"); if (el) el.remove(); }

// thêm dòng này ngay sau: window.removeDungeonControls = removeDungeonControls;

// === Phím điều khiển === function handleDungeonKey(e) { if (currentMapLevel !== "dungeon") return; const k = e.key.toLowerCase(); if (k === "arrowup" || k === "w") movePlayer(0, -1); if (k === "arrowdown" || k === "s") movePlayer(0, 1); if (k === "arrowleft" || k === "a") movePlayer(-1, 0); if (k === "arrowright" || k === "d") movePlayer(1, 0); }

let lastTrapTriggered = null; // để tránh spam bẫy liên tục let nearestDoor = null; // dùng cho vực

// === HP & Chết === function getMaxHP() { return calcFinalStat(player.stats.MaxHP || 100, "MaxHP"); } window.modifyHP = modifyHP;

function modifyHP(amount, reason = "") { const maxHP = getMaxHP(); player.stats.HP = Math.min(maxHP, Math.max(0, (Number(player.stats.HP) || maxHP) + Number(amount)));

if (reason) console.log(${reason}! HP hiện tại: ${player.stats.HP}/${maxHP});

// ⚰️ Nếu chết → Thoát dungeon if (player.stats.HP <= 0) { console.log("💀 HP xuống 0 — thoát khỏi Dungeon!"); player.stats.HP = Math.max(1, Math.floor(maxHP * 0.2)); safeUpdateBars && safeUpdateBars(); try { exitDungeon(); } catch (e) { console.warn("⚠️ exitDungeon() chưa định nghĩa:", e); } }

safeUpdateBars && safeUpdateBars(); }

function canMove() { const curStamina = Number(player.stats.SucBen || 0); return curStamina > 0; }

// === Xử lý hiệu ứng môi trường === function handleEnvironmentEffect(x, y, tile) { const map = dungeon.map; const now = Date.now();

player._lastEffectPos = player._lastEffectPos || {};

switch (tile) { // =================🔥 LAVA================= case "🔥": { modifyHP(-6, "🔥 Bỏng khi chạm");

  const last = player._lastEffectPos.burn || {};
  const sameTile = last.x === x && last.y === y;

  // Nếu vừa bước lên ô mới 🔥
  if (!sameTile) {
    applyEffect("burn", {
      power: 1.2,
      duration: 4000,
      tickInterval: 1000,
      maxStacks: 8
    });
    const total = activeEffects.filter(e => e.type === "burn").length;
    console.log(`🔥 +stack (bước lên) → tổng ${total}`);
    player._lastEffectPos.burn = { x, y, enteredAt: now, lastTickAt: now };
  } 
  // Nếu đang đứng yên trên cùng ô 🔥
  else if (now - last.lastTickAt > 1200) {
    applyEffect("burn", {
      power: 0.6,
      duration: 4000,
      tickInterval: 1000,
      maxStacks: 8
    });
    const total = activeEffects.filter(e => e.type === "burn").length;
    console.log(`🔥 +stack (đứng yên) → tổng ${total}`);
    player._lastEffectPos.burn.lastTickAt = now;
  }
  break;
}

// =================☠️ POISON=================
case "☠️": {
  modifyHP(-2, "☠️ Trúng độc nhẹ");

  const last = player._lastEffectPos.poison || {};
  const sameTile = last.x === x && last.y === y;

  // Nếu mới bước vào vùng độc
  if (!sameTile) {
    applyEffect("poison", {
      power: 1.0,
      duration: 6000,
      tickInterval: 1500,
      maxStacks: 10
    });
    const total = activeEffects.filter(e => e.type === "poison").length;
    console.log(`☠️ +stack (bước lên) → tổng ${total}`);
    player._lastEffectPos.poison = { x, y, enteredAt: now, lastTickAt: now };
  }
  // Nếu vẫn đứng yên trên vùng độc
  else if (now - last.lastTickAt > 1800) {
    applyEffect("poison", {
      power: 0.5,
      duration: 6000,
      tickInterval: 1500,
      maxStacks: 10
    });
    const total = activeEffects.filter(e => e.type === "poison").length;
    console.log(`☠️ +stack (đứng yên) → tổng ${total}`);
    player._lastEffectPos.poison.lastTickAt = now;
  }
  break;
}

// =================❄️ ICE=================
case "❄️": {
  applyEffect("freeze", { power: 0.8, duration: 4000, tickInterval: 1000, maxStacks: 4 });
  console.log("❄️ Hiệu ứng đóng băng kích hoạt!");

  // Logic trượt
  let dx = x - (playerPos.prevX ?? x);
  let dy = y - (playerPos.prevY ?? y);
  if (!dx && !dy) {
    const dirs = [
      { x: 1, y: 0 }, { x: -1, y: 0 },
      { x: 0, y: 1 }, { x: 0, y: -1 }
    ];
    const rnd = dirs[Math.floor(Math.random() * dirs.length)];
    dx = rnd.x; dy = rnd.y;
  }
  let slideCount = 0;
  while (slideCount < 3) {
    const nextX = x + dx, nextY = y + dy;
    if (map[nextY]?.[nextX] === 0) {
      playerPos = { x: nextX, y: nextY };
      x = nextX; y = nextY; slideCount++;
    } else break;
  }
  break;
}

// =================🧨 TRAP=================
case "🧨":
  if (lastTrapTriggered && Date.now() - lastTrapTriggered < 1000) return;
  lastTrapTriggered = Date.now();
  modifyHP(-10, "🧨 Bị bẫy kích hoạt");
  map[y][x] = 0;
  break;

// =================🕳️ HOLE=================
case "🕳️":
  if (Math.random() < 0.15) {
    console.log("🕳️ Rơi sâu... Sang tầng tiếp theo!");
    nextDungeonFloor();
  } else {
    const emptyTiles = [];
    for (let yy = 1; yy < map.length - 1; yy++) {
      for (let xx = 1; xx < map[0].length - 1; xx++) {
        if (map[yy][xx] === 0) emptyTiles.push({ x: xx, y: yy });
      }
    }
    if (emptyTiles.length)
      playerPos = emptyTiles[Math.floor(Math.random() * emptyTiles.length)];
  }
  break;

// =================🌀 STAIRS=================
case "🌀":
  nextDungeonFloor();
  return;

}

playerPos.prevX = x; playerPos.prevY = y; }

// === HỆ THỐNG SƯƠNG MÙ NÂNG CAO === let baseLightRadius = 6; // bán kính ánh sáng gốc let fogEffectStrength = 0; // cường độ hiệu ứng hiện tại (0 → không, 1 → tối đa)

function checkFogProximity(dungeon) { if (!dungeon?.map) return;

const px = playerPos.x; const py = playerPos.y; const fogRange = 6; // khoảng cách tối đa bị ảnh hưởng let nearestDist = Infinity;

// tìm tile 🌫️ gần nhất for (let y = Math.max(0, py - fogRange); y <= Math.min(dungeon.h - 1, py + fogRange); y++) { for (let x = Math.max(0, px - fogRange); x <= Math.min(dungeon.w - 1, px + fogRange); x++) { if (dungeon.map[y][x] === "🌫️") { const dist = Math.hypot(px - x, py - y); if (dist < nearestDist) nearestDist = dist; } } }

// nếu có tile 🌫️ trong phạm vi if (nearestDist <= fogRange) { // càng gần thì hiệu ứng càng mạnh const intensity = 1 - Math.min(1, nearestDist / fogRange); fogEffectStrength = Math.min(1, fogEffectStrength + (intensity - fogEffectStrength) * 0.2); } else { // không còn sương gần -> giảm dần hiệu ứng fogEffectStrength += (0 - fogEffectStrength) * 0.1; }

// áp dụng hiệu ứng lên bán kính ánh sáng const fogMultiplier = 0.5 + (1 - fogEffectStrength) * 0.5; lightRadius = baseLightRadius * fogMultiplier;

// (tuỳ chọn) log debug nhẹ // console.log(🌫️ Near fog: dist=${nearestDist.toFixed(2)} → strength=${fogEffectStrength.toFixed(2)} → light=${lightRadius.toFixed(2)}); }

// === Di chuyển & tương tác === function movePlayer(dx, dy) { if (!dungeon) return;

if (!canMove()) { console.log("⚠️ Sức Bền = 0, không thể di chuyển!"); return; }

const nx = playerPos.x + dx; const ny = playerPos.y + dy; if (nx < 0 || ny < 0 || nx >= dungeon.w || ny >= dungeon.h) return;

const t = dungeon.map[ny][nx]; if (t === 1) return; // Tường

// ✅ Tiêu hao stamina mỗi bước const stepCost = 1; player.stats.SucBen = Math.max(0, (Number(player.stats.SucBen) || 0) - stepCost); safeUpdateBars && safeUpdateBars(); refreshPlayerUI();

// ✅ Ghi trạng thái di chuyển để hiển thị emoji động player.moving = true; player.moveDir = { dx, dy }; clearTimeout(player._moveTimeout); player._moveTimeout = setTimeout(() => { player.moving = false; }, 250);

// ✅ Lưu vị trí cũ playerPos.prevX = playerPos.x; playerPos.prevY = playerPos.y; playerPos = { x: nx, y: ny };

// ✅ Hiệu ứng môi trường (nếu có) handleEnvironmentEffect(nx, ny, t);

// ====================== RƯƠNG THƯỜNG ====================== if (t === "C") { dungeon.map[ny][nx] = 0;

const floor = window.currentFloor || 1; const level = player.stats.Level || 1; const items = gameData.items || []; const techniques = gameData.techniques || [];

if (items.length === 0 && techniques.length === 0) { console.log("🎁 Rương trống (không có dữ liệu vật phẩm hoặc công pháp)!"); return; }

// 🧭 Lấy loại thế giới hiện tại let worldType = window.currentWorldType || "tieu"; if (worldType === "tieu") worldType = "tiểu"; if (worldType === "trung") worldType = "trung"; if (worldType === "dai") worldType = "đại";

// 🎯 Mốc cấp khuyến nghị let minLv = 10, maxLv = 39; if (worldType === "trung") { minLv = 40; maxLv = 69; } if (worldType === "đại") { minLv = 70; maxLv = 99; }

// --- Độ hiếm cơ bản --- let rarityWeights = { "Hoàng": 50, "Huyền": 30, "Địa": 10, "Thiên": 6, "Thần": 3, "Nguyên Sơ": 1 }; if (floor >= 5) rarityWeights = { "Hoàng": 35, "Huyền": 30, "Địa": 15, "Thiên": 10, "Thần": 7, "Nguyên Sơ": 3 }; if (floor >= 10) rarityWeights = { "Hoàng": 20, "Huyền": 25, "Địa": 20, "Thiên": 15, "Thần": 12, "Nguyên Sơ": 8 };

// --- Điều chỉnh theo cấp nhân vật --- if (level > maxLv) { rarityWeights["Hoàng"] += 30; rarityWeights["Huyền"] += 15; } else if (level < minLv) { rarityWeights["Địa"] += 10; rarityWeights["Thiên"] += 10; rarityWeights["Thần"] += 5; rarityWeights["Nguyên Sơ"] += 5; }

// --- Random độ hiếm --- const rarityList = Object.entries(rarityWeights); const totalWeight = rarityList.reduce((a, b) => a + b[1], 0); let r = Math.random() * totalWeight; let chosenRarity = "Hoàng"; for (const [rar, w] of rarityList) { if (r < w) { chosenRarity = rar; break; } r -= w; }

// --- Chọn vật phẩm hoặc công pháp --- let dropType = "item"; let selected = null;

if (Math.random() < 0.1 && techniques.length > 0) { // 10% rơi công pháp const techPool = techniques.filter(t => (t.rarity || "Hoàng") === chosenRarity); selected = techPool.length > 0 ? techPool[Math.floor(Math.random() * techPool.length)] : techniques[Math.floor(Math.random() * techniques.length)]; dropType = "tech"; }

if (!selected) { const pool = items.filter(it => (it.rarity || "Hoàng") === chosenRarity); selected = pool.length > 0 ? pool[Math.floor(Math.random() * pool.length)] : items[Math.floor(Math.random() * items.length)]; }

if (selected) { if (dropType === "item") { addItem(selected.id, 1); console.log(🎁 Mở rương và nhận được: ${selected.name} (${selected.rarity || "Hoàng"})); } else { player.techniques = player.techniques || []; if (!player.techniques.find(t => t.id === selected.id)) { player.techniques.push({ id: selected.id, lvl: 1, exp: 0 }); console.log(📜 Học được công pháp mới: ${selected.name} (${selected.rarity || "Hoàng"})); } else { console.log(📘 Bạn đã biết công pháp này.); } }

$('modal').style.display = 'flex';
$('modalCard').innerHTML = `
  <h3>🎁 Nhặt được rương!</h3>
  <p>Bạn nhận được: <b>${selected.name}</b> (${selected.rarity || "Hoàng"})</p>
  <p><i>${selected.description || ''}</i></p>
  <div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>
`;

}

refreshPlayerUI(); drawDungeon(dungeonCtx); } // ====================== EXP ====================== else if (t === "E") { dungeon.map[ny][nx] = 0; const enemy = generateEnemy("normal"); showCombatUI(enemy); refreshPlayerUI(); }

// ====================== BOSS ====================== else if (t === "👹") { dungeon.map[ny][nx] = 0; const enemy = generateEnemy("boss"); showCombatUI(enemy); refreshPlayerUI(); const cx = nx, cy = ny; if (dungeon.map[cy] && dungeon.map[cy][cx + 2] === 0) dungeon.map[cy][cx + 2] = "🎁"; if (dungeon.map[cy] && dungeon.map[cy][cx - 2] === 0) dungeon.map[cy][cx - 2] = "🌀"; drawDungeon(dungeonCtx); }

// ====================== RƯƠNG BOSS ====================== else if (t === "🎁") { dungeon.map[ny][nx] = 0; console.log("🏆 Mở rương Boss!");

const level = player.stats.Level || 1; const items = gameData.items || []; const techniques = gameData.techniques || [];

if (items.length === 0 && techniques.length === 0) { console.log("🎁 Rương boss trống (không có dữ liệu vật phẩm hoặc công pháp)!"); return; }

// 🧭 Lấy loại thế giới let worldType = window.currentWorldType || "tieu"; if (worldType === "tieu") worldType = "tiểu"; if (worldType === "trung") worldType = "trung"; if (worldType === "dai") worldType = "đại";

// 🎯 Mốc cấp khuyến nghị let minLv = 10, maxLv = 39; if (worldType === "trung") { minLv = 40; maxLv = 69; } if (worldType === "đại") { minLv = 70; maxLv = 99; }

// --- Độ hiếm cao hơn --- let rarityWeights = { "Hoàng": 15, "Huyền": 25, "Địa": 25, "Thiên": 20, "Thần": 10, "Nguyên Sơ": 5 }; if (worldType === "đại") rarityWeights = { "Hoàng": 10, "Huyền": 20, "Địa": 25, "Thiên": 25, "Thần": 12, "Nguyên Sơ": 8 };

if (level > maxLv) { rarityWeights["Hoàng"] += 10; rarityWeights["Huyền"] += 5; } else if (level < minLv) { rarityWeights["Thiên"] += 10; rarityWeights["Thần"] += 5; rarityWeights["Nguyên Sơ"] += 5; }

// --- Random độ hiếm --- const rarityList = Object.entries(rarityWeights); const totalWeight = rarityList.reduce((a, b) => a + b[1], 0); let r = Math.random() * totalWeight; let chosenRarity = "Hoàng"; for (const [rar, w] of rarityList) { if (r < w) { chosenRarity = rar; break; } r -= w; }

// --- Chọn vật phẩm hoặc công pháp --- let dropType = "item"; let selected = null;

if (Math.random() < 0.25 && techniques.length > 0) { // 25% cơ hội rơi công pháp const techPool = techniques.filter(t => (t.rarity || "Hoàng") === chosenRarity); selected = techPool.length > 0 ? techPool[Math.floor(Math.random() * techPool.length)] : techniques[Math.floor(Math.random() * techniques.length)]; dropType = "tech"; }

if (!selected) { const pool = items.filter(it => (it.rarity || "Hoàng") === chosenRarity); selected = pool.length > 0 ? pool[Math.floor(Math.random() * pool.length)] : items[Math.floor(Math.random() * items.length)]; }

// --- Nhận thưởng --- if (dropType === "item") { addItem(selected.id, 1); console.log(🏆 Nhận được vật phẩm hiếm từ Boss: ${selected.name} (${selected.rarity || "Hoàng"})); } else { player.techniques = player.techniques || []; if (!player.techniques.find(t => t.id === selected.id)) { player.techniques.push({ id: selected.id, lvl: 1, exp: 0 }); console.log(📜 Học được công pháp từ Boss: ${selected.name} (${selected.rarity || "Hoàng"})); } else { console.log(📘 Bạn đã biết công pháp này.); } }

$('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>🏆 Rương Boss Mở Ra!</h3> <p>Bạn nhận được: <b>${selected.name}</b> (${selected.rarity || "Hoàng"})</p> <p><i>${selected.description || ''}</i></p> <div style="margin-top:8px"><button onclick="closeModal()">Đóng</button></div>;

const flash = document.createElement("div"); flash.innerText = "✨"; Object.assign(flash.style, { position: "fixed", left: "50%", top: "40%", fontSize: "80px", transform: "translate(-50%, -50%)", animation: "fadeOut 1.5s ease-out forwards" }); document.body.appendChild(flash); setTimeout(()=>flash.remove(), 1500);

refreshPlayerUI(); drawDungeon(dungeonCtx); }

// ====================== SỰ KIỆN "?" ====================== else if (t === "?") { dungeon.map[ny][nx] = 0; console.log("❓ Khám phá bí ẩn!");

const type = dungeon.type || "default";
const rand = Math.random();

if (type === "volcano") { 
  if (rand < 0.4) {
    console.log("🔥 Trúng bẫy lửa! Mất 10% HP.");
    const lost = Math.floor(player.stats.MaxHP * 0.1);
    player.stats.HP = Math.max(0, player.stats.HP - lost);
  } else if (rand < 0.7) {
    console.log("👹 Xuất hiện quái vật lửa!");
    dungeon.map[ny][nx] = "👹";
  } else {
    console.log("🎁 Nhặt được rương giữa dung nham!");
    dungeon.map[ny][nx] = "C";
  }
}
else if (type === "ice") {
  if (rand < 0.4) {
    console.log("❄️ Trượt ngã trên băng! Mất 5% thể lực.");
    const lost = Math.floor(player.stats.MaxSucBen * 0.05);
    player.stats.SucBen = Math.max(0, player.stats.SucBen - lost);
  } else if (rand < 0.8) {
    console.log("🧊 Nhặt được tinh thể băng!");
    dungeon.map[ny][nx] = "C";
  } else {
    console.log("👻 Quái băng xuất hiện!");
    dungeon.map[ny][nx] = "👹";
  }
}
else if (type === "cave") {
  if (rand < 0.5) {
    console.log("🕷️ Đụng phải quái hang!");
    dungeon.map[ny][nx] = "👹";
  } else {
    console.log("💎 Phát hiện rương ẩn trong bóng tối!");
    dungeon.map[ny][nx] = "C";
  }
}
else if (type === "jungle") {
  if (rand < 0.4) {
    console.log("🌿 Dính bẫy dây leo! Giảm thể lực 10%");
    const lost = Math.floor(player.stats.MaxSucBen * 0.1);
    player.stats.SucBen = Math.max(0, player.stats.SucBen - lost);
  } else if (rand < 0.8) {
    console.log("🐍 Rắn độc phục kích!");
    dungeon.map[ny][nx] = "👹";
  } else {
    console.log("🍃 Tìm thấy hòm cổ trong rừng!");
    dungeon.map[ny][nx] = "C";
  }
}
else {
  if (rand < 0.3) {
    console.log("🎁 Phát hiện rương bí ẩn!");
    dungeon.map[ny][nx] = "C";
  } else if (rand < 0.6) {
    console.log("👹 Một kẻ địch bất ngờ xuất hiện!");
    dungeon.map[ny][nx] = "👹";
  } else {
    console.log("✨ Không có gì đặc biệt... nhưng không khí hơi kỳ lạ.");
  }
}

refreshPlayerUI();
drawDungeon(dungeonCtx);

}

// ====================== CỬA & DỊCH CHUYỂN ====================== else if (t === "🌀") { console.log(🌀 Dịch chuyển đến tầng ${window.currentFloor + 1}!); nextDungeonFloor(); return; } else if (t === "D" || t === "G") { handleDoorAt(nx, ny, t); return; }

// ✅ Cập nhật mini-map & render markRoomVisited(dungeon); drawRoomMiniMap(); drawDungeon(dungeonCtx); }

function handleDoorAt(nx, ny, t) { const isGold = (t === "G");

// đảm bảo dungeon có id dungeon.id = dungeon.id || (crypto?.randomUUID ? crypto.randomUUID() : ('d_' + Math.random().toString(36).slice(2, 9))); window.dungeonGraph = window.dungeonGraph || {};

// tìm đối tượng cửa tại ô đang bước vào const thisDoor = (dungeon.doors || []).find(d => d.x === nx && d.y === ny); if (!thisDoor) { console.warn("Không tìm thấy door object tại", nx, ny); return; }

// --- xác định hướng tương đối của cửa để vẽ mini-map --- let doorDir = "down"; const px = playerPos.prevX ?? playerPos.x; const py = playerPos.prevY ?? playerPos.y; if (thisDoor.x < px) doorDir = "left"; else if (thisDoor.x > px) doorDir = "right"; else if (thisDoor.y < py) doorDir = "up"; else if (thisDoor.y > py) doorDir = "down"; // ------------------------------------------------------

// --- nếu cửa đã liên kết tới dungeon khác --- if (thisDoor.toDungeonId && window.dungeonGraph[thisDoor.toDungeonId]) { const targetDungeon = window.dungeonGraph[thisDoor.toDungeonId]; const prevDungeonId = dungeon.id; dungeon = targetDungeon; inBossRoom = !!dungeon.isBossRoom; if (inBossRoom) window.bossRoomCreated = false;

// tìm cửa đích
let linkedDoor = null;
if (thisDoor.toDoorId) linkedDoor = (dungeon.doors || []).find(d => d.id === thisDoor.toDoorId);
if (!linkedDoor) linkedDoor = (dungeon.doors || []).find(d => d.toDungeonId === prevDungeonId);
if (!linkedDoor) linkedDoor = (dungeon.doors || [])[0];

if (linkedDoor && linkedDoor.openTo)
  playerPos = { x: linkedDoor.openTo.x, y: linkedDoor.openTo.y };
else if (linkedDoor) {
  const tryCoords = [
    { x: linkedDoor.x, y: linkedDoor.y - 1 },
    { x: linkedDoor.x, y: linkedDoor.y + 1 },
    { x: linkedDoor.x - 1, y: linkedDoor.y },
    { x: linkedDoor.x + 1, y: linkedDoor.y }
  ];
  let placed = false;
  for (const c of tryCoords) {
    if (dungeon.map[c.y] && dungeon.map[c.y][c.x] === 0) {
      playerPos = { x: c.x, y: c.y };
      placed = true;
      break;
    }
  }
  if (!placed) playerPos = { ...dungeon.playerStart };
} else playerPos = { ...dungeon.playerStart };

markRoomVisited(dungeon);
drawRoomMiniMap();
drawDungeon(dungeonCtx);
return;

}

// --- nếu cửa chưa liên kết -> tạo dungeon mới hoặc boss --- const fromDungeonId = dungeon.id; const worldType = window.currentWorldType || (selectedWorld?.type ?? "tieu"); const worldKey = selectedWorld ? (selectedWorld.id || selectedWorld.type) : worldType; ensureWorldQuota(worldKey, worldType);

if (isGold) { // 🟡 TẠO BOSS ROOM const bossRoom = generateBossRoom(); bossRoom.id = (crypto?.randomUUID ? crypto.randomUUID() : ('d_' + Math.random().toString(36).slice(2, 9))); bossRoom.prevId = fromDungeonId; bossRoom.isBossRoom = true; bossRoom.doors = bossRoom.doors || [];

const backX = Math.floor(bossRoom.w / 2);
const backY = bossRoom.h - 2;

const bossDoorId = (crypto?.randomUUID ? crypto.randomUUID() : ('door_' + Math.random().toString(36).slice(2, 9)));
bossRoom.map[backY][backX] = "G";
bossRoom.map[backY - 1][backX] = 0;

const bossBackDoor = {
  id: bossDoorId,
  x: backX, y: backY,
  openTo: { x: backX, y: backY - 1 },
  special: true,
  toDungeonId: fromDungeonId,
  toDoorId: thisDoor.id
};
bossRoom.doors.push(bossBackDoor);

thisDoor.toDungeonId = bossRoom.id;
thisDoor.toDoorId = bossDoorId;

window.dungeonGraph[bossRoom.id] = bossRoom;
window.dungeonGraph[fromDungeonId] = window.dungeonGraph[fromDungeonId] || dungeon;

dungeon = bossRoom;
inBossRoom = true;
playerPos = { x: backX, y: backY - 1 };
bossRoom._canSpawnChildren = true;

linkMiniMapRooms(window.dungeonGraph[fromDungeonId], dungeon, thisDoor,
  (dungeon.doors || []).find(d => d.toDungeonId === fromDungeonId || d.toDoorId === thisDoor.id)
);
markRoomVisited(dungeon);
drawRoomMiniMap();
drawDungeon(dungeonCtx);
return;

}

// 🧩 TẠO DUNGEON THƯỜNG const canSpawnFromThis = !!dungeon._canSpawnChildren; if (!canSpawnFromThis) { console.log("⚠️ Phòng hiện tại không được phép sinh thêm (quota đã hết hoặc boss)."); drawDungeon(dungeonCtx); return; }

ensureWorldQuota(worldKey, worldType); const canSpawnFurther = (window.worldRoomQuota[worldKey] > 0); if (canSpawnFurther) consumeRoomQuota(worldKey);

const newDungeon = generateDungeon(dungeon.theme, dungeon.w, dungeon.h); newDungeon.id = (crypto?.randomUUID ? crypto.randomUUID() : ('d_' + Math.random().toString(36).slice(2, 9))); newDungeon._canSpawnChildren = canSpawnFurther;

// 1️⃣ Xác định hướng đối diện let oppositeDir; switch (doorDir) { case "up": oppositeDir = "down"; break; case "down": oppositeDir = "up"; break; case "left": oppositeDir = "right"; break; case "right": oppositeDir = "left"; break; default: oppositeDir = "up"; break; }

// 2️⃣ Tạo cửa đối hướng let doorBX, doorBY, openX, openY; const margin = 1;

if (oppositeDir === "up") { doorBX = Math.floor(newDungeon.w / 2); doorBY = margin; openX = doorBX; openY = doorBY + 1; } else if (oppositeDir === "down") { doorBX = Math.floor(newDungeon.w / 2); doorBY = newDungeon.h - margin - 1; openX = doorBX; openY = doorBY - 1; } else if (oppositeDir === "left") { doorBX = margin; doorBY = Math.floor(newDungeon.h / 2); openX = doorBX + 1; openY = doorBY; } else if (oppositeDir === "right") { doorBX = newDungeon.w - margin - 1; doorBY = Math.floor(newDungeon.h / 2); openX = doorBX - 1; openY = doorBY; }

newDungeon.map[doorBY][doorBX] = "D"; newDungeon.map[openY][openX] = 0;

// Tạo đường thông giữa cửa và trung tâm const centerX = Math.floor(newDungeon.w / 2); const centerY = Math.floor(newDungeon.h / 2); let cx = openX, cy = openY; while (cx !== centerX || cy !== centerY) { if (cx < centerX) cx++; else if (cx > centerX) cx--; if (cy < centerY) cy++; else if (cy > centerY) cy--; if (newDungeon.map[cy] && newDungeon.map[cy][cx] === 1) newDungeon.map[cy][cx] = 0; }

// 4️⃣ Tạo object cửa đối diện const doorBId = (crypto?.randomUUID ? crypto.randomUUID() : ('door_' + Math.random().toString(36).slice(2, 9))); const doorB = { id: doorBId, x: doorBX, y: doorBY, openTo: { x: openX, y: openY }, special: false, toDungeonId: dungeon.id, toDoorId: thisDoor.id };

thisDoor.toDungeonId = newDungeon.id; thisDoor.toDoorId = doorBId;

window.dungeonGraph[newDungeon.id] = newDungeon; window.dungeonGraph[dungeon.id] = dungeon; newDungeon.doors = newDungeon.doors || []; newDungeon.doors.push(doorB);

dungeon = newDungeon; inBossRoom = false; playerPos = { x: openX, y: openY };

linkMiniMapRooms(window.dungeonGraph[fromDungeonId], dungeon, thisDoor, doorB); markRoomVisited(dungeon); drawRoomMiniMap(); drawDungeon(dungeonCtx); }

// ==================== MINI-MAP PHÒNG (CẢI TIẾN) ====================

let roomMapVisited = {}; // { dungeonId: true } let roomMapPositions = {}; // { dungeonId: {x, y} } let roomMapCanvas = null;

// --- Mini-map toggle: mặc định tắt --- function setupMiniMapToggle() { const old=document.getElementById("toggleMiniMapBtn"); if(old) old.remove();

initRoomMiniMap(); // tạo canvas const btn=document.createElement("button"); btn.id="toggleMiniMapBtn"; btn.textContent="🧭"; Object.assign(btn.style,{ position:"fixed",top:"10px",right:"10px", width:"44px",height:"44px",fontSize:"22px", borderRadius:"10px",border:"1px solid rgba(255,255,255,0.2)", background:"rgba(0,0,0,0.35)",color:"#fff", backdropFilter:"blur(6px)",cursor:"pointer",zIndex:"10000" });

const map=document.getElementById("roomMiniMap"); if(map){map.style.display="none";btn.style.opacity="0.7";} btn.onclick=()=>{ const map=document.getElementById("roomMiniMap"); if(!map) return; const hidden=map.style.display==="none"; map.style.display=hidden?"block":"none"; btn.style.opacity=hidden?"1":"0.7"; }; document.body.appendChild(btn); }

// Khởi tạo/tái sử dụng mini-map function initRoomMiniMap() { if (!document.getElementById("roomMiniMap")) { const c = document.createElement("canvas"); c.id = "roomMiniMap"; Object.assign(c.style, { position: "fixed", top: "10px", right: "10px", width: "180px", height: "180px", background: "rgba(0,0,0,0.5)", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.2)", zIndex: "9999", imageRendering: "pixelated" }); document.body.appendChild(c); } roomMapCanvas = document.getElementById("roomMiniMap");

if (!window.dungeonGraph) window.dungeonGraph = {};

// đảm bảo dungeon có id if (dungeon && !dungeon.id) dungeon.id = 'd_' + Math.random().toString(36).slice(2, 9);

// Khi lần đầu vào mini-map, nếu chưa có vị trí cho phòng hiện tại -> đặt làm trung tâm (0,0) if (dungeon && !roomMapPositions[dungeon.id]) { roomMapPositions[dungeon.id] = { x: 0, y: 0 }; roomMapVisited[dungeon.id] = true; } }

// Đánh dấu phòng đã ghé qua (dùng khi spawn hoặc khi link) function markRoomVisited(d) { if (!d || !d.id) return; roomMapVisited[d.id] = true; if (!roomMapPositions[d.id]) { // nếu chưa có vị trí (lỗi fallback) -> đặt gần origin roomMapPositions[d.id] = { x: 0, y: 0 }; } drawRoomMiniMap(); }

// helper: xác định hướng cửa trên phòng 'fromDungeon' dựa vào tọa độ cửa (doorA) function determineExitDirection(fromDungeon, doorA) { if (!fromDungeon || !doorA) return { dx: 0, dy: 1 };

const { w, h } = fromDungeon; const margins = { left: doorA.x, right: w - 1 - doorA.x, top: doorA.y, bottom: h - 1 - doorA.y };

// Hướng về phía cạnh gần nhất const minDist = Math.min(margins.left, margins.right, margins.top, margins.bottom); if (minDist === margins.left) return { dx: -1, dy: 0 }; if (minDist === margins.right) return { dx: 1, dy: 0 }; if (minDist === margins.top) return { dx: 0, dy: -1 }; if (minDist === margins.bottom) return { dx: 0, dy: 1 };

// fallback return { dx: 0, dy: 1 }; }

// helper: tính offset phụ để phân tách nhiều cửa trên cùng một cạnh function computeOrthogonalOffset(fromDungeon, doorA, dir) { // dir: {dx,dy} với một trong hai là 0 const w = fromDungeon.w, h = fromDungeon.h; // khi đi ngang (dx != 0) -> offset theo trục y dựa doorA.y - center if (dir.dx !== 0) { const rel = doorA.y - h / 2; const scale = Math.max(1, Math.floor(h / 6)); // chia khu vực thành vài phần return Math.round(rel / scale); // có thể âm/0/dương } // khi đi dọc (dy != 0) -> offset theo trục x const rel = doorA.x - w / 2; const scale = Math.max(1, Math.floor(w / 6)); return Math.round(rel / scale); }

// helper: kiểm tra vị trí đã có phòng khác chưa function isPosOccupied(pos) { for (const id in roomMapPositions) { const p = roomMapPositions[id]; if (p.x === pos.x && p.y === pos.y) return id; // trả về id phòng đang chiếm } return null; }

// helper: tìm vị trí trống gần desiredPos (spiral search) function findNearestFreePos(desiredPos, maxRadius = 6) { // spiral offsets const dirs = [[1,0],[0,1],[-1,0],[0,-1]]; if (!isPosOccupied(desiredPos)) return desiredPos; for (let r = 1; r <= maxRadius; r++) { // iterate ring r let x = desiredPos.x - r; let y = desiredPos.y - r; // go around perimeter for (let side = 0; side < 4; side++) { const steps = r * 2; for (let s = 0; s < steps; s++) { if (!isPosOccupied({ x, y })) return { x, y }; x += dirs[side][0]; y += dirs[side][1]; } } } // fallback: move further right until free let fx = desiredPos.x, fy = desiredPos.y; for (let i = 1; i < 50; i++) { if (!isPosOccupied({ x: fx + i, y: fy })) return { x: fx + i, y: fy }; if (!isPosOccupied({ x: fx - i, y: fy })) return { x: fx - i, y: fy }; if (!isPosOccupied({ x: fx, y: fy + i })) return { x: fx, y: fy + i }; if (!isPosOccupied({ x: fx, y: fy - i })) return { x: fx, y: fy - i }; } return desiredPos; // xấu nhưng trả về để không crash }

/* Link two rooms on mini-map in a deterministic, non-overlapping way.

fromDungeon: dungeon object we are coming FROM toDungeon: dungeon object we are going TO doorA: door object in fromDungeon (the door we stepped onto) doorB: door object in toDungeon that links back (optional) */ function linkMiniMapRooms(fromDungeon, toDungeon, doorA, doorB) { if (!fromDungeon || !toDungeon || !doorA) return;

// ensure from has position if (!roomMapPositions[fromDungeon.id]) { roomMapPositions[fromDungeon.id] = { x: 0, y: 0 }; roomMapVisited[fromDungeon.id] = true; }

// if toDungeon already positioned, nothing to do if (roomMapPositions[toDungeon.id]) return;

// 1) compute primary direction using fromDungeon & doorA const dir = determineExitDirection(fromDungeon, doorA); // {dx,dy}

// 2) compute orthogonal offset based on doorA location so two north-doors differ by offset const orth = computeOrthogonalOffset(fromDungeon, doorA, dir); // integer, can be negative

// desired base position (one cell away in dir) let desired = { x: roomMapPositions[fromDungeon.id].x + dir.dx, y: roomMapPositions[fromDungeon.id].y + dir.dy };

// apply orthogonal offset if (dir.dx !== 0) { desired.y += orth; // moving left/right, offset along y } else if (dir.dy !== 0) { desired.x += orth; // moving up/down, offset along x }

// 3) if doorB exists we can further nudge towards doorB relative orientation if (doorB) { // compute doorB offset relative to toDungeon center, normalized similar to orth to better align const w2 = toDungeon.w, h2 = toDungeon.h; const relXB = doorB.x - w2 / 2; const relYB = doorB.y - h2 / 2; // if directions disagree, try to align sign if (Math.abs(relXB) > Math.abs(relYB)) { // horizontal bias desired.y += Math.round(relYB / Math.max(1, Math.floor(h2 / 6))); desired.x += Math.sign(relXB); // nudge horizontally small } else { desired.x += Math.round(relXB / Math.max(1, Math.floor(w2 / 6))); desired.y += Math.sign(relYB); } }

// 4) avoid collision: if desired occupied, find nearest free pos const occupiedId = isPosOccupied(desired); if (occupiedId) { const freePos = findNearestFreePos(desired, 8); desired = freePos; }

// assign roomMapPositions[toDungeon.id] = { x: desired.x, y: desired.y }; roomMapVisited[toDungeon.id] = true;

// draw drawRoomMiniMap(); }

// Vẽ mini-map (có nối) function drawRoomMiniMap() { const c = roomMapCanvas; if (!c || !dungeon) return; const ctx = c.getContext("2d"); const cw = c.width = 180; const ch = c.height = 180;

ctx.clearRect(0, 0, cw, ch);

const rooms = Object.entries(roomMapPositions); if (!rooms.length) return;

// === 1️⃣ Tính phạm vi phòng === let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (const [, pos] of rooms) { minX = Math.min(minX, pos.x); maxX = Math.max(maxX, pos.x); minY = Math.min(minY, pos.y); maxY = Math.max(maxY, pos.y); }

const roomCountX = maxX - minX + 1; const roomCountY = maxY - minY + 1;

// === 2️⃣ Tự động scale để vừa khung (padding nhỏ) === const padding = 20; const cellX = (cw - padding * 2) / Math.max(roomCountX, 1); const cellY = (ch - padding * 2) / Math.max(roomCountY, 1); const cell = Math.min(cellX, cellY, 26); // không to quá

// === 3️⃣ Tính offset để canh giữa tất cả === const totalW = roomCountX * cell; const totalH = roomCountY * cell; const offsetX = (cw - totalW) / 2 - minX * cell; const offsetY = (ch - totalH) / 2 - minY * cell;

// === 4️⃣ Vẽ các đường nối (rút ngắn hợp lý) === ctx.lineWidth = 2; ctx.strokeStyle = "rgba(200,200,200,0.25)"; for (const id in roomMapPositions) { const pos = roomMapPositions[id]; const dObj = window.dungeonGraph && window.dungeonGraph[id]; if (!dObj || !dObj.doors) continue;

for (const door of dObj.doors) {
  const targetId = door.toDungeonId;
  if (!roomMapPositions[targetId]) continue;

  const p1 = roomMapPositions[id];
  const p2 = roomMapPositions[targetId];
  const x1 = offsetX + p1.x * cell;
  const y1 = offsetY + p1.y * cell;
  const x2 = offsetX + p2.x * cell;
  const y2 = offsetY + p2.y * cell;

  // điều chỉnh để nối từ mép phòng này sang mép phòng kia

const dx = x2 - x1; const dy = y2 - y1; const dist = Math.hypot(dx, dy); const nx = dx / dist, ny = dy / dist;

// bán kính “phòng” nhỏ ~4px const roomHalf = 4;

ctx.beginPath(); ctx.moveTo(x1 + nx * roomHalf, y1 + ny * roomHalf); ctx.lineTo(x2 - nx * roomHalf, y2 - ny * roomHalf); ctx.stroke(); } }

// === 5️⃣ Vẽ phòng (ô vuông) === for (const id in roomMapPositions) { const pos = roomMapPositions[id]; const x = offsetX + pos.x * cell; const y = offsetY + pos.y * cell; const visited = !!roomMapVisited[id]; ctx.fillStyle = visited ? "rgba(255,255,255,0.9)" : "rgba(80,80,80,0.35)"; ctx.fillRect(x - 4, y - 4, 8, 8); }

// === 6️⃣ Phòng hiện tại (vàng) === const curPos = roomMapPositions[dungeon.id]; if (curPos) { const x = offsetX + curPos.x * cell; const y = offsetY + curPos.y * cell; ctx.fillStyle = "gold"; ctx.fillRect(x - 5, y - 5, 10, 10); ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.strokeRect(x - 7, y - 7, 14, 14); }

// === 7️⃣ Viền khung mini-map (tùy chọn, giúp test) === // === Hiển thị số tầng === ctx.fillStyle = "rgba(255,255,255,0.9)"; ctx.font = "14px sans-serif"; ctx.textAlign = "left"; ctx.fillText(Tầng ${window.currentFloor}, 10, 18); ctx.strokeStyle = "rgba(255,255,255,0.15)"; ctx.strokeRect(0, 0, cw, ch); }

// === Biểu tượng nhân vật động có hướng + hiệu ứng nhún === let playerWalkToggle = false; // luân phiên 🚶 / 🏃 let lastWalkAnimTime = 0; // thời điểm đổi frame gần nhất let lastMoveDir = { dx: 0, dy: 0 }; // hướng di chuyển gần nhất let walkBobPhase = 0; // pha dao động "nhún" let lastBobTime = 0;

// ========== NHÂN VẬT CÓ BIỂU CẢM SỐNG (v5 - 🥴 + hiệu ứng ngủ 💤) ==========

let idleStartTime = 0; // thời điểm bắt đầu đứng yên let lastIdleMood = "🙂"; // biểu cảm hiện tại let sleepStage = 0; // 0=thức,1=🥱,2=😴,3=😪 let justWokeUp = false; // vừa tỉnh sau 😪 let wakeFaceUntil = 0; // hết thời gian 🥴 let lastRandomBlink = 0; // thay biểu cảm ngẫu nhiên let zFloatPhase = 0; // hiệu ứng "💤" nổi

function drawPlayerSymbol(ctx, px, py) { const now = performance.now(); const walkInterval = 250; const bobSpeed = 7; const bobHeight = 1;

// --- ghi nhớ hướng --- if (player.moveDir) lastMoveDir = { ...player.moveDir };

// --- đổi frame khi di chuyển --- if (player.moving && now - lastWalkAnimTime > walkInterval) { playerWalkToggle = !playerWalkToggle; lastWalkAnimTime = now; }

let sym = "🙂"; let flip = false;

// ========================================================= // 1️⃣ Khi đang di chuyển // ========================================================= if (player.moving) { // Nếu vừa thoát khỏi giấc ngủ sâu → đánh dấu vừa tỉnh if (sleepStage === 3) { justWokeUp = true; }

// Icon di chuyển bình thường
if (lastMoveDir.dx !== 0) {
  sym = playerWalkToggle ? "🚶" : "🏃";
  flip = lastMoveDir.dx > 0;
} else if (lastMoveDir.dy !== 0) {
  sym = "🧍"; // đi lên/xuống
}

// reset trạng thái idle
idleStartTime = now;
lastIdleMood = "🙂";
sleepStage = 0;

}

// ========================================================= // 2️⃣ Khi đứng yên // ========================================================= else { if (idleStartTime === 0) idleStartTime = now; const idleTime = (now - idleStartTime) / 1000;

// Nếu vừa tỉnh và dừng lại → 🥴 trong 2s
if (justWokeUp && now > wakeFaceUntil) {
  sym = "🥴";
  wakeFaceUntil = now + 2000;
  justWokeUp = false;
  sleepStage = 0;
}
else if (now < wakeFaceUntil) {
  sym = "🥴";
}
else {
  // Biểu cảm ngủ dần dần
  if (idleTime > 25) {
    sym = "😪";
    sleepStage = 3;
  } else if (idleTime > 18) {
    sym = "😴";
    sleepStage = 2;
  } else if (idleTime > 12) {
    sym = "🥱";
    sleepStage = 1;
  } else {
    // Bình thường / biểu cảm phụ
    sleepStage = 0;
    if (now - lastRandomBlink > 4000 + Math.random() * 4000) {
      const r = Math.random();
      if (r < 0.25) lastIdleMood = "😎";
      else if (r < 0.5) lastIdleMood = "👀";
      else lastIdleMood = "🙂";
      lastRandomBlink = now;
    }
    sym = lastIdleMood;
  }
}

if (lastMoveDir.dx > 0) flip = true;

}

// ========================================================= // 3️⃣ Hiệu ứng nhún // ========================================================= if (player.moving) { const delta = (now - lastBobTime) / 1000; walkBobPhase += delta * bobSpeed; lastBobTime = now; } else { walkBobPhase = 0; lastBobTime = now; }

const bobOffset = player.moving ? Math.sin(walkBobPhase * Math.PI * 2) * bobHeight : 0;

// ========================================================= // 4️⃣ Vẽ nhân vật // ========================================================= ctx.save(); ctx.translate(px, py + bobOffset); if (flip) ctx.scale(-1, 1);

ctx.font = "22px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(sym, 0, 0);

// ========================================================= // 5️⃣ Hiệu ứng "💤" khi ngủ // ========================================================= if (sleepStage >= 2) { // 😴 hoặc 😪 zFloatPhase += 0.03; const floatY = Math.sin(zFloatPhase) * 5 - 25; // dao động lên xuống ctx.font = "16px monospace"; ctx.fillText("💤", 0, floatY); }

ctx.restore(); }

// === ICON HIỆU ỨNG XOAY QUANH NHÂN VẬT (🔥☠️❄️) === function drawEffectIcons(ctx, px, py) { if (!window.activeEffects || !activeEffects.length) return; const now = Date.now(); const total = activeEffects.length;

const orbitR = 26; // bán kính quay quanh nhân vật const iconSize = 16; // kích thước nhỏ gọn

activeEffects.forEach((e, i) => { const remaining = Math.max(0, e.expires - now); const progress = e.initialDuration ? remaining / e.initialDuration : 0; const angle = ((i / total) * Math.PI * 2) + (now / 900); // quay đều const pulse = 0.8 + 0.2 * Math.sin(now / 300 + i); // nhấp nháy nhẹ

const x = px + Math.cos(angle) * orbitR;
const y = py + Math.sin(angle) * orbitR;

// Chọn icon và màu ánh sáng
let icon = "✨";
let glow = "rgba(255,255,255,0.5)";
if (e.type === "burn") { icon = "🔥"; glow = "rgba(255,80,0,0.8)"; }
else if (e.type === "poison") { icon = "☠️"; glow = "rgba(100,255,100,0.8)"; }
else if (e.type === "freeze") { icon = "❄️"; glow = "rgba(120,200,255,0.9)"; }

// Nếu đang tan → làm nổ sáng nhẹ rồi mờ dần
const alpha = e.fadingOut ? e.alpha ** 1.5 : pulse * 0.9;
const scale = e.fadingOut ? 1 + (1 - e.alpha) * 1.5 : 1;

ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.globalAlpha = alpha;
ctx.font = `${iconSize}px monospace`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowBlur = 6;
ctx.shadowColor = glow;
ctx.fillText(icon, 0, 0);
ctx.restore();

});

// Hiển thị số stack (🔥x3, ☠️x2, ...) const grouped = {}; activeEffects.forEach(e => grouped[e.type] = (grouped[e.type] || 0) + 1); const offsetY = -38; let offsetX = -12; Object.entries(grouped).forEach(([type, count]) => { const sym = type === "burn" ? "🔥" : type === "poison" ? "☠️" : "❄️"; ctx.save(); ctx.font = "15px monospace"; ctx.globalAlpha = 0.9; ctx.fillText(${sym}x${count}, px + offsetX, py + offsetY); offsetX += 45; ctx.restore(); }); }

// === Vẽ dungeon (gọi mỗi frame) function drawDungeon(ctx) { if (!ctx || !dungeon) return; dungeonBlink += 0.06; const map = dungeon.map, w = dungeon.w, h = dungeon.h; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

const trait = DUNGEON_TRAITS[dungeon.theme] || {}; const baseColor = trait.floorColor || "#444"; const theme = dungeon.theme || "cave";

// 🌄 Gradient nền tùy theo theme const grad = ctx.createLinearGradient(0, 0, 0, ctx.canvas.height); switch (theme) { case "desert": grad.addColorStop(0, "#f7e8b3"); grad.addColorStop(1, "#d8b871"); break; case "ice": grad.addColorStop(0, "#b8e0ff"); grad.addColorStop(1, "#8cb4e8"); break; case "jungle": grad.addColorStop(0, "#204d24"); grad.addColorStop(1, "#357a38"); break; case "volcano": grad.addColorStop(0, "#400000"); grad.addColorStop(1, "#7a1f00"); break; case "cave": grad.addColorStop(0, "#1b1b1b"); grad.addColorStop(1, "#2f2f2f"); break; case "swamp": grad.addColorStop(0, "#2f3229"); grad.addColorStop(1, "#444b3a"); break; case "mountain": grad.addColorStop(0, "#4a4a4a"); grad.addColorStop(1, "#6e6e6e"); break; case "boss": grad.addColorStop(0, "#4a0000"); grad.addColorStop(0.5, "#7a1a00"); grad.addColorStop(1, "#a83a00"); break; default: grad.addColorStop(0, "#222"); grad.addColorStop(1, "#333"); break; } ctx.fillStyle = grad; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

// === camera === const viewW = Math.floor(ctx.canvas.width / tileSize); const viewH = Math.floor(ctx.canvas.height / tileSize); const halfW = Math.floor(viewW / 2), halfH = Math.floor(viewH / 2); let camX = Math.max(0, Math.min(w - viewW, playerPos.x - halfW)); let camY = Math.max(0, Math.min(h - viewH, playerPos.y - halfH));

// === nền & tường === for (let y = 0; y < viewH; y++) { for (let x = 0; x < viewW; x++) { const mapX = camX + x, mapY = camY + y; const t = map[mapY]?.[mapX]; if (t == null) continue;

  // --- Nếu là tường ---
  if (t === 1) {
    const wallSym = (trait.wallSymbol || "⬛").trim();
    ctx.font = `${tileSize}px monospace`;
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText(wallSym, x * tileSize + tileSize / 2, y * tileSize + tileSize / 2);
    continue;
  }
  
  // --- Biểu tượng đặc biệt ---
  if (["C","E","?","D","G","🔥","❄️","☠️","🧨","🕳️","👹","🎁","🌀","🌫️"].includes(t)) {
    ctx.font = "20px monospace";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    const sym = {
      "C":"💰","E":"👾","?":"❓","D":"🚪",
      "G":"🚪✨","🔥":"🔥","❄️":"❄️","☠️":"☠️",
      "🧨":"🧨","🕳️":"🕳️","👹":"👹","🎁":"🎁","🌀":"🌀","🌫️":"🌫️"
    }[t];
    ctx.fillText(sym, x * tileSize + tileSize / 2, y * tileSize + tileSize / 2);
  }
}

}

// === Vẽ player === const px = (playerPos.x - camX) * tileSize + tileSize / 2; const py = (playerPos.y - camY) * tileSize + tileSize / 2; drawPlayerSymbol(ctx, px, py); drawEffectIcons(ctx, px, py);

// === Vẽ particle (phát hiện phòng boss) const isBossRoom = !!dungeon.isBossRoom; updateDungeonParticles(ctx, theme, trait.fog, isBossRoom);

// === Hiệu ứng nhịp tim quanh player khi ở phòng boss if (isBossRoom) { drawBossLightPulse(ctx, px, py, lightRadius * tileSize * 1.3); } else { const g = ctx.createRadialGradient(px, py, 0, px, py, lightRadius * tileSize); g.addColorStop(0, "rgba(255,255,255,0)"); g.addColorStop(1, "rgba(0,0,0,0.45)"); ctx.fillStyle = g; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); window._bossLightFade = 0; }

try { safeUpdateBars(); } catch (e) {}

// === Particle động === updateDungeonParticles(ctx, theme);

// === Fog (sương mù) === if (trait.fog === "frost") ctx.fillStyle = "rgba(200,240,255,0.2)"; else if (trait.fog === "heat") ctx.fillStyle = "rgba(255,100,0,0.15)"; else if (trait.fog === "water") ctx.fillStyle = "rgba(0,80,200,0.2)"; else if (trait.fog === "dense") ctx.fillStyle = "rgba(30,30,30,0.35)"; else if (trait.fog === "light") ctx.fillStyle = "rgba(255,255,200,0.1)"; if (trait.fog) ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

// === Ánh sáng quanh player === const g = ctx.createRadialGradient(px, py, 0, px, py, lightRadius * tileSize); g.addColorStop(0, "rgba(255,255,255,0)"); g.addColorStop(1, "rgba(0,0,0,0.45)"); ctx.fillStyle = g; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

// cập nhật HUD try { safeUpdateBars(); } catch(e){} }

// === Reset hạt khi đổi tầng hoặc thoát dungeon === let dungeonParticles = []; let dungeonParticleTick = 0;

function resetDungeonParticles() { dungeonParticles = []; dungeonParticleTick = 0; }

// 🌋 Hiệu ứng particle động cho dungeon (kéo dài, có boss mode) function updateDungeonParticles(ctx, theme, fogType, isBossRoom = false) { const MAX_PARTICLES = isBossRoom ? 100 : 70; dungeonParticleTick++;

// Tạo hạt mới nếu thiếu while (dungeonParticles.length < MAX_PARTICLES) { dungeonParticles.push({ x: Math.random() * ctx.canvas.width, y: Math.random() * ctx.canvas.height, s: Math.random() * 2.2 + 0.5, v: Math.random() * 0.4 + 0.1, alpha: Math.random() * 0.7 + 0.2, drift: Math.random() * 0.5 - 0.25, life: Math.random() * 1000 + 1000, // sống rất lâu (20–40s) age: Math.random() * 500, colorShift: Math.random(), }); }

// Màu và kiểu hạt theo khu vực let color = "rgba(255,255,255,0.3)"; let mode = "float";

switch (theme) { case "ice": color = "rgba(210,240,255,0.8)"; mode = "snow"; break; case "tundra": color = "rgba(230,250,255,0.9)"; mode = "snow"; break; case "volcano": color = "rgba(255,120,40,0.5)"; mode = "ash"; break; case "desert": color = "rgba(255,220,150,0.4)"; mode = "sand"; break; case "jungle": color = "rgba(140,255,140,0.5)"; mode = "firefly"; break; case "swamp": color = "rgba(180,255,160,0.3)"; mode = "mist"; break; case "deepsea": color = "rgba(120,200,255,0.4)"; mode = "bubble"; break; case "sea": color = "rgba(80,180,255,0.3)"; mode = "bubble"; break; case "cave": color = "rgba(220,220,255,0.25)";mode = "dust"; break; case "mountain": color = "rgba(255,255,255,0.3)"; mode = "wind"; break; case "wasteland": color = "rgba(255,210,160,0.3)"; mode = "dust"; break; case "ruins": color = "rgba(255,255,200,0.2)"; mode = "mote"; break; case "grassland": color = "rgba(200,255,200,0.4)"; mode = "pollen"; break; case "plain": color = "rgba(255,255,180,0.4)"; mode = "pollen"; break; }

// Boss room có hiệu ứng đặc biệt if (isBossRoom) { color = "rgba(255,80,40,0.55)"; mode = "ember"; }

ctx.globalAlpha = 1; ctx.fillStyle = color;

for (let p of dungeonParticles) { p.age++;

// Làm hạt lung linh nhẹ
const flicker = 0.7 + 0.3 * Math.sin(dungeonParticleTick * 0.05 + p.colorShift * Math.PI * 2);
const fade = Math.max(0, 1 - p.age / p.life);
ctx.globalAlpha = p.alpha * fade * flicker;

ctx.beginPath();
ctx.arc(p.x, p.y, p.s, 0, Math.PI * 2);
ctx.fill();

// Di chuyển tùy mode
switch (mode) {
  case "snow":      p.y += p.v; p.x += Math.sin(p.y * 0.02) * 0.3; break;
  case "ash":       p.y -= p.v; p.x += p.drift * 0.3; break;
  case "sand":      p.x += p.drift * 1.5; p.y += Math.sin(p.x * 0.03) * 0.1; break;
  case "firefly":   p.x += Math.sin(p.y * 0.05) * 0.5; p.y += Math.cos(p.x * 0.05) * 0.2; break;
  case "bubble":    p.y -= p.v * 0.8; p.x += Math.sin(p.y * 0.02) * 0.2; break;
  case "dust":      p.y -= p.v * 0.2; p.x += p.drift * 0.2; break;
  case "mist":      p.y += p.v * 0.1; p.x += Math.sin(p.y * 0.03) * 0.3; break;
  case "wind":      p.x += p.v * 0.8; p.y += Math.sin(p.x * 0.02) * 0.3; break;
  case "pollen":    p.y += Math.sin(p.x * 0.02) * 0.2; p.x += p.drift * 0.5; break;
  case "ember":     p.y -= p.v * 0.4; p.x += Math.sin(p.y * 0.04) * 0.6; break;
  default:          p.y += Math.sin(p.x * 0.02) * 0.1; break;
}

// Reset hạt khi vượt giới hạn hoặc chết
if (
  p.y > ctx.canvas.height + 10 || p.y < -10 ||
  p.x < -10 || p.x > ctx.canvas.width + 10 ||
  p.age > p.life
) {
  Object.assign(p, {
    x: Math.random() * ctx.canvas.width,
    y: Math.random() * ctx.canvas.height,
    s: Math.random() * 2.2 + 0.5,
    v: Math.random() * 0.4 + 0.1,
    alpha: Math.random() * 0.7 + 0.2,
    drift: Math.random() * 0.5 - 0.25,
    life: Math.random() * 1000 + 1000,
    age: 0,
    colorShift: Math.random(),
  });
}

}

ctx.globalAlpha = 1; }

function drawBossLightPulse(ctx, px, py, radius) { if (!ctx) return;

const now = performance.now(); // 🫀 Nhịp tim dao động từ 0.4 → 1.0 const pulse = 0.4 + 0.6 * Math.abs(Math.sin(now / 250));

// Hiệu ứng fade-in khi mới vào boss room window._bossLightFade = Math.min(1, (window._bossLightFade || 0) + 0.03);

// Gradient ánh sáng đỏ cam mạnh hơn và mở rộng hơn một chút const g = ctx.createRadialGradient(px, py, 0, px, py, radius); g.addColorStop(0, rgba(255, 80, 40, ${0.35 * window._bossLightFade})); g.addColorStop(0.4, rgba(255, 40, 20, ${0.25 * pulse * window._bossLightFade})); g.addColorStop(0.7, rgba(120, 0, 0, ${0.15 * window._bossLightFade})); g.addColorStop(1, "rgba(0, 0, 0, 0.65)");

ctx.save(); ctx.globalCompositeOperation = "lighter"; // làm sáng lớp boss ctx.fillStyle = g; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.restore(); }

// === Dungeon Loop ===

function refreshPlayerUI(full = false) { try { safeUpdateBars?.(); // HP, Mana, Stamina if (typeof updateExpBar === "function") updateExpBar(); if (typeof renderStats === "function") renderStats(); if (typeof renderInventory === "function") renderInventory(); if (typeof renderActiveEffects === "function") renderActiveEffects(); if (full) console.log("🔄 Làm mới toàn bộ giao diện nhân vật!"); } catch (err) { console.warn("⚠️ refreshPlayerUI lỗi:", err); } }

let lastUIRefresh = 0; // thời điểm cập nhật UI gần nhất

function dungeonLoop() { updateEffects(); checkFogProximity?.(dungeon); drawDungeon?.(dungeonCtx);

// 🕒 Cập nhật UI mỗi 0.5 giây const now = Date.now(); if (now - lastUIRefresh > 500) { // khoảng cách giữa 2 lần refresh refreshPlayerUI?.(); // cập nhật thanh máu, stamina, exp, inventory... lastUIRefresh = now; }

dungeonLoopId = requestAnimationFrame(dungeonLoop); }

function startDungeonLoop() { resetDungeonParticles(); if (dungeonLoopId) cancelAnimationFrame(dungeonLoopId); dungeonLoopId = requestAnimationFrame(dungeonLoop); }

function stopDungeonLoop() { if (dungeonLoopId) cancelAnimationFrame(dungeonLoopId); dungeonLoopId = null; resetDungeonParticles(); }

if (!window.currentFloor) window.currentFloor = 1;

// Khóa chống gọi trùng tầng let nextFloorLock = false;

// --- Qua tầng kế tiếp trong dungeon --- function nextDungeonFloor() { console.log("⬇️ Chuẩn bị sang tầng kế tiếp...");

// --- Tăng tầng --- if (!window.currentFloor) window.currentFloor = 1; window.currentFloor++;

// --- Xác định loại thế giới --- const worldType = window.currentWorldType || "tieu"; const baseTheme = window.dungeonBaseTheme || "cave";

// --- Ghi log tầng mới --- let worldLabel = "Tiểu Thiên Thế Giới"; if (worldType === "trung") worldLabel = "Trung Thiên Thế Giới"; else if (worldType === "dai") worldLabel = "Đại Thiên Thế Giới";

const colorMap = { dai: "color:#ff4444;font-weight:bold;", trung: "color:#ffbb33;font-weight:bold;", tieu: "color:#44ff44;font-weight:bold;" }; console.log(%c⬇️ Sang tầng ${window.currentFloor} của ${worldLabel}, colorMap[worldType] || "");

// --- Reset quota cho tầng mới (mỗi tầng mới là một dungeon độc lập) --- const worldKey = selectedWorld ? (selectedWorld.id || selectedWorld.type) : worldType; window.worldRoomQuota[worldKey] = initialQuotaForWorld(worldType); console.log(🗺️ Reset quota: ${window.worldRoomQuota[worldKey]} phòng cho tầng ${window.currentFloor});

// --- Tạo dungeon mới cùng theme --- const newDungeon = generateDungeon(baseTheme, 25, 18, worldType); newDungeon._canSpawnChildren = true; // cho phép sinh thêm phòng dungeon = newDungeon; playerPos = { ...dungeon.playerStart };

// --- Reset boss state --- inBossRoom = false; window.bossRoomCreated = false;

// --- Reset mini-map & tạo lại ngay --- const mm = document.getElementById("roomMiniMap"); if (mm) mm.remove(); const toggleBtn = document.getElementById("toggleMiniMapBtn"); if (toggleBtn) toggleBtn.remove();

roomMapVisited = {}; roomMapPositions = {}; roomMapCanvas = null; window.roomMapCenter = null;

initRoomMiniMap(); markRoomVisited(dungeon); drawRoomMiniMap(); setupMiniMapToggle();

// --- Cập nhật canvas & vòng lặp --- drawDungeon(dungeonCtx); startDungeonLoop();

console.log(🌌 Đã sang tầng ${window.currentFloor} (theme: ${baseTheme}, worldType: ${worldType})); }

function findClosestWallToPlayer(dungeon) { const { map, w, h } = dungeon; // ưu tiên tìm wall có ô sàn kề nó (trong vùng trung tâm) for (let y = 1; y < h - 1; y++) { for (let x = 1; x < w - 1; x++) { if (map[y][x] === 1) { if (map[y-1] && map[y-1][x] === 0) return { x, y, openTo: { x, y: y-1 } }; if (map[y+1] && map[y+1][x] === 0) return { x, y, openTo: { x, y: y+1 } }; if (map[y] && map[y][x-1] === 0) return { x, y, openTo: { x: x-1, y } }; if (map[y] && map[y][x+1] === 0) return { x, y, openTo: { x: x+1, y } }; } } } return null; }

// --- VÒNG LẶP CHÍNH --- let rafId = null; let tick = 0; function startLoop(){ if(rafId) cancelAnimationFrame(rafId);

(function loop(){ ctx.clearRect(0,0,W,H);

if(currentMapLevel==="cosmos") {
  drawBackground();
  drawAsteroids();
  drawSpiralNebulaBackground();
  drawWorldRings();
  drawPlanets();
  drawNebulaForeground();
        drawCore(t);

}
else if(currentMapLevel==="world") {
  drawWorld();
  renderRegions(ctx);
  
}
else if(currentMapLevel==="region") {
  drawRegion();
}
else if(currentMapLevel==="dungeon") {

if (dungeon) drawDungeon(ctx); }

t++;
rafId = requestAnimationFrame(loop);

})(); }

// khởi động lần đầu let resizeTimeout = null; window.addEventListener('resize', ()=> { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(()=> { resizeCanvas(); }, 120); });

// ban đầu gọi resizeCanvas → init tất cả arrays, rồi start loop resizeCanvas(); startLoop(); }); // --- END MAP MODULE ---

// ==================== HỆ THỐNG HIỆU ỨNG (🔥 ☠️ ❄️) ==================== window.updateEffects = updateEffects; window.applyEffect = applyEffect;

// Danh sách hiệu ứng đang hoạt động (stackable) var activeEffects = window.activeEffects = window.activeEffects || [];

function applyEffect(effectId, options = {}) { const defaultDurations = { burn: 5000, poison: 7000, freeze: 4000 }; const defaultIntervals = { burn: 1000, poison: 1500, freeze: 1000 };

// Chỉ số kháng (Khang) const resist = calcFinalStat(player.stats.Khang || 0, "Khang") || 0; const durationFactor = 1 - Math.min(0.6, resist / 200); // Giảm thời gian tối đa 60%

const sameType = activeEffects.filter(e => e.type === effectId); const maxStacks = options.maxStacks || 5;

// Nếu đã đạt stack tối đa -> tăng cường stack cũ if (sameType.length >= maxStacks) { const oldest = sameType[0]; oldest.power += (options.power || 1) * 0.5; oldest.expires += (options.duration || defaultDurations[effectId]) * 0.3 * durationFactor; console.log(💥 ${effectId} đạt giới hạn stack, tăng cường sức mạnh.); return; }

// Tạo stack mới const now = Date.now(); const newEffect = { id: ${effectId}_${now}_${Math.floor(Math.random() * 9999)}, type: effectId, created: now, expires: now + ((options.duration || defaultDurations[effectId]) * durationFactor), tickInterval: options.tickInterval || defaultIntervals[effectId], lastTick: now, power: options.power || 1, source: options.source || "environment", initialDuration: options.duration || defaultDurations[effectId],

};

activeEffects.push(newEffect); console.log(🌀 Hiệu ứng [${effectId}] stack mới (${sameType.length + 1}/${maxStacks})); }

function updateEffects() { const now = Date.now(); const resist = calcFinalStat(player.stats.Khang || 0, "Khang") || 0; const resistFactor = 1 - Math.min(0.8, resist / 100); // giảm damage tối đa 80%

activeEffects = activeEffects.filter(e => { if (now >= e.expires) return false;

if (now - e.lastTick >= e.tickInterval) {
  e.lastTick = now;

  let dmg = 0, label = "";
  switch (e.type) {
    case "burn": dmg = 2 * e.power; label = "🔥 Bỏng"; break;
    case "poison": dmg = 1.5 * e.power; label = "☠️ Độc"; break;
    case "freeze": dmg = 1 * e.power; label = "❄️ Lạnh cóng"; break;
  }

  dmg = Math.ceil(dmg * resistFactor);
  if (dmg > 0) modifyHP(-dmg, label);
}
return true;

}); }

function safeNum(v, d = 0) { const n = Number(v); return isNaN(n) ? d : n; }

function getMaxHP() { const base = safeNum(player.stats.MaxHP, 100); const theChat = calcFinalStat(player.stats.TheChat || 0, "TheChat"); return base + theChat * 10; // Thể Chất mỗi điểm +10 HP } function getMaxMana() { const base = safeNum(player.stats.MaxMana, 50); const triLuc = calcFinalStat(player.stats.TriLuc || 0, "TriLuc"); return base + triLuc * 5; // Trí Lực mỗi điểm +5 Mana } function getMaxStamina() { const base = safeNum(player.stats.MaxSucBen, 20); const theChat = calcFinalStat(player.stats.TheChat || 0, "TheChat"); return base + Math.floor(theChat / 2); // Thể Chất ảnh hưởng nhẹ thể lực } // === Hồi phục chỉ số chung, tự động dừng khi đầy === function recoverStat(statKey, amount) { if (!player || !player.stats) return; const maxKey = "Max" + statKey; const current = Number(player.stats[statKey]) || 0; const max = Number(player.stats[maxKey]) || 0; if (current >= max) return; // đầy rồi thì bỏ qua

player.stats[statKey] = Math.min(max, current + amount); }

function regenerationTick() { if (!player || !player.stats) return;

// Nếu nhân vật đã chết thì không hồi if (player.stats.HP <= 0) return;

const maxHP = getMaxHP(); const maxMana = getMaxMana(); const maxSt = getMaxStamina();

const theChat = calcFinalStat(player.stats.TheChat || 0, "TheChat"); const triLuc = calcFinalStat(player.stats.TriLuc || 0, "TriLuc");

// --- Hệ số hồi cơ bản --- let hpMult = 1.0, manaMult = 1.0, stMult = 1.0;

// ⚔️ Giảm hồi khi đang chiến đấu if (player.state === "combat") { hpMult *= 0.5; manaMult *= 0.7; stMult *= 0.6; }

// 🧘 Tăng hồi khi ngồi thiền if (player.state === "meditating") { hpMult *= 2.0; manaMult *= 2.5; stMult *= 1.8; }

// 💀 Giảm hồi khi bị trọng thương if (player.debuffs?.includes("bleeding")) hpMult *= 0.5; if (player.debuffs?.includes("poison")) hpMult *= 0.7; if (player.debuffs?.includes("manaBurn")) manaMult *= 0.5; if (player.debuffs?.includes("fatigue")) stMult *= 0.5;

// 🩸 Buff tăng hồi phục if (player.buffs?.includes("regenAura")) hpMult *= 1.5; if (player.buffs?.includes("manaFlow")) manaMult *= 1.8; if (player.buffs?.includes("ironWill")) stMult *= 1.4;

// --- Tính lượng hồi thực tế --- const regenHP = Math.max(1, Math.floor((maxHP * 0.005 + theChat * 0.2) * hpMult)); const regenMana = Math.max(1, Math.floor((maxMana * 0.004 + triLuc * 0.15) * manaMult)); const regenSt = Math.max(1, Math.floor((maxSt * 0.01 + theChat * 0.1) * stMult));

// --- Hồi từng thanh, dừng nếu đầy --- if (player.stats.HP < maxHP) player.stats.HP = Math.min(maxHP, safeNum(player.stats.HP, maxHP) + regenHP);

if (player.stats.Mana < maxMana) player.stats.Mana = Math.min(maxMana, safeNum(player.stats.Mana, maxMana) + regenMana);

if (player.stats.SucBen < maxSt) player.stats.SucBen = Math.min(maxSt, safeNum(player.stats.SucBen, maxSt) + regenSt);

// --- Cập nhật giao diện --- if (typeof safeUpdateBars === "function") safeUpdateBars(); if (typeof refreshPlayerUI === "function") refreshPlayerUI(); }

// Gọi tự động mỗi 5 giây setInterval(regenerationTick, 5000);

function generateEnemy(type="normal"){ const playerLv = player.stats.Level || 1; const worldType = window.currentWorldType || "tieu"; const worldMul = worldType==="dai" ? 2.5 : (worldType==="trung" ? 1.5 : 1); const level = Math.max(1, playerLv + (type==="boss" ? 3 : (Math.random()*2-1)));

const hp = Math.floor((50 + level12) * worldMul * (type==="boss"?2:1)); const atk = Math.floor((8 + level2) * worldMul * (type==="boss"?1.5:1)); const def = Math.floor((5 + level*1.2) * worldMul * (type==="boss"?1.3:1));

// xác suất có công pháp const hasTech = Math.random() < (type==="boss"?0.6:0.2); let tech = null; if (hasTech && gameData.techniques) { tech = gameData.techniques[Math.floor(Math.random() * gameData.techniques.length)]; }

return { name: type==="boss" ? "Boss Cổ Thần" : "Quái Vật", type, level, stats:{HP:hp,Atk:atk,Def:def}, tech, element: type==="boss"?"🔥":"☠️", alive:true }; }

function showCombatUI(enemy){ // Xóa UI cũ nếu có const old = document.getElementById("combatUI"); if(old) old.remove();

const wrap = document.createElement("div"); wrap.id = "combatUI"; Object.assign(wrap.style,{ position:"fixed",left:"0",top:"0",width:"100%",height:"100%", background:"rgba(0,0,0,0.85)",zIndex:"15000", display:"flex",flexDirection:"column",justifyContent:"space-between", alignItems:"center",color:"#fff" });

// --- Trên: tên và thanh máu --- const top = document.createElement("div"); Object.assign(top.style,{width:"100%",padding:"12px",textAlign:"center"}); top.innerHTML = <div style="font-size:20px">⚔️ ${enemy.name} (Lv ${enemy.level})</div> <div style="background:#333;height:10px;border-radius:8px;margin-top:4px;overflow:hidden;"> <div id="enemyHpBar" style="background:#ff4747;width:100%;height:100%;transition:width 0.2s;"></div> </div>; wrap.appendChild(top);

// --- Giữa: nhân vật vs quái --- const mid = document.createElement("div"); Object.assign(mid.style,{flex:"1",display:"flex",alignItems:"center",justifyContent:"space-around"}); mid.innerHTML = <div id="playerSide" style="font-size:60px;">👤</div> <div id="vsText" style="font-size:30px;">VS</div> <div id="enemySide" style="font-size:60px;">💀</div>; wrap.appendChild(mid);

// --- Dưới: nút tấn công & kỹ năng --- const bottom = document.createElement("div"); Object.assign(bottom.style,{ width:"100%",padding:"8px",display:"flex", justifyContent:"center",gap:"8px",flexWrap:"wrap",marginBottom:"12px" });

// Nút đánh thường const atkBtn = document.createElement("button"); atkBtn.innerText = "🗡️ Tấn công"; Object.assign(atkBtn.style,{padding:"10px 18px",fontSize:"18px",borderRadius:"8px",cursor:"pointer"}); atkBtn.onclick = ()=> playerAttack(enemy); bottom.appendChild(atkBtn);

// Kỹ năng trang bị equippedSkills.forEach(id=>{ if(!id) return; const sk = gameData.skills.find(s=>s.id===id); if(!sk) return; const btn = document.createElement("button"); btn.innerText = sk.name; Object.assign(btn.style,{padding:"8px 12px",fontSize:"14px",borderRadius:"6px",cursor:"pointer"}); btn.onclick = ()=> playerAttack(enemy,sk); bottom.appendChild(btn); });

wrap.appendChild(bottom); document.body.appendChild(wrap); }

function playerAttack(enemy, skill=null){ if(!enemy.alive) return; const dmg = calcDamage(player.stats.TheChat || 10, enemy.stats.Def, !!skill); enemy.stats.HP -= dmg; document.getElementById("enemyHpBar").style.width = Math.max(0, enemy.stats.HP / 100 * 100) + "%"; console.log(💥 Bạn gây ${dmg} sát thương cho ${enemy.name});

if(enemy.stats.HP <= 0){ enemy.alive = false; console.log("🏆 Đã hạ gục kẻ địch!"); endCombat(enemy); return; }

setTimeout(()=>{ const enemyDmg = calcDamage(enemy.stats.Atk, player.stats.Khang || 5); player.stats.HP = Math.max(0, player.stats.HP - enemyDmg); console.log(😵 ${enemy.name} phản công gây ${enemyDmg} sát thương!); safeUpdateBars(); if(player.stats.HP <= 0){ console.log("💀 Bạn đã bại trận..."); document.getElementById("combatUI").remove(); } }, 600); }

function endCombat(enemy){ document.getElementById("combatUI")?.remove(); applyCombatResult(enemy); }

function resetGame(){ if(confirm("Bạn có chắc chắn muốn reset dữ liệu?")){ try{ localStorage.removeItem('rpgSave'); }catch(e){} player = JSON.parse(JSON.stringify(playerDefault)); saveGame(); location.reload(); } }

document.getElementById("btnMinus10").addEventListener("click", minusTenPercent);

function minusTenPercent() { // Giả sử bạn lưu max HP, Mana, Stamina trong player.stats const lostHP = Math.floor(player.stats.MaxHP * 0.1); const lostMana = Math.floor(player.stats.MaxMana * 0.1); const lostSta = Math.floor(player.stats.MaxSucBen * 0.1);

// Trừ đi, không để âm player.stats.HP = Math.max(0, player.stats.HP - lostHP); player.stats.Mana = Math.max(0, player.stats.Mana - lostMana); player.stats.SucBen = Math.max(0, player.stats.SucBen - lostSta);

// Hiện thông báo $('modal').style.display = 'flex'; $('modalCard').innerHTML = <h3>Thông báo</h3> <p>-10% HP (${lostHP}), -10% Mana (${lostMana}), -10% Stamina (${lostSta})</p> <p>Còn lại: HP ${player.stats.HP}, Mana ${player.stats.Mana}, Stamina ${player.stats.SucBen}</p> <div style="display:flex;gap:8px;margin-top:8px"> <button onclick="closeModal()">Đóng</button> </div>;

renderStats(); updateBars(); saveGame(); }

function init(){
  loadGame();
  if(!player) player = JSON.parse(JSON.stringify(playerDefault));
  if(!player.xpToNext) player.xpToNext = 100;
 // after loadGame()

player.skillSlots = player.skillSlots || Array(10).fill(null); // ensure slots exist resumeTrainingIfAny(); // resume offline training if any renderInventory(); renderSkills(); renderTechniques(); renderStats(); updateHeader(); drawCharacterBase(); renderEquipmentSlots(); loadGame(); animateChar(); updateBars();

  // nav
  $('btnHome').onclick = ()=> { removeDungeonControls(); showView('start'); };

$('btnInv').onclick = ()=&gt; { removeDungeonControls(); showView('inv'); }; $('btnSkills').onclick = ()=> { removeDungeonControls(); showView('skills'); }; $('btnMap').onclick = ()=&gt; { // nếu chuyển tới map, showView sẽ tái tạo controls nếu đang ở dungeon showView('map'); }; $('btnReset').onclick = resetGame;

  document.querySelectorAll('.equip-slot').forEach(el=>{ el.addEventListener('click', ()=> openEquipSlot(el.dataset.slot)); });
        const modalCard = $('modalCard'); if(modalCard) modalCard.addEventListener('click', (ev)=> ev.stopPropagation());
  if(!$('invMax').innerText) $('invMax').innerText = 24;

  // dropdown filter listeners
  const filterToggle = $('filterToggle'); const filterOptions = $('filterOptions');
  filterToggle && filterToggle.addEventListener('click', (e)=>{ e.stopPropagation(); filterOptions.style.display = filterOptions.style.display === 'block' ? 'none' : 'block'; });
  filterOptions && filterOptions.querySelectorAll('button').forEach(btn=>{
    btn.addEventListener('click', ()=>{
      currentInvFilter = btn.dataset.filter || 'all';
      const labelMap = { 

all: 'Tất cả', technique: 'Công pháp', skill: 'Kỹ năng', equip: 'Trang bị', potion: 'Đan dược', other: 'Khác' }

      const filt = $('invFilter'); if(filt) filt.innerText = labelMap[currentInvFilter] || currentInvFilter;
      filterOptions.style.display = 'none';
      renderInventory();
    });
  });

  // close filter when clicking outside
  document.addEventListener('click', (e)=>{ if(!filterOptions.contains(e.target) && !filterToggle.contains(e.target)){ filterOptions.style.display = 'none'; } });

  saveGame();
}

window.onload = init;

document.getElementById("btnAdd10Lv").onclick = add10Levels; window.openItem = openItem; window.useItem = useItem; window.closeModal = closeModal; window.viewSkill = viewSkill; window.trainSkill = trainSkill; window.openEquipSlot = openEquipSlot; window.learnTechnique = learnTechnique;

</script>

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors