RPG
Gemini 3.6 Flash · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 68,727 bytes · SHA-256 541a3ed64b36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chronicles of Aethelgard - Action RPG</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body, html {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #0d0f12;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #e2e8f0;
}
#game-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #05070a;
}
canvas#gameCanvas {
background-color: #121820;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
image-rendering: pixelated;
cursor: crosshair;
}
/* UI Overlay */
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
z-index: 10;
}
/* HUD Header */
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.hud-card {
background: rgba(15, 23, 42, 0.85);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
padding: 12px 18px;
backdrop-filter: blur(8px);
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
display: flex;
align-items: center;
gap: 15px;
}
/* Health Display (Hearts) */
.health-container {
display: flex;
align-items: center;
gap: 6px;
}
.heart-icon {
width: 28px;
height: 28px;
display: inline-block;
position: relative;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
}
/* Bars & Stats */
.stat-bars {
display: flex;
flex-direction: column;
gap: 6px;
}
.bar-bg {
width: 140px;
height: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.2);
}
.bar-fill-stamina {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #38ef7d, #11998e);
transition: width 0.1s linear;
}
/* Economy & Equipment HUD */
.hud-right {
display: flex;
gap: 12px;
}
.currency-badge {
font-size: 18px;
font-weight: bold;
color: #ffd700;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
display: flex;
align-items: center;
gap: 6px;
}
.weapon-badge {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
}
.weapon-slot {
width: 42px;
height: 42px;
background: rgba(0,0,0,0.5);
border: 2px solid #3b82f6;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
position: relative;
}
.weapon-slot.ranged {
border-color: #10b981;
}
.hotkey-tag {
position: absolute;
bottom: -4px;
right: -4px;
background: #1e293b;
color: #cbd5e1;
font-size: 10px;
padding: 1px 4px;
border-radius: 4px;
border: 1px solid #475569;
}
/* Notification Banner */
#banner-notify {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(15, 23, 42, 0.95);
border: 2px solid #f59e0b;
color: #fbbf24;
padding: 12px 28px;
border-radius: 30px;
font-size: 16px;
font-weight: bold;
letter-spacing: 0.5px;
box-shadow: 0 10px 25px rgba(0,0,0,0.6);
opacity: 0;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
z-index: 30;
text-align: center;
}
#banner-notify.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Interaction & Quest Prompt */
#interaction-prompt {
position: absolute;
bottom: 90px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
border: 1px solid rgba(255,255,255,0.3);
padding: 8px 18px;
border-radius: 20px;
font-size: 14px;
color: #fff;
display: none;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.key-cap {
background: #334155;
border: 1px solid #94a3b8;
border-bottom: 3px solid #64748b;
padding: 2px 7px;
border-radius: 5px;
font-size: 12px;
font-weight: bold;
}
/* Boss Bar */
#boss-bar-container {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 60%;
max-width: 500px;
display: none;
flex-direction: column;
align-items: center;
gap: 6px;
background: rgba(15, 23, 42, 0.85);
padding: 10px 16px;
border-radius: 10px;
border: 1px solid rgba(239, 68, 68, 0.5);
}
.boss-name {
font-size: 14px;
font-weight: bold;
color: #ef4444;
text-transform: uppercase;
letter-spacing: 1px;
}
.boss-bar-bg {
width: 100%;
height: 14px;
background: #1f2937;
border-radius: 7px;
overflow: hidden;
border: 1px solid #374151;
}
.boss-bar-fill {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #dc2626, #f87171);
transition: width 0.2s ease-out;
}
/* Controls Overlay Bar */
.controls-hint {
position: absolute;
bottom: 15px;
left: 20px;
font-size: 12px;
color: #94a3b8;
background: rgba(15, 23, 42, 0.7);
padding: 6px 12px;
border-radius: 6px;
display: flex;
gap: 12px;
}
/* Modal Screens (Shop, Inventory, Pause, Game Over) */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(5, 7, 10, 0.85);
backdrop-filter: blur(6px);
display: none;
justify-content: center;
align-items: center;
z-index: 50;
pointer-events: auto;
}
.modal-card {
background: #1e293b;
border: 2px solid #475569;
border-radius: 16px;
width: 90%;
max-width: 650px;
padding: 24px;
box-shadow: 0 20px 40px rgba(0,0,0,0.8);
color: #f1f5f9;
display: flex;
flex-direction: column;
gap: 18px;
max-height: 85vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #334155;
padding-bottom: 12px;
}
.modal-title {
font-size: 22px;
font-weight: bold;
color: #f59e0b;
}
.close-btn {
background: #334155;
border: none;
color: #cbd5e1;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.close-btn:hover { background: #475569; color: #fff; }
/* Inventory Items Grid */
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 12px;
}
.shop-item-card, .inventory-item-card {
background: #0f172a;
border: 1px solid #334155;
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
transition: all 0.2s ease;
position: relative;
}
.shop-item-card:hover, .inventory-item-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
background: #1e293b;
}
.item-icon {
font-size: 32px;
}
.item-name {
font-size: 13px;
font-weight: bold;
color: #f8fafc;
}
.item-desc {
font-size: 11px;
color: #94a3b8;
line-height: 1.3;
}
.item-price {
font-size: 12px;
font-weight: bold;
color: #ffd700;
margin-top: auto;
}
.buy-btn, .use-btn {
margin-top: 6px;
width: 100%;
padding: 6px 10px;
background: #2563eb;
border: none;
border-radius: 6px;
color: #fff;
font-size: 12px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.buy-btn:hover, .use-btn:hover { background: #1d4ed8; }
.buy-btn:disabled { background: #475569; cursor: not-allowed; opacity: 0.6; }
/* Touch Controls for Mobile/Tablet */
#mobile-controls {
display: none;
position: absolute;
bottom: 20px;
left: 0;
width: 100%;
padding: 0 20px;
justify-content: space-between;
pointer-events: auto;
z-index: 20;
}
.d-pad {
width: 120px;
height: 120px;
position: relative;
}
.d-btn {
position: absolute;
width: 40px;
height: 40px;
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
border-radius: 8px;
color: #fff;
font-size: 18px;
display: flex;
justify-content: center;
align-items: center;
}
.d-btn:active { background: rgba(255,255,255,0.5); }
.d-up { top: 0; left: 40px; }
.d-down { bottom: 0; left: 40px; }
.d-left { top: 40px; left: 0; }
.d-right { top: 40px; right: 0; }
.action-btns {
display: flex;
gap: 12px;
align-items: flex-end;
}
.act-btn {
width: 54px;
height: 54px;
border-radius: 50%;
background: rgba(37, 99, 235, 0.7);
border: 2px solid rgba(255,255,255,0.5);
color: #fff;
font-weight: bold;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
}
.act-btn:active { transform: scale(0.92); }
.act-btn.ranged-btn { background: rgba(16, 185, 129, 0.7); }
.act-btn.dash-btn { background: rgba(245, 158, 11, 0.7); }
/* Title Screen & Game Over */
.title-card {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.title-card h1 {
font-size: 38px;
color: #f59e0b;
letter-spacing: 2px;
text-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
}
.start-btn {
padding: 14px 36px;
font-size: 18px;
font-weight: bold;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: #fff;
border: none;
border-radius: 30px;
cursor: pointer;
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.5);
transition: transform 0.2s, box-shadow 0.2s;
}
.start-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.7);
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="960" height="640"></canvas>
<!-- UI Overlay -->
<div id="ui-layer">
<!-- HUD Top -->
<div class="hud-top">
<div class="hud-card">
<!-- Hearts Container -->
<div class="health-container" id="hearts-wrapper">
<!-- Hearts rendered dynamically -->
</div>
<div class="stat-bars">
<div style="font-size:10px; color:#94a3b8; font-weight:bold;">STAMINA</div>
<div class="bar-bg">
<div class="bar-fill-stamina" id="stamina-fill"></div>
</div>
</div>
</div>
<div class="hud-right">
<div class="hud-card">
<div class="currency-badge">
<span>🪙</span>
<span id="gold-count">0</span>
</div>
</div>
<div class="hud-card weapon-badge">
<div class="weapon-slot" id="melee-slot" title="Melee Weapon (Left Click / Space)">
<span id="melee-icon">🗡️</span>
<span class="hotkey-tag">L-CLK</span>
</div>
<div class="weapon-slot ranged" id="ranged-slot" style="opacity:0.4;" title="Ranged Weapon (Right Click / Shift - Unlocked Mid-Game)">
<span id="ranged-icon">🔒</span>
<span class="hotkey-tag">R-CLK</span>
</div>
</div>
</div>
</div>
<!-- Controls Hint -->
<div class="controls-hint">
<span><b>WASD / Arrows</b> Move</span> |
<span><b>Space / Left Click</b> Attack</span> |
<span><b>Shift / Right Click</b> Ranged</span> |
<span><b>C</b> Dash</span> |
<span><b>E</b> Interact</span> |
<span><b>I / Tab</b> Inventory</span>
</div>
<!-- Interaction Prompt -->
<div id="interaction-prompt">
<span class="key-cap">E</span>
<span id="prompt-text">Interact</span>
</div>
<!-- Boss Bar -->
<div id="boss-bar-container">
<div class="boss-name" id="boss-name-text">ANCIENT GOLEM GUARDIAN</div>
<div class="boss-bar-bg">
<div class="boss-bar-fill" id="boss-bar-fill"></div>
</div>
</div>
<!-- Notification Banner -->
<div id="banner-notify">Item Acquired!</div>
</div>
<!-- Mobile Touch UI -->
<div id="mobile-controls">
<div class="d-pad">
<div class="d-btn d-up" id="btn-up">▲</div>
<div class="d-btn d-down" id="btn-down">▼</div>
<div class="d-btn d-left" id="btn-left">◀</div>
<div class="d-btn d-right" id="btn-right">▶</div>
</div>
<div class="action-btns">
<div class="act-btn dash-btn" id="btn-dash">⚡</div>
<div class="act-btn ranged-btn" id="btn-ranged">🏹</div>
<div class="act-btn" id="btn-attack">⚔️</div>
</div>
</div>
<!-- Title / Start Screen Modal -->
<div class="modal-overlay" id="modal-title" style="display: flex;">
<div class="modal-card title-card">
<h1>CHRONICLES OF AETHELGARD</h1>
<p style="color: #cbd5e1; max-width: 480px; line-height: 1.6; font-size: 14px;">
Explore the enchanted realm, battle ancient foes, unlock legendary weapon evolutions, and seek the Sacred Bow to restore peace to the fallen kingdom!
</p>
<div style="background: rgba(0,0,0,0.3); padding: 12px 20px; border-radius: 10px; font-size: 13px; text-align: left; width: 100%;">
<p style="color:#f59e0b; font-weight:bold; margin-bottom: 6px;">🎯 Gameplay & Progression Highlights:</p>
<ul style="margin-left: 20px; color: #94a3b8; line-height: 1.5;">
<li><b>Health:</b> Tracked by <b>Hearts</b> (Each Heart = 2 HP). Starts at <b>3 HP</b> (1.5 Hearts), upgradeable to <b>5 HP</b>!</li>
<li><b>Weapon Evolution:</b> Rusty Dagger ➔ Knight's Broadsword ➔ Rune Flameblade.</li>
<li><b>Ranged Weapon:</b> Defeat the Sunken Temple Guardian to discover the Gale Bow!</li>
<li><b>In-World Save Shrines:</b> Save progress and fully restore your health anytime.</li>
</ul>
</div>
<button class="start-btn" id="btn-start-game">BEGIN ADVENTURE</button>
</div>
</div>
<!-- Shop Modal -->
<div class="modal-overlay" id="modal-shop">
<div class="modal-card">
<div class="modal-header">
<div class="modal-title">🏪 Village Shop & Armory</div>
<button class="close-btn" id="btn-close-shop">✕</button>
</div>
<div class="grid-container" id="shop-items-container">
<!-- Populated by JS -->
</div>
</div>
</div>
<!-- Inventory Modal -->
<div class="modal-overlay" id="modal-inventory">
<div class="modal-card">
<div class="modal-header">
<div class="modal-title">🎒 Adventurer's Satchel</div>
<button class="close-btn" id="btn-close-inventory">✕</button>
</div>
<div style="font-size: 13px; color: #94a3b8; margin-bottom: 8px;">Active Equipment & Accessories:</div>
<div class="grid-container" id="inventory-items-container">
<!-- Populated by JS -->
</div>
</div>
</div>
<!-- Game Over / Victory Modal -->
<div class="modal-overlay" id="modal-end">
<div class="modal-card title-card">
<h1 id="end-title">GAME OVER</h1>
<p id="end-message" style="color: #cbd5e1; font-size: 15px;"></p>
<button class="start-btn" id="btn-restart-game">RESPAWN AT SAVE SHRINE</button>
</div>
</div>
</div>
<script>
/* ==========================================================================
1. AUDIO SYNTHESIZER (Web Audio API)
========================================================================== */
class SoundEngine {
constructor() {
this.ctx = null;
this.enabled = true;
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
this.ctx = new AudioContext();
}
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
play(type) {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
switch(type) {
case 'step':
osc.type = 'triangle';
osc.frequency.setValueAtTime(120, t);
osc.frequency.exponentialRampToValueAtTime(40, t + 0.08);
gain.gain.setValueAtTime(0.08, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.08);
osc.start(t);
osc.stop(t + 0.08);
break;
case 'swing':
osc.type = 'sine';
osc.frequency.setValueAtTime(280, t);
osc.frequency.exponentialRampToValueAtTime(80, t + 0.15);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
osc.start(t);
osc.stop(t + 0.15);
break;
case 'hit':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(180, t);
osc.frequency.exponentialRampToValueAtTime(40, t + 0.12);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
osc.start(t);
osc.stop(t + 0.12);
break;
case 'bow':
osc.type = 'sine';
osc.frequency.setValueAtTime(400, t);
osc.frequency.exponentialRampToValueAtTime(900, t + 0.1);
gain.gain.setValueAtTime(0.18, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.12);
osc.start(t);
osc.stop(t + 0.12);
break;
case 'pickup':
osc.type = 'sine';
osc.frequency.setValueAtTime(523.25, t); // C5
osc.frequency.setValueAtTime(659.25, t + 0.08); // E5
osc.frequency.setValueAtTime(783.99, t + 0.16); // G5
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.28);
osc.start(t);
osc.stop(t + 0.28);
break;
case 'coin':
osc.type = 'sine';
osc.frequency.setValueAtTime(987.77, t); // B5
osc.frequency.setValueAtTime(1318.51, t + 0.06); // E6
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.18);
osc.start(t);
osc.stop(t + 0.18);
break;
case 'shop':
osc.type = 'triangle';
osc.frequency.setValueAtTime(440, t);
osc.frequency.setValueAtTime(554.37, t + 0.1);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.22);
osc.start(t);
osc.stop(t + 0.22);
break;
case 'save':
osc.type = 'sine';
osc.frequency.setValueAtTime(329.63, t); // E4
osc.frequency.setValueAtTime(440, t + 0.12); // A4
osc.frequency.setValueAtTime(554.37, t + 0.24); // C#5
osc.frequency.setValueAtTime(659.25, t + 0.36); // E5
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.5);
osc.start(t);
osc.stop(t + 0.5);
break;
case 'hurt':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(120, t);
osc.frequency.linearRampToValueAtTime(50, t + 0.2);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
osc.start(t);
osc.stop(t + 0.2);
break;
case 'boss_defeat':
osc.type = 'square';
osc.frequency.setValueAtTime(200, t);
osc.frequency.exponentialRampToValueAtTime(800, t + 0.4);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.6);
osc.start(t);
osc.stop(t + 0.6);
break;
case 'dash':
osc.type = 'sine';
osc.frequency.setValueAtTime(300, t);
osc.frequency.exponentialRampToValueAtTime(150, t + 0.1);
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
osc.start(t);
osc.stop(t + 0.1);
break;
}
}
}
const sound = new SoundEngine();
/* ==========================================================================
2. GAME STATE & CONSTANTS
========================================================================== */
const CANVAS_WIDTH = 960;
const CANVAS_HEIGHT = 640;
const MAP_SIZE = 2400; // 2400x2400 world
// Game Worlds / Zones
const ZONES = {
VILLAGE: { name: "Oakhaven Village", bg: "#1e293b", floor: "#283548", wall: "#0f172a", boundary: { x: 0, y: 0, w: 1200, h: 1200 } },
FOREST: { name: "Whispering Woods", bg: "#064e3b", floor: "#065f46", wall: "#022c22", boundary: { x: 1200, y: 0, w: 1200, h: 1200 } },
TEMPLE: { name: "Sunken Temple", bg: "#1e1b4b", floor: "#312e81", wall: "#0f172a", boundary: { x: 0, y: 1200, w: 1200, h: 1200 } },
DUNGEON: { name: "Dread Caverns", bg: "#450a0a", floor: "#7f1d1d", wall: "#290606", boundary: { x: 1200, y: 1200, w: 1200, h: 1200 } }
};
// Weapon Definitions
const MELEE_WEAPONS = [
{ id: 'dagger', name: "Rusty Dagger", dmg: 2, range: 45, arc: Math.PI * 0.5, cooldown: 250, color: '#cbd5e1', icon: '🗡️' },
{ id: 'broadsword', name: "Knight's Broadsword", dmg: 4, range: 65, arc: Math.PI * 0.75, cooldown: 350, color: '#38bdf8', icon: '⚔️' },
{ id: 'flameblade', name: "Rune Flameblade", dmg: 7, range: 85, arc: Math.PI * 0.9, cooldown: 280, color: '#f97316', icon: '🔥' }
];
const RANGED_WEAPON = { id: 'gale_bow', name: "Gale Bow", dmg: 3, speed: 12, cooldown: 400, color: '#34d399', icon: '🏹' };
// Shop Catalog
const SHOP_ITEMS = [
{ id: 'health_potion', name: "Health Potion", cost: 15, icon: '🧪', desc: "Restores 2 HP immediately.", type: 'consumable' },
{ id: 'broadsword_upgrade', name: "Knight's Broadsword", cost: 40, icon: '⚔️', desc: "Melee Upgrade 1: Increased range, higher cleave damage.", type: 'upgrade_melee', reqLevel: 1 },
{ id: 'flameblade_upgrade', name: "Rune Flameblade", cost: 100, icon: '🔥', desc: "Melee Upgrade 2: High damage flame arcs that burn foes.", type: 'upgrade_melee', reqLevel: 2 },
{ id: 'swiftness_ring', name: "Ring of Swiftness", cost: 35, icon: '💍', desc: "Increases movement speed by 25%.", type: 'accessory' },
{ id: 'amulet_vitality', name: "Amulet of Vitality", cost: 60, icon: '📿', desc: "Permanently adds 1 Max HP Heart Slot (up to 5 HP).", type: 'accessory_hp' },
{ id: 'power_elixir', name: "Elixir of Might", cost: 50, icon: '🍷', desc: "+2 Attack Damage bonus to all attacks.", type: 'accessory_dmg' }
];
/* Game State Object */
const gameState = {
started: false,
paused: false,
gameOver: false,
victory: false,
// Player Stats
hp: 3, // Current HP (Starts at 3)
maxHp: 3, // Max HP (Starts at 3, max 5)
stamina: 100,
maxStamina: 100,
gold: 0,
// Equipment & Progression
meleeLevel: 0, // 0: Dagger, 1: Broadsword, 2: Flameblade
hasRanged: false, // Discovered halfway through game (Sunken Temple Guardian)
activeWeaponType: 'melee', // 'melee' or 'ranged'
// Inventory
inventory: [],
equippedAccessories: {
swiftness: false,
might: false
},
// World Save
lastSavePoint: { x: 300, y: 300, zone: "Oakhaven Village" },
// Bosses Defeated
bosses: {
templeGuardian: false, // Drops Gale Bow
dreadLord: false // Final Boss
}
};
/* Controls State */
const keys = {};
const mouse = { x: 0, y: 0, worldX: 0, worldY: 0, downLeft: false, downRight: false };
/* ==========================================================================
3. PLAYER CLASS & ENTITIES
========================================================================== */
class Player {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 18;
this.baseSpeed = 3.8;
this.vx = 0;
this.vy = 0;
// Combat timers
this.lastMeleeAttack = 0;
this.lastRangedAttack = 0;
this.invulnerableUntil = 0;
this.isAttackingMelee = false;
this.meleeAttackAngle = 0;
this.meleeAttackProgress = 0; // 0 to 1
// Dash mechanics
this.isDashing = false;
this.dashEndTime = 0;
this.dashCooldown = 0;
// Visual animation
this.facingAngle = 0;
this.walkCycle = 0;
this.isMoving = false;
}
get speed() {
let spd = this.baseSpeed;
if (gameState.equippedAccessories.swiftness) spd *= 1.25;
if (this.isDashing) spd *= 2.5;
return spd;
}
get damageBonus() {
return gameState.equippedAccessories.might ? 2 : 0;
}
update(dt) {
if (gameState.gameOver) return;
// Stamina regen
if (!this.isDashing && gameState.stamina < gameState.maxStamina) {
gameState.stamina = Math.min(gameState.maxStamina, gameState.stamina + 25 * dt);
}
// Handle dash
if (this.isDashing && Date.now() > this.dashEndTime) {
this.isDashing = false;
}
// Movement input
let dx = 0;
let dy = 0;
if (keys['KeyW'] || keys['ArrowUp'] || keys['up']) dy -= 1;
if (keys['KeyS'] || keys['ArrowDown'] || keys['down']) dy += 1;
if (keys['KeyA'] || keys['ArrowLeft'] || keys['left']) dx -= 1;
if (keys['KeyD'] || keys['ArrowRight'] || keys['right']) dx += 1;
if (dx !== 0 && dy !== 0) {
dx *= 0.7071;
dy *= 0.7071;
}
this.isMoving = dx !== 0 || dy !== 0;
// Dash trigger (C key or Dash button)
if ((keys['KeyC'] || keys['dash']) && !this.isDashing && gameState.stamina >= 30 && Date.now() > this.dashCooldown) {
this.isDashing = true;
this.dashEndTime = Date.now() + 200;
this.dashCooldown = Date.now() + 600;
gameState.stamina -= 30;
sound.play('dash');
// Spawn dash dust particles
for (let i = 0; i < 8; i++) {
particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 3, (Math.random() - 0.5) * 3, '#94a3b8', 12, 4));
}
}
// Move player
const moveDist = this.speed * dt * 60;
let newX = this.x + dx * moveDist;
let newY = this.y + dy * moveDist;
// Collision with world boundaries & obstacles
if (!checkWorldCollision(newX, this.y, this.radius)) this.x = newX;
if (!checkWorldCollision(this.x, newY, this.radius)) this.y = newY;
// Update facing angle towards mouse
this.facingAngle = Math.atan2(mouse.worldY - this.y, mouse.worldX - this.x);
// Walking audio feedback & animation
if (this.isMoving) {
this.walkCycle += dt * 10;
if (Math.sin(this.walkCycle) > 0.8 && Math.random() < 0.2) {
sound.play('step');
}
}
// Handle Attacks
if (mouse.downLeft || keys['Space'] || keys['attack']) {
this.performMeleeAttack();
} else if ((mouse.downRight || keys['ShiftLeft'] || keys['ranged']) && gameState.hasRanged) {
this.performRangedAttack();
}
// Update Melee Swing animation state
if (this.isAttackingMelee) {
const w = MELEE_WEAPONS[gameState.meleeLevel];
const elapsed = Date.now() - this.lastMeleeAttack;
this.meleeAttackProgress = Math.min(1, elapsed / (w.cooldown * 0.6));
if (this.meleeAttackProgress >= 1) {
this.isAttackingMelee = false;
}
}
}
performMeleeAttack() {
const weapon = MELEE_WEAPONS[gameState.meleeLevel];
if (Date.now() - this.lastMeleeAttack < weapon.cooldown) return;
this.lastMeleeAttack = Date.now();
this.isAttackingMelee = true;
this.meleeAttackAngle = this.facingAngle;
this.meleeAttackProgress = 0;
sound.play('swing');
const totalDmg = weapon.dmg + this.damageBonus;
// Check hits on enemies
enemies.forEach(enemy => {
const dist = Math.hypot(enemy.x - this.x, enemy.y - this.y);
if (dist <= weapon.range + enemy.radius) {
const angleToEnemy = Math.atan2(enemy.y - this.y, enemy.x - this.x);
let diff = Math.abs(this.meleeAttackAngle - angleToEnemy);
while (diff > Math.PI) diff -= Math.PI * 2;
diff = Math.abs(diff);
if (diff <= weapon.arc / 2) {
enemy.takeDamage(totalDmg, this.facingAngle);
// Flameblade burn/fire effect particles
if (weapon.id === 'flameblade') {
for (let i = 0; i < 6; i++) {
particles.push(new Particle(enemy.x, enemy.y, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 5, '#f97316', 15, 5));
}
}
}
}
});
// Break breakables (pots/chests)
breakables.forEach(b => {
const dist = Math.hypot(b.x - this.x, b.y - this.y);
if (dist <= weapon.range + b.radius) {
b.destroy();
}
});
}
performRangedAttack() {
if (!gameState.hasRanged) return;
if (Date.now() - this.lastRangedAttack < RANGED_WEAPON.cooldown) return;
this.lastRangedAttack = Date.now();
sound.play('bow');
const angle = this.facingAngle;
projectiles.push(new Projectile(
this.x + Math.cos(angle) * 20,
this.y + Math.sin(angle) * 20,
Math.cos(angle) * RANGED_WEAPON.speed,
Math.sin(angle) * RANGED_WEAPON.speed,
RANGED_WEAPON.dmg + this.damageBonus,
true
));
}
takeDamage(amount) {
if (Date.now() < this.invulnerableUntil || this.isDashing) return;
gameState.hp = Math.max(0, gameState.hp - amount);
this.invulnerableUntil = Date.now() + 800; // 0.8s i-frames
sound.play('hurt');
screenShake = 12;
// Hurt Particles
for (let i = 0; i < 10; i++) {
particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, '#ef4444', 20, 4));
}
updateHUD();
if (gameState.hp <= 0) {
triggerGameOver(false);
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
// Flash during i-frames
if (Date.now() < this.invulnerableUntil && Math.floor(Date.now() / 80) % 2 === 0) {
ctx.globalAlpha = 0.5;
}
// Shadow
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
ctx.beginPath();
ctx.ellipse(0, 14, 16, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Walking tilt/animation
const walkOffset = this.isMoving ? Math.sin(this.walkCycle * 2) * 3 : 0;
// Body (Adventurer Coat)
ctx.rotate(this.facingAngle);
// Armor Visual Feedback based on equipped accessories
ctx.fillStyle = gameState.equippedAccessories.might ? '#b91c1c' : (gameState.equippedAccessories.swiftness ? '#047857' : '#2563eb');
ctx.beginPath();
ctx.arc(0, walkOffset, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = '#1e293b';
ctx.stroke();
// Head / Helmet
ctx.fillStyle = '#fde047';
ctx.beginPath();
ctx.arc(4, walkOffset, 8, 0, Math.PI * 2);
ctx.fill();
// Visor / Eyes
ctx.fillStyle = '#0f172a';
ctx.fillRect(8, walkOffset - 3, 3, 6);
// Draw Weapon Visual Representation
const currentWeapon = MELEE_WEAPONS[gameState.meleeLevel];
ctx.fillStyle = currentWeapon.color;
if (this.isAttackingMelee) {
// Swing Arc Effect
const swingAngle = (this.meleeAttackProgress - 0.5) * currentWeapon.arc;
ctx.save();
ctx.rotate(swingAngle);
ctx.fillStyle = currentWeapon.color;
ctx.beginPath();
ctx.moveTo(10, 0);
ctx.lineTo(currentWeapon.range, -10);
ctx.lineTo(currentWeapon.range + 10, 0);
ctx.lineTo(currentWeapon.range, 10);
ctx.closePath();
ctx.fill();
// Arc slash visual streak
ctx.strokeStyle = currentWeapon.color;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, currentWeapon.range, -currentWeapon.arc / 2, currentWeapon.arc / 2);
ctx.stroke();
ctx.restore();
} else {
// Weapon Holstered / Ready in hand
ctx.fillRect(12, 10 + walkOffset, currentWeapon.range * 0.4, 4);
}
// Ranged Bow Visual if held
if (gameState.hasRanged) {
ctx.strokeStyle = '#10b981';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(8, -10 + walkOffset, 12, -Math.PI * 0.4, Math.PI * 0.4);
ctx.stroke();
}
ctx.restore();
}
}
/* ==========================================================================
4. ENEMY CLASSES & AI
========================================================================== */
class Enemy {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type; // 'goblin', 'skeleton', 'guardian_boss', 'dread_lord'
this.isBoss = type.includes('boss') || type.includes('lord');
// Configure enemy stats
switch(type) {
case 'goblin':
this.hp = 6;
this.maxHp = 6;
this.radius = 16;
this.speed = 2.2;
this.damage = 1;
this.color = '#22c55e';
this.goldReward = 8;
break;
case 'skeleton':
this.hp = 12;
this.maxHp = 12;
this.radius = 18;
this.speed = 1.8;
this.damage = 1;
this.color = '#cbd5e1';
this.goldReward = 15;
break;
case 'guardian_boss': // Mid-game Boss (Sunken Temple)
this.hp = 65;
this.maxHp = 65;
this.radius = 36;
this.speed = 1.4;
this.damage = 2;
this.color = '#818cf8';
this.goldReward = 80;
break;
case 'dread_lord': // Final Boss (Dread Caverns)
this.hp = 120;
this.maxHp = 120;
this.radius = 42;
this.speed = 1.6;
this.damage = 2;
this.color = '#ef4444';
this.goldReward = 200;
break;
}
this.vx = 0;
this.vy = 0;
this.lastAttackTime = 0;
this.attackCooldown = this.isBoss ? 1200 : 1000;
this.hurtTimer = 0;
}
update(dt, player) {
if (this.hurtTimer > 0) this.hurtTimer -= dt;
const dist = Math.hypot(player.x - this.x, player.y - this.y);
// Aggro & Chasing AI
const aggroRange = this.isBoss ? 600 : 320;
if (dist < aggroRange) {
const angle = Math.atan2(player.y - this.y, player.x - this.x);
// Move towards player
if (dist > this.radius + player.radius - 5) {
let moveX = Math.cos(angle) * this.speed * dt * 60;
let moveY = Math.sin(angle) * this.speed * dt * 60;
if (!checkWorldCollision(this.x + moveX, this.y, this.radius)) this.x += moveX;
if (!checkWorldCollision(this.x, this.y + moveY, this.radius)) this.y += moveY;
}
// Melee attack player
if (dist <= this.radius + player.radius + 8) {
if (Date.now() - this.lastAttackTime > this.attackCooldown) {
this.lastAttackTime = Date.now();
player.takeDamage(this.damage);
}
}
// Boss Ranged Attacks
if (this.isBoss && Math.random() < 0.015 && Date.now() - this.lastAttackTime > 800) {
this.lastAttackTime = Date.now();
projectiles.push(new Projectile(
this.x, this.y,
Math.cos(angle) * 6,
Math.sin(angle) * 6,
this.damage,
false
));
}
}
}
takeDamage(amount, knockbackAngle) {
this.hp -= amount;
this.hurtTimer = 0.2;
sound.play('hit');
// Knockback
if (!this.isBoss) {
this.x += Math.cos(knockbackAngle) * 15;
this.y += Math.sin(knockbackAngle) * 15;
}
// Floating Damage Number
damageNumbers.push(new DamageNumber(this.x, this.y - 15, amount));
// Impact particles
for (let i = 0; i < 5; i++) {
particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, this.color, 12, 3));
}
if (this.hp <= 0) {
this.die();
}
}
die() {
// Drop Gold
const coinsCount = Math.floor(this.goldReward / 5);
for (let i = 0; i < coinsCount; i++) {
pickups.push(new Pickup(
this.x + (Math.random() - 0.5) * 30,
this.y + (Math.random() - 0.5) * 30,
'gold',
5
));
}
// Mid-game Boss Defeated: Unlocks Gale Bow!
if (this.type === 'guardian_boss') {
gameState.bosses.templeGuardian = true;
gameState.hasRanged = true;
sound.play('boss_defeat');
showBanner("🔑 GUARDIAN DEFEATED! UNLOCKED: GALE BOW 🏹");
document.getElementById('ranged-slot').style.opacity = '1';
document.getElementById('ranged-icon').textContent = RANGED_WEAPON.icon;
updateHUD();
}
// Final Boss Defeated: Game Victory!
if (this.type === 'dread_lord') {
gameState.bosses.dreadLord = true;
sound.play('boss_defeat');
triggerGameOver(true);
}
// Death Explosion particles
for (let i = 0; i < 16; i++) {
particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 8, (Math.random() - 0.5) * 8, this.color, 25, 5));
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
if (this.hurtTimer > 0) {
ctx.fillStyle = '#ffffff';
} else {
ctx.fillStyle = this.color;
}
// Shadow
ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
ctx.beginPath();
ctx.ellipse(0, this.radius * 0.8, this.radius, this.radius * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
// Enemy Body
ctx.fillStyle = this.hurtTimer > 0 ? '#ffffff' : this.color;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = '#0f172a';
ctx.stroke();
// Boss crown/details
if (this.isBoss) {
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.moveTo(-12, -this.radius);
ctx.lineTo(-6, -this.radius - 10);
ctx.lineTo(0, -this.radius - 4);
ctx.lineTo(6, -this.radius - 10);
ctx.lineTo(12, -this.radius);
ctx.closePath();
ctx.fill();
}
// Enemy Health Bar (if damaged)
if (this.hp < this.maxHp && !this.isBoss) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(-20, -this.radius - 12, 40, 6);
ctx.fillStyle = '#ef4444';
ctx.fillRect(-20, -this.radius - 12, (this.hp / this.maxHp) * 40, 6);
}
ctx.restore();
}
}
/* ==========================================================================
5. PROJECTILES, PICKUPS & ENVIRONMENT
========================================================================== */
class Projectile {
constructor(x, y, vx, vy, damage, isPlayer) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.damage = damage;
this.isPlayer = isPlayer;
this.radius = isPlayer ? 5 : 7;
this.life = 120; // 2 seconds lifespan
}
update(dt) {
this.x += this.vx * dt * 60;
this.y += this.vy * dt * 60;
this.life--;
// Collision with walls
if (checkWorldCollision(this.x, this.y, this.radius)) {
this.life = 0;
}
// Collision with targets
if (this.isPlayer) {
enemies.forEach(enemy => {
if (Math.hypot(enemy.x - this.x, enemy.y - this.y) < enemy.radius + this.radius) {
enemy.takeDamage(this.damage, Math.atan2(this.vy, this.vx));
this.life = 0;
}
});
} else {
if (Math.hypot(player.x - this.x, player.y - this.y) < player.radius + this.radius) {
player.takeDamage(this.damage);
this.life = 0;
}
}
}
draw(ctx) {
ctx.save();
ctx.fillStyle = this.isPlayer ? '#34d399' : '#f87171';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class Pickup {
constructor(x, y, type, value = 1) {
this.x = x;
this.y = y;
this.type = type; // 'gold', 'heart'
this.value = value;
this.radius = 10;
this.bobble = Math.random() * Math.PI * 2;
}
update(dt, player) {
this.bobble += dt * 4;
const dist = Math.hypot(player.x - this.x, player.y - this.y);
// Magnet attraction to player
if (dist < 80) {
this.x += (player.x - this.x) * 0.1;
this.y += (player.y - this.y) * 0.1;
}
if (dist < player.radius + this.radius) {
if (this.type === 'gold') {
gameState.gold += this.value;
sound.play('coin');
} else if (this.type === 'heart') {
gameState.hp = Math.min(gameState.maxHp, gameState.hp + 2);
sound.play('pickup');
}
updateHUD();
return true; // Destroyed
}
return false;
}
draw(ctx) {
ctx.save();
const yOffset = Math.sin(this.bobble) * 4;
ctx.translate(this.x, this.y + yOffset);
if (this.type === 'gold') {
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(0, 0, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#b45309';
ctx.lineWidth = 2;
ctx.stroke();
} else {
ctx.fillStyle = '#ef4444';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('❤️', 0, 4);
}
ctx.restore();
}
}
class Breakable {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 16;
}
destroy() {
sound.play('hit');
// Drop gold or heart
if (Math.random() < 0.7) {
pickups.push(new Pickup(this.x, this.y, 'gold', 5));
} else {
pickups.push(new Pickup(this.x, this.y, 'heart', 2));
}
for (let i = 0; i < 8; i++) {
particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 5, '#b45309', 15, 4));
}
const idx = breakables.indexOf(this);
if (idx > -1) breakables.splice(idx, 1);
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.fillStyle = '#78350f';
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.lineWidth = 3;
ctx.strokeStyle = '#451a03';
ctx.stroke();
ctx.restore();
}
}
class SaveShrine {
constructor(x, y, name) {
this.x = x;
this.y = y;
this.name = name;
this.radius = 24;
}
activate() {
gameState.lastSavePoint = { x: this.x, y: this.y, zone: this.name };
gameState.hp = gameState.maxHp; // Heal to full on save!
updateHUD();
sound.play('save');
showBanner(`✨ SAVED & HEALED AT ${this.name.toUpperCase()} SHRINE ✨`);
for (let i = 0; i < 20; i++) {
particles.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6, '#38bdf8', 30, 4));
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
// Shrine Base
ctx.fillStyle = '#334155';
ctx.fillRect(-20, -20, 40, 40);
// Glowing Orb
const glow = Math.sin(Date.now() / 300) * 3 + 12;
ctx.fillStyle = '#38bdf8';
ctx.beginPath();
ctx.arc(0, -10, glow, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
/* Particles & Floating Damage Text */
class Particle {
constructor(x, y, vx, vy, color, life, size) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.color = color;
this.maxLife = life;
this.life = life;
this.size = size;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life--;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = Math.max(0, this.life / this.maxLife);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class DamageNumber {
constructor(x, y, text) {
this.x = x;
this.y = y;
this.text = text;
this.life = 40;
}
update() {
this.y -= 0.8;
this.life--;
}
draw(ctx) {
ctx.save();
ctx.globalAlpha = Math.max(0, this.life / 40);
ctx.font = 'bold 16px sans-serif';
ctx.fillStyle = '#fbbf24';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 3;
ctx.strokeText(this.text, this.x, this.y);
ctx.fillText(this.text, this.x, this.y);
ctx.restore();
}
}
/* ==========================================================================
6. WORLD INSTANTIATION & COLLISION
========================================================================== */
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let player = new Player(300, 300);
let enemies = [];
let projectiles = [];
let pickups = [];
let breakables = [];
let saveShrines = [];
let particles = [];
let damageNumbers = [];
let worldObstacles = [];
let screenShake = 0;
function initWorld() {
enemies = [];
projectiles = [];
pickups = [];
breakables = [];
saveShrines = [];
particles = [];
damageNumbers = [];
worldObstacles = [];
// Save Shrines across regions
saveShrines.push(new SaveShrine(300, 200, "Village"));
saveShrines.push(new SaveShrine(1500, 200, "Whispering Forest"));
saveShrines.push(new SaveShrine(300, 1400, "Sunken Temple"));
saveShrines.push(new SaveShrine(1500, 1400, "Dread Caverns"));
// Obstacles / Trees / Rocks
// Village Fence / River boundaries
worldObstacles.push({ x: 1180, y: 0, w: 40, h: 2400 }); // Zone divider vertical
worldObstacles.push({ x: 0, y: 1180, w: 2400, h: 40 }); // Zone divider horizontal
// Add gaps in zone dividers for doorways/bridges
worldObstacles.push({ x: 600, y: 400, w: 80, h: 80 }); // Village trees/houses
worldObstacles.push({ x: 400, y: 800, w: 100, h: 100 });
// Populate Enemies per Zone
// Forest (Zone 2)
for (let i = 0; i < 14; i++) {
enemies.push(new Enemy(1300 + Math.random() * 900, 100 + Math.random() * 900, 'goblin'));
breakables.push(new Breakable(1300 + Math.random() * 900, 100 + Math.random() * 900));
}
// Temple (Zone 3 - Mid Game)
for (let i = 0; i < 12; i++) {
enemies.push(new Enemy(100 + Math.random() * 900, 1300 + Math.random() * 900, 'skeleton'));
breakables.push(new Breakable(100 + Math.random() * 900, 1300 + Math.random() * 900));
}
// Mid-game Boss: Temple Guardian (Guardians Gale Bow)
enemies.push(new Enemy(600, 1800, 'guardian_boss'));
// Dungeon (Zone 4 - End Game)
for (let i = 0; i < 15; i++) {
enemies.push(new Enemy(1300 + Math.random() * 900, 1300 + Math.random() * 900, Math.random() < 0.5 ? 'goblin' : 'skeleton'));
breakables.push(new Breakable(1300 + Math.random() * 900, 1300 + Math.random() * 900));
}
// Final Boss: Dread Lord
enemies.push(new Enemy(1800, 1800, 'dread_lord'));
}
function checkWorldCollision(x, y, radius) {
if (x - radius < 0 || x + radius > MAP_SIZE || y - radius < 0 || y + radius > MAP_SIZE) {
return true;
}
for (let obs of worldObstacles) {
// Doorway passages
if (obs.x === 1180 && (y > 500 && y < 700 || y > 1700 && y < 1900)) continue;
if (obs.y === 1180 && (x > 500 && x < 700 || x > 1700 && x < 1900)) continue;
if (x + radius > obs.x && x - radius < obs.x + obs.w &&
y + radius > obs.y && y - radius < obs.y + obs.h) {
return true;
}
}
return false;
}
/* ==========================================================================
7. UI, HUD & SHOP CONTROLLER
========================================================================== */
function updateHUD() {
// Render Heart Shapes
// Requirement: Health tracked visually using shapes/symbols (hearts). Each heart = 2 HP.
// Starts at 3 HP (1.5 hearts), upgradeable to 5 HP (2.5 hearts).
const heartsContainer = document.getElementById('hearts-wrapper');
heartsContainer.innerHTML = '';
const totalHearts = Math.ceil(gameState.maxHp / 2);
let currentHp = gameState.hp;
for (let i = 0; i < totalHearts; i++) {
const heartSvg = document.createElement('div');
heartSvg.className = 'heart-icon';
if (currentHp >= 2) {
// Full Heart (2 points)
heartSvg.innerHTML = `<svg viewBox="0 0 32 32" fill="#ef4444"><path d="M16 28s-12-7.5-12-16a7 7 0 0 1 12-4.5A7 7 0 0 1 28 12c0 8.5-12 16-12 16z"/></svg>`;
currentHp -= 2;
} else if (currentHp === 1) {
// Half Heart (1 point)
heartSvg.innerHTML = `<svg viewBox="0 0 32 32"><defs><linearGradient id="half"><stop offset="50%" stop-color="#ef4444"/><stop offset="50%" stop-color="#334155"/></linearGradient></defs><path fill="url(#half)" d="M16 28s-12-7.5-12-16a7 7 0 0 1 12-4.5A7 7 0 0 1 28 12c0 8.5-12 16-12 16z"/></svg>`;
currentHp -= 1;
} else {
// Empty Heart (0 points)
heartSvg.innerHTML = `<svg viewBox="0 0 32 32" fill="#334155"><path d="M16 28s-12-7.5-12-16a7 7 0 0 1 12-4.5A7 7 0 0 1 28 12c0 8.5-12 16-12 16z"/></svg>`;
}
heartsContainer.appendChild(heartSvg);
}
// Stamina Bar
const staminaPct = (gameState.stamina / gameState.maxStamina) * 100;
document.getElementById('stamina-fill').style.width = `${staminaPct}%`;
// Gold Count
document.getElementById('gold-count').textContent = gameState.gold;
// Melee Weapon Icon
const currentWeapon = MELEE_WEAPONS[gameState.meleeLevel];
document.getElementById('melee-icon').textContent = currentWeapon.icon;
}
function showBanner(text) {
const banner = document.getElementById('banner-notify');
banner.textContent = text;
banner.classList.add('show');
setTimeout(() => banner.classList.remove('show'), 3000);
}
function openShop() {
gameState.paused = true;
sound.play('shop');
const shopContainer = document.getElementById('shop-items-container');
shopContainer.innerHTML = '';
SHOP_ITEMS.forEach(item => {
const card = document.createElement('div');
card.className = 'shop-item-card';
// Check if melee upgrade is already purchased or available
let canBuy = gameState.gold >= item.cost;
let isPurchased = false;
if (item.type === 'upgrade_melee') {
if (gameState.meleeLevel >= item.reqLevel) {
isPurchased = true;
canBuy = false;
} else if (gameState.meleeLevel < item.reqLevel - 1) {
canBuy = false; // Must buy previous level first
}
} else if (item.type === 'accessory' && gameState.equippedAccessories.swiftness) {
isPurchased = true;
canBuy = false;
} else if (item.type === 'accessory_dmg' && gameState.equippedAccessories.might) {
isPurchased = true;
canBuy = false;
} else if (item.type === 'accessory_hp' && gameState.maxHp >= 5) {
isPurchased = true;
canBuy = false;
}
card.innerHTML = `
<div class="item-icon">${item.icon}</div>
<div class="item-name">${item.name}</div>
<div class="item-desc">${item.desc}</div>
<div class="item-price">🪙 ${item.cost} Gold</div>
<button class="buy-btn" ${!canBuy || isPurchased ? 'disabled' : ''}>
${isPurchased ? 'OWNED' : 'BUY'}
</button>
`;
const btn = card.querySelector('.buy-btn');
if (canBuy && !isPurchased) {
btn.addEventListener('click', () => {
gameState.gold -= item.cost;
sound.play('coin');
if (item.type === 'consumable') {
gameState.inventory.push(item);
} else if (item.type === 'upgrade_melee') {
gameState.meleeLevel = item.reqLevel;
showBanner(`🗡️ WEAPON EVOLVED: ${MELEE_WEAPONS[gameState.meleeLevel].name}!`);
} else if (item.type === 'accessory') {
gameState.equippedAccessories.swiftness = true;
gameState.inventory.push(item);
showBanner(`⚡ EQUIPPED: Ring of Swiftness!`);
} else if (item.type === 'accessory_dmg') {
gameState.equippedAccessories.might = true;
gameState.inventory.push(item);
showBanner(`🍷 ELIXIR ABSORBED: +2 Attack Power!`);
} else if (item.type === 'accessory_hp') {
// Health Expansion: Max HP increased to 5 total points!
gameState.maxHp = Math.min(5, gameState.maxHp + 2); // Add +2 HP (1 Heart) up to 5 total
gameState.hp = gameState.maxHp;
gameState.inventory.push(item);
showBanner(`💖 MAX HEALTH INCREASED TO ${gameState.maxHp} HP!`);
}
updateHUD();
openShop(); // Refresh shop display
});
}
shopContainer.appendChild(card);
});
document.getElementById('modal-shop').style.display = 'flex';
}
function openInventory() {
gameState.paused = true;
const invContainer = document.getElementById('inventory-items-container');
invContainer.innerHTML = '';
if (gameState.inventory.length === 0) {
invContainer.innerHTML = '<div style="color:#94a3b8; font-size:13px; grid-column: 1/-1;">Your satchel is currently empty. Visit the village shop or explore the world to acquire items!</div>';
} else {
gameState.inventory.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'inventory-item-card';
card.innerHTML = `
<div class="item-icon">${item.icon}</div>
<div class="item-name">${item.name}</div>
<div class="item-desc">${item.desc}</div>
${item.type === 'consumable' ? `<button class="use-btn">USE</button>` : `<div style="font-size:10px; color:#10b981; font-weight:bold; margin-top: auto;">ACTIVE</div>`}
`;
if (item.type === 'consumable') {
card.querySelector('.use-btn').addEventListener('click', () => {
gameState.hp = Math.min(gameState.maxHp, gameState.hp + 2);
gameState.inventory.splice(index, 1);
sound.play('pickup');
updateHUD();
openInventory();
});
}
invContainer.appendChild(card);
});
}
document.getElementById('modal-inventory').style.display = 'flex';
}
/* Close Modals */
document.getElementById('btn-close-shop').addEventListener('click', () => {
document.getElementById('modal-shop').style.display = 'none';
gameState.paused = false;
});
document.getElementById('btn-close-inventory').addEventListener('click', () => {
document.getElementById('modal-inventory').style.display = 'none';
gameState.paused = false;
});
/* Game Over / Restart Handler */
function triggerGameOver(isVictory) {
gameState.gameOver = true;
const modal = document.getElementById('modal-end');
const title = document.getElementById('end-title');
const msg = document.getElementById('end-message');
const btn = document.getElementById('btn-restart-game');
if (isVictory) {
title.textContent = "🏆 VICTORY ATTAINED!";
title.style.color = "#38ef7d";
msg.textContent = "You have vanquished the Dread Lord and cleansed the realm of Aethelgard! Your courage and legend will be remembered for generations.";
btn.textContent = "PLAY AGAIN";
} else {
title.textContent = "💀 YOU HAVE FALLEN";
title.style.color = "#ef4444";
msg.textContent = "The darkness overcame your strength. Respawn at your last saved shrine with restored vitality.";
btn.textContent = "RESPAWN AT SHRINE";
}
modal.style.display = 'flex';
}
document.getElementById('btn-restart-game').addEventListener('click', () => {
document.getElementById('modal-end').style.display = 'none';
gameState.gameOver = false;
gameState.hp = gameState.maxHp;
player.x = gameState.lastSavePoint.x;
player.y = gameState.lastSavePoint.y;
updateHUD();
});
/* ==========================================================================
8. MAIN GAME LOOP & INPUT LISTENERS
========================================================================== */
let lastTime = performance.now();
function gameLoop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
if (gameState.started && !gameState.paused && !gameState.gameOver) {
updateGame(dt);
}
renderGame();
requestAnimationFrame(gameLoop);
}
function updateGame(dt) {
player.update(dt);
// Update Projectiles
for (let i = projectiles.length - 1; i >= 0; i--) {
projectiles[i].update(dt);
if (projectiles[i].life <= 0) projectiles.splice(i, 1);
}
// Update Enemies
let activeBoss = null;
for (let i = enemies.length - 1; i >= 0; i--) {
enemies[i].update(dt, player);
if (enemies[i].isBoss && Math.hypot(enemies[i].x - player.x, enemies[i].y - player.y) < 600) {
activeBoss = enemies[i];
}
if (enemies[i].hp <= 0) enemies.splice(i, 1);
}
// Update Boss HUD
const bossHud = document.getElementById('boss-bar-container');
if (activeBoss) {
bossHud.style.display = 'flex';
document.getElementById('boss-name-text').textContent = activeBoss.type === 'guardian_boss' ? 'TEMPLE GUARDIAN GOLEM' : 'DREAD LORD OF AETHELGARD';
document.getElementById('boss-bar-fill').style.width = `${(activeBoss.hp / activeBoss.maxHp) * 100}%`;
} else {
bossHud.style.display = 'none';
}
// Update Pickups
for (let i = pickups.length - 1; i >= 0; i--) {
if (pickups[i].update(dt, player)) pickups.splice(i, 1);
}
// Update Particles & Floating Text
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].life <= 0) particles.splice(i, 1);
}
for (let i = damageNumbers.length - 1; i >= 0; i--) {
damageNumbers[i].update();
if (damageNumbers[i].life <= 0) damageNumbers.splice(i, 1);
}
// Interactions (Save Shrine & Shop)
let interactionAvailable = false;
let promptText = "";
// Check distance to Village Shop NPC (X: 300, Y: 400)
if (Math.hypot(player.x - 300, player.y - 400) < 50) {
interactionAvailable = true;
promptText = "Open Village Shop & Armory";
if (keys['KeyE']) {
keys['KeyE'] = false;
openShop();
}
}
// Check distance to Save Shrines
saveShrines.forEach(shrine => {
if (Math.hypot(player.x - shrine.x, player.y - shrine.y) < 45) {
interactionAvailable = true;
promptText = `Save Game & Heal (${shrine.name})`;
if (keys['KeyE']) {
keys['KeyE'] = false;
shrine.activate();
}
}
});
const promptEl = document.getElementById('interaction-prompt');
if (interactionAvailable) {
promptEl.style.display = 'flex';
document.getElementById('prompt-text').textContent = promptText;
} else {
promptEl.style.display = 'none';
}
// Screen Shake dampening
if (screenShake > 0) screenShake *= 0.85;
}
function renderGame() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Camera Offset tracking Player
ctx.save();
let camX = player.x - CANVAS_WIDTH / 2;
let camY = player.y - CANVAS_HEIGHT / 2;
// Screen Shake offset
if (screenShake > 0.5) {
camX += (Math.random() - 0.5) * screenShake;
camY += (Math.random() - 0.5) * screenShake;
}
ctx.translate(-camX, -camY);
// Draw World Floor / Zones
Object.values(ZONES).forEach(zone => {
const b = zone.boundary;
ctx.fillStyle = zone.bg;
ctx.fillRect(b.x, b.y, b.w, b.h);
// Tile Grid Lines
ctx.strokeStyle = zone.floor;
ctx.lineWidth = 2;
for (let x = b.x; x < b.x + b.w; x += 100) {
ctx.beginPath();
ctx.moveTo(x, b.y);
ctx.lineTo(x, b.y + b.h);
ctx.stroke();
}
for (let y = b.y; y < b.y + b.h; y += 100) {
ctx.beginPath();
ctx.moveTo(b.x, y);
ctx.lineTo(b.x + b.w, y);
ctx.stroke();
}
});
// Draw Village Shopkeeper NPC
ctx.fillStyle = '#f59e0b';
ctx.beginPath();
ctx.arc(300, 400, 20, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText("SHOP", 300, 375);
// Draw Save Shrines
saveShrines.forEach(s => s.draw(ctx));
// Draw World Obstacles / Walls
ctx.fillStyle = '#0f172a';
worldObstacles.forEach(obs => {
ctx.fillRect(obs.x, obs.y, obs.w, obs.h);
});
// Draw Breakable Containers
breakables.forEach(b => b.draw(ctx));
// Draw Pickups
pickups.forEach(p => p.draw(ctx));
// Draw Projectiles
projectiles.forEach(p => p.draw(ctx));
// Draw Enemies
enemies.forEach(e => e.draw(ctx));
// Draw Player
player.draw(ctx);
// Draw Particles & Floating Numbers
particles.forEach(p => p.draw(ctx));
damageNumbers.forEach(d => d.draw(ctx));
ctx.restore();
}
/* Key & Mouse Event Listeners */
window.addEventListener('keydown', e => {
keys[e.code] = true;
// Inventory Toggle
if (e.code === 'KeyI' || e.code === 'Tab') {
e.preventDefault();
const invModal = document.getElementById('modal-inventory');
if (invModal.style.display === 'flex') {
invModal.style.display = 'none';
gameState.paused = false;
} else {
openInventory();
}
}
});
window.addEventListener('keyup', e => {
keys[e.code] = false;
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
// Convert screen coords to world coords
const camX = player.x - CANVAS_WIDTH / 2;
const camY = player.y - CANVAS_HEIGHT / 2;
mouse.worldX = mouse.x + camX;
mouse.worldY = mouse.y + camY;
});
canvas.addEventListener('mousedown', e => {
sound.init();
if (e.button === 0) mouse.downLeft = true;
if (e.button === 2) mouse.downRight = true;
});
canvas.addEventListener('mouseup', e => {
if (e.button === 0) mouse.downLeft = false;
if (e.button === 2) mouse.downRight = false;
});
canvas.addEventListener('contextmenu', e => e.preventDefault());
// Mobile / Touch controls binding
const setupTouchBtn = (id, keyName) => {
const btn = document.getElementById(id);
if (!btn) return;
btn.addEventListener('touchstart', (e) => { e.preventDefault(); keys[keyName] = true; });
btn.addEventListener('touchend', (e) => { e.preventDefault(); keys[keyName] = false; });
};
setupTouchBtn('btn-up', 'up');
setupTouchBtn('btn-down', 'down');
setupTouchBtn('btn-left', 'left');
setupTouchBtn('btn-right', 'right');
setupTouchBtn('btn-attack', 'attack');
setupTouchBtn('btn-ranged', 'ranged');
setupTouchBtn('btn-dash', 'dash');
// Start Game Button
document.getElementById('btn-start-game').addEventListener('click', () => {
sound.init();
document.getElementById('modal-title').style.display = 'none';
gameState.started = true;
initWorld();
updateHUD();
});
// Start Loop
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.6-flash","provider":"openrouter","persona":"full","sessionId":"70d0e7d4-dd59-4ada-8cd3-7d308c65f166","tokensIn":132561,"tokensOut":27059,"tokensTotal":159620,"turns":6,"toolCalls":5,"failedToolCalls":0,"timestamp":"2026-07-21T19:23:17.537Z"} -->