Code game RPG fantasy 2D

Tôi sẽ làm theo hướng game RPG fantasy 2D dạng demo, tách đúng 3 file HTML / CSS / JS, có combat, skill, mana, HP, EXP, level, quái vật, loot, critical, combo, cooldown, hiệu ứng chiến đấu và giao diện kiểu game thật. rpg-game/ ├── index.html ├── style.css └── script.js

22/09/2026 Quản trị viên 9 Lập trình game
<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Chronicles of Eldoria — RPG Demo</title>

    <meta name="description"
          content="Chronicles of Eldoria - game nhập vai fantasy RPG demo được phát triển bởi MUATHEMEWPGIARE.COM">

    <link rel="stylesheet" href="style.css">
</head>

<body>

<div class="game-shell">

    <!-- TOP BAR -->
    <header class="topbar">

        <div class="brand">
            <div class="brand-icon">⚔</div>

            <div>
                <strong>ELDORIA</strong>
                <span>CHRONICLES</span>
            </div>
        </div>

        <div class="top-status">

            <div class="currency">
                <span>◈</span>
                <strong id="gold">1,850</strong>
            </div>

            <div class="currency crystal">
                <span>◆</span>
                <strong id="crystal">120</strong>
            </div>

            <button class="top-btn" id="inventoryBtn">
                🎒
                <span>Túi đồ</span>
            </button>

        </div>

    </header>


    <!-- MAIN GAME -->
    <main class="game">

        <!-- LEFT PANEL -->
        <aside class="hero-panel">

            <div class="hero-card">

                <div class="hero-avatar">
                    <div class="avatar-glow"></div>
                    <div class="avatar-character">🧙</div>

                    <div class="level-badge">
                        LV <span id="level">12</span>
                    </div>
                </div>

                <h1>Arthas</h1>

                <p class="hero-class">ARCANE KNIGHT</p>

                <div class="xp-wrapper">
                    <div class="bar-label">
                        <span>KINH NGHIỆM</span>
                        <b id="xpText">720 / 1000</b>
                    </div>

                    <div class="xp-bar">
                        <div id="xpFill"></div>
                    </div>
                </div>

            </div>


            <!-- PLAYER STATS -->
            <section class="stats">

                <h3>THÔNG SỐ</h3>

                <div class="stat-row">
                    <span>⚔ Sức mạnh</span>
                    <b id="power">142</b>
                </div>

                <div class="stat-row">
                    <span>🛡 Phòng thủ</span>
                    <b id="defense">86</b>
                </div>

                <div class="stat-row">
                    <span>💥 Bạo kích</span>
                    <b id="crit">18%</b>
                </div>

                <div class="stat-row">
                    <span>⚡ Tốc độ</span>
                    <b>112</b>
                </div>

            </section>


            <!-- QUEST -->
            <section class="quest">

                <div class="section-title">
                    <span>NHIỆM VỤ</span>
                    <b>01</b>
                </div>

                <h4>Hắc Lang Hoang Dã</h4>

                <p>Đánh bại 3 Hắc Lang để nhận thưởng.</p>

                <div class="quest-progress">
                    <span id="questProgress">1 / 3</span>
                    <div>
                        <i id="questFill"></i>
                    </div>
                </div>

                <div class="quest-reward">
                    <span>🎁 +300 EXP</span>
                    <span>◈ +250</span>
                </div>

            </section>

        </aside>


        <!-- BATTLE FIELD -->
        <section class="battle-area">

            <div class="battle-header">

                <div>
                    <span class="location-label">KHU VỰC</span>
                    <h2>RỪNG BÓNG ĐÊM</h2>
                </div>

                <div class="danger">
                    <span></span>
                    NGUY HIỂM CAO
                </div>

            </div>


            <!-- ARENA -->
            <div class="arena">

                <div class="moon"></div>

                <div class="mist mist-one"></div>
                <div class="mist mist-two"></div>

                <div class="mountain mountain-one"></div>
                <div class="mountain mountain-two"></div>

                <div class="ground"></div>


                <!-- PLAYER -->
                <div class="fighter player" id="player">

                    <div class="fighter-shadow"></div>

                    <div class="character-body">
                        🧙
                    </div>

                    <div class="fighter-name">
                        ARTHAS
                    </div>

                </div>


                <!-- ENEMY -->
                <div class="fighter enemy" id="enemy">

                    <div class="enemy-health">

                        <div class="enemy-title">
                            <span>HẮC LANG</span>
                            <b id="enemyLevel">LV 10</b>
                        </div>

                        <div class="enemy-hp">
                            <i id="enemyHpFill"></i>
                        </div>

                    </div>

                    <div class="character-body">
                        🐺
                    </div>

                    <div class="fighter-name">
                        FERAL WOLF
                    </div>

                </div>


                <!-- DAMAGE NUMBERS -->
                <div id="damageLayer"></div>

                <!-- BATTLE LOG -->
                <div class="battle-log" id="battleLog">
                    <div>⚔ Một Hắc Lang xuất hiện...</div>
                </div>

            </div>


            <!-- PLAYER RESOURCES -->
            <div class="resources">

                <div class="resource hp">
                    <div class="resource-head">
                        <span>HP</span>
                        <strong id="hpText">1,240 / 1,240</strong>
                    </div>

                    <div class="resource-bar">
                        <i id="hpFill"></i>
                    </div>
                </div>


                <div class="resource mana">
                    <div class="resource-head">
                        <span>MP</span>
                        <strong id="manaText">620 / 620</strong>
                    </div>

                    <div class="resource-bar">
                        <i id="manaFill"></i>
                    </div>
                </div>

            </div>


            <!-- SKILL BAR -->
            <div class="skill-section">

                <div class="skill-header">
                    <span>KỸ NĂNG</span>
                    <small>Nhấn phím 1 — 5 để sử dụng</small>
                </div>

                <div class="skills">

                    <button class="skill normal" data-skill="attack">

                        <span class="skill-key">1</span>

                        <div class="skill-icon">
                            ⚔
                        </div>

                        <div class="cooldown"></div>

                        <div class="skill-info">
                            <strong>ĐOẠN KIẾM</strong>
                            <small>Đòn đánh cơ bản</small>
                        </div>

                    </button>


                    <button class="skill fire" data-skill="fire">

                        <span class="skill-key">2</span>

                        <div class="skill-icon">
                            🔥
                        </div>

                        <div class="cooldown"></div>

                        <div class="skill-info">
                            <strong>HỎA NGỤC</strong>
                            <small>−45 MP</small>
                        </div>

                    </button>


                    <button class="skill ice" data-skill="ice">

                        <span class="skill-key">3</span>

                        <div class="skill-icon">
                            ❄
                        </div>

                        <div class="cooldown"></div>

                        <div class="skill-info">
                            <strong>BĂNG PHONG</strong>
                            <small>−60 MP</small>
                        </div>

                    </button>


                    <button class="skill lightning" data-skill="lightning">

                        <span class="skill-key">4</span>

                        <div class="skill-icon">
                            ⚡
                        </div>

                        <div class="cooldown"></div>

                        <div class="skill-info">
                            <strong>THIÊN LÔI</strong>
                            <small>−80 MP</small>
                        </div>

                    </button>


                    <button class="skill ultimate" data-skill="ultimate">

                        <span class="skill-key">5</span>

                        <div class="skill-icon">
                            ☄
                        </div>

                        <div class="cooldown"></div>

                        <div class="skill-info">
                            <strong>DIỆT THẾ</strong>
                            <small>−150 MP</small>
                        </div>

                    </button>

                </div>

            </div>

        </section>


        <!-- RIGHT PANEL -->
        <aside class="right-panel">

            <div class="panel-title">
                <span>CHIẾN TÍCH</span>
                <small>SESSION</small>
            </div>

            <div class="battle-stats">

                <div>
                    <span>Đã hạ</span>
                    <strong id="kills">1</strong>
                </div>

                <div>
                    <span>Combo</span>
                    <strong id="combo">0</strong>
                </div>

                <div>
                    <span>Sát thương</span>
                    <strong id="damage">0</strong>
                </div>

            </div>


            <div class="loot-panel">

                <div class="panel-title">
                    <span>VẬT PHẨM</span>
                </div>

                <div class="loot empty" id="loot">
                    <div>✦</div>
                    <p>Chưa có vật phẩm</p>
                </div>

            </div>


            <div class="controls">

                <button id="healBtn">
                    <span>✚</span>
                    HỒI PHỤC
                    <small>100 HP</small>
                </button>

                <button id="newEnemyBtn">
                    <span>⚔</span>
                    KẺ ĐỊCH MỚI
                </button>

            </div>

        </aside>

    </main>


    <!-- FOOTER -->
    <footer>

        <span>© 2026 MUATHEMEWPGIARE.COM</span>

        <span>
            CHRONICLES OF ELDORIA
            <b>•</b>
            RPG DEMO
        </span>

    </footer>

</div>


<!-- INVENTORY MODAL -->
<div class="modal" id="inventoryModal">

    <div class="modal-box">

        <button class="modal-close" id="closeInventory">×</button>

        <div class="modal-heading">
            <span>TÚI ĐỒ</span>
            <small>INVENTORY</small>
        </div>

        <div class="inventory-grid">

            <div class="item legendary">
                <span>⚔</span>
                <b>Kiếm Hoàng Kim</b>
                <small>Legendary</small>
            </div>

            <div class="item rare">
                <span>💎</span>
                <b>Tinh Thạch Mana</b>
                <small>Rare</small>
            </div>

            <div class="item">
                <span>🧪</span>
                <b>Bình HP</b>
                <small>x5</small>
            </div>

            <div class="item">
                <span>🛡</span>
                <b>Khiên Bóng Đêm</b>
                <small>Epic</small>
            </div>

        </div>

    </div>

</div>


<script src="script.js"></script>

</body>
</html>
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800;900&display=swap');

:root {
    --bg: #080a12;
    --panel: rgba(16, 19, 31, .94);
    --panel-2: #121624;

    --purple: #8c5cff;
    --purple-light: #b998ff;

    --cyan: #35d9ff;
    --blue: #4778ff;

    --red: #ff416c;
    --orange: #ff9a3c;

    --green: #35e59b;

    --text: #f6f7ff;
    --muted: #858ca4;

    --border: rgba(255,255,255,.08);
}

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    min-height: 100%;
}

body {
    background:
        radial-gradient(circle at 50% -10%, rgba(107, 63, 255, .18), transparent 38%),
        #06070c;

    color: var(--text);

    font-family:
        "Be Vietnam Pro",
        system-ui,
        sans-serif;

    overflow-x: hidden;
}

button {
    font-family: inherit;
}

.game-shell {
    min-height: 100vh;
}


/* ==============================
   TOP BAR
============================== */

.topbar {
    height: 76px;

    display: flex;
    align-items: center;
    justify-content: space-between;

    padding: 0 32px;

    border-bottom: 1px solid var(--border);

    background: rgba(8,10,18,.88);

    backdrop-filter: blur(18px);

    position: relative;
    z-index: 20;
}

.brand {
    display: flex;
    align-items: center;
    gap: 12px;
}

.brand-icon {
    width: 42px;
    height: 42px;

    display: grid;
    place-items: center;

    border: 1px solid rgba(140,92,255,.45);

    background:
        linear-gradient(
            145deg,
            rgba(140,92,255,.25),
            rgba(53,217,255,.08)
        );

    color: var(--purple-light);

    box-shadow:
        0 0 28px rgba(140,92,255,.15);

    font-size: 21px;
}

.brand strong {
    display: block;

    font-size: 17px;
    letter-spacing: 3px;
}

.brand span {
    display: block;

    margin-top: 2px;

    color: var(--muted);

    font-size: 8px;
    font-weight: 700;
    letter-spacing: 4px;
}

.top-status {
    display: flex;
    align-items: center;
    gap: 10px;
}

.currency {
    height: 38px;

    display: flex;
    align-items: center;
    gap: 7px;

    padding: 0 13px;

    background: rgba(255,255,255,.035);

    border: 1px solid var(--border);

    font-size: 13px;
}

.currency span {
    color: #ffb82e;
}

.currency.crystal span {
    color: var(--cyan);
}

.top-btn {
    height: 38px;

    padding: 0 15px;

    border: 1px solid rgba(140,92,255,.35);

    background: rgba(140,92,255,.08);

    color: white;

    cursor: pointer;

    transition: .25s;
}

.top-btn:hover {
    background: rgba(140,92,255,.2);
    transform: translateY(-2px);
}


/* ==============================
   MAIN
============================== */

.game {
    max-width: 1700px;

    min-height: calc(100vh - 116px);

    margin: auto;

    padding: 24px;

    display: grid;

    grid-template-columns: 250px minmax(500px, 1fr) 220px;

    gap: 18px;
}


/* ==============================
   PANELS
============================== */

.hero-panel,
.right-panel {
    background:
        linear-gradient(
            180deg,
            rgba(18,22,36,.96),
            rgba(9,11,18,.96)
        );

    border: 1px solid var(--border);

    box-shadow:
        0 20px 60px rgba(0,0,0,.3);
}

.hero-card {
    padding: 26px 20px 20px;

    text-align: center;

    border-bottom: 1px solid var(--border);
}

.hero-avatar {
    position: relative;

    width: 105px;
    height: 105px;

    margin: auto;

    display: grid;
    place-items: center;

    border-radius: 50%;

    border: 1px solid rgba(140,92,255,.45);

    background:
        radial-gradient(
            circle,
            rgba(140,92,255,.2),
            rgba(140,92,255,.02) 65%
        );
}

.avatar-glow {
    position: absolute;

    inset: -12px;

    border-radius: 50%;

    border: 1px solid rgba(140,92,255,.12);

    box-shadow:
        0 0 45px rgba(140,92,255,.22);
}

.avatar-character {
    font-size: 58px;

    filter:
        drop-shadow(0 10px 20px rgba(0,0,0,.7));
}

.level-badge {
    position: absolute;

    right: -4px;
    bottom: 2px;

    padding: 5px 8px;

    background: #181126;

    border: 1px solid rgba(140,92,255,.5);

    color: var(--purple-light);

    font-size: 10px;
    font-weight: 800;
}

.hero-card h1 {
    margin: 17px 0 3px;

    font-size: 20px;
}

.hero-class {
    margin: 0;

    color: var(--purple-light);

    font-size: 9px;
    font-weight: 800;

    letter-spacing: 2px;
}

.xp-wrapper {
    margin-top: 21px;

    text-align: left;
}

.bar-label,
.resource-head {
    display: flex;
    justify-content: space-between;

    color: var(--muted);

    font-size: 8px;
    font-weight: 700;

    letter-spacing: 1px;
}

.bar-label b,
.resource-head strong {
    color: #d8d9e7;

    font-size: 9px;
}

.xp-bar,
.resource-bar,
.enemy-hp {
    height: 6px;

    margin-top: 7px;

    overflow: hidden;

    background: #25283a;
}

.xp-bar div {
    width: 72%;
    height: 100%;

    background:
        linear-gradient(
            90deg,
            #7245ff,
            #b992ff
        );

    box-shadow: 0 0 14px rgba(140,92,255,.55);
}


/* ==============================
   STATS
============================== */

.stats {
    padding: 20px;
}

.stats h3 {
    margin: 0 0 14px;

    color: #777e97;

    font-size: 9px;
    letter-spacing: 2px;
}

.stat-row {
    display: flex;
    justify-content: space-between;

    padding: 10px 0;

    border-bottom: 1px solid rgba(255,255,255,.04);

    font-size: 10px;
}

.stat-row span {
    color: #9ba1b5;
}

.stat-row b {
    color: #eee;
}


/* ==============================
   QUEST
============================== */

.quest {
    margin: 0 15px 15px;

    padding: 15px;

    border: 1px solid rgba(255,255,255,.06);

    background: rgba(255,255,255,.025);
}

.section-title {
    display: flex;
    justify-content: space-between;

    color: #848ba2;

    font-size: 9px;
    font-weight: 800;

    letter-spacing: 1.5px;
}

.section-title b {
    color: var(--purple-light);
}

.quest h4 {
    margin: 15px 0 5px;

    font-size: 11px;
}

.quest p {
    margin: 0 0 12px;

    color: var(--muted);

    font-size: 9px;
    line-height: 1.7;
}

.quest-progress {
    font-size: 8px;
    color: #9ea4b9;
}

.quest-progress > div {
    height: 4px;

    margin-top: 6px;

    background: #242739;
}

.quest-progress i {
    display: block;

    width: 33%;

    height: 100%;

    background: var(--green);
}

.quest-reward {
    display: flex;
    justify-content: space-between;

    margin-top: 12px;

    color: #8e94aa;

    font-size: 8px;
}


/* ==============================
   BATTLE
============================== */

.battle-area {
    min-width: 0;
}

.battle-header {
    display: flex;
    align-items: center;
    justify-content: space-between;

    height: 63px;

    padding: 0 20px;

    background: rgba(13,16,27,.95);

    border: 1px solid var(--border);
    border-bottom: 0;
}

.location-label {
    color: var(--purple-light);

    font-size: 8px;
    font-weight: 800;

    letter-spacing: 2px;
}

.battle-header h2 {
    margin: 4px 0 0;

    font-size: 17px;
    letter-spacing: 1px;
}

.danger {
    display: flex;
    align-items: center;
    gap: 8px;

    color: #ff718d;

    font-size: 8px;
    font-weight: 800;
}

.danger span {
    width: 7px;
    height: 7px;

    border-radius: 50%;

    background: var(--red);

    box-shadow:
        0 0 10px var(--red);

    animation: pulse 1.2s infinite;
}


/* ==============================
   ARENA
============================== */

.arena {
    position: relative;

    height: 450px;

    overflow: hidden;

    border: 1px solid var(--border);

    background:
        radial-gradient(
            circle at 70% 20%,
            rgba(113,86,180,.2),
            transparent 28%
        ),
        linear-gradient(
            180deg,
            #10142a 0%,
            #141a31 48%,
            #090b12 100%
        );
}

.moon {
    position: absolute;

    width: 90px;
    height: 90px;

    right: 15%;
    top: 45px;

    border-radius: 50%;

    background:
        radial-gradient(
            circle at 35% 35%,
            #f0e9ff,
            #aaa4d0 60%,
            #665e93
        );

    box-shadow:
        0 0 70px rgba(191,177,255,.22);
}

.mist {
    position: absolute;

    width: 70%;
    height: 70px;

    left: 5%;

    bottom: 100px;

    background: rgba(100,111,160,.07);

    filter: blur(20px);

    border-radius: 50%;
}

.mist-two {
    left: 35%;
    bottom: 140px;
}

.mountain {
    position: absolute;

    bottom: 75px;

    width: 0;
    height: 0;

    border-left: 180px solid transparent;
    border-right: 180px solid transparent;
    border-bottom: 200px solid rgba(8,10,20,.85);
}

.mountain-one {
    left: 10%;
}

.mountain-two {
    right: 4%;

    transform: scale(.8);
}

.ground {
    position: absolute;

    bottom: 0;
    left: 0;
    right: 0;

    height: 105px;

    background:
        linear-gradient(
            180deg,
            rgba(12,15,22,.15),
            #07090e
        );

    border-top: 1px solid rgba(255,255,255,.04);
}


/* ==============================
   FIGHTERS
============================== */

.fighter {
    position: absolute;

    bottom: 72px;

    width: 150px;

    text-align: center;

    z-index: 4;
}

.fighter.player {
    left: 18%;
}

.fighter.enemy {
    right: 17%;
}

.character-body {
    position: relative;

    font-size: 92px;

    filter:
        drop-shadow(
            0 18px 18px rgba(0,0,0,.65)
        );

    animation:
        idleFloat 2.7s ease-in-out infinite;
}

.enemy .character-body {
    font-size: 82px;

    animation-delay: -.8s;
}

.fighter-shadow {
    position: absolute;

    width: 105px;
    height: 25px;

    left: 22px;
    bottom: 4px;

    border-radius: 50%;

    background: rgba(0,0,0,.55);

    filter: blur(5px);
}

.fighter-name {
    margin-top: 2px;

    font-size: 9px;
    font-weight: 900;

    letter-spacing: 2px;

    color: #d6d8e4;
}

.enemy-health {
    width: 125px;

    margin: 0 auto 8px;
}

.enemy-title {
    display: flex;
    justify-content: space-between;

    margin-bottom: 4px;

    color: #ffb1bd;

    font-size: 7px;
    font-weight: 800;
}

.enemy-hp {
    height: 5px;

    margin: 0;
}

.enemy-hp i {
    display: block;

    width: 100%;
    height: 100%;

    background:
        linear-gradient(
            90deg,
            #f02c50,
            #ff667d
        );

    transition: width .35s ease;
}


/* ==============================
   DAMAGE
============================== */

#damageLayer {
    position: absolute;

    inset: 0;

    pointer-events: none;

    z-index: 20;
}

.damage {
    position: absolute;

    color: white;

    font-size: 26px;
    font-weight: 900;

    text-shadow:
        0 3px 8px #000;

    animation: damageFloat 850ms forwards;
}

.damage.critical {
    color: #ffcf4a;

    font-size: 36px;
}

.damage.magic {
    color: #a878ff;
}


/* ==============================
   LOG
============================== */

.battle-log {
    position: absolute;

    left: 15px;
    bottom: 15px;

    width: 230px;

    padding: 9px 11px;

    background: rgba(5,7,12,.68);

    border-left: 2px solid var(--purple);

    backdrop-filter: blur(10px);

    color: #8f95aa;

    font-size: 8px;

    z-index: 10;
}


/* ==============================
   RESOURCES
============================== */

.resources {
    display: grid;

    grid-template-columns: 1fr 1fr;

    gap: 15px;

    padding: 16px 0;
}

.resource-bar {
    height: 8px;
}

.resource-bar i {
    display: block;

    height: 100%;

    width: 100%;

    transition: width .35s;
}

.hp .resource-bar i {
    background:
        linear-gradient(
            90deg,
            #e63254,
            #ff6680
        );

    box-shadow:
        0 0 15px rgba(255,65,108,.25);
}

.mana .resource-bar i {
    background:
        linear-gradient(
            90deg,
            #3a75ff,
            #6edcff
        );

    box-shadow:
        0 0 15px rgba(53,217,255,.25);
}


/* ==============================
   SKILLS
============================== */

.skill-section {
    border: 1px solid var(--border);

    background: rgba(13,16,27,.95);
}

.skill-header {
    display: flex;
    justify-content: space-between;

    padding: 11px 14px;

    border-bottom: 1px solid var(--border);

    color: #858ca2;

    font-size: 8px;
    font-weight: 800;

    letter-spacing: 1.5px;
}

.skill-header small {
    color: #555c72;

    font-size: 7px;
    font-weight: 500;

    letter-spacing: 0;
}

.skills {
    display: grid;

    grid-template-columns: repeat(5, 1fr);
}

.skill {
    position: relative;

    min-width: 0;

    height: 75px;

    display: flex;
    align-items: center;

    gap: 8px;

    padding: 10px;

    border: 0;
    border-right: 1px solid var(--border);

    background: rgba(255,255,255,.015);

    color: white;

    text-align: left;

    cursor: pointer;

    overflow: hidden;

    transition: .2s;
}

.skill:last-child {
    border-right: 0;
}

.skill:hover {
    background: rgba(255,255,255,.06);
}

.skill:active {
    transform: scale(.97);
}

.skill-key {
    position: absolute;

    top: 5px;
    left: 6px;

    color: #697087;

    font-size: 8px;
}

.skill-icon {
    width: 40px;
    height: 40px;

    flex: 0 0 40px;

    display: grid;
    place-items: center;

    border: 1px solid rgba(255,255,255,.08);

    background: #171a28;

    font-size: 20px;
}

.skill-info {
    min-width: 0;
}

.skill-info strong {
    display: block;

    white-space: nowrap;

    overflow: hidden;

    text-overflow: ellipsis;

    font-size: 8px;
}

.skill-info small {
    display: block;

    margin-top: 4px;

    color: #686f84;

    font-size: 7px;
}

.skill.fire .skill-icon {
    box-shadow: inset 0 -3px 0 #ff623d;
}

.skill.ice .skill-icon {
    box-shadow: inset 0 -3px 0 #3dbdff;
}

.skill.lightning .skill-icon {
    box-shadow: inset 0 -3px 0 #c48aff;
}

.skill.ultimate .skill-icon {
    box-shadow:
        inset 0 -3px 0 #ffcb45,
        0 0 15px rgba(255,203,69,.15);
}

.cooldown {
    position: absolute;

    inset: 0;

    transform-origin: bottom;

    transform: scaleY(0);

    background: rgba(0,0,0,.72);

    transition: transform linear;
}

.skill.disabled {
    pointer-events: none;
    filter: grayscale(.6);
}


/* ==============================
   RIGHT PANEL
============================== */

.right-panel {
    padding: 18px;
}

.panel-title {
    display: flex;
    justify-content: space-between;

    padding-bottom: 13px;

    border-bottom: 1px solid var(--border);

    color: #b9bdd0;

    font-size: 9px;
    font-weight: 800;

    letter-spacing: 1.5px;
}

.panel-title small {
    color: #565d72;

    font-size: 7px;
}

.battle-stats {
    display: grid;

    grid-template-columns: repeat(3, 1fr);

    gap: 6px;

    margin: 15px 0 25px;
}

.battle-stats div {
    padding: 10px 5px;

    background: rgba(255,255,255,.025);

    text-align: center;
}

.battle-stats span {
    display: block;

    color: #626a80;

    font-size: 7px;
}

.battle-stats strong {
    display: block;

    margin-top: 5px;

    color: #eee;

    font-size: 14px;
}

.loot-panel {
    margin-top: 10px;
}

.loot {
    min-height: 150px;

    margin-top: 12px;

    display: grid;
    place-items: center;

    align-content: center;

    border: 1px dashed rgba(255,255,255,.08);
}

.loot div {
    color: #4b5164;

    font-size: 26px;
}

.loot p {
    margin: 8px 0 0;

    color: #565d71;

    font-size: 8px;
}

.loot.has-item {
    display: block;

    padding: 12px;
}

.loot-item {
    display: flex;
    align-items: center;
    gap: 10px;

    padding: 10px;

    background: rgba(140,92,255,.08);

    border: 1px solid rgba(140,92,255,.18);

    color: #c6b4ff;

    font-size: 9px;
}

.controls {
    display: grid;

    gap: 8px;

    margin-top: 25px;
}

.controls button {
    min-height: 42px;

    border: 1px solid var(--border);

    background: rgba(255,255,255,.035);

    color: #c4c8d8;

    cursor: pointer;

    font-size: 9px;
    font-weight: 800;

    transition: .2s;
}

.controls button:hover {
    background: rgba(140,92,255,.12);
    border-color: rgba(140,92,255,.3);
}

.controls button span {
    color: var(--purple-light);

    margin-right: 5px;
}

.controls small {
    display: block;

    margin-top: 3px;

    color: #5e657a;

    font-size: 7px;
}


/* ==============================
   FOOTER
============================== */

footer {
    height: 40px;

    display: flex;
    justify-content: space-between;
    align-items: center;

    padding: 0 32px;

    border-top: 1px solid var(--border);

    color: #4e5568;

    font-size: 7px;
    letter-spacing: 1px;
}

footer b {
    color: var(--purple);
    margin: 0 5px;
}


/* ==============================
   MODAL
============================== */

.modal {
    position: fixed;

    inset: 0;

    z-index: 100;

    display: none;
    place-items: center;

    background: rgba(0,0,0,.7);

    backdrop-filter: blur(10px);
}

.modal.active {
    display: grid;
}

.modal-box {
    position: relative;

    width: min(500px, calc(100% - 30px));

    padding: 25px;

    background: #101321;

    border: 1px solid rgba(140,92,255,.3);

    box-shadow:
        0 30px 100px rgba(0,0,0,.7);

    animation: modalIn .3s ease;
}

.modal-close {
    position: absolute;

    right: 15px;
    top: 12px;

    border: 0;

    background: none;

    color: #72798d;

    font-size: 25px;

    cursor: pointer;
}

.modal-heading {
    padding-bottom: 15px;

    border-bottom: 1px solid var(--border);

    font-weight: 800;
}

.modal-heading small {
    display: block;

    margin-top: 4px;

    color: #666d83;

    font-size: 7px;
}

.inventory-grid {
    display: grid;

    grid-template-columns: repeat(4, 1fr);

    gap: 10px;

    margin-top: 20px;
}

.item {
    padding: 15px 8px;

    border: 1px solid var(--border);

    background: rgba(255,255,255,.025);

    text-align: center;
}

.item span {
    display: block;

    margin-bottom: 10px;

    font-size: 28px;
}

.item b {
    display: block;

    font-size: 8px;
}

.item small {
    display: block;

    margin-top: 4px;

    color: #62697d;

    font-size: 7px;
}

.item.legendary {
    border-color: rgba(255,203,69,.3);
}

.item.legendary span {
    color: #ffca43;
}

.item.rare {
    border-color: rgba(53,217,255,.25);
}


/* ==============================
   ANIMATIONS
============================== */

@keyframes idleFloat {

    0%,
    100% {
        transform: translateY(0);
    }

    50% {
        transform: translateY(-7px);
    }
}

@keyframes pulse {

    0%,
    100% {
        opacity: .45;
    }

    50% {
        opacity: 1;
    }
}

@keyframes damageFloat {

    0% {
        opacity: 0;
        transform: translateY(10px) scale(.7);
    }

    15% {
        opacity: 1;
    }

    100% {
        opacity: 0;
        transform: translateY(-75px) scale(1.15);
    }
}

@keyframes modalIn {

    from {
        opacity: 0;
        transform: translateY(20px) scale(.96);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

@keyframes hit {

    0% {
        transform: translateX(0);
    }

    20% {
        transform: translateX(-12px);
    }

    40% {
        transform: translateX(12px);
    }

    60% {
        transform: translateX(-8px);
    }

    100% {
        transform: translateX(0);
    }
}

.hit {
    animation: hit .35s ease;
}


/* ==============================
   RESPONSIVE
============================== */

@media (max-width: 1200px) {

    .game {
        grid-template-columns: 210px minmax(450px, 1fr);
    }

    .right-panel {
        display: none;
    }

}

@media (max-width: 800px) {

    .topbar {
        padding: 0 15px;
    }

    .top-btn span {
        display: none;
    }

    .game {
        grid-template-columns: 1fr;

        padding: 12px;
    }

    .hero-panel {
        display: grid;

        grid-template-columns: 1fr 1fr;
    }

    .quest {
        grid-column: 1 / -1;
    }

    .arena {
        height: 390px;
    }

    .skills {
        grid-template-columns: repeat(5, 1fr);
    }

    .skill {
        height: 65px;

        justify-content: center;
    }

    .skill-info {
        display: none;
    }

    .skill-icon {
        width: 38px;
        height: 38px;
    }

    .fighter.player {
        left: 8%;
    }

    .fighter.enemy {
        right: 8%;
    }

    footer {
        padding: 0 15px;
    }
}

@media (max-width: 500px) {

    .currency {
        display: none;
    }

    .brand strong {
        font-size: 13px;
    }

    .battle-header h2 {
        font-size: 13px;
    }

    .danger {
        display: none;
    }

    .resources {
        grid-template-columns: 1fr;
    }

    .arena {
        height: 360px;
    }

    .fighter {
        width: 110px;
    }

    .character-body {
        font-size: 68px;
    }

    .enemy .character-body {
        font-size: 62px;
    }

    .battle-log {
        width: 170px;
    }

    .hero-panel {
        display: block;
    }

    .inventory-grid {
        grid-template-columns: repeat(2, 1fr);
    }

}
/* =========================================================
   CHRONICLES OF ELDORIA
   RPG BATTLE ENGINE
   Author: MUATHEMEWPGIARE.COM
========================================================= */

"use strict";


/* =========================================================
   PLAYER
========================================================= */

const player = {

    level: 12,

    hp: 1240,
    maxHp: 1240,

    mana: 620,
    maxMana: 620,

    xp: 720,
    nextXp: 1000,

    gold: 1850,
    crystal: 120,

    power: 142,
    defense: 86,
    crit: 18,

    kills: 1,

    combo: 0,

    totalDamage: 0

};


/* =========================================================
   ENEMY
========================================================= */

const enemy = {

    level: 10,

    hp: 920,
    maxHp: 920,

    defense: 38,

    alive: true

};


/* =========================================================
   SKILLS
========================================================= */

const skills = {

    attack: {

        name: "Đoạn Kiếm",

        damage: [80, 125],

        mana: 0,

        cooldown: 650,

        type: "physical"

    },

    fire: {

        name: "Hỏa Ngục",

        damage: [170, 240],

        mana: 45,

        cooldown: 2200,

        type: "magic"

    },

    ice: {

        name: "Băng Phong",

        damage: [210, 290],

        mana: 60,

        cooldown: 3500,

        type: "magic"

    },

    lightning: {

        name: "Thiên Lôi",

        damage: [290, 390],

        mana: 80,

        cooldown: 5200,

        type: "magic"

    },

    ultimate: {

        name: "Diệt Thế",

        damage: [650, 850],

        mana: 150,

        cooldown: 12000,

        type: "ultimate"

    }

};


const cooldowns = {};


/* =========================================================
   DOM
========================================================= */

const $ = selector => document.querySelector(selector);

const hpFill = $("#hpFill");
const manaFill = $("#manaFill");
const xpFill = $("#xpFill");

const hpText = $("#hpText");
const manaText = $("#manaText");
const xpText = $("#xpText");

const enemyHpFill = $("#enemyHpFill");

const goldElement = $("#gold");
const crystalElement = $("#crystal");

const killsElement = $("#kills");
const comboElement = $("#combo");
const damageElement = $("#damage");

const damageLayer = $("#damageLayer");

const battleLog = $("#battleLog");

const playerElement = $("#player");
const enemyElement = $("#enemy");

const questProgress = $("#questProgress");
const questFill = $("#questFill");

const loot = $("#loot");

const inventoryModal = $("#inventoryModal");


/* =========================================================
   UTILITY
========================================================= */

function random(min, max) {

    return Math.floor(
        Math.random() * (max - min + 1)
    ) + min;

}


function clamp(value, min, max) {

    return Math.max(
        min,
        Math.min(max, value)
    );

}


function formatNumber(value) {

    return value.toLocaleString("en-US");

}


/* =========================================================
   UI UPDATE
========================================================= */

function updateUI() {

    const hpPercent =
        (player.hp / player.maxHp) * 100;

    const manaPercent =
        (player.mana / player.maxMana) * 100;

    const xpPercent =
        (player.xp / player.nextXp) * 100;

    const enemyPercent =
        (enemy.hp / enemy.maxHp) * 100;


    hpFill.style.width =
        `${clamp(hpPercent, 0, 100)}%`;

    manaFill.style.width =
        `${clamp(manaPercent, 0, 100)}%`;

    xpFill.style.width =
        `${clamp(xpPercent, 0, 100)}%`;

    enemyHpFill.style.width =
        `${clamp(enemyPercent, 0, 100)}%`;


    hpText.textContent =
        `${formatNumber(player.hp)} / ${formatNumber(player.maxHp)}`;

    manaText.textContent =
        `${formatNumber(player.mana)} / ${formatNumber(player.maxMana)}`;

    xpText.textContent =
        `${formatNumber(player.xp)} / ${formatNumber(player.nextXp)}`;


    goldElement.textContent =
        formatNumber(player.gold);

    crystalElement.textContent =
        formatNumber(player.crystal);


    killsElement.textContent =
        player.kills;

    comboElement.textContent =
        player.combo;

    damageElement.textContent =
        formatNumber(player.totalDamage);


    questProgress.textContent =
        `${Math.min(player.kills, 3)} / 3`;

    questFill.style.width =
        `${Math.min(player.kills / 3 * 100, 100)}%`;


    $("#level").textContent =
        player.level;

}


/* =========================================================
   BATTLE LOG
========================================================= */

function log(message) {

    battleLog.innerHTML =
        `<div>${message}</div>`;

}


/* =========================================================
   DAMAGE NUMBER
========================================================= */

function showDamage(

    amount,

    critical = false,

    magic = false

) {

    const number =
        document.createElement("div");

    number.className =
        "damage";

    if (critical) {

        number.classList.add("critical");

    }

    if (magic) {

        number.classList.add("magic");

    }

    number.textContent =
        critical
            ? `CRIT! ${amount}`
            : `-${amount}`;


    const enemyRect =
        enemyElement.getBoundingClientRect();

    const arenaRect =
        $(".arena").getBoundingClientRect();


    number.style.left =
        `${enemyRect.left - arenaRect.left + 50 + random(-20,20)}px`;

    number.style.top =
        `${enemyRect.top - arenaRect.top + 30}px`;


    damageLayer.appendChild(number);


    setTimeout(() => {

        number.remove();

    }, 900);

}


/* =========================================================
   PLAYER HIT EFFECT
========================================================= */

function hitEffect(element) {

    element.classList.remove("hit");

    void element.offsetWidth;

    element.classList.add("hit");

}


/* =========================================================
   SKILL COOLDOWN
========================================================= */

function startCooldown(skillName, button, duration) {

    cooldowns[skillName] = true;

    button.classList.add("disabled");

    const overlay =
        button.querySelector(".cooldown");

    overlay.style.transition =
        `transform ${duration}ms linear`;

    overlay.style.transform =
        "scaleY(1)";


    setTimeout(() => {

        overlay.style.transform =
            "scaleY(0)";

    }, 30);


    setTimeout(() => {

        cooldowns[skillName] = false;

        button.classList.remove("disabled");

    }, duration);

}


/* =========================================================
   DAMAGE CALCULATION
========================================================= */

function calculateDamage(skill) {

    let damage =
        random(
            skill.damage[0],
            skill.damage[1]
        );


    if (skill.type === "physical") {

        damage +=
            Math.floor(
                player.power * .35
            );

        damage -=
            enemy.defense;

    } else {

        damage +=
            Math.floor(
                player.power * .65
            );

    }


    damage =
        Math.max(1, damage);


    let critical = false;


    if (
        Math.random() * 100
        < player.crit
    ) {

        critical = true;

        damage =
            Math.floor(
                damage * 1.85
            );

    }


    if (player.combo >= 3) {

        damage =
            Math.floor(
                damage * 1.08
            );

    }


    return {

        damage,

        critical

    };

}


/* =========================================================
   CAST SKILL
========================================================= */

function castSkill(skillName, button) {

    const skill =
        skills[skillName];


    if (!skill) {

        return;

    }


    if (cooldowns[skillName]) {

        log("⏳ Kỹ năng đang hồi chiêu.");

        return;

    }


    if (!enemy.alive) {

        log("⚠ Kẻ địch đã bị đánh bại.");

        return;

    }


    if (player.mana < skill.mana) {

        log("💧 Không đủ Mana.");

        return;

    }


    player.mana -= skill.mana;


    const result =
        calculateDamage(skill);


    enemy.hp =
        Math.max(
            0,
            enemy.hp - result.damage
        );


    player.totalDamage +=
        result.damage;


    player.combo++;


    if (player.combo > 10) {

        player.combo = 10;

    }


    showDamage(
        result.damage,
        result.critical,
        skill.type !== "physical"
    );


    hitEffect(enemyElement);


    let prefix = "⚔";


    if (skill.type === "magic") {

        prefix = "✨";

    }

    if (skill.type === "ultimate") {

        prefix = "☄";

    }


    log(
        `${prefix} ${skill.name} gây <b>${formatNumber(result.damage)}</b> sát thương.`
    );


    if (result.critical) {

        log(
            `🔥 BẠO KÍCH! Sát thương tăng mạnh.`
        );

    }


    startCooldown(
        skillName,
        button,
        skill.cooldown
    );


    updateUI();


    if (enemy.hp <= 0) {

        enemyDefeated();

    } else {

        setTimeout(enemyAttack, 650);

    }

}


/* =========================================================
   ENEMY ATTACK
========================================================= */

function enemyAttack() {

    if (!enemy.alive) {

        return;

    }


    const damage =
        Math.max(
            12,
            random(35, 70)
            - Math.floor(player.defense * .25)
        );


    player.hp =
        Math.max(
            0,
            player.hp - damage
        );


    hitEffect(playerElement);


    log(
        `🐺 Hắc Lang phản công gây <b>${damage}</b> sát thương.`
    );


    showPlayerDamage(damage);


    if (player.hp <= 0) {

        playerDefeated();

    }


    updateUI();

}


/* =========================================================
   PLAYER DAMAGE NUMBER
========================================================= */

function showPlayerDamage(amount) {

    const number =
        document.createElement("div");

    number.className =
        "damage";

    number.style.color =
        "#ff6b82";

    number.textContent =
        `-${amount}`;


    const playerRect =
        playerElement.getBoundingClientRect();

    const arenaRect =
        $(".arena").getBoundingClientRect();


    number.style.left =
        `${playerRect.left - arenaRect.left + 40}px`;

    number.style.top =
        `${playerRect.top - arenaRect.top + 30}px`;


    damageLayer.appendChild(number);


    setTimeout(() => {

        number.remove();

    }, 900);

}


/* =========================================================
   ENEMY DEFEATED
========================================================= */

function enemyDefeated() {

    enemy.alive = false;

    enemyElement.style.opacity = "0";

    enemyElement.style.transform =
        "scale(.65) translateY(40px)";

    player.kills++;

    const xpReward =
        random(180, 260);

    player.xp += xpReward;

    player.gold +=
        random(90, 160);


    log(
        `🏆 Hạ gục Hắc Lang! +${xpReward} EXP`
    );


    checkLevelUp();


    dropLoot();


    updateUI();

}


/* =========================================================
   LEVEL UP
========================================================= */

function checkLevelUp() {

    while (
        player.xp >= player.nextXp
    ) {

        player.xp -=
            player.nextXp;

        player.level++;

        player.nextXp =
            Math.floor(
                player.nextXp * 1.25
            );


        player.maxHp += 130;

        player.maxMana += 65;

        player.hp =
            player.maxHp;

        player.mana =
            player.maxMana;

        player.power += 16;

        player.defense += 8;

        player.crit += 1;


        log(
            `✨ LEVEL UP! Bạn đã đạt cấp ${player.level}.`
        );

    }

}


/* =========================================================
   LOOT
========================================================= */

function dropLoot() {

    const rewards = [

        {
            icon: "💎",
            name: "Tinh Thạch Bóng Đêm"
        },

        {
            icon: "🧪",
            name: "Bình hồi phục lớn"
        },

        {
            icon: "⚔",
            name: "Mảnh Kiếm Hoàng Kim"
        },

        {
            icon: "🔮",
            name: "Đá Mana Cổ Đại"
        }

    ];


    const item =
        rewards[
            random(
                0,
                rewards.length - 1
            )
        ];


    loot.classList.remove("empty");

    loot.classList.add("has-item");


    loot.innerHTML = `

        <div class="loot-item">

            <span style="font-size:22px">
                ${item.icon}
            </span>

            <div>
                <strong>
                    ${item.name}
                </strong>

                <small style="
                    display:block;
                    margin-top:4px;
                    color:#62697d;
                    font-size:7px;
                ">
                    Đã nhận
                </small>
            </div>

        </div>

    `;

}


/* =========================================================
   NEW ENEMY
========================================================= */

function spawnEnemy() {

    enemy.level =
        random(
            player.level - 2,
            player.level + 1
        );


    enemy.maxHp =
        600 +
        enemy.level * 55;


    enemy.hp =
        enemy.maxHp;


    enemy.defense =
        25 +
        enemy.level * 2;


    enemy.alive = true;


    $("#enemyLevel").textContent =
        `LV ${enemy.level}`;


    enemyElement.style.opacity = "1";

    enemyElement.style.transform =
        "";


    player.combo = 0;


    log(
        `🐺 Một Hắc Lang cấp ${enemy.level} xuất hiện!`
    );


    updateUI();

}


/* =========================================================
   PLAYER DEFEATED
========================================================= */

function playerDefeated() {

    log(
        "☠ Bạn đã gục ngã. Đang hồi sinh..."
    );


    setTimeout(() => {

        player.hp =
            Math.floor(
                player.maxHp * .45
            );

        player.mana =
            Math.floor(
                player.maxMana * .45
            );

        player.combo = 0;

        log(
            "✨ Arthas đã hồi sinh."
        );

        updateUI();

    }, 1600);

}


/* =========================================================
   HEAL
========================================================= */

$("#healBtn").addEventListener(
    "click",
    () => {

        if (
            player.hp >=
            player.maxHp
        ) {

            log(
                "❤️ HP đang đầy."
            );

            return;

        }


        const heal =
            Math.min(
                100,
                player.maxHp - player.hp
            );


        player.hp += heal;


        log(
            `❤️ Hồi phục ${heal} HP.`
        );


        updateUI();

    }
);


/* =========================================================
   NEW ENEMY BUTTON
========================================================= */

$("#newEnemyBtn").addEventListener(
    "click",
    spawnEnemy
);


/* =========================================================
   SKILL EVENTS
========================================================= */

document
    .querySelectorAll(".skill")
    .forEach(button => {

        button.addEventListener(
            "click",
            () => {

                castSkill(
                    button.dataset.skill,
                    button
                );

            }
        );

    });


/* =========================================================
   KEYBOARD
========================================================= */

document.addEventListener(
    "keydown",
    event => {

        const keys = {

            "1": "attack",
            "2": "fire",
            "3": "ice",
            "4": "lightning",
            "5": "ultimate"

        };


        const skillName =
            keys[event.key];


        if (!skillName) {

            return;

        }


        const button =
            document.querySelector(
                `[data-skill="${skillName}"]`
            );


        if (button) {

            castSkill(
                skillName,
                button
            );

        }

    }
);


/* =========================================================
   INVENTORY
========================================================= */

$("#inventoryBtn").addEventListener(
    "click",
    () => {

        inventoryModal.classList.add(
            "active"
        );

    }
);


$("#closeInventory").addEventListener(
    "click",
    () => {

        inventoryModal.classList.remove(
            "active"
        );

    }
);


inventoryModal.addEventListener(
    "click",
    event => {

        if (
            event.target ===
            inventoryModal
        ) {

            inventoryModal.classList.remove(
                "active"
            );

        }

    }
);


/* =========================================================
   INITIALIZE
========================================================= */

updateUI();

log(
    "⚔ Một Hắc Lang xuất hiện..."
);
Live Preview
Ctrl1 HTML Ctrl2 CSS Ctrl3 JS Ctrl↵ Reload
Chia sẻ Demo này · Gửi cho bạn bè hoặc đồng nghiệp