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
<!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..."
);
Ctrl1 HTML
Ctrl2 CSS
Ctrl3 JS
Ctrl↵ Reload