1 / static /index.html
droplyvictor89's picture
Upload index.html
3aaa34c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DARKRIFT — Multiplayer Dungeon RPG</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323:wght@400&display=swap');
:root {
--bg: #0a0a0f;
--bg2: #0f0f1a;
--bg3: #141428;
--panel: #0d0d1f;
--border: #2a2a4a;
--border2: #3a3a6a;
--accent: #7c3aed;
--accent2: #a855f7;
--gold: #f59e0b;
--gold2: #fcd34d;
--red: #ef4444;
--green: #22c55e;
--blue: #3b82f6;
--cyan: #06b6d4;
--orange: #f97316;
--text: #e2e8f0;
--text2: #94a3b8;
--text3: #64748b;
--warrior: #ef4444;
--mage: #8b5cf6;
--archer: #22c55e;
--healer: #f59e0b;
--rogue: #06b6d4;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'VT323', monospace;
font-size: 18px;
min-height: 100vh;
overflow-x: hidden;
}
/* Scanline overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.05) 2px, rgba(0,0,0,0.05) 4px);
pointer-events: none;
z-index: 9999;
}
canvas { display: none; }
/* SCREENS */
.screen { display: none; min-height: 100vh; }
.screen.active { display: flex; flex-direction: column; }
/* ===== TITLE SCREEN ===== */
#title-screen {
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0a0f 70%);
position: relative;
overflow: hidden;
}
#title-screen::before {
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(1px 1px at 20% 30%, rgba(124,58,237,0.4) 0%, transparent 100%),
radial-gradient(1px 1px at 80% 70%, rgba(168,85,247,0.3) 0%, transparent 100%),
radial-gradient(1px 1px at 50% 50%, rgba(245,158,11,0.2) 0%, transparent 100%);
animation: starfield 8s ease-in-out infinite alternate;
}
@keyframes starfield {
0% { opacity: 0.5; transform: scale(1); }
100% { opacity: 1; transform: scale(1.05); }
}
.title-content {
position: relative;
text-align: center;
z-index: 1;
}
.title-logo {
font-family: 'Press Start 2P', monospace;
font-size: clamp(32px, 6vw, 64px);
color: var(--gold);
text-shadow:
0 0 20px rgba(245,158,11,0.8),
0 0 40px rgba(245,158,11,0.4),
4px 4px 0 #92400e;
animation: titlePulse 2s ease-in-out infinite;
letter-spacing: 4px;
}
@keyframes titlePulse {
0%, 100% { text-shadow: 0 0 20px rgba(245,158,11,0.8), 0 0 40px rgba(245,158,11,0.4), 4px 4px 0 #92400e; }
50% { text-shadow: 0 0 30px rgba(245,158,11,1), 0 0 60px rgba(245,158,11,0.6), 4px 4px 0 #92400e; }
}
.title-sub {
font-family: 'VT323', monospace;
font-size: 22px;
color: var(--accent2);
margin-top: 8px;
letter-spacing: 6px;
text-transform: uppercase;
}
.title-art {
font-size: 80px;
margin: 30px 0;
filter: drop-shadow(0 0 20px rgba(124,58,237,0.6));
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.title-buttons {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
margin-top: 30px;
}
.btn {
font-family: 'Press Start 2P', monospace;
font-size: 12px;
padding: 14px 32px;
border: 2px solid var(--border2);
background: var(--panel);
color: var(--text);
cursor: pointer;
position: relative;
transition: all 0.15s;
min-width: 220px;
text-align: center;
clip-path: polygon(8px 0%, 100% 0%, calc(100% - 8px) 100%, 0% 100%);
}
.btn:hover {
background: var(--accent);
border-color: var(--accent2);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(124,58,237,0.5);
}
.btn-gold {
border-color: var(--gold);
color: var(--gold);
}
.btn-gold:hover {
background: var(--gold);
color: #000;
box-shadow: 0 4px 20px rgba(245,158,11,0.5);
}
.btn-danger {
border-color: var(--red);
color: var(--red);
}
.btn-danger:hover {
background: var(--red);
color: white;
box-shadow: 0 4px 20px rgba(239,68,68,0.5);
}
.btn-green {
border-color: var(--green);
color: var(--green);
}
.btn-green:hover {
background: var(--green);
color: #000;
box-shadow: 0 4px 20px rgba(34,197,94,0.5);
}
.version-tag {
position: absolute;
bottom: 20px;
right: 20px;
font-size: 12px;
color: var(--text3);
}
/* ===== CLASS SELECT ===== */
#class-screen {
padding: 30px 20px;
background: radial-gradient(ellipse at top, #1a0a2e 0%, #0a0a0f 60%);
}
.screen-title {
font-family: 'Press Start 2P', monospace;
font-size: clamp(14px, 3vw, 22px);
color: var(--gold);
text-align: center;
margin-bottom: 8px;
text-shadow: 0 0 15px rgba(245,158,11,0.5);
}
.screen-subtitle {
text-align: center;
color: var(--text2);
margin-bottom: 30px;
font-size: 20px;
}
.classes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
max-width: 1000px;
margin: 0 auto 30px;
}
.class-card {
background: var(--panel);
border: 2px solid var(--border);
padding: 20px 16px;
cursor: pointer;
transition: all 0.2s;
position: relative;
text-align: center;
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
}
.class-card:hover, .class-card.selected {
transform: translateY(-4px);
}
.class-card[data-class="warrior"]:hover, .class-card[data-class="warrior"].selected {
border-color: var(--warrior);
box-shadow: 0 0 20px rgba(239,68,68,0.3);
background: rgba(239,68,68,0.05);
}
.class-card[data-class="mage"]:hover, .class-card[data-class="mage"].selected {
border-color: var(--mage);
box-shadow: 0 0 20px rgba(139,92,246,0.3);
background: rgba(139,92,246,0.05);
}
.class-card[data-class="archer"]:hover, .class-card[data-class="archer"].selected {
border-color: var(--archer);
box-shadow: 0 0 20px rgba(34,197,94,0.3);
background: rgba(34,197,94,0.05);
}
.class-card[data-class="healer"]:hover, .class-card[data-class="healer"].selected {
border-color: var(--healer);
box-shadow: 0 0 20px rgba(245,158,11,0.3);
background: rgba(245,158,11,0.05);
}
.class-card[data-class="rogue"]:hover, .class-card[data-class="rogue"].selected {
border-color: var(--rogue);
box-shadow: 0 0 20px rgba(6,182,212,0.3);
background: rgba(6,182,212,0.05);
}
.class-icon { font-size: 48px; margin-bottom: 10px; display: block; }
.class-name {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
margin-bottom: 10px;
}
.class-card[data-class="warrior"] .class-name { color: var(--warrior); }
.class-card[data-class="mage"] .class-name { color: var(--mage); }
.class-card[data-class="archer"] .class-name { color: var(--archer); }
.class-card[data-class="healer"] .class-name { color: var(--healer); }
.class-card[data-class="rogue"] .class-name { color: var(--rogue); }
.class-desc { font-size: 14px; color: var(--text2); line-height: 1.4; margin-bottom: 12px; }
.stat-bars { display: flex; flex-direction: column; gap: 4px; }
.stat-row { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.stat-label { width: 40px; color: var(--text3); font-size: 12px; }
.stat-bar-bg { flex: 1; height: 6px; background: var(--bg3); border-radius: 0; }
.stat-bar-fill { height: 100%; transition: width 0.3s; }
.name-input-area {
text-align: center;
margin-bottom: 20px;
}
.pixel-input {
font-family: 'Press Start 2P', monospace;
font-size: 11px;
background: var(--panel);
border: 2px solid var(--border2);
color: var(--text);
padding: 12px 16px;
width: 280px;
outline: none;
clip-path: polygon(6px 0%, 100% 0%, calc(100% - 6px) 100%, 0% 100%);
}
.pixel-input:focus { border-color: var(--accent2); box-shadow: 0 0 10px rgba(168,85,247,0.3); }
.input-label {
display: block;
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: var(--text3);
margin-bottom: 8px;
}
/* ===== LOBBY SCREEN ===== */
#lobby-screen {
padding: 30px 20px;
background: radial-gradient(ellipse at center, #0f0a1e 0%, #0a0a0f 70%);
}
.lobby-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
max-width: 900px;
margin: 0 auto;
}
@media (max-width: 600px) { .lobby-layout { grid-template-columns: 1fr; } }
.panel {
background: var(--panel);
border: 1px solid var(--border);
padding: 20px;
clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px));
}
.panel-title {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
color: var(--accent2);
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
letter-spacing: 2px;
}
.room-code-display {
font-family: 'Press Start 2P', monospace;
font-size: 32px;
color: var(--gold);
text-align: center;
letter-spacing: 8px;
padding: 20px;
background: var(--bg3);
border: 1px solid var(--gold);
margin-bottom: 12px;
text-shadow: 0 0 10px rgba(245,158,11,0.5);
animation: codePulse 2s ease-in-out infinite;
}
@keyframes codePulse {
0%, 100% { box-shadow: 0 0 5px rgba(245,158,11,0.2); }
50% { box-shadow: 0 0 15px rgba(245,158,11,0.4); }
}
.player-slot {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
margin-bottom: 8px;
background: var(--bg3);
border: 1px solid var(--border);
min-height: 54px;
position: relative;
}
.player-slot.empty {
opacity: 0.4;
border-style: dashed;
}
.slot-icon { font-size: 28px; }
.slot-info { flex: 1; }
.slot-name { font-family: 'Press Start 2P', monospace; font-size: 9px; color: var(--text); }
.slot-class { font-size: 13px; color: var(--text3); margin-top: 3px; }
.slot-you {
font-family: 'Press Start 2P', monospace; font-size: 7px;
color: var(--gold); background: rgba(245,158,11,0.1);
border: 1px solid var(--gold); padding: 2px 6px;
}
.slot-ready {
font-size: 12px;
color: var(--green);
}
.slot-host-badge {
font-family: 'Press Start 2P', monospace; font-size: 7px;
color: var(--accent2); background: rgba(168,85,247,0.1);
border: 1px solid var(--accent2); padding: 2px 6px;
margin-left: 4px;
}
.join-area { display: flex; flex-direction: column; gap: 10px; }
.join-row { display: flex; gap: 8px; }
.difficulty-selector {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.diff-btn {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
padding: 10px 6px;
border: 2px solid var(--border);
background: var(--bg3);
color: var(--text2);
cursor: pointer;
text-align: center;
transition: all 0.15s;
}
.diff-btn:hover, .diff-btn.active {
border-color: var(--accent2);
color: var(--accent2);
background: rgba(168,85,247,0.1);
}
.diff-btn.easy.active { border-color: var(--green); color: var(--green); background: rgba(34,197,94,0.1); }
.diff-btn.hard.active { border-color: var(--red); color: var(--red); background: rgba(239,68,68,0.1); }
/* ===== GAME SCREEN ===== */
#game-screen {
padding: 0;
background: #050508;
min-height: 100vh;
}
.game-layout {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
/* TOP HUD */
.game-hud-top {
background: linear-gradient(180deg, #0a0a18 0%, transparent 100%);
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
z-index: 10;
}
.hud-wave {
font-family: 'Press Start 2P', monospace;
font-size: 10px;
color: var(--gold);
}
.hud-title {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: var(--accent2);
letter-spacing: 3px;
}
.hud-turn-indicator {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
padding: 4px 10px;
border: 1px solid var(--accent2);
color: var(--accent2);
animation: turnBlink 1s ease-in-out infinite;
}
@keyframes turnBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.hud-turn-indicator.your-turn {
border-color: var(--green);
color: var(--green);
animation: none;
box-shadow: 0 0 10px rgba(34,197,94,0.3);
}
/* MAIN GAME AREA */
.game-main {
display: grid;
grid-template-columns: 200px 1fr 200px;
overflow: hidden;
}
@media (max-width: 768px) {
.game-main { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
}
/* PLAYER PANEL */
.players-panel {
background: rgba(10,10,20,0.9);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 10px 8px;
}
.panel-header {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
color: var(--text3);
letter-spacing: 2px;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border);
text-align: center;
}
.player-card {
background: var(--bg3);
border: 1px solid var(--border);
padding: 10px 8px;
margin-bottom: 8px;
position: relative;
transition: all 0.2s;
}
.player-card.active-turn {
border-color: var(--green);
box-shadow: 0 0 10px rgba(34,197,94,0.2);
}
.player-card.dead {
opacity: 0.4;
filter: grayscale(1);
}
.pc-header { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.pc-icon { font-size: 22px; }
.pc-info { flex: 1; min-width: 0; }
.pc-name {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pc-class { font-size: 12px; color: var(--text3); }
.hp-bar-container { margin-bottom: 4px; }
.hp-label { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 2px; }
.hp-bar-bg { height: 6px; background: #1a0a0a; border: 1px solid #3a1a1a; }
.hp-bar-fill { height: 100%; background: linear-gradient(90deg, var(--red), #ff6b6b); transition: width 0.5s; }
.hp-bar-fill.high { background: linear-gradient(90deg, var(--green), #86efac); }
.hp-bar-fill.mid { background: linear-gradient(90deg, var(--gold), #fcd34d); }
.mp-bar-bg { height: 4px; background: #0a0a1a; border: 1px solid #1a1a3a; }
.mp-bar-fill { height: 100%; background: linear-gradient(90deg, var(--blue), #93c5fd); transition: width 0.5s; }
.status-icons { display: flex; gap: 3px; margin-top: 4px; flex-wrap: wrap; }
.status-icon { font-size: 12px; cursor: help; }
/* BATTLE AREA */
.battle-area {
position: relative;
overflow: hidden;
background: #050508;
}
#battle-canvas {
display: block !important;
width: 100%;
height: 100%;
}
.battle-overlay {
position: absolute;
inset: 0;
pointer-events: none;
}
.damage-float {
position: absolute;
font-family: 'Press Start 2P', monospace;
font-size: 14px;
font-weight: bold;
pointer-events: none;
animation: floatDmg 1.2s ease-out forwards;
z-index: 100;
}
@keyframes floatDmg {
0% { opacity: 1; transform: translateY(0) scale(1); }
50% { opacity: 1; transform: translateY(-30px) scale(1.2); }
100% { opacity: 0; transform: translateY(-60px) scale(0.8); }
}
/* ENEMIES PANEL */
.enemies-panel {
background: rgba(10,10,20,0.9);
border-left: 1px solid var(--border);
overflow-y: auto;
padding: 10px 8px;
}
.enemy-card {
background: linear-gradient(135deg, #1a0a0a, #0f0808);
border: 1px solid #3a1a1a;
padding: 10px 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.enemy-card:hover {
border-color: var(--red);
box-shadow: 0 0 10px rgba(239,68,68,0.2);
}
.enemy-card.targeted {
border-color: var(--gold);
box-shadow: 0 0 12px rgba(245,158,11,0.3);
animation: targetPulse 0.8s ease-in-out infinite;
}
@keyframes targetPulse {
0%, 100% { box-shadow: 0 0 8px rgba(245,158,11,0.3); }
50% { box-shadow: 0 0 20px rgba(245,158,11,0.6); }
}
.enemy-card.dead { opacity: 0.3; pointer-events: none; }
.ec-header { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
.ec-icon { font-size: 26px; }
.ec-info { flex: 1; }
.ec-name { font-family: 'Press Start 2P', monospace; font-size: 7px; color: var(--red); }
.ec-type { font-size: 11px; color: var(--text3); }
.target-hint {
font-size: 11px;
color: var(--text3);
text-align: center;
margin-top: 8px;
padding: 4px;
border: 1px dashed var(--border);
}
/* BOTTOM ACTIONS */
.game-actions {
background: linear-gradient(0deg, #0a0a18 0%, transparent 100%);
border-top: 1px solid var(--border);
padding: 10px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.abilities-row {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.ability-btn {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
padding: 10px 14px;
border: 2px solid var(--border2);
background: var(--panel);
color: var(--text);
cursor: pointer;
transition: all 0.15s;
position: relative;
min-width: 90px;
text-align: center;
clip-path: polygon(6px 0%, 100% 0%, calc(100% - 6px) 100%, 0% 100%);
}
.ability-btn:hover:not(:disabled) {
transform: translateY(-2px);
background: var(--accent);
border-color: var(--accent2);
box-shadow: 0 4px 15px rgba(124,58,237,0.4);
}
.ability-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ability-btn .mana-cost {
display: block;
font-size: 7px;
color: var(--blue);
margin-top: 4px;
}
.ability-btn .cooldown-badge {
position: absolute;
top: -6px;
right: -6px;
background: var(--red);
color: white;
font-size: 7px;
padding: 2px 4px;
font-family: 'Press Start 2P', monospace;
}
.actions-row {
display: flex;
gap: 8px;
justify-content: center;
}
.action-btn {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
padding: 8px 16px;
border: 2px solid var(--border);
background: var(--bg3);
color: var(--text2);
cursor: pointer;
transition: all 0.15s;
clip-path: polygon(6px 0%, 100% 0%, calc(100% - 6px) 100%, 0% 100%);
}
.action-btn:hover:not(:disabled) { border-color: var(--text2); color: var(--text); background: var(--panel); }
.action-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.action-btn.end-turn { border-color: var(--green); color: var(--green); }
.action-btn.end-turn:hover:not(:disabled) { background: rgba(34,197,94,0.15); box-shadow: 0 0 10px rgba(34,197,94,0.3); }
/* COMBAT LOG */
.combat-log-area {
max-width: 500px;
margin: 0 auto;
}
.combat-log {
background: rgba(0,0,0,0.6);
border: 1px solid var(--border);
height: 70px;
overflow-y: auto;
padding: 6px 10px;
font-size: 13px;
}
.log-entry { padding: 1px 0; }
.log-entry.damage { color: #fca5a5; }
.log-entry.heal { color: #86efac; }
.log-entry.skill { color: #c4b5fd; }
.log-entry.system { color: var(--text3); }
.log-entry.death { color: var(--red); font-family: 'Press Start 2P', monospace; font-size: 10px; }
.log-entry.boss { color: var(--gold); }
/* WAVE ANNOUNCEMENT */
.wave-announcement {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.85);
z-index: 1000;
animation: waveAnnounce 2.5s ease forwards;
}
@keyframes waveAnnounce {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; pointer-events: none; }
}
.wave-text {
text-align: center;
}
.wave-number {
font-family: 'Press Start 2P', monospace;
font-size: clamp(24px, 5vw, 48px);
color: var(--red);
text-shadow: 0 0 30px rgba(239,68,68,0.8);
display: block;
margin-bottom: 10px;
}
.wave-subtitle {
font-family: 'Press Start 2P', monospace;
font-size: clamp(12px, 2.5vw, 20px);
color: var(--gold);
display: block;
}
/* GAME OVER / VICTORY */
.result-screen {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.9);
z-index: 2000;
display: none;
}
.result-screen.show { display: flex; }
.result-content {
text-align: center;
padding: 40px;
background: var(--panel);
border: 2px solid var(--border2);
clip-path: polygon(16px 0%, 100% 0%, calc(100% - 16px) 100%, 0% 100%);
max-width: 500px;
width: 90%;
}
.result-title {
font-family: 'Press Start 2P', monospace;
font-size: clamp(24px, 5vw, 40px);
margin-bottom: 16px;
}
.result-title.victory { color: var(--gold); text-shadow: 0 0 20px rgba(245,158,11,0.6); }
.result-title.defeat { color: var(--red); text-shadow: 0 0 20px rgba(239,68,68,0.6); }
.result-stats {
margin: 20px 0;
font-size: 16px;
color: var(--text2);
line-height: 2;
}
/* NOTIFICATION */
.notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-family: 'Press Start 2P', monospace;
font-size: 10px;
padding: 12px 24px;
background: var(--panel);
border: 2px solid var(--accent2);
color: var(--accent2);
z-index: 3000;
animation: notifSlide 3s ease forwards;
max-width: 90vw;
text-align: center;
}
@keyframes notifSlide {
0% { opacity: 0; transform: translateX(-50%) translateY(-20px); }
15% { opacity: 1; transform: translateX(-50%) translateY(0); }
85% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}
/* WAITING SPINNER */
.waiting-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
display: flex; align-items: center; justify-content: center;
z-index: 500; display: none;
}
.waiting-overlay.show { display: flex; }
.waiting-box {
text-align: center;
font-family: 'Press Start 2P', monospace;
font-size: 12px;
color: var(--accent2);
}
.spinner {
font-size: 40px;
display: block;
margin-bottom: 16px;
animation: spin 1s linear infinite;
}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* SCROLLBAR */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border2); }
/* ROOM CODE COPY */
.copy-hint { font-size: 12px; color: var(--text3); text-align: center; cursor: pointer; }
.copy-hint:hover { color: var(--gold); }
/* Pixel decorations */
.pixel-deco {
position: absolute;
font-size: 12px;
opacity: 0.15;
pointer-events: none;
}
.flicker { animation: flicker 3s ease-in-out infinite; }
@keyframes flicker {
0%, 90%, 100% { opacity: 0.15; }
92%, 96% { opacity: 0.05; }
94%, 98% { opacity: 0.2; }
}
</style>
</head>
<body>
<!-- TITLE SCREEN -->
<div id="title-screen" class="screen active">
<div class="title-content">
<div class="title-art">⚔️</div>
<div class="title-logo">DARKRIFT</div>
<div class="title-sub">Dungeon Challenges — Multiplayer RPG</div>
<div class="title-buttons" style="margin-top:40px">
<button class="btn btn-gold" onclick="goToClassSelect('create')">⚔ CREATE ROOM</button>
<button class="btn" onclick="goToClassSelect('join')">🗡 JOIN ROOM</button>
</div>
</div>
<div class="version-tag">v1.0 · PIXEL ART EDITION</div>
</div>
<!-- CLASS SELECT SCREEN -->
<div id="class-screen" class="screen">
<div class="screen-title">CHOOSE YOUR CLASS</div>
<div class="screen-subtitle">Select wisely — your team needs balance</div>
<div class="classes-grid" id="classes-grid">
<div class="class-card" data-class="warrior" onclick="selectClass('warrior',this)">
<span class="class-icon">🛡️</span>
<div class="class-name">WARRIOR</div>
<div class="class-desc">Absorbs damage. Taunts enemies. Protects allies.</div>
<div class="stat-bars">
<div class="stat-row"><span class="stat-label">HP</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:90%;background:var(--warrior)"></div></div></div>
<div class="stat-row"><span class="stat-label">ATK</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:60%;background:var(--warrior)"></div></div></div>
<div class="stat-row"><span class="stat-label">DEF</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:95%;background:var(--warrior)"></div></div></div>
<div class="stat-row"><span class="stat-label">SPD</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:35%;background:var(--warrior)"></div></div></div>
</div>
</div>
<div class="class-card" data-class="mage" onclick="selectClass('mage',this)">
<span class="class-icon">🔮</span>
<div class="class-name">MAGE</div>
<div class="class-desc">Devastating AoE spells. Low HP but massive damage.</div>
<div class="stat-bars">
<div class="stat-row"><span class="stat-label">HP</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:35%;background:var(--mage)"></div></div></div>
<div class="stat-row"><span class="stat-label">ATK</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:95%;background:var(--mage)"></div></div></div>
<div class="stat-row"><span class="stat-label">DEF</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:20%;background:var(--mage)"></div></div></div>
<div class="stat-row"><span class="stat-label">SPD</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:65%;background:var(--mage)"></div></div></div>
</div>
</div>
<div class="class-card" data-class="archer" onclick="selectClass('archer',this)">
<span class="class-icon">🏹</span>
<div class="class-name">ARCHER</div>
<div class="class-desc">Fast attacks. Bleeding DoT. Snipe from safety.</div>
<div class="stat-bars">
<div class="stat-row"><span class="stat-label">HP</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:55%;background:var(--archer)"></div></div></div>
<div class="stat-row"><span class="stat-label">ATK</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:75%;background:var(--archer)"></div></div></div>
<div class="stat-row"><span class="stat-label">DEF</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:40%;background:var(--archer)"></div></div></div>
<div class="stat-row"><span class="stat-label">SPD</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:90%;background:var(--archer)"></div></div></div>
</div>
</div>
<div class="class-card" data-class="healer" onclick="selectClass('healer',this)">
<span class="class-icon"></span>
<div class="class-name">HEALER</div>
<div class="class-desc">Restores HP. Removes debuffs. Resurrects allies.</div>
<div class="stat-bars">
<div class="stat-row"><span class="stat-label">HP</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:60%;background:var(--healer)"></div></div></div>
<div class="stat-row"><span class="stat-label">ATK</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:30%;background:var(--healer)"></div></div></div>
<div class="stat-row"><span class="stat-label">DEF</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:55%;background:var(--healer)"></div></div></div>
<div class="stat-row"><span class="stat-label">SPD</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:70%;background:var(--healer)"></div></div></div>
</div>
</div>
<div class="class-card" data-class="rogue" onclick="selectClass('rogue',this)">
<span class="class-icon">🗡️</span>
<div class="class-name">ROGUE</div>
<div class="class-desc">Critical strikes. Stealth. Poisons and bleeds.</div>
<div class="stat-bars">
<div class="stat-row"><span class="stat-label">HP</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:50%;background:var(--rogue)"></div></div></div>
<div class="stat-row"><span class="stat-label">ATK</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:85%;background:var(--rogue)"></div></div></div>
<div class="stat-row"><span class="stat-label">DEF</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:30%;background:var(--rogue)"></div></div></div>
<div class="stat-row"><span class="stat-label">SPD</span><div class="stat-bar-bg"><div class="stat-bar-fill" style="width:95%;background:var(--rogue)"></div></div></div>
</div>
</div>
</div>
<div class="name-input-area">
<label class="input-label">YOUR HERO NAME</label>
<input class="pixel-input" id="hero-name-input" type="text" placeholder="Enter name..." maxlength="16">
</div>
<div style="text-align:center;display:flex;gap:12px;justify-content:center">
<button class="btn" onclick="showScreen('title-screen')">← BACK</button>
<button class="btn btn-gold" id="confirm-class-btn" onclick="confirmClass()" disabled>CONFIRM →</button>
</div>
</div>
<!-- LOBBY SCREEN -->
<div id="lobby-screen" class="screen">
<div class="screen-title" style="margin-bottom:6px">DARKRIFT LOBBY</div>
<div class="screen-subtitle">Gather your party before descending</div>
<div class="lobby-layout">
<div>
<div class="panel">
<div class="panel-title">⚡ ROOM CODE</div>
<div class="room-code-display" id="room-code-display">----</div>
<div class="copy-hint" onclick="copyRoomCode()">📋 Click to copy code</div>
</div>
<div class="panel" style="margin-top:16px">
<div class="panel-title">⚙ DUNGEON SETTINGS</div>
<div style="font-size:14px;color:var(--text3);margin-bottom:10px">DIFFICULTY</div>
<div class="difficulty-selector" id="difficulty-selector">
<div class="diff-btn easy active" data-diff="easy" onclick="setDifficulty('easy',this)">EASY</div>
<div class="diff-btn" data-diff="normal" onclick="setDifficulty('normal',this)">NORMAL</div>
<div class="diff-btn hard" data-diff="hard" onclick="setDifficulty('hard',this)">HARD</div>
</div>
<div id="host-controls" style="display:none">
<button class="btn btn-green" onclick="startGame()" id="start-btn" style="width:100%;margin-top:8px" disabled>
▶ START DUNGEON
</button>
</div>
<div id="join-area-lobby" style="display:none">
<div class="join-area">
<label class="input-label" style="display:block">JOIN WITH CODE</label>
<div class="join-row">
<input class="pixel-input" id="join-code-input" type="text" placeholder="XXXX" maxlength="4" style="width:100%;text-transform:uppercase;letter-spacing:4px">
</div>
<button class="btn btn-gold" onclick="joinRoom()" style="width:100%">JOIN ROOM →</button>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-title">👥 PARTY — <span id="player-count">0</span>/5</div>
<div id="player-slots">
<div class="player-slot empty"><span class="slot-icon">👤</span><div class="slot-info"><div class="slot-name">Waiting...</div><div class="slot-class">Empty slot</div></div></div>
<div class="player-slot empty"><span class="slot-icon">👤</span><div class="slot-info"><div class="slot-name">Waiting...</div><div class="slot-class">Empty slot</div></div></div>
<div class="player-slot empty"><span class="slot-icon">👤</span><div class="slot-info"><div class="slot-name">Waiting...</div><div class="slot-class">Empty slot</div></div></div>
<div class="player-slot empty"><span class="slot-icon">👤</span><div class="slot-info"><div class="slot-name">Waiting...</div><div class="slot-class">Empty slot</div></div></div>
<div class="player-slot empty"><span class="slot-icon">👤</span><div class="slot-info"><div class="slot-name">Waiting...</div><div class="slot-class">Empty slot</div></div></div>
</div>
<div style="margin-top:16px;font-size:13px;color:var(--text3);text-align:center">
Share the room code with friends<br>Min. 1 player to start
</div>
</div>
</div>
</div>
<!-- GAME SCREEN -->
<div id="game-screen" class="screen">
<div class="game-layout">
<!-- TOP HUD -->
<div class="game-hud-top">
<div class="hud-wave" id="hud-wave">WAVE 1/5</div>
<div class="hud-title">⚔ DARKRIFT ⚔</div>
<div class="hud-turn-indicator" id="turn-indicator">ENEMY TURN</div>
</div>
<!-- MAIN -->
<div class="game-main">
<!-- PLAYERS LEFT -->
<div class="players-panel">
<div class="panel-header">PARTY</div>
<div id="player-cards-container"></div>
</div>
<!-- BATTLE CANVAS -->
<div class="battle-area">
<canvas id="battle-canvas"></canvas>
<div class="battle-overlay" id="battle-overlay"></div>
</div>
<!-- ENEMIES RIGHT -->
<div class="enemies-panel">
<div class="panel-header">ENEMIES</div>
<div id="enemy-cards-container"></div>
<div class="target-hint" id="target-hint">← Select target</div>
</div>
</div>
<!-- BOTTOM ACTIONS -->
<div class="game-actions">
<div class="combat-log-area" style="width:100%">
<div class="combat-log" id="combat-log"></div>
</div>
<div class="abilities-row" id="abilities-row"></div>
<div class="actions-row">
<button class="action-btn" id="basic-attack-btn" onclick="basicAttack()" disabled>⚔ ATTACK</button>
<button class="action-btn" id="defend-btn" onclick="defend()" disabled>🛡 DEFEND</button>
<button class="action-btn end-turn" id="end-turn-btn" onclick="endTurn()" disabled>NEXT →</button>
</div>
</div>
</div>
</div>
<!-- RESULT SCREEN -->
<div class="result-screen" id="result-screen">
<div class="result-content">
<div class="result-title" id="result-title">VICTORY!</div>
<div style="font-size:60px;margin:16px 0" id="result-emoji">🏆</div>
<div class="result-stats" id="result-stats"></div>
<button class="btn btn-gold" onclick="location.reload()" style="margin-top:16px;width:100%">PLAY AGAIN</button>
</div>
</div>
<!-- WAITING OVERLAY -->
<div class="waiting-overlay" id="waiting-overlay">
<div class="waiting-box">
<span class="spinner"></span>
<div id="waiting-text">Connecting...</div>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
import { getDatabase, ref, set, get, update, push, onValue, off, remove } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-database.js";
const firebaseConfig = {
apiKey: "AIzaSyAbk1fE4rHGqZ_ULBCOJ4OnBe8aJ1AJ26Y",
authDomain: "firstgame-db7be.firebaseapp.com",
databaseURL: "https://firstgame-db7be-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "firstgame-db7be",
storageBucket: "firstgame-db7be.firebasestorage.app",
messagingSenderId: "778641081622",
appId: "1:778641081622:web:d220de8c9135991f3d3c37",
measurementId: "G-M49M84FDXS"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
// ========================
// GAME DATA DEFINITIONS
// ========================
const CLASS_DATA = {
warrior: {
icon: '🛡️', color: '#ef4444', name: 'Warrior',
hp: 180, mp: 60, atk: 28, def: 20, spd: 3,
abilities: [
{ id: 'shield_bash', name: 'Shield Bash', icon: '🛡', mpCost: 15, cooldown: 2, desc: 'Stun + damage', type: 'damage', power: 1.2, effect: 'stun' },
{ id: 'battle_cry', name: 'Battle Cry', icon: '📣', mpCost: 20, cooldown: 3, desc: 'Boost party ATK', type: 'buff', power: 1.3, effect: 'atkUp', target: 'allies' },
{ id: 'taunt', name: 'Taunt', icon: '😤', mpCost: 10, cooldown: 2, desc: 'Force enemies to attack you', type: 'taunt', power: 1, effect: 'taunt' },
]
},
mage: {
icon: '🔮', color: '#8b5cf6', name: 'Mage',
hp: 90, mp: 140, atk: 52, def: 8, spd: 6,
abilities: [
{ id: 'fireball', name: 'Fireball', icon: '🔥', mpCost: 25, cooldown: 0, desc: 'AoE fire damage', type: 'aoe', power: 1.5 },
{ id: 'arcane_bolt', name: 'Arcane Bolt', icon: '⚡', mpCost: 15, cooldown: 0, desc: 'Single target + silence', type: 'damage', power: 1.8, effect: 'silence' },
{ id: 'frost_nova', name: 'Frost Nova', icon: '❄️', mpCost: 30, cooldown: 3, desc: 'Freeze all enemies', type: 'aoe', power: 0.8, effect: 'freeze' },
]
},
archer: {
icon: '🏹', color: '#22c55e', name: 'Archer',
hp: 120, mp: 90, atk: 38, def: 12, spd: 9,
abilities: [
{ id: 'rapid_fire', name: 'Rapid Fire', icon: '🏹', mpCost: 20, cooldown: 1, desc: 'Hit 3 times fast', type: 'multi', power: 0.7, hits: 3 },
{ id: 'poison_arrow', name: 'Poison Arrow', icon: '☠️', mpCost: 15, cooldown: 0, desc: 'Poison DoT', type: 'damage', power: 1.0, effect: 'poison' },
{ id: 'snipe', name: 'Snipe', icon: '🎯', mpCost: 35, cooldown: 3, desc: 'Massive single hit', type: 'damage', power: 3.0 },
]
},
healer: {
icon: '✨', color: '#f59e0b', name: 'Healer',
hp: 110, mp: 160, atk: 18, def: 14, spd: 7,
abilities: [
{ id: 'heal', name: 'Heal', icon: '💚', mpCost: 20, cooldown: 0, desc: 'Restore ally HP', type: 'heal', power: 1.5, target: 'ally' },
{ id: 'mass_heal', name: 'Mass Heal', icon: '💫', mpCost: 40, cooldown: 3, desc: 'Heal entire party', type: 'heal', power: 0.8, target: 'allies' },
{ id: 'resurrect', name: 'Revive', icon: '🌟', mpCost: 60, cooldown: 5, desc: 'Resurrect fallen ally', type: 'revive', power: 0.5, target: 'deadAlly' },
]
},
rogue: {
icon: '🗡️', color: '#06b6d4', name: 'Rogue',
hp: 100, mp: 100, atk: 44, def: 10, spd: 11,
abilities: [
{ id: 'backstab', name: 'Backstab', icon: '🗡', mpCost: 15, cooldown: 0, desc: '60% crit chance', type: 'damage', power: 2.0, critChance: 0.6 },
{ id: 'smoke_bomb', name: 'Smoke Bomb', icon: '💨', mpCost: 20, cooldown: 2, desc: 'Dodge next attack', type: 'buff', power: 1, effect: 'dodge' },
{ id: 'fan_of_knives', name: 'Fan of Knives', icon: '🔪', mpCost: 30, cooldown: 2, desc: 'Hit all enemies', type: 'aoe', power: 0.9 },
]
}
};
const WAVE_CONFIGS = [
{ name: "THE DESCENT BEGINS", enemies: ['skeleton', 'goblin'], boss: false },
{ name: "DARKNESS RISES", enemies: ['skeleton', 'goblin', 'skeleton'], boss: false },
{ name: "THE DUNGEON ROARS", enemies: ['orc', 'goblin', 'orc'], boss: false },
{ name: "ELITE GUARDS", enemies: ['orc_elite', 'skeleton_mage'], boss: false },
{ name: "THE LICH KING", enemies: ['lich_king'], boss: true },
];
const ENEMY_TYPES = {
goblin: { name: 'Goblin', icon: '👺', hp: 60, atk: 15, def: 5, spd: 8, reward: 20, color: '#22c55e', size: 0.7 },
skeleton: { name: 'Skeleton', icon: '💀', hp: 80, atk: 20, def: 8, spd: 5, reward: 25, color: '#94a3b8', size: 0.8 },
orc: { name: 'Orc Brute', icon: '👹', hp: 140, atk: 30, def: 15, spd: 3, reward: 40, color: '#ef4444', size: 1.0 },
skeleton_mage: { name: 'Bone Mage', icon: '🧙', hp: 100, atk: 40, def: 5, spd: 7, reward: 55, color: '#8b5cf6', size: 0.9 },
orc_elite: { name: 'Orc Elite', icon: '👿', hp: 200, atk: 35, def: 20, spd: 4, reward: 70, color: '#f97316', size: 1.1 },
lich_king: { name: 'LICH KING', icon: '💠', hp: 500, atk: 55, def: 25, spd: 6, reward: 300, color: '#c084fc', size: 1.5, boss: true },
};
// ========================
// STATE
// ========================
let myId = 'p_' + Math.random().toString(36).substr(2, 8);
let myClass = null;
let myName = '';
let roomCode = null;
let isHost = false;
let joinMode = 'create';
let selectedTarget = null;
let gameState = null;
let abilityCooldowns = {};
let listeners = [];
let selectedDifficulty = 'easy';
let animFrame = null;
let canvasCtx = null;
let lastCanvas = null;
// ========================
// SCREEN MANAGEMENT
// ========================
window.showScreen = function(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
};
window.goToClassSelect = function(mode) {
joinMode = mode;
showScreen('class-screen');
};
window.selectClass = function(cls, el) {
myClass = cls;
document.querySelectorAll('.class-card').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
updateConfirmBtn();
};
window.updateConfirmBtn = function() {
const nameVal = document.getElementById('hero-name-input').value.trim();
document.getElementById('confirm-class-btn').disabled = !myClass || !nameVal;
};
document.getElementById('hero-name-input').addEventListener('input', updateConfirmBtn);
window.confirmClass = async function() {
myName = document.getElementById('hero-name-input').value.trim();
if (!myClass || !myName) return;
showWaiting('Setting up...');
if (joinMode === 'create') {
roomCode = generateRoomCode();
isHost = true;
await createRoom();
hideWaiting();
setupLobby();
showScreen('lobby-screen');
} else {
hideWaiting();
setupLobby();
showScreen('lobby-screen');
}
};
// ========================
// LOBBY
// ========================
function setupLobby() {
const codeDisplay = document.getElementById('room-code-display');
const hostControls = document.getElementById('host-controls');
const joinArea = document.getElementById('join-area-lobby');
if (isHost) {
codeDisplay.textContent = roomCode;
hostControls.style.display = 'block';
joinArea.style.display = 'none';
} else {
codeDisplay.textContent = '----';
hostControls.style.display = 'none';
joinArea.style.display = 'block';
}
}
window.copyRoomCode = function() {
if (roomCode) {
navigator.clipboard.writeText(roomCode).catch(() => {});
showNotification('Code copied: ' + roomCode);
}
};
window.setDifficulty = function(diff, el) {
selectedDifficulty = diff;
document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
if (isHost && roomCode) {
update(ref(db, `rooms/${roomCode}`), { difficulty: diff });
}
};
async function createRoom() {
const playerData = buildPlayerData();
await set(ref(db, `rooms/${roomCode}`), {
host: myId,
status: 'lobby',
difficulty: selectedDifficulty,
created: Date.now(),
players: { [myId]: playerData }
});
listenLobby();
}
window.joinRoom = async function() {
const code = document.getElementById('join-code-input').value.trim().toUpperCase();
if (code.length !== 4) { showNotification('Enter a 4-letter code!'); return; }
showWaiting('Joining room...');
const snap = await get(ref(db, `rooms/${code}`));
if (!snap.exists()) { hideWaiting(); showNotification('Room not found!'); return; }
const roomData = snap.val();
const playerCount = Object.keys(roomData.players || {}).length;
if (playerCount >= 5) { hideWaiting(); showNotification('Room is full!'); return; }
if (roomData.status !== 'lobby') { hideWaiting(); showNotification('Game already started!'); return; }
roomCode = code;
isHost = false;
const playerData = buildPlayerData();
await update(ref(db, `rooms/${roomCode}/players/${myId}`), playerData);
document.getElementById('room-code-display').textContent = roomCode;
hideWaiting();
listenLobby();
};
function buildPlayerData() {
const cls = CLASS_DATA[myClass];
return {
id: myId,
name: myName,
class: myClass,
hp: cls.hp,
maxHp: cls.hp,
mp: cls.mp,
maxMp: cls.mp,
atk: cls.atk,
def: cls.def,
spd: cls.spd,
alive: true,
statusEffects: {}
};
}
function listenLobby() {
const roomRef = ref(db, `rooms/${roomCode}`);
const unsubscribe = onValue(roomRef, (snap) => {
if (!snap.exists()) return;
const data = snap.val();
updateLobbyUI(data);
if (data.status === 'playing') {
clearListeners();
startGameClient(data);
}
});
listeners.push({ ref: roomRef, fn: unsubscribe });
}
function updateLobbyUI(data) {
const players = data.players || {};
const count = Object.keys(players).length;
document.getElementById('player-count').textContent = count;
const slots = document.getElementById('player-slots');
slots.innerHTML = '';
const playerArr = Object.values(players);
for (let i = 0; i < 5; i++) {
const p = playerArr[i];
const slot = document.createElement('div');
if (p) {
slot.className = 'player-slot';
const cls = CLASS_DATA[p.class];
slot.innerHTML = `
<span class="slot-icon">${cls.icon}</span>
<div class="slot-info">
<div class="slot-name">${p.name} ${p.id === myId ? '<span class="slot-you">YOU</span>' : ''} ${data.host === p.id ? '<span class="slot-host-badge">HOST</span>' : ''}</div>
<div class="slot-class">${cls.name}</div>
</div>
<span class="slot-ready">●</span>
`;
} else {
slot.className = 'player-slot empty';
slot.innerHTML = `<span class="slot-icon">👤</span><div class="slot-info"><div class="slot-name">Waiting...</div><div class="slot-class">Empty slot</div></div>`;
}
slots.appendChild(slot);
}
if (isHost) {
document.getElementById('start-btn').disabled = count < 1;
}
}
window.startGame = async function() {
const snap = await get(ref(db, `rooms/${roomCode}`));
const roomData = snap.val();
const players = Object.values(roomData.players);
// Sort players by speed
const turnOrder = players.map(p => p.id).sort((a, b) => {
return (roomData.players[b].spd || 0) - (roomData.players[a].spd || 0);
});
const initWave = buildWave(0, roomData.difficulty);
const gameData = {
status: 'playing',
wave: 0,
totalWaves: WAVE_CONFIGS.length,
turnOrder: turnOrder,
currentTurnIndex: 0,
currentTurn: turnOrder[0],
phase: 'player',
enemies: initWave,
log: [],
score: 0,
difficulty: roomData.difficulty
};
await update(ref(db, `rooms/${roomCode}`), gameData);
};
function buildWave(waveIndex, difficulty) {
const config = WAVE_CONFIGS[waveIndex];
const diffMult = difficulty === 'easy' ? 0.8 : difficulty === 'hard' ? 1.4 : 1.0;
const enemies = {};
config.enemies.forEach((type, i) => {
const base = ENEMY_TYPES[type];
const id = `enemy_${i}_${type}`;
enemies[id] = {
id,
type,
name: base.name,
icon: base.icon,
hp: Math.floor(base.hp * diffMult),
maxHp: Math.floor(base.hp * diffMult),
atk: Math.floor(base.atk * diffMult),
def: base.def,
spd: base.spd,
alive: true,
statusEffects: {},
boss: base.boss || false,
x: 0, y: 0 // canvas positions set later
};
});
return enemies;
}
// ========================
// GAME CLIENT
// ========================
function startGameClient(roomData) {
gameState = roomData;
showScreen('game-screen');
setupCanvas();
initAbilityCooldowns();
listenGame();
renderGame();
showWaveAnnouncement(roomData.wave);
}
function listenGame() {
const roomRef = ref(db, `rooms/${roomCode}`);
const unsubscribe = onValue(roomRef, (snap) => {
if (!snap.exists()) return;
gameState = snap.val();
renderGame();
checkGameEnd();
});
listeners.push({ ref: roomRef, fn: unsubscribe });
}
function clearListeners() {
listeners.forEach(l => off(l.ref));
listeners = [];
}
function initAbilityCooldowns() {
if (!myClass) return;
CLASS_DATA[myClass].abilities.forEach(a => {
abilityCooldowns[a.id] = 0;
});
}
// ========================
// CANVAS RENDERING
// ========================
function setupCanvas() {
const canvas = document.getElementById('battle-canvas');
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
canvasCtx = canvas.getContext('2d');
lastCanvas = canvas;
drawBattleScene();
}
function drawBattleScene() {
if (!canvasCtx || !lastCanvas) return;
const ctx = canvasCtx;
const W = lastCanvas.width;
const H = lastCanvas.height;
// Background - dark dungeon
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, W, H);
// Underground layers
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0a0810');
grad.addColorStop(0.5, '#050508');
grad.addColorStop(1, '#080510');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
// Floor
ctx.fillStyle = '#1a1428';
ctx.fillRect(0, H * 0.72, W, H * 0.28);
// Floor tiles pixel effect
ctx.strokeStyle = '#0f0f20';
ctx.lineWidth = 1;
const tileW = 40, tileH = 20;
for (let x = 0; x < W; x += tileW) {
for (let y = Math.floor(H * 0.72); y < H; y += tileH) {
ctx.strokeRect(x, y, tileW, tileH);
}
}
// Wall bricks
ctx.fillStyle = '#1f1830';
for (let bx = 0; bx < W; bx += 50) {
for (let by = 0; by < H * 0.72; by += 24) {
const offset = (Math.floor(by / 24) % 2) * 25;
ctx.fillRect(bx + offset, by, 48, 22);
ctx.strokeStyle = '#0d0b18';
ctx.strokeRect(bx + offset, by, 48, 22);
}
}
// Torches
drawTorch(ctx, W * 0.2, H * 0.35);
drawTorch(ctx, W * 0.5, H * 0.35);
drawTorch(ctx, W * 0.8, H * 0.35);
// Ambient particles
drawAmbientParticles(ctx, W, H);
}
function drawTorch(ctx, x, y) {
// Torch bracket
ctx.fillStyle = '#8B7355';
ctx.fillRect(x - 3, y, 6, 16);
// Flame glow
const time = Date.now() / 1000;
const flicker = Math.sin(time * 7 + x) * 0.3 + 0.7;
const radGrad = ctx.createRadialGradient(x, y - 5, 0, x, y - 5, 25 * flicker);
radGrad.addColorStop(0, `rgba(255, 200, 50, 0.6)`);
radGrad.addColorStop(0.5, `rgba(255, 100, 0, 0.3)`);
radGrad.addColorStop(1, 'rgba(255, 50, 0, 0)');
ctx.fillStyle = radGrad;
ctx.fillRect(x - 25, y - 30, 50, 50);
// Flame emoji approximation
ctx.font = `${14 * flicker}px serif`;
ctx.textAlign = 'center';
ctx.fillText('🔥', x, y);
}
function drawAmbientParticles(ctx, W, H) {
// Floating dust particles
const time = Date.now() / 3000;
for (let i = 0; i < 15; i++) {
const px = (Math.sin(i * 2.3 + time) * 0.5 + 0.5) * W;
const py = (Math.cos(i * 1.7 + time * 0.5) * 0.3 + 0.3) * H;
ctx.fillStyle = `rgba(124, 58, 237, ${0.1 + Math.sin(i + time) * 0.05})`;
ctx.beginPath();
ctx.arc(px, py, 1.5, 0, Math.PI * 2);
ctx.fill();
}
}
let animRunning = false;
function startCanvasAnimation() {
if (animRunning) return;
animRunning = true;
function loop() {
if (!animRunning) return;
drawBattleScene();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
// ========================
// GAME RENDERING (UI)
// ========================
function renderGame() {
if (!gameState) return;
startCanvasAnimation();
renderPlayerCards();
renderEnemyCards();
renderAbilities();
updateHUD();
updateActionButtons();
}
function renderPlayerCards() {
if (!gameState) return;
const container = document.getElementById('player-cards-container');
const players = gameState.players || {};
container.innerHTML = '';
Object.values(players).forEach(p => {
const cls = CLASS_DATA[p.class] || {};
const hpPct = Math.max(0, (p.hp / p.maxHp) * 100);
const mpPct = Math.max(0, (p.mp / p.maxMp) * 100);
const hpClass = hpPct > 60 ? 'high' : hpPct > 30 ? 'mid' : '';
const isActive = gameState.currentTurn === p.id;
const isDead = !p.alive;
const card = document.createElement('div');
card.className = `player-card${isActive ? ' active-turn' : ''}${isDead ? ' dead' : ''}`;
card.innerHTML = `
<div class="pc-header">
<span class="pc-icon">${cls.icon || '👤'}</span>
<div class="pc-info">
<div class="pc-name">${p.name}${p.id === myId ? ' ★' : ''}</div>
<div class="pc-class">${cls.name || p.class}</div>
</div>
</div>
<div class="hp-bar-container">
<div class="hp-label"><span>HP</span><span>${p.hp}/${p.maxHp}</span></div>
<div class="hp-bar-bg"><div class="hp-bar-fill ${hpClass}" style="width:${hpPct}%"></div></div>
</div>
<div style="margin-top:4px">
<div class="mp-bar-bg"><div class="mp-bar-fill" style="width:${mpPct}%"></div></div>
</div>
<div class="status-icons">${renderStatusIcons(p.statusEffects)}</div>
`;
container.appendChild(card);
});
}
function renderStatusIcons(effects) {
if (!effects) return '';
const iconMap = { stun: '😵', poison: '☠️', freeze: '❄️', dodge: '💨', atkUp: '⚔️↑', taunt: '😤', burn: '🔥', silence: '🔇' };
return Object.keys(effects).filter(e => effects[e] > 0).map(e => `<span class="status-icon" title="${e}: ${effects[e]} turns">${iconMap[e] || '?'}</span>`).join('');
}
function renderEnemyCards() {
if (!gameState) return;
const container = document.getElementById('enemy-cards-container');
const enemies = gameState.enemies || {};
container.innerHTML = '';
Object.values(enemies).forEach(e => {
const hpPct = Math.max(0, (e.hp / e.maxHp) * 100);
const isTargeted = selectedTarget === e.id;
const isDead = !e.alive;
const card = document.createElement('div');
card.className = `enemy-card${isTargeted ? ' targeted' : ''}${isDead ? ' dead' : ''}`;
card.dataset.id = e.id;
card.onclick = () => targetEnemy(e.id);
card.innerHTML = `
<div class="ec-header">
<span class="ec-icon">${e.icon}</span>
<div class="ec-info">
<div class="ec-name">${e.name}${e.boss ? ' 👑' : ''}</div>
<div class="ec-type">${e.hp}/${e.maxHp} HP</div>
</div>
</div>
<div class="hp-bar-container">
<div class="hp-bar-bg"><div class="hp-bar-fill" style="width:${hpPct}%;background:${hpPct > 50 ? '#ef4444' : hpPct > 25 ? '#f97316' : '#fcd34d'}"></div></div>
</div>
<div class="status-icons">${renderStatusIcons(e.statusEffects)}</div>
`;
container.appendChild(card);
});
}
function renderAbilities() {
if (!myClass || !gameState) return;
const row = document.getElementById('abilities-row');
const isMyTurn = gameState.currentTurn === myId;
const me = gameState.players?.[myId];
row.innerHTML = '';
CLASS_DATA[myClass].abilities.forEach(ability => {
const cd = abilityCooldowns[ability.id] || 0;
const hasMP = (me?.mp || 0) >= ability.mpCost;
const canUse = isMyTurn && cd === 0 && hasMP && me?.alive;
const btn = document.createElement('button');
btn.className = 'ability-btn';
btn.disabled = !canUse;
btn.onclick = () => useAbility(ability.id);
btn.innerHTML = `
${ability.icon} ${ability.name}
<span class="mana-cost">💧${ability.mpCost}</span>
${cd > 0 ? `<span class="cooldown-badge">${cd}</span>` : ''}
`;
row.appendChild(btn);
});
}
function updateHUD() {
if (!gameState) return;
const waveEl = document.getElementById('hud-wave');
const turnEl = document.getElementById('turn-indicator');
const wave = gameState.wave || 0;
waveEl.textContent = `WAVE ${wave + 1}/${WAVE_CONFIGS.length}`;
const isMyTurn = gameState.currentTurn === myId;
turnEl.textContent = isMyTurn ? '⚡ YOUR TURN' : `${gameState.players?.[gameState.currentTurn]?.name || 'ENEMY'}'S TURN`;
turnEl.className = 'hud-turn-indicator' + (isMyTurn ? ' your-turn' : '');
}
function updateActionButtons() {
if (!gameState) return;
const isMyTurn = gameState.currentTurn === myId;
const me = gameState.players?.[myId];
const alive = me?.alive;
document.getElementById('basic-attack-btn').disabled = !isMyTurn || !alive;
document.getElementById('defend-btn').disabled = !isMyTurn || !alive;
document.getElementById('end-turn-btn').disabled = !isMyTurn || !alive;
}
// ========================
// COMBAT ACTIONS
// ========================
function targetEnemy(id) {
if (!gameState || gameState.currentTurn !== myId) return;
const enemy = gameState.enemies?.[id];
if (!enemy || !enemy.alive) return;
selectedTarget = id;
renderEnemyCards();
document.getElementById('target-hint').textContent = `Target: ${enemy.name}`;
}
window.basicAttack = async function() {
if (!selectedTarget) { showNotification('Select a target first!'); return; }
if (!isMyTurn()) return;
const me = gameState.players[myId];
const target = gameState.enemies[selectedTarget];
if (!target || !target.alive) { showNotification('Invalid target!'); return; }
const dmg = Math.max(1, me.atk - target.def + Math.floor(Math.random() * 10) - 5);
await applyDamageToEnemy(selectedTarget, dmg, `${me.name} attacks ${target.name} for ${dmg} damage!`);
await advanceTurn();
};
window.defend = async function() {
if (!isMyTurn()) return;
const me = gameState.players[myId];
const effects = me.statusEffects || {};
effects.defending = 2;
await update(ref(db, `rooms/${roomCode}/players/${myId}/statusEffects`), effects);
addLog(`${me.name} takes a defensive stance!`, 'skill');
await advanceTurn();
};
window.useAbility = async function(abilityId) {
if (!isMyTurn()) return;
const ability = CLASS_DATA[myClass].abilities.find(a => a.id === abilityId);
if (!ability) return;
const me = gameState.players[myId];
if (me.mp < ability.mpCost) { showNotification('Not enough MP!'); return; }
if (ability.type === 'damage' || ability.type === 'multi') {
if (!selectedTarget) { showNotification('Select a target first!'); return; }
const target = gameState.enemies[selectedTarget];
if (!target?.alive) { showNotification('Select a valid target!'); return; }
if (ability.type === 'multi') {
for (let i = 0; i < (ability.hits || 1); i++) {
const dmg = Math.max(1, Math.floor(me.atk * ability.power) - target.def + Math.floor(Math.random() * 8));
await applyDamageToEnemy(selectedTarget, dmg, `${me.name} hits ${target.name} for ${dmg}! (${i+1}/${ability.hits})`);
}
} else {
let dmg = Math.max(1, Math.floor(me.atk * ability.power) - target.def + Math.floor(Math.random() * 10));
const isCrit = ability.critChance && Math.random() < ability.critChance;
if (isCrit) { dmg = Math.floor(dmg * 1.8); }
await applyDamageToEnemy(selectedTarget, dmg, `${me.name} uses ${ability.name}${isCrit ? ' [CRIT!]' : ''} for ${dmg} damage!`);
if (ability.effect) {
const effects = { ...(gameState.enemies[selectedTarget]?.statusEffects || {}) };
effects[ability.effect] = 3;
await update(ref(db, `rooms/${roomCode}/enemies/${selectedTarget}/statusEffects`), effects);
}
}
} else if (ability.type === 'aoe') {
const enemies = gameState.enemies || {};
for (const [eid, enemy] of Object.entries(enemies)) {
if (!enemy.alive) continue;
const dmg = Math.max(1, Math.floor(me.atk * ability.power) - enemy.def + Math.floor(Math.random() * 8));
await applyDamageToEnemy(eid, dmg, `${me.name} hits ${enemy.name} for ${dmg}!`);
}
addLog(`${me.name} uses ${ability.name}!`, 'skill');
if (ability.effect) {
for (const [eid, enemy] of Object.entries(enemies)) {
if (!enemy.alive) continue;
const effects = { ...(enemy.statusEffects || {}) };
effects[ability.effect] = 2;
await update(ref(db, `rooms/${roomCode}/enemies/${eid}/statusEffects`), effects);
}
}
} else if (ability.type === 'heal') {
const players = gameState.players || {};
const targets = ability.target === 'allies'
? Object.values(players).filter(p => p.alive)
: [Object.values(players).find(p => p.alive && p.id !== myId) || me];
for (const t of targets) {
const healAmt = Math.floor(me.atk * ability.power + 20 + Math.random() * 15);
const newHp = Math.min(t.maxHp, t.hp + healAmt);
await update(ref(db, `rooms/${roomCode}/players/${t.id}`), { hp: newHp });
addLog(`${me.name} heals ${t.name} for ${healAmt} HP!`, 'heal');
showDamageFloat(`+${healAmt}`, '#22c55e');
}
} else if (ability.type === 'revive') {
const deadPlayer = Object.values(gameState.players || {}).find(p => !p.alive);
if (!deadPlayer) { showNotification('No fallen allies!'); return; }
const reviveHp = Math.floor(deadPlayer.maxHp * ability.power);
await update(ref(db, `rooms/${roomCode}/players/${deadPlayer.id}`), { hp: reviveHp, alive: true });
addLog(`${me.name} revives ${deadPlayer.name}!`, 'heal');
} else if (ability.type === 'buff') {
const effects = { ...(me.statusEffects || {}) };
effects[ability.effect] = 3;
await update(ref(db, `rooms/${roomCode}/players/${myId}/statusEffects`), effects);
addLog(`${me.name} uses ${ability.name}!`, 'skill');
}
// Deduct MP
const newMp = Math.max(0, me.mp - ability.mpCost);
await update(ref(db, `rooms/${roomCode}/players/${myId}`), { mp: newMp });
// Set cooldown locally
abilityCooldowns[abilityId] = ability.cooldown;
await advanceTurn();
};
async function applyDamageToEnemy(enemyId, dmg, logMsg) {
const enemy = (await get(ref(db, `rooms/${roomCode}/enemies/${enemyId}`))).val();
if (!enemy) return;
const newHp = Math.max(0, enemy.hp - dmg);
const alive = newHp > 0;
await update(ref(db, `rooms/${roomCode}/enemies/${enemyId}`), { hp: newHp, alive });
addLog(logMsg, 'damage');
showDamageFloat(`-${dmg}`, '#ef4444');
if (!alive) {
addLog(`${enemy.name} has been defeated!`, 'death');
const snap = await get(ref(db, `rooms/${roomCode}`));
const data = snap.val();
const score = (data.score || 0) + ENEMY_TYPES[enemy.type]?.reward || 20;
await update(ref(db, `rooms/${roomCode}`), { score });
}
}
async function advanceTurn() {
if (!gameState) return;
const snap = await get(ref(db, `rooms/${roomCode}`));
const data = snap.val();
const enemies = data.enemies || {};
const allDead = Object.values(enemies).every(e => !e.alive);
if (allDead) {
const nextWave = (data.wave || 0) + 1;
if (nextWave >= WAVE_CONFIGS.length) {
await update(ref(db, `rooms/${roomCode}`), { status: 'victory' });
return;
}
const newEnemies = buildWave(nextWave, data.difficulty);
await update(ref(db, `rooms/${roomCode}`), {
wave: nextWave,
enemies: newEnemies,
currentTurnIndex: 0,
currentTurn: data.turnOrder[0]
});
showWaveAnnouncement(nextWave);
return;
}
// Check if all players dead
const players = data.players || {};
const allPlayersDead = Object.values(players).every(p => !p.alive);
if (allPlayersDead) {
await update(ref(db, `rooms/${roomCode}`), { status: 'defeat' });
return;
}
const order = data.turnOrder || [];
let nextIndex = ((data.currentTurnIndex || 0) + 1) % order.length;
// Skip dead players
let safety = 0;
while (players[order[nextIndex]] && !players[order[nextIndex]].alive && safety < order.length) {
nextIndex = (nextIndex + 1) % order.length;
safety++;
}
await update(ref(db, `rooms/${roomCode}`), {
currentTurnIndex: nextIndex,
currentTurn: order[nextIndex]
});
// If next turn is enemy (not in players), do enemy turn
if (!players[order[nextIndex]]) {
// Slight delay for visual clarity
setTimeout(() => doEnemyTurn(), 1200);
}
}
async function doEnemyTurn() {
const snap = await get(ref(db, `rooms/${roomCode}`));
const data = snap.val();
if (!data || data.status !== 'playing') return;
const players = Object.values(data.players || {}).filter(p => p.alive);
if (!players.length) return;
const enemies = Object.values(data.enemies || {}).filter(e => e.alive);
for (const enemy of enemies) {
await enemyAct(enemy, players, data);
// Tick status effects
const newEffects = { ...(enemy.statusEffects || {}) };
let skip = false;
if (newEffects.stun > 0) { skip = true; newEffects.stun--; }
if (newEffects.freeze > 0) { skip = true; newEffects.freeze--; }
if (newEffects.poison > 0) {
const target = players[Math.floor(Math.random() * players.length)];
const poisonDmg = 8;
const newHp = Math.max(0, target.hp - poisonDmg);
// Actually enemy has poison, damage itself
const selfHp = Math.max(0, enemy.hp - poisonDmg);
await update(ref(db, `rooms/${roomCode}/enemies/${enemy.id}`), { hp: selfHp, alive: selfHp > 0 });
newEffects.poison--;
}
await update(ref(db, `rooms/${roomCode}/enemies/${enemy.id}/statusEffects`), newEffects);
}
// Advance to next player turn
const updatedSnap = await get(ref(db, `rooms/${roomCode}`));
const updatedData = updatedSnap.val();
const order = updatedData.turnOrder || [];
const players2 = updatedData.players || {};
let nextIndex = 0; // go back to first player
let safety = 0;
while (players2[order[nextIndex]] && !players2[order[nextIndex]].alive && safety < order.length) {
nextIndex = (nextIndex + 1) % order.length;
safety++;
}
await update(ref(db, `rooms/${roomCode}`), {
currentTurnIndex: nextIndex,
currentTurn: order[nextIndex]
});
// Tick player status effects and regen MP
for (const p of Object.values(updatedData.players || {})) {
if (!p.alive) continue;
const effects = { ...(p.statusEffects || {}) };
Object.keys(effects).forEach(k => { if (effects[k] > 0) effects[k]--; });
const newMp = Math.min(p.maxMp, p.mp + 10); // MP regen per round
await update(ref(db, `rooms/${roomCode}/players/${p.id}`), { statusEffects: effects, mp: newMp });
}
// Decrement local cooldowns
Object.keys(abilityCooldowns).forEach(k => {
if (abilityCooldowns[k] > 0) abilityCooldowns[k]--;
});
}
async function enemyAct(enemy, players, data) {
// Target taunt player first, otherwise random
let target = players.find(p => p.statusEffects?.taunt > 0) || players[Math.floor(Math.random() * players.length)];
// Check if target is defending
const defBonus = target.statusEffects?.defending > 0 ? 15 : 0;
const dmg = Math.max(1, enemy.atk - target.def - defBonus + Math.floor(Math.random() * 8) - 3);
const newHp = Math.max(0, target.hp - dmg);
const alive = newHp > 0;
await update(ref(db, `rooms/${roomCode}/players/${target.id}`), { hp: newHp, alive });
addLog(`${enemy.name} attacks ${target.name} for ${dmg} damage!`, 'damage');
if (!alive) addLog(`${target.name} has fallen!`, 'death');
}
window.endTurn = async function() {
if (!isMyTurn()) return;
await advanceTurn();
};
function isMyTurn() {
return gameState?.currentTurn === myId && gameState?.players?.[myId]?.alive;
}
// ========================
// COMBAT LOG
// ========================
function addLog(msg, type = 'system') {
const log = document.getElementById('combat-log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `> ${msg}`;
log.appendChild(entry);
log.scrollTop = log.scrollHeight;
}
// ========================
// DAMAGE FLOAT
// ========================
function showDamageFloat(text, color) {
const overlay = document.getElementById('battle-overlay');
const el = document.createElement('div');
el.className = 'damage-float';
el.style.color = color;
el.style.left = (30 + Math.random() * 40) + '%';
el.style.top = (30 + Math.random() * 30) + '%';
el.textContent = text;
overlay.appendChild(el);
setTimeout(() => el.remove(), 1200);
}
// ========================
// WAVE ANNOUNCEMENT
// ========================
function showWaveAnnouncement(waveIndex) {
const config = WAVE_CONFIGS[waveIndex];
const el = document.createElement('div');
el.className = 'wave-announcement';
el.innerHTML = `
<div class="wave-text">
<span class="wave-number">WAVE ${waveIndex + 1}</span>
<span class="wave-subtitle">${config.name}</span>
</div>
`;
document.body.appendChild(el);
setTimeout(() => el.remove(), 2500);
}
// ========================
// GAME END CHECK
// ========================
function checkGameEnd() {
if (!gameState) return;
if (gameState.status === 'victory') showResult('victory');
if (gameState.status === 'defeat') showResult('defeat');
}
function showResult(type) {
const screen = document.getElementById('result-screen');
const title = document.getElementById('result-title');
const emoji = document.getElementById('result-emoji');
const stats = document.getElementById('result-stats');
title.textContent = type === 'victory' ? 'VICTORY!' : 'DEFEATED...';
title.className = 'result-title ' + type;
emoji.textContent = type === 'victory' ? '🏆' : '💀';
stats.innerHTML = `
Score: ${gameState?.score || 0}<br>
Waves cleared: ${gameState?.wave || 0}/${WAVE_CONFIGS.length}<br>
Players: ${Object.values(gameState?.players || {}).filter(p => p.alive).length} survived
`;
screen.classList.add('show');
animRunning = false;
}
// ========================
// NOTIFICATIONS
// ========================
window.showNotification = function(msg) {
const el = document.createElement('div');
el.className = 'notification';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => el.remove(), 3000);
};
// ========================
// UTILS
// ========================
function generateRoomCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
let code = '';
for (let i = 0; i < 4; i++) code += chars[Math.floor(Math.random() * chars.length)];
return code;
}
function showWaiting(text) {
document.getElementById('waiting-text').textContent = text;
document.getElementById('waiting-overlay').classList.add('show');
}
function hideWaiting() {
document.getElementById('waiting-overlay').classList.remove('show');
}
// Handle window resize for canvas
window.addEventListener('resize', () => {
if (document.getElementById('game-screen').classList.contains('active')) {
setupCanvas();
}
});
</script>
</body>
</html>