Warden
Gemini 3.7 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 112,765 bytes · SHA-256 89c5711674ff
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>The Warden's Corrosion — Aurelion's Sunken Observatory</title>
<style>
:root {
--gold: #f4c542;
--gold-glow: rgba(244, 197, 66, 0.4);
--ember: #e65c00;
--solar: #ffe066;
--slate: #1a2332;
--slate-deep: #0d131d;
--corrosion: #090a0f;
--iron: #3a4756;
--aether: #4ee0e6;
--aether-glow: rgba(78, 224, 230, 0.5);
--heart-red: #e63946;
--bg-overlay: rgba(10, 14, 22, 0.88);
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
margin: 0;
padding: 0;
}
body, html {
width: 100%;
height: 100%;
overflow: hidden;
background-color: var(--corrosion);
font-family: 'Cinzel', 'Palatino Linotype', 'Book Antiqua', Palatino, serif;
color: #e2e8f0;
}
#game-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(circle at center, #151d2a 0%, #080a0f 100%);
}
canvas#gameCanvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* HUD */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
padding: 16px 24px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
z-index: 10;
}
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.hud-stats {
display: flex;
flex-direction: column;
gap: 8px;
}
.hearts-container {
display: flex;
gap: 8px;
align-items: center;
}
.heart-box {
width: 28px;
height: 28px;
position: relative;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.8));
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.heart-box.pulse {
transform: scale(1.3);
}
.gold-display {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: bold;
color: var(--gold);
text-shadow: 0 0 10px var(--gold-glow), 0 2px 4px #000;
background: rgba(13, 19, 29, 0.7);
padding: 4px 12px;
border-radius: 20px;
border: 1px solid rgba(244, 197, 66, 0.3);
width: fit-content;
}
.gold-icon {
width: 18px;
height: 18px;
background: radial-gradient(circle, #ffe277 30%, #d49c12 80%);
border-radius: 50%;
display: inline-block;
border: 1px solid #fff399;
box-shadow: 0 0 6px var(--gold);
}
.zone-title-banner {
text-align: right;
pointer-events: none;
}
.zone-name {
font-size: 20px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: #e0e6ed;
text-shadow: 0 2px 8px rgba(0,0,0,0.9), 0 0 12px rgba(244, 197, 66, 0.3);
}
.zone-sub {
font-size: 12px;
letter-spacing: 1px;
color: #94a3b8;
text-transform: uppercase;
}
/* Hotbar / Active Item HUD */
.hud-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
width: 100%;
}
.quick-slots {
display: flex;
gap: 12px;
}
.slot-box {
background: rgba(13, 19, 29, 0.85);
border: 2px solid var(--iron);
border-radius: 8px;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.6);
color: #f1f5f9;
}
.slot-box.active {
border-color: var(--gold);
box-shadow: 0 0 12px var(--gold-glow);
}
.slot-key {
background: #202b3c;
border: 1px solid #475569;
border-radius: 4px;
padding: 2px 6px;
font-size: 11px;
font-weight: bold;
color: #cbd5e1;
}
.slot-label {
font-size: 13px;
font-weight: 600;
}
.slot-count {
color: var(--gold);
font-weight: bold;
}
/* Screen overlay flash */
#screen-flash {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 8;
transition: background 0.15s ease-out;
}
/* Vignette & Shimmer */
#shimmer-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
box-shadow: inset 0 0 100px rgba(0,0,0,0.8);
z-index: 5;
transition: box-shadow 0.5s ease;
}
/* Notification Banner / Toast */
#notification-toast {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(13, 20, 32, 0.95);
border: 2px solid var(--gold);
padding: 12px 30px;
border-radius: 8px;
color: #fff;
font-size: 16px;
font-weight: bold;
letter-spacing: 1.5px;
text-transform: uppercase;
box-shadow: 0 0 25px var(--gold-glow), 0 8px 30px rgba(0,0,0,0.8);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
z-index: 20;
text-align: center;
}
#notification-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* Modal Windows (Inventory, Shop, Dialogues, Start Screen, Death, Victory) */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-overlay);
backdrop-filter: blur(6px);
display: none;
justify-content: center;
align-items: center;
z-index: 30;
pointer-events: auto;
}
.modal-box {
background: #0f1623;
border: 2px solid #334155;
border-radius: 12px;
padding: 28px;
width: 90%;
max-width: 580px;
box-shadow: 0 0 40px rgba(0,0,0,0.9), inset 0 0 20px rgba(244, 197, 66, 0.05);
position: relative;
display: flex;
flex-direction: column;
gap: 18px;
}
.modal-title {
font-size: 22px;
font-weight: 700;
color: var(--gold);
letter-spacing: 2px;
text-transform: uppercase;
text-align: center;
border-bottom: 1px solid rgba(244, 197, 66, 0.25);
padding-bottom: 10px;
}
.btn {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border: 1px solid var(--gold);
color: #f8fafc;
padding: 10px 20px;
font-size: 15px;
font-family: inherit;
font-weight: 600;
letter-spacing: 1px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
text-transform: uppercase;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
background: var(--gold);
color: #0b0f19;
box-shadow: 0 0 15px var(--gold-glow);
transform: translateY(-2px);
}
.btn:disabled {
border-color: #475569;
color: #64748b;
background: #1e293b;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Inventory & Shop Grid */
.grid-list {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
max-height: 360px;
overflow-y: auto;
padding-right: 6px;
}
.item-card {
background: #182233;
border: 1px solid #334155;
border-radius: 8px;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.item-card.equipped {
border-color: var(--gold);
background: #1e2c44;
}
.item-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.item-name {
font-weight: 700;
font-size: 15px;
color: #f1f5f9;
}
.item-desc {
font-size: 12px;
color: #94a3b8;
line-height: 1.3;
}
.item-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background: #334155;
color: #cbd5e1;
width: fit-content;
}
/* Interaction Prompt Prompt overlay in world */
#interaction-prompt {
position: absolute;
bottom: 90px;
left: 50%;
transform: translateX(-50%);
background: rgba(13, 20, 32, 0.9);
border: 1px solid var(--gold);
padding: 6px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
color: #f8fafc;
pointer-events: none;
display: none;
box-shadow: 0 0 15px var(--gold-glow);
z-index: 15;
}
/* Touch controls for mobile/tablets */
#touch-controls {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
display: none;
justify-content: space-between;
pointer-events: none;
z-index: 25;
}
.touch-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(26, 35, 50, 0.7);
border: 2px solid var(--gold);
color: #fff;
font-size: 14px;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
touch-action: manipulation;
}
/* Lore/Dialogue Box */
#dialogue-modal {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
width: 90%;
max-width: 680px;
background: rgba(10, 15, 25, 0.95);
border: 2px solid var(--gold);
border-radius: 8px;
padding: 20px 24px;
color: #e2e8f0;
z-index: 35;
display: none;
box-shadow: 0 0 30px rgba(0,0,0,0.9);
}
#dialogue-title {
font-size: 16px;
color: var(--gold);
font-weight: bold;
margin-bottom: 6px;
letter-spacing: 1px;
text-transform: uppercase;
}
#dialogue-body {
font-size: 14px;
line-height: 1.5;
color: #cbd5e1;
margin-bottom: 12px;
}
#dialogue-close-hint {
font-size: 12px;
color: #64748b;
text-align: right;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="960" height="540"></canvas>
<div id="screen-flash"></div>
<div id="shimmer-overlay"></div>
<!-- HUD -->
<div id="hud">
<div class="hud-top">
<div class="hud-stats">
<div class="hearts-container" id="hearts-container"></div>
<div class="gold-display">
<span class="gold-icon"></span>
<span id="gold-amount">0</span>
</div>
</div>
<div class="zone-title-banner">
<div class="zone-name" id="zone-name">The Ashen Threshold</div>
<div class="zone-sub" id="zone-sub">Zone I — Outer Observatory</div>
</div>
</div>
<div class="hud-bottom">
<div class="quick-slots">
<div class="slot-box" id="slot-weapon">
<span class="slot-key">LMB / J</span>
<span class="slot-label" id="weapon-name">Ember Blade</span>
</div>
<div class="slot-box" id="slot-ranged" style="display:none;">
<span class="slot-key">RMB / K</span>
<span class="slot-label">Aether Shard</span>
<span class="slot-count" id="ammo-count">∞</span>
</div>
<div class="slot-box" id="slot-item1" style="cursor:pointer; pointer-events:auto;" onclick="game.useQuickItem('vitalStem')">
<span class="slot-key">1</span>
<span class="slot-label">Vital Stem: <b id="vital-stem-count">0</b></span>
</div>
<div class="slot-box" id="slot-item2" style="cursor:pointer; pointer-events:auto;" onclick="game.useQuickItem('bindingSeal')">
<span class="slot-key">2</span>
<span class="slot-label">Binding Seal: <b id="binding-seal-count">0</b></span>
</div>
</div>
<div style="pointer-events: auto;">
<button class="btn" style="padding: 6px 14px; font-size: 12px;" onclick="game.toggleInventory()">[I] Inventory</button>
</div>
</div>
</div>
<div id="interaction-prompt">[E] Interact</div>
<div id="notification-toast"></div>
<!-- Dialogue Modal -->
<div id="dialogue-modal" onclick="game.closeDialogue()">
<div id="dialogue-title">Altar of Ancient Knowledge</div>
<div id="dialogue-body">Text goes here...</div>
<div id="dialogue-close-hint">Press [E] or Click to dismiss</div>
</div>
<!-- Inventory Modal -->
<div class="modal-overlay" id="inventory-modal">
<div class="modal-box">
<div class="modal-title">Warden's Satchel & Equipment</div>
<div class="grid-list" id="inventory-list"></div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<span style="font-size:12px; color:#94a3b8;">Click an accessory to equip/unequip, or consumable to use.</span>
<button class="btn" onclick="game.toggleInventory()">Close</button>
</div>
</div>
</div>
<!-- Shop Modal -->
<div class="modal-overlay" id="shop-modal">
<div class="modal-box">
<div class="modal-title">Observatory Altar Merchant</div>
<div class="grid-list" id="shop-list"></div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<div class="gold-display">
<span class="gold-icon"></span>
<span id="shop-gold-amount">0</span>
</div>
<button class="btn" onclick="game.closeShop()">Leave Altar</button>
</div>
</div>
</div>
<!-- Start / Title Screen -->
<div class="modal-overlay" id="title-screen" style="display: flex;">
<div class="modal-box" style="text-align: center; max-width: 640px;">
<h1 style="color: var(--gold); font-size: 32px; letter-spacing: 4px; margin-bottom: 4px; text-shadow: 0 0 15px var(--gold-glow);">THE WARDEN'S CORROSION</h1>
<p style="color: #94a3b8; font-size: 14px; margin-bottom: 20px; font-style: italic;">Aurelion's Sunken Observatory</p>
<p style="font-size: 14px; line-height: 1.6; color: #cbd5e1; margin-bottom: 24px; text-align: left; background: rgba(0,0,0,0.3); padding: 14px; border-radius: 6px; border-left: 3px solid var(--gold);">
The black-iron Corrosion eats through the floating celestial archive. As the <b>Last Warden</b>, you must delve through six crumbling zones, recover forgotten weapon altars, uncover the mid-game <b>Aether Shard</b>, expand your life essence, and reignite the <b>Core Beacon</b>.
</p>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px; text-align: left; font-size: 13px; color: #94a3b8; margin-bottom: 24px;">
<div><b>WASD / Arrows:</b> Move</div>
<div><b>LMB / J / Space:</b> Attack</div>
<div><b>RMB / K:</b> Fire Aether Shard</div>
<div><b>E:</b> Interact / Save / Shop</div>
<div><b>1 / 2:</b> Quick Consumable</div>
<div><b>I:</b> Inventory / Equip Gear</div>
</div>
<button class="btn" style="font-size: 18px; padding: 12px 36px;" onclick="game.startGame()">Awaken Warden</button>
</div>
</div>
<!-- Game Over Screen -->
<div class="modal-overlay" id="game-over-screen">
<div class="modal-box" style="text-align: center;">
<div class="modal-title" style="color: var(--heart-red);">The Warden Has Fallen</div>
<p style="color: #94a3b8; font-size: 14px; margin: 15px 0 25px;">The black corrosion has overcome your celestial essence. Return to your last activated Sanctuary Stone to rekindle the flame.</p>
<button class="btn" onclick="game.respawnAtSanctuary()">Return to Sanctuary Stone</button>
</div>
</div>
<!-- Victory Screen -->
<div class="modal-overlay" id="victory-screen">
<div class="modal-box" style="text-align: center; max-width: 620px;">
<div class="modal-title" style="color: var(--solar); font-size: 28px;">Core Beacon Reignited!</div>
<p style="color: #cbd5e1; font-size: 15px; line-height: 1.6; margin: 18px 0;">
The golden fire surges through the bronze conduits of Aurelion's Sunken Observatory. The creeping iron corrosion shatters into stardust, and the ancient celestial gears turn once more in eternal balance.
</p>
<div id="victory-stats" style="background: rgba(0,0,0,0.4); padding: 14px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; color: var(--gold);"></div>
<button class="btn" onclick="game.restartGame()">Explore Anew</button>
</div>
</div>
<!-- Touch Controls (Mobile Friendly) -->
<div id="touch-controls">
<div style="display:flex; gap:10px;">
<div class="touch-btn" id="touch-left">←</div>
<div class="touch-btn" id="touch-up">↑</div>
<div class="touch-btn" id="touch-down">↓</div>
<div class="touch-btn" id="touch-right">→</div>
</div>
<div style="display:flex; gap:10px;">
<div class="touch-btn" id="touch-atk" style="background:rgba(230,92,0,0.6);">ATK</div>
<div class="touch-btn" id="touch-ranged" style="background:rgba(78,224,230,0.6);">FIRE</div>
<div class="touch-btn" id="touch-use" style="background:rgba(244,197,66,0.6);">USE</div>
</div>
</div>
</div>
<script>
/**
* THE WARDEN'S CORROSION
* Audio Synthesis Engine using Web Audio API
*/
class SoundEngine {
constructor() {
this.ctx = null;
this.initialized = false;
}
init() {
if (this.initialized) return;
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
this.initialized = true;
} catch (e) {
console.warn("AudioContext not supported", e);
}
}
// Soft stone / bronze footstep
playFootstep(zoneType = 1) {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
// Zone acoustics
let freq = 80 + Math.random() * 40;
if (zoneType === 2) freq = 120 + Math.random() * 60; // metallic grate
if (zoneType === 4) freq = 60 + Math.random() * 30; // damp sunken echo
osc.type = zoneType === 2 ? 'triangle' : 'sine';
osc.frequency.setValueAtTime(freq, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.08);
gain.gain.setValueAtTime(0.04, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.08);
}
// Base Ember Blade attack
playAttackBase() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
// Whoosh
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(450, t);
osc.frequency.exponentialRampToValueAtTime(120, t + 0.12);
gain.gain.setValueAtTime(0.12, t);
gain.gain.linearRampToValueAtTime(0.01, t + 0.12);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.12);
}
// Upgrade 1: Ignited Edge attack (flame crackle + deeper resonant thud)
playAttackIgnited() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc1 = this.ctx.createOscillator();
const osc2 = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc1.type = 'sawtooth';
osc1.frequency.setValueAtTime(380, t);
osc1.frequency.exponentialRampToValueAtTime(90, t + 0.18);
osc2.type = 'triangle';
osc2.frequency.setValueAtTime(600, t);
osc2.frequency.linearRampToValueAtTime(200, t + 0.15);
gain.gain.setValueAtTime(0.16, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
osc1.connect(gain);
osc2.connect(gain);
gain.connect(this.ctx.destination);
osc1.start(t);
osc2.start(t);
osc1.stop(t + 0.2);
osc2.stop(t + 0.2);
}
// Upgrade 2: Solar Falchion (heavy wind-up + resonant clang + low bass rumble)
playAttackSolar() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const bass = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(260, t);
osc.frequency.exponentialRampToValueAtTime(70, t + 0.28);
bass.type = 'sine';
bass.frequency.setValueAtTime(90, t);
bass.frequency.exponentialRampToValueAtTime(35, t + 0.35);
gain.gain.setValueAtTime(0.22, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35);
osc.connect(gain);
bass.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
bass.start(t);
osc.stop(t + 0.35);
bass.stop(t + 0.35);
}
// Ranged Weapon: Aether Shard crystal twang + impact chime
playAetherShoot() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1100, t);
osc.frequency.exponentialRampToValueAtTime(520, t + 0.16);
gain.gain.setValueAtTime(0.18, t);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.16);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.16);
}
playAetherHit() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1600, t);
osc.frequency.exponentialRampToValueAtTime(800, t + 0.25);
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.25);
}
// Enemy hit impact
playHitEnemy() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(180, t);
osc.frequency.exponentialRampToValueAtTime(50, t + 0.12);
gain.gain.setValueAtTime(0.16, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.12);
}
// Damage taken (dull body-thud + brief high-pitch dissonant chime)
playDamagePlayer() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const thud = this.ctx.createOscillator();
const dissonance = this.ctx.createOscillator();
const gain = this.ctx.createGain();
thud.type = 'triangle';
thud.frequency.setValueAtTime(120, t);
thud.frequency.exponentialRampToValueAtTime(30, t + 0.2);
dissonance.type = 'sawtooth';
dissonance.frequency.setValueAtTime(870, t);
dissonance.frequency.exponentialRampToValueAtTime(420, t + 0.25);
gain.gain.setValueAtTime(0.25, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
thud.connect(gain);
dissonance.connect(gain);
gain.connect(this.ctx.destination);
thud.start(t);
dissonance.start(t);
thud.stop(t + 0.25);
dissonance.stop(t + 0.25);
}
// Gold / currency pickup (metallic clink)
playGoldPickup() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(987.77, t); // B5
osc.frequency.setValueAtTime(1318.51, t + 0.05); // E6
gain.gain.setValueAtTime(0.12, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.15);
}
// Item pickup (ascending melodic chime)
playItemPickup() {
if (!this.ctx) return;
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
notes.forEach((freq, idx) => {
const t = this.ctx.currentTime + idx * 0.06;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, t);
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.18);
});
}
// Rare / Accessory or Heart Container (resonant chord)
playMajorPickup() {
if (!this.ctx) return;
const freqs = [392.00, 493.88, 587.33, 783.99, 1174.66]; // G chord
freqs.forEach((f, i) => {
const t = this.ctx.currentTime + (i * 0.04);
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(f, t);
gain.gain.setValueAtTime(0.15, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.8);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.8);
});
}
// Sanctuary Stone save chord
playSaveSound() {
if (!this.ctx) return;
const freqs = [220, 277.18, 329.63, 440, 554.37];
freqs.forEach(f => {
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(f, t);
gain.gain.setValueAtTime(0.12, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1.2);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 1.2);
});
}
// Shop purchase confirmation chime
playShopPurchase() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
[659.25, 880, 1318.51].forEach((freq, idx) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, t + idx * 0.06);
gain.gain.setValueAtTime(0.14, t + idx * 0.06);
gain.gain.exponentialRampToValueAtTime(0.001, t + idx * 0.06 + 0.3);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t + idx * 0.06);
osc.stop(t + idx * 0.06 + 0.3);
});
}
// Switch / Pressure plate click
playSwitch() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(300, t);
osc.frequency.exponentialRampToValueAtTime(100, t + 0.1);
gain.gain.setValueAtTime(0.2, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.1);
}
// Break wall sound
playBreakWall() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(140, t);
osc.frequency.exponentialRampToValueAtTime(30, t + 0.3);
gain.gain.setValueAtTime(0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.3);
}
// Enemy Freeze sound (Binding Seal)
playFreeze() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, t);
osc.frequency.exponentialRampToValueAtTime(1800, t + 0.25);
gain.gain.setValueAtTime(0.18, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.35);
}
}
</script>
<script>
/**
* DATA & STATE DEFINITIONS
*/
const ZONES = [
{
id: 1,
name: "The Ashen Threshold",
sub: "Zone I — Outer Observatory Gate",
bgFloor: "#1a2332",
bgWall: "#0f1622",
accent: "#3a4756",
width: 1600,
height: 900
},
{
id: 2,
name: "The Rusted Archives",
sub: "Zone II — Mechanical Library & Altar Market",
bgFloor: "#24201c",
bgWall: "#151210",
accent: "#6b4c30",
width: 1800,
height: 1000
},
{
id: 3,
name: "The Collapsed Gallery",
sub: "Zone III — Weapon Altar & Plate Sanctum",
bgFloor: "#2b1e1a",
bgWall: "#170f0d",
accent: "#8c3b1e",
width: 2000,
height: 1000
},
{
id: 4,
name: "The Sunken Vault",
sub: "Zone IV — Crystalline Depths (Mid-Game Turning Point)",
bgFloor: "#12252a",
bgWall: "#0a1417",
accent: "#1f5f6b",
width: 2000,
height: 1100
},
{
id: 5,
name: "The Twin Spire",
sub: "Zone V — Ascent of the Sun & Solar Altar",
bgFloor: "#2b2518",
bgWall: "#17140a",
accent: "#8c7224",
width: 2200,
height: 1100
},
{
id: 6,
name: "The Core Chamber",
sub: "Zone VI — Aurelion's Heart & Beacon Climax",
bgFloor: "#1a1e2b",
bgWall: "#0d101a",
accent: "#433a75",
width: 1800,
height: 1200
}
];
class Particle {
constructor(x, y, vx, vy, color, size, life, shape = 'circle', gravity = 0) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.color = color;
this.size = size;
this.maxLife = life;
this.life = life;
this.shape = shape;
this.gravity = gravity;
}
update(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
this.vy += this.gravity * dt;
this.life -= dt;
}
draw(ctx) {
const alpha = Math.max(0, this.life / this.maxLife);
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = this.color;
ctx.strokeStyle = this.color;
if (this.shape === 'spark') {
ctx.lineWidth = this.size;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - this.vx * 0.08, this.y - this.vy * 0.08);
ctx.stroke();
} else if (this.shape === 'ember') {
ctx.shadowBlur = 8;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
class Projectile {
constructor(x, y, vx, vy, isPlayer, damage, color = '#4ee0e6') {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.isPlayer = isPlayer;
this.damage = damage;
this.color = color;
this.radius = 7;
this.life = 2.5; // seconds
this.trailTimer = 0;
}
update(dt, particles) {
this.x += this.vx * dt;
this.y += this.vy * dt;
this.life -= dt;
this.trailTimer += dt;
if (this.trailTimer > 0.03) {
this.trailTimer = 0;
particles.push(new Particle(
this.x + (Math.random() - 0.5) * 4,
this.y + (Math.random() - 0.5) * 4,
(Math.random() - 0.5) * 15,
(Math.random() - 0.5) * 15,
this.color,
3,
0.3,
'ember'
));
}
}
draw(ctx) {
ctx.save();
ctx.shadowBlur = 12;
ctx.shadowColor = this.color;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius * 0.65, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class FloatingText {
constructor(x, y, text, color = '#f4c542', size = 16) {
this.x = x;
this.y = y;
this.text = text;
this.color = color;
this.size = size;
this.life = 0.9;
this.maxLife = 0.9;
}
update(dt) {
this.y -= 28 * dt;
this.life -= dt;
}
draw(ctx) {
const alpha = Math.max(0, this.life / this.maxLife);
ctx.save();
ctx.globalAlpha = alpha;
ctx.font = `bold ${this.size}px 'Cinzel', serif`;
ctx.fillStyle = this.color;
ctx.shadowColor = '#000';
ctx.shadowBlur = 4;
ctx.textAlign = 'center';
ctx.fillText(this.text, this.x, this.y);
ctx.restore();
}
}
</script>
<script>
/**
* ENTITIES & WORLD OBJECTS
*/
class CorruptedConstruct {
constructor(x, y, type = 'sentinel') {
this.x = x;
this.y = y;
this.type = type; // 'crawler', 'sentinel', 'knight', 'boss'
this.radius = 18;
this.vx = 0;
this.vy = 0;
this.angle = 0;
this.state = 'idle'; // idle, chase, attack, hurt, frozen
this.freezeTimer = 0;
this.hitTimer = 0;
this.attackCooldown = 0;
this.isDead = false;
// Type specific stats
if (type === 'crawler') {
this.maxHp = 4;
this.hp = 4;
this.speed = 110;
this.damage = 1;
this.goldDrop = [6, 12];
this.color = '#7d8a99';
this.coreColor = '#e65c00';
this.radius = 14;
} else if (type === 'sentinel') {
this.maxHp = 8;
this.hp = 8;
this.speed = 80;
this.damage = 2;
this.goldDrop = [10, 18];
this.color = '#475569';
this.coreColor = '#f43f5e';
this.radius = 18;
} else if (type === 'knight') {
this.maxHp = 16;
this.hp = 16;
this.speed = 65;
this.damage = 2;
this.goldDrop = [18, 30];
this.color = '#334155';
this.coreColor = '#8b5cf6';
this.radius = 24;
} else if (type === 'boss') {
this.maxHp = 65;
this.hp = 65;
this.speed = 55;
this.damage = 2;
this.goldDrop = [60, 100];
this.color = '#1e293b';
this.coreColor = '#ec4899';
this.radius = 36;
}
}
update(dt, player, walls, soundEngine, particles, floatingTexts) {
if (this.isDead) return;
if (this.freezeTimer > 0) {
this.freezeTimer -= dt;
return;
}
if (this.hitTimer > 0) {
this.hitTimer -= dt;
}
if (this.attackCooldown > 0) {
this.attackCooldown -= dt;
}
const distToPlayer = Math.hypot(player.x - this.x, player.y - this.y);
const angleToPlayer = Math.atan2(player.y - this.y, player.x - this.x);
this.angle = angleToPlayer;
// AI logic
const aggroRange = this.type === 'boss' ? 700 : 380;
if (distToPlayer < aggroRange && !player.isDead) {
this.state = 'chase';
// Move towards player
let targetVx = Math.cos(angleToPlayer) * this.speed;
let targetVy = Math.sin(angleToPlayer) * this.speed;
let newX = this.x + targetVx * dt;
let newY = this.y + targetVy * dt;
// Check wall collision
let canMoveX = true;
let canMoveY = true;
for (const w of walls) {
if (w.broken) continue;
if (circleRectCollide(newX, this.y, this.radius, w.x, w.y, w.w, w.h)) canMoveX = false;
if (circleRectCollide(this.x, newY, this.radius, w.x, w.y, w.w, w.h)) canMoveY = false;
}
if (canMoveX) this.x = newX;
if (canMoveY) this.y = newY;
// Melee attack contact
if (distToPlayer < this.radius + player.radius + 6) {
if (this.attackCooldown <= 0) {
player.takeDamage(this.damage, soundEngine, particles, floatingTexts);
this.attackCooldown = 1.0;
}
}
// Boss special projectile attack
if (this.type === 'boss' && distToPlayer > 120 && this.attackCooldown <= 0) {
if (Math.random() < 0.03) {
this.attackCooldown = 2.0;
// Spawn 3 spreading corrupted bolts
for (let offset of [-0.25, 0, 0.25]) {
const bAngle = angleToPlayer + offset;
game.projectiles.push(new Projectile(
this.x + Math.cos(bAngle) * 30,
this.y + Math.sin(bAngle) * 30,
Math.cos(bAngle) * 220,
Math.sin(bAngle) * 220,
false,
2,
'#e11d48'
));
}
}
}
} else {
this.state = 'idle';
}
}
takeDamage(dmg, soundEngine, particles, floatingTexts, damageType = 'physical') {
if (this.isDead) return;
this.hp -= dmg;
this.hitTimer = 0.15;
soundEngine.playHitEnemy();
// Floating text
let txtColor = '#f8fafc';
if (damageType === 'ember') txtColor = '#fb923c';
if (damageType === 'solar') txtColor = '#fde047';
if (damageType === 'aether') txtColor = '#38bdf8';
floatingTexts.push(new FloatingText(this.x, this.y - 20, `-${dmg}`, txtColor, 18));
// Particle effect
const sparkColor = damageType === 'ember' ? '#ff6600' : (damageType === 'solar' ? '#ffe066' : '#94a3b8');
for (let i = 0; i < 6; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 120,
(Math.random() - 0.5) * 120,
sparkColor,
2.5,
0.3,
'spark'
));
}
if (this.hp <= 0) {
this.isDead = true;
// Death explosion particles
for (let i = 0; i < 14; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 160,
(Math.random() - 0.5) * 160,
this.coreColor,
3.5,
0.5,
'ember',
30
));
}
// Drop gold
const goldAmt = Math.floor(this.goldDrop[0] + Math.random() * (this.goldDrop[1] - this.goldDrop[0] + 1));
game.spawnGoldLoot(this.x, this.y, goldAmt);
}
}
draw(ctx) {
if (this.isDead) return;
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
// Flash white/red when hit
if (this.hitTimer > 0) {
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(0, 0, this.radius + 3, 0, Math.PI * 2);
ctx.fill();
}
// Frozen visual (blue ice overlay)
if (this.freezeTimer > 0) {
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 3;
ctx.shadowBlur = 10;
ctx.shadowColor = '#38bdf8';
ctx.beginPath();
ctx.arc(0, 0, this.radius + 6, 0, Math.PI * 2);
ctx.stroke();
}
// Construct Body (Rusted armor shape)
ctx.fillStyle = this.color;
ctx.strokeStyle = '#1e293b';
ctx.lineWidth = 2;
if (this.type === 'crawler') {
// Spider / mechanical bug shape
ctx.beginPath();
ctx.ellipse(0, 0, this.radius, this.radius * 0.7, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Mandibles / spikes
ctx.fillStyle = '#94a3b8';
ctx.fillRect(8, -6, 8, 3);
ctx.fillRect(8, 3, 8, 3);
} else if (this.type === 'boss') {
// Large mechanical core with orbital bronze gear teeth
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Gear teeth outer
ctx.fillStyle = '#d97706';
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2;
ctx.fillRect(Math.cos(a) * 32 - 4, Math.sin(a) * 32 - 4, 8, 8);
}
} else {
// Sentinel / Knight armor plate
ctx.beginPath();
ctx.rect(-this.radius, -this.radius, this.radius * 2, this.radius * 2);
ctx.fill();
ctx.stroke();
// Pauldrons
ctx.fillStyle = '#64748b';
ctx.fillRect(-this.radius - 3, -this.radius * 0.8, 4, this.radius * 1.6);
}
// Corrupted glowing core eye
ctx.shadowBlur = 12;
ctx.shadowColor = this.coreColor;
ctx.fillStyle = this.coreColor;
ctx.beginPath();
ctx.arc(this.type === 'boss' ? 0 : 4, 0, this.radius * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Health Bar overhead for larger enemies / boss
if (this.hp < this.maxHp) {
const barW = this.radius * 2;
const barH = 5;
const barX = this.x - barW / 2;
const barY = this.y - this.radius - 12;
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(barX, barY, barW, barH);
ctx.fillStyle = this.type === 'boss' ? '#ec4899' : '#e63946';
ctx.fillRect(barX, barY, barW * (Math.max(0, this.hp) / this.maxHp), barH);
ctx.strokeStyle = '#000';
ctx.strokeRect(barX, barY, barW, barH);
}
}
}
</script>
<script>
/**
* PLAYER CLASS & VISUAL PROGRESSION
*/
class Player {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 16;
this.vx = 0;
this.vy = 0;
this.baseSpeed = 190;
this.speed = 190;
this.angle = 0;
this.isDead = false;
// Health Progression: starts at 3 containers (6 HP total), up to 5 containers (10 HP max)
this.maxContainers = 3;
this.hp = 6; // each container is 2 HP
this.invulnerableTimer = 0;
this.attackTimer = 0;
this.attackCooldown = 0.28; // base
this.isAttacking = false;
this.slashArc = 0;
// Weapon & Upgrade State:
// weaponLevel: 0 = Ember Blade (base), 1 = Ignited Edge (Zone 3), 2 = Solar Falchion (Zone 5)
this.weaponLevel = 0;
this.hasAetherShard = false; // Zone 4 discovery
this.aetherCooldown = 0;
// Inventory Accessories equipped:
this.equipped = {
wardensBand: false, // wrist ring, +attack speed
phantomLantern: false, // blue aura, reveals secrets
shadowCloak: false // cloak hem, reduces trap damage
};
// Consumable counts
this.vitalStems = 1;
this.bindingSeals = 1;
// Footstep timer
this.footstepTimer = 0;
this.emberTrailTimer = 0;
}
get maxHp() {
return this.maxContainers * 2;
}
update(dt, keys, mouseWorldPos, walls, soundEngine, particles, floatingTexts, zoneId) {
if (this.isDead) return;
if (this.invulnerableTimer > 0) {
this.invulnerableTimer -= dt;
}
if (this.aetherCooldown > 0) {
this.aetherCooldown -= dt;
}
// Determine target speed (Warden's Band slight speed buff)
this.speed = this.baseSpeed;
if (this.equipped.wardensBand) {
this.attackCooldown = this.weaponLevel === 2 ? 0.28 : (this.weaponLevel === 1 ? 0.20 : 0.24);
} else {
this.attackCooldown = this.weaponLevel === 2 ? 0.36 : (this.weaponLevel === 1 ? 0.25 : 0.30);
}
// Input Movement
let dx = 0;
let dy = 0;
if (keys['KeyW'] || keys['ArrowUp']) dy -= 1;
if (keys['KeyS'] || keys['ArrowDown']) dy += 1;
if (keys['KeyA'] || keys['ArrowLeft']) dx -= 1;
if (keys['KeyD'] || keys['ArrowRight']) dx += 1;
if (dx !== 0 && dy !== 0) {
const invLen = 1 / Math.SQRT2;
dx *= invLen;
dy *= invLen;
}
const moveX = dx * this.speed * dt;
const moveY = dy * this.speed * dt;
let targetX = this.x + moveX;
let targetY = this.y + moveY;
// Wall collision checking
let canMoveX = true;
let canMoveY = true;
for (const w of walls) {
if (w.broken) continue;
if (circleRectCollide(targetX, this.y, this.radius, w.x, w.y, w.w, w.h)) canMoveX = false;
if (circleRectCollide(this.x, targetY, this.radius, w.x, w.y, w.w, w.h)) canMoveY = false;
}
if (canMoveX) this.x = targetX;
if (canMoveY) this.y = targetY;
// Aim angle towards mouse
this.angle = Math.atan2(mouseWorldPos.y - this.y, mouseWorldPos.x - this.x);
// Footsteps
if (dx !== 0 || dy !== 0) {
this.footstepTimer += dt;
if (this.footstepTimer > 0.32) {
this.footstepTimer = 0;
soundEngine.playFootstep(zoneId);
}
// Ember trail for Upgrade 1 (Ignited Edge) and Upgrade 2 (Solar Falchion)
if (this.weaponLevel >= 1) {
this.emberTrailTimer += dt;
if (this.emberTrailTimer > 0.08) {
this.emberTrailTimer = 0;
const trailColor = this.weaponLevel === 2 ? '#fde047' : '#ea580c';
particles.push(new Particle(
this.x + (Math.random() - 0.5) * 10,
this.y + (Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
trailColor,
Math.random() * 2.5 + 1,
0.4,
'ember'
));
}
}
}
// Attack swing animation update
if (this.isAttacking) {
this.attackTimer += dt;
if (this.attackTimer >= this.attackCooldown) {
this.isAttacking = false;
}
}
}
performMeleeAttack(soundEngine, particles, enemies, breakables) {
if (this.isAttacking) return;
this.isAttacking = true;
this.attackTimer = 0;
let damage = 2; // base
let range = 52;
let arc = Math.PI * 0.55; // base arc
let dmgType = 'physical';
if (this.weaponLevel === 1) {
// Ignited Edge (Zone 3): Fire / Ember damage, faster attack
damage = 3;
range = 58;
dmgType = 'ember';
soundEngine.playAttackIgnited();
} else if (this.weaponLevel === 2) {
// Solar Falchion (Zone 5): Light / Solar damage, wider arc sweep & screen shimmer
damage = 5;
range = 72;
arc = Math.PI * 0.85;
dmgType = 'solar';
soundEngine.playAttackSolar();
game.triggerShimmer();
} else {
soundEngine.playAttackBase();
}
// Attack slash arc particles
const steps = 10;
for (let i = 0; i < steps; i++) {
const a = this.angle - arc / 2 + (arc * (i / steps));
const px = this.x + Math.cos(a) * (range * 0.85);
const py = this.y + Math.sin(a) * (range * 0.85);
const slashColor = this.weaponLevel === 2 ? '#fef08a' : (this.weaponLevel === 1 ? '#ff7700' : '#e2e8f0');
particles.push(new Particle(
px,
py,
Math.cos(a) * 40,
Math.sin(a) * 40,
slashColor,
this.weaponLevel === 2 ? 4 : 2.5,
0.2,
'spark'
));
}
// Check hit on Corrupted Constructs
for (const enemy of enemies) {
if (enemy.isDead) continue;
const d = Math.hypot(enemy.x - this.x, enemy.y - this.y);
if (d <= range + enemy.radius) {
const enemyAngle = Math.atan2(enemy.y - this.y, enemy.x - this.x);
let diff = Math.abs(normalizeAngle(enemyAngle - this.angle));
if (diff <= arc / 2 + 0.3) {
enemy.takeDamage(damage, soundEngine, particles, game.floatingTexts, dmgType);
}
}
}
// Check hit on Breakable Walls / Caches
for (const b of breakables) {
if (b.broken) continue;
// Solar Falchion breaks walls in 1 hit, base blade in 2-3 hits
const d = Math.hypot((b.x + b.w / 2) - this.x, (b.y + b.h / 2) - this.y);
if (d <= range + 35) {
const breakDmg = this.weaponLevel === 2 ? 3 : (this.weaponLevel === 1 ? 2 : 1);
b.hit(breakDmg, soundEngine, particles);
}
}
}
performRangedAttack(mouseWorldPos, soundEngine, particles) {
if (!this.hasAetherShard) return;
if (this.aetherCooldown > 0) return;
this.aetherCooldown = 0.35;
soundEngine.playAetherShoot();
const shootAngle = Math.atan2(mouseWorldPos.y - this.y, mouseWorldPos.x - this.x);
const speed = 440;
game.projectiles.push(new Projectile(
this.x + Math.cos(shootAngle) * 20,
this.y + Math.sin(shootAngle) * 20,
Math.cos(shootAngle) * speed,
Math.sin(shootAngle) * speed,
true,
3,
'#4ee0e6'
));
// Recoil spark
for (let i = 0; i < 4; i++) {
particles.push(new Particle(
this.x + Math.cos(shootAngle) * 16,
this.y + Math.sin(shootAngle) * 16,
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 50,
'#4ee0e6',
2.5,
0.2,
'spark'
));
}
}
takeDamage(dmg, soundEngine, particles, floatingTexts) {
if (this.isDead || this.invulnerableTimer > 0) return;
// Shadow cloak mitigates 1 damage if hit
if (this.equipped.shadowCloak && dmg > 1 && Math.random() < 0.4) {
dmg -= 1;
floatingTexts.push(new FloatingText(this.x, this.y - 34, "SHADOW WARD", '#a855f7', 12));
}
this.hp -= dmg;
this.invulnerableTimer = 0.7; // I-frames
soundEngine.playDamagePlayer();
game.triggerScreenFlash('rgba(230, 57, 70, 0.45)');
floatingTexts.push(new FloatingText(this.x, this.y - 25, `-${dmg} HP`, '#ef4444', 20));
// Blood / Aether burst
for (let i = 0; i < 10; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 120,
(Math.random() - 0.5) * 120,
'#ef4444',
3,
0.4,
'ember'
));
}
game.updateHUD();
if (this.hp <= 0) {
this.hp = 0;
this.isDead = true;
game.onPlayerDeath();
}
}
heal(amount, particles, floatingTexts) {
const oldHp = this.hp;
this.hp = Math.min(this.maxHp, this.hp + amount);
const gained = this.hp - oldHp;
if (gained > 0) {
floatingTexts.push(new FloatingText(this.x, this.y - 25, `+${gained} HP`, '#22c55e', 20));
for (let i = 0; i < 12; i++) {
particles.push(new Particle(
this.x + (Math.random() - 0.5) * 20,
this.y + (Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 30,
-40 - Math.random() * 30,
'#4ade80',
3,
0.6,
'ember'
));
}
}
game.updateHUD();
}
draw(ctx) {
if (this.isDead) return;
ctx.save();
ctx.translate(this.x, this.y);
// I-frame blinking
if (this.invulnerableTimer > 0 && Math.floor(Date.now() / 60) % 2 === 0) {
ctx.globalAlpha = 0.4;
}
// Phantom Lantern light halo if equipped
if (this.equipped.phantomLantern) {
ctx.save();
const grad = ctx.createRadialGradient(0, 0, 10, 0, 0, 140);
grad.addColorStop(0, 'rgba(78, 224, 230, 0.25)');
grad.addColorStop(1, 'rgba(78, 224, 230, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, 140, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.rotate(this.angle);
// 1. Cloak / Armor Visuals
// Base cloak: Deep slate blue #1e293b
// Shadow Cloak token: Dark cloth trim
ctx.fillStyle = this.equipped.shadowCloak ? '#0b0f19' : '#1e293b';
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2;
// Cloak base body
ctx.beginPath();
ctx.ellipse(0, 0, this.radius, this.radius * 0.85, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
if (this.equipped.shadowCloak) {
// Dark cloth trim outline
ctx.strokeStyle = '#8b5cf6';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// Cloak shimmering hem for Upgrade 2 (Solar Falchion)
if (this.weaponLevel === 2) {
ctx.strokeStyle = '#fde047';
ctx.lineWidth = 2;
ctx.shadowBlur = 8;
ctx.shadowColor = '#fde047';
ctx.beginPath();
ctx.arc(0, 0, this.radius + 1, 0, Math.PI * 2);
ctx.stroke();
ctx.shadowBlur = 0;
}
// 2. Head / Hood
ctx.fillStyle = '#334155';
ctx.beginPath();
ctx.arc(0, 0, this.radius * 0.55, 0, Math.PI * 2);
ctx.fill();
// Golden warden crest on forehead
ctx.fillStyle = '#f4c542';
ctx.fillRect(2, -2, 5, 4);
// 3. Off-hand / Dual-ready stance if Aether Shard acquired
if (this.hasAetherShard) {
// Off-hand crystal orb
ctx.save();
ctx.translate(6, -14);
ctx.fillStyle = '#4ee0e6';
ctx.shadowBlur = 10;
ctx.shadowColor = '#4ee0e6';
ctx.beginPath();
ctx.arc(0, 0, 4.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 4. Warden's Band accessory token on right wrist
if (this.equipped.wardensBand) {
ctx.fillStyle = '#e2e8f0';
ctx.shadowBlur = 6;
ctx.shadowColor = '#ffffff';
ctx.beginPath();
ctx.arc(6, 12, 3, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
}
// 5. Main Hand Weapon (Ember Blade / Ignited Edge / Solar Falchion)
ctx.save();
ctx.translate(8, 10); // Main hand position
// If attacking, rotate weapon swing
if (this.isAttacking) {
const progress = this.attackTimer / this.attackCooldown;
ctx.rotate(-0.8 + progress * 2.2);
}
if (this.weaponLevel === 0) {
// Base Ember Blade: Steel blade with bronze hilt
ctx.fillStyle = '#94a3b8';
ctx.fillRect(0, -2, 22, 4);
ctx.fillStyle = '#d97706';
ctx.fillRect(-3, -4, 4, 8);
} else if (this.weaponLevel === 1) {
// Upgrade 1: Ignited Edge (glowing orange flame blade)
ctx.fillStyle = '#ea580c';
ctx.shadowBlur = 12;
ctx.shadowColor = '#f97316';
ctx.fillRect(0, -2.5, 26, 5);
// Flame tip
ctx.fillStyle = '#fde047';
ctx.fillRect(22, -1.5, 6, 3);
// Bronze hilt
ctx.fillStyle = '#b45309';
ctx.fillRect(-3, -5, 4, 10);
} else if (this.weaponLevel === 2) {
// Upgrade 2: Solar Falchion (wider golden curved blade + intense solar halo)
ctx.fillStyle = '#facc15';
ctx.shadowBlur = 18;
ctx.shadowColor = '#fef08a';
ctx.beginPath();
ctx.moveTo(0, -4);
ctx.lineTo(28, -6);
ctx.lineTo(36, 0);
ctx.lineTo(28, 6);
ctx.lineTo(0, 4);
ctx.closePath();
ctx.fill();
// Solar core inlay
ctx.fillStyle = '#ffffff';
ctx.fillRect(4, -1, 20, 2);
// Gold ornate guard
ctx.fillStyle = '#eab308';
ctx.fillRect(-4, -6, 5, 12);
}
ctx.restore();
ctx.restore();
}
}
</script>
<script>
/**
* ENVIRONMENT ELEMENTS: Breakables, Sanctuary Stones, Pressure Plates, Ranged Switches, Gates, Altars
*/
class Wall {
constructor(x, y, w, h, isCrack = false, hp = 3, dropsItem = null) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.isCrack = isCrack; // hairline cracks breakable wall
this.hp = hp;
this.maxHp = hp;
this.broken = false;
this.dropsItem = dropsItem;
}
hit(dmg, soundEngine, particles) {
if (!this.isCrack || this.broken) return;
this.hp -= dmg;
soundEngine.playBreakWall();
for (let i = 0; i < 8; i++) {
particles.push(new Particle(
this.x + this.w / 2 + (Math.random() - 0.5) * this.w,
this.y + this.h / 2 + (Math.random() - 0.5) * this.h,
(Math.random() - 0.5) * 80,
(Math.random() - 0.5) * 80,
'#64748b',
3,
0.3,
'spark'
));
}
if (this.hp <= 0) {
this.broken = true;
soundEngine.playBreakWall();
// Wall collapse burst
for (let i = 0; i < 18; i++) {
particles.push(new Particle(
this.x + this.w / 2,
this.y + this.h / 2,
(Math.random() - 0.5) * 160,
(Math.random() - 0.5) * 160,
'#475569',
4,
0.6,
'ember',
50
));
}
// Drop item or container if configured
if (this.dropsItem) {
if (this.dropsItem === 'container4') {
game.spawnHealthContainer(this.x + this.w / 2, this.y + this.h / 2, 4);
} else if (this.dropsItem === 'aetherShard') {
game.spawnWorldItem(this.x + this.w / 2, this.y + this.h / 2, 'aetherShard', "Aether Shard", "Ancient crystalline ranged projectile focus!");
} else if (this.dropsItem === 'wardensBand') {
game.spawnWorldItem(this.x + this.w / 2, this.y + this.h / 2, 'wardensBand', "Warden's Band", "Accessory: Boosts melee attack speed!");
}
}
}
}
draw(ctx, phantomLanternActive) {
if (this.broken) return;
if (this.isCrack) {
// Breakable wall with hairline cracks
ctx.fillStyle = '#1e293b';
ctx.fillRect(this.x, this.y, this.w, this.h);
// Crack lines
ctx.strokeStyle = (phantomLanternActive ? '#38bdf8' : '#64748b');
ctx.lineWidth = phantomLanternActive ? 2 : 1.5;
if (phantomLanternActive) {
ctx.shadowBlur = 8;
ctx.shadowColor = '#38bdf8';
}
ctx.beginPath();
ctx.moveTo(this.x + 4, this.y + 4);
ctx.lineTo(this.x + this.w * 0.4, this.y + this.h * 0.6);
ctx.lineTo(this.x + this.w * 0.7, this.y + this.h * 0.3);
ctx.lineTo(this.x + this.w - 4, this.y + this.h - 4);
ctx.stroke();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#0f172a';
ctx.strokeRect(this.x, this.y, this.w, this.h);
} else {
// Solid Observatory Stone
ctx.fillStyle = '#0f172a';
ctx.fillRect(this.x, this.y, this.w, this.h);
// Bronze trim edge
ctx.strokeStyle = '#334155';
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.w, this.h);
}
}
}
class SanctuaryStone {
constructor(x, y, stoneId, zoneId) {
this.x = x;
this.y = y;
this.stoneId = stoneId;
this.zoneId = zoneId;
this.activated = false;
this.radius = 24;
this.sparkleTimer = 0;
}
update(dt, particles) {
this.sparkleTimer += dt;
if (this.sparkleTimer > 0.15) {
this.sparkleTimer = 0;
const color = this.activated ? '#fde047' : '#94a3b8';
particles.push(new Particle(
this.x + (Math.random() - 0.5) * 24,
this.y + (Math.random() - 0.5) * 24,
(Math.random() - 0.5) * 15,
-15 - Math.random() * 20,
color,
2,
0.7,
'ember'
));
}
}
activate(player, soundEngine, particles, floatingTexts) {
this.activated = true;
player.hp = player.maxHp; // Full restore upon rest
game.lastSanctuaryZone = this.zoneId;
game.lastSanctuaryPos = { x: this.x, y: this.y + 40 };
soundEngine.playSaveSound();
game.triggerScreenFlash('rgba(244, 197, 66, 0.35)');
game.showToast("Sanctuary Stone Rekindled — Progress & Vitality Restored");
floatingTexts.push(new FloatingText(this.x, this.y - 40, "SANCTUARY PRESERVED", '#f4c542', 16));
// Save burst
for (let i = 0; i < 25; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 140,
(Math.random() - 0.5) * 140,
'#facc15',
3.5,
0.8,
'ember'
));
}
game.updateHUD();
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
// Stone Pedestal base
ctx.fillStyle = '#1e293b';
ctx.strokeStyle = '#475569';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Glowing Central Monolith
const stoneColor = this.activated ? '#facc15' : '#64748b';
ctx.fillStyle = stoneColor;
if (this.activated) {
ctx.shadowBlur = 18;
ctx.shadowColor = '#fde047';
}
ctx.beginPath();
ctx.moveTo(0, -18);
ctx.lineTo(12, 0);
ctx.lineTo(0, 18);
ctx.lineTo(-12, 0);
ctx.closePath();
ctx.fill();
// Floating celestial ring
ctx.strokeStyle = this.activated ? '#fef08a' : '#475569';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.ellipse(0, 0, 20, 8, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
}
class PressurePlate {
constructor(x, y, targetDoorId) {
this.x = x;
this.y = y;
this.w = 40;
this.h = 40;
this.targetDoorId = targetDoorId;
this.isPressed = false;
}
update(player, enemies, soundEngine) {
let wasPressed = this.isPressed;
let nowPressed = false;
// Player stepping on plate
if (pointInRect(player.x, player.y, this.x, this.y, this.w, this.h)) {
nowPressed = true;
}
// Enemies stepping on plate
for (const e of enemies) {
if (!e.isDead && pointInRect(e.x, e.y, this.x, this.y, this.w, this.h)) {
nowPressed = true;
break;
}
}
this.isPressed = nowPressed;
if (!wasPressed && this.isPressed) {
soundEngine.playSwitch();
game.setDoorState(this.targetDoorId, true);
} else if (wasPressed && !this.isPressed) {
// Optional toggle or latch
// Keep open if latch or close if hold
}
}
draw(ctx) {
ctx.fillStyle = this.isPressed ? '#d97706' : '#78350f';
ctx.strokeStyle = '#b45309';
ctx.lineWidth = 2;
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.strokeRect(this.x, this.y, this.w, this.h);
// Inner bronze depression
ctx.fillStyle = this.isPressed ? '#f59e0b' : '#451a03';
ctx.fillRect(this.x + 6, this.y + 6, this.w - 12, this.h - 12);
}
}
class RangedSwitch {
constructor(x, y, targetDoorId) {
this.x = x;
this.y = y;
this.radius = 16;
this.targetDoorId = targetDoorId;
this.activated = false;
}
hit(soundEngine, particles, floatingTexts) {
if (this.activated) return;
this.activated = true;
soundEngine.playAetherHit();
soundEngine.playSwitch();
game.setDoorState(this.targetDoorId, true);
floatingTexts.push(new FloatingText(this.x, this.y - 20, "SWITCH ENGAGED", '#4ee0e6', 15));
for (let i = 0; i < 15; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 120,
(Math.random() - 0.5) * 120,
'#4ee0e6',
3,
0.5,
'spark'
));
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.fillStyle = '#0f172a';
ctx.strokeStyle = '#334155';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Crystal Core
const color = this.activated ? '#4ee0e6' : '#0284c7';
ctx.fillStyle = color;
if (this.activated) {
ctx.shadowBlur = 15;
ctx.shadowColor = '#4ee0e6';
}
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.lineTo(8, 0);
ctx.lineTo(0, 10);
ctx.lineTo(-8, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
class GateDoor {
constructor(id, x, y, w, h, isOpen = false) {
this.id = id;
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.isOpen = isOpen;
}
draw(ctx) {
if (this.isOpen) {
// Open glowing gateway frame
ctx.strokeStyle = '#22c55e';
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.w, this.h);
return;
}
// Heavy iron/bronze portcullis gate
ctx.fillStyle = '#1e293b';
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.strokeStyle = '#f59e0b';
ctx.lineWidth = 3;
ctx.strokeRect(this.x, this.y, this.w, this.h);
// Bars
ctx.fillStyle = '#475569';
if (this.w > this.h) {
for (let bx = this.x + 8; bx < this.x + this.w; bx += 14) {
ctx.fillRect(bx, this.y, 4, this.h);
}
} else {
for (let by = this.y + 8; by < this.y + this.h; by += 14) {
ctx.fillRect(this.x, by, this.w, 4);
}
}
}
}
class WeaponAltar {
constructor(x, y, upgradeLevel, name, description) {
this.x = x;
this.y = y;
this.upgradeLevel = upgradeLevel; // 1 = Ignited Edge (Zone 3), 2 = Solar Falchion (Zone 5)
this.name = name;
this.description = description;
this.activated = false;
this.radius = 28;
}
activate(player, soundEngine, particles, floatingTexts) {
if (this.activated) return;
this.activated = true;
player.weaponLevel = this.upgradeLevel;
soundEngine.playMajorPickup();
game.triggerScreenFlash(this.upgradeLevel === 2 ? 'rgba(255, 224, 102, 0.45)' : 'rgba(230, 92, 0, 0.45)');
game.triggerShimmer(2500);
const title = this.upgradeLevel === 1 ? "Weapon Forged: Ignited Edge!" : "Weapon Forged: Solar Falchion!";
game.showToast(title);
game.openDialogue(this.name, this.description);
floatingTexts.push(new FloatingText(this.x, this.y - 45, "WEAPON UPGRADED", '#fde047', 18));
// Giant eruption particles
for (let i = 0; i < 40; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 180,
(Math.random() - 0.5) * 180,
this.upgradeLevel === 2 ? '#facc15' : '#ea580c',
4,
0.9,
'ember'
));
}
game.updateHUD();
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
// Altar Stone Base
ctx.fillStyle = '#1e293b';
ctx.strokeStyle = this.upgradeLevel === 2 ? '#eab308' : '#ea580c';
ctx.lineWidth = 3;
ctx.fillRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2);
ctx.strokeRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2);
// Altar Center Weapon Projection
if (!this.activated) {
const glowColor = this.upgradeLevel === 2 ? '#fef08a' : '#fb923c';
ctx.fillStyle = glowColor;
ctx.shadowBlur = 18;
ctx.shadowColor = glowColor;
// Floating blade icon
ctx.beginPath();
ctx.moveTo(0, -18);
ctx.lineTo(6, 12);
ctx.lineTo(0, 16);
ctx.lineTo(-6, 12);
ctx.closePath();
ctx.fill();
} else {
// Activated remnant
ctx.fillStyle = '#64748b';
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
</script>
<script>
/**
* COLLECTIBLES & LOOT: Gold, Heart Containers, Accessories, Consumables
*/
class GoldLoot {
constructor(x, y, amount) {
this.x = x;
this.y = y;
this.amount = amount;
this.radius = 10;
this.collected = false;
this.sparkleTimer = 0;
}
update(dt, player, soundEngine, particles, floatingTexts) {
if (this.collected) return;
this.sparkleTimer += dt;
const dist = Math.hypot(player.x - this.x, player.y - this.y);
// Magnet pull towards player if close
if (dist < 80) {
this.x += (player.x - this.x) * 6 * dt;
this.y += (player.y - this.y) * 6 * dt;
}
if (dist < player.radius + this.radius) {
this.collected = true;
game.gold += this.amount;
soundEngine.playGoldPickup();
floatingTexts.push(new FloatingText(this.x, this.y - 18, `+${this.amount} Sigils`, '#facc15', 13));
// Gold swirl particles
for (let i = 0; i < 6; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 60,
(Math.random() - 0.5) * 60,
'#fde047',
2,
0.3,
'spark'
));
}
game.updateHUD();
}
}
draw(ctx) {
if (this.collected) return;
ctx.save();
ctx.translate(this.x, this.y);
ctx.fillStyle = '#f59e0b';
ctx.shadowBlur = 8;
ctx.shadowColor = '#fde047';
ctx.beginPath();
ctx.arc(0, 0, this.radius * 0.75, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fef08a';
ctx.beginPath();
ctx.arc(0, 0, this.radius * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class HealthContainerLoot {
constructor(x, y, containerIndex) {
this.x = x;
this.y = y;
this.containerIndex = containerIndex; // 4 or 5
this.radius = 16;
this.collected = false;
this.floatOffset = 0;
}
update(dt, player, soundEngine, particles, floatingTexts) {
if (this.collected) return;
this.floatOffset += dt * 3;
const dist = Math.hypot(player.x - this.x, player.y - this.y);
if (dist < player.radius + this.radius) {
this.collected = true;
player.maxContainers = Math.min(5, player.maxContainers + 1);
player.hp = player.maxHp; // Heal to full on expansion
soundEngine.playMajorPickup();
game.triggerScreenFlash('rgba(230, 57, 70, 0.4)');
game.triggerShimmer(2000);
game.showToast(`Vital Essence Expanded! Container #${player.maxContainers} Acquired!`);
floatingTexts.push(new FloatingText(this.x, this.y - 30, "+1 HEALTH CONTAINER", '#e63946', 18));
// Golden aura particles around player
for (let i = 0; i < 35; i++) {
particles.push(new Particle(
player.x,
player.y,
(Math.random() - 0.5) * 150,
(Math.random() - 0.5) * 150,
'#f43f5e',
3.5,
0.8,
'ember'
));
}
game.updateHUD();
}
}
draw(ctx) {
if (this.collected) return;
const dy = Math.sin(this.floatOffset) * 5;
ctx.save();
ctx.translate(this.x, this.y + dy);
ctx.shadowBlur = 16;
ctx.shadowColor = '#e63946';
// Draw Ornate Heart Shape
ctx.fillStyle = '#e63946';
ctx.beginPath();
ctx.moveTo(0, 5);
ctx.bezierCurveTo(-14, -10, -14, -22, 0, -12);
ctx.bezierCurveTo(14, -22, 14, -10, 0, 5);
ctx.fill();
ctx.fillStyle = '#fca5a5';
ctx.beginPath();
ctx.arc(-4, -12, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class WorldItemLoot {
constructor(x, y, itemKey, itemName, itemDesc) {
this.x = x;
this.y = y;
this.itemKey = itemKey;
this.itemName = itemName;
this.itemDesc = itemDesc;
this.radius = 16;
this.collected = false;
this.floatOffset = 0;
}
update(dt, player, soundEngine, particles, floatingTexts) {
if (this.collected) return;
this.floatOffset += dt * 3.5;
const dist = Math.hypot(player.x - this.x, player.y - this.y);
if (dist < player.radius + this.radius) {
this.collected = true;
if (this.itemKey === 'aetherShard') {
player.hasAetherShard = true;
document.getElementById('slot-ranged').style.display = 'flex';
soundEngine.playMajorPickup();
game.triggerScreenFlash('rgba(78, 224, 230, 0.45)');
game.showToast("Discovered Ranged Weapon: Aether Shard!");
game.openDialogue("The Aether Shard Recovered", "In the crystalline depths of the Sunken Vault, you unearthed the Aether Shard. Press [RMB] or [K] to fire long-range radiant bolts to trigger distant mechanisms and pierce tough foes from afar.");
} else if (this.itemKey === 'vitalStem') {
player.vitalStems += 1;
soundEngine.playItemPickup();
game.showToast("Acquired Vital Stem (+4 HP Consumable)");
} else if (this.itemKey === 'bindingSeal') {
player.bindingSeals += 1;
soundEngine.playItemPickup();
game.showToast("Acquired Binding Seal (Freeze Consumable)");
} else {
// Accessories: wardensBand, phantomLantern, shadowCloak
game.inventory.push(this.itemKey);
player.equipped[this.itemKey] = true; // Auto-equip first find
soundEngine.playMajorPickup();
game.showToast(`Equipped Accessory: ${this.itemName}`);
game.openDialogue(this.itemName, this.itemDesc);
}
floatingTexts.push(new FloatingText(this.x, this.y - 25, `ACQUIRED: ${this.itemName}`, '#facc15', 14));
for (let i = 0; i < 20; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 120,
(Math.random() - 0.5) * 120,
'#fde047',
3,
0.6,
'ember'
));
}
game.updateHUD();
}
}
draw(ctx) {
if (this.collected) return;
const dy = Math.sin(this.floatOffset) * 4;
ctx.save();
ctx.translate(this.x, this.y + dy);
let color = '#facc15';
if (this.itemKey === 'aetherShard' || this.itemKey === 'phantomLantern') color = '#38bdf8';
if (this.itemKey === 'vitalStem') color = '#4ade80';
if (this.itemKey === 'shadowCloak') color = '#a855f7';
ctx.shadowBlur = 14;
ctx.shadowColor = color;
ctx.fillStyle = color;
// Diamond / Orb item frame
ctx.beginPath();
ctx.moveTo(0, -12);
ctx.lineTo(12, 0);
ctx.lineTo(0, 12);
ctx.lineTo(-12, 0);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(0, 0, 4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class BeaconAltar {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 45;
this.activated = false;
this.spinAngle = 0;
}
update(dt) {
this.spinAngle += dt * 1.5;
}
activate(player, soundEngine, particles) {
if (this.activated) return;
this.activated = true;
soundEngine.playSaveSound();
soundEngine.playMajorPickup();
game.triggerScreenFlash('rgba(255, 224, 102, 0.7)');
game.triggerShimmer(5000);
for (let i = 0; i < 80; i++) {
particles.push(new Particle(
this.x,
this.y,
(Math.random() - 0.5) * 260,
(Math.random() - 0.5) * 260,
'#facc15',
5,
1.5,
'ember'
));
}
setTimeout(() => {
game.onVictory();
}, 1800);
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
// Large Bronze Pedestal
ctx.fillStyle = '#1e293b';
ctx.strokeStyle = '#d97706';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Rotating celestial gear teeth
ctx.save();
ctx.rotate(this.spinAngle);
ctx.fillStyle = '#b45309';
for (let i = 0; i < 12; i++) {
const a = (i / 12) * Math.PI * 2;
ctx.fillRect(Math.cos(a) * 40 - 4, Math.sin(a) * 40 - 4, 8, 8);
}
ctx.restore();
// Core Beacon flame
const beaconColor = this.activated ? '#ffffff' : '#f59e0b';
ctx.fillStyle = beaconColor;
ctx.shadowBlur = this.activated ? 40 : 20;
ctx.shadowColor = '#fde047';
ctx.beginPath();
ctx.arc(0, 0, 18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
</script>
<script>
/**
* MAIN GAME ENGINE & ZONE MAP BUILDERS
*/
// Math / Utility helpers
function pointInRect(px, py, rx, ry, rw, rh) {
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
function circleRectCollide(cx, cy, cr, rx, ry, rw, rh) {
const testX = Math.max(rx, Math.min(cx, rx + rw));
const testY = Math.max(ry, Math.min(cy, ry + rh));
const distX = cx - testX;
const distY = cy - testY;
return (distX * distX + distY * distY) < (cr * cr);
}
function normalizeAngle(a) {
while (a < -Math.PI) a += Math.PI * 2;
while (a > Math.PI) a -= Math.PI * 2;
return a;
}
class GameEngine {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.sound = new SoundEngine();
this.currentZoneId = 1;
this.player = new Player(200, 450);
this.gold = 0;
this.inventory = []; // list of item keys acquired
// Sanctuary Stone tracking
this.lastSanctuaryZone = 1;
this.lastSanctuaryPos = { x: 200, y: 450 };
// World entities for current zone
this.walls = [];
this.enemies = [];
this.sanctuaryStones = [];
this.pressurePlates = [];
this.rangedSwitches = [];
this.doors = [];
this.weaponAltars = [];
this.goldLoots = [];
this.healthContainerLoots = [];
this.worldItems = [];
this.beaconAltar = null;
// Ephemeral visual arrays
this.projectiles = [];
this.particles = [];
this.floatingTexts = [];
// Camera
this.camX = 0;
this.camY = 0;
// Controls state
this.keys = {};
this.mouseScreenPos = { x: 480, y: 270 };
this.mouseWorldPos = { x: 480, y: 270 };
// Game state loop
this.isRunning = false;
this.isPaused = false;
this.lastTime = 0;
// Shop stock counts
this.shopVitalStems = 3;
this.shopBindingSeals = 2;
this.shopContainer5Available = true;
// Shop proximity
this.nearShop = false;
// Bind events
this.bindEvents();
this.updateHUD();
}
bindEvents() {
window.addEventListener('keydown', (e) => {
this.sound.init();
this.keys[e.code] = true;
if (e.code === 'KeyI') {
this.toggleInventory();
}
if (e.code === 'KeyE') {
this.handleInteract();
}
if (e.code === 'Digit1') {
this.useQuickItem('vitalStem');
}
if (e.code === 'Digit2') {
this.useQuickItem('bindingSeal');
}
if (e.code === 'KeyJ' || e.code === 'Space') {
this.player.performMeleeAttack(this.sound, this.particles, this.enemies, this.walls);
}
if (e.code === 'KeyK') {
this.player.performRangedAttack(this.mouseWorldPos, this.sound, this.particles);
}
});
window.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
this.canvas.addEventListener('mousemove', (e) => {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
this.mouseScreenPos.x = (e.clientX - rect.left) * scaleX;
this.mouseScreenPos.y = (e.clientY - rect.top) * scaleY;
});
this.canvas.addEventListener('mousedown', (e) => {
this.sound.init();
if (e.button === 0) {
// Left click: melee attack
this.player.performMeleeAttack(this.sound, this.particles, this.enemies, this.walls);
} else if (e.button === 2) {
// Right click: ranged attack
e.preventDefault();
this.player.performRangedAttack(this.mouseWorldPos, this.sound, this.particles);
}
});
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// Touch controls binding
const setupTouchBtn = (id, onDown, onUp) => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('touchstart', (e) => { e.preventDefault(); this.sound.init(); onDown(); });
el.addEventListener('touchend', (e) => { e.preventDefault(); onUp(); });
};
setupTouchBtn('touch-left', () => this.keys['KeyA'] = true, () => this.keys['KeyA'] = false);
setupTouchBtn('touch-right', () => this.keys['KeyD'] = true, () => this.keys['KeyD'] = false);
setupTouchBtn('touch-up', () => this.keys['KeyW'] = true, () => this.keys['KeyW'] = false);
setupTouchBtn('touch-down', () => this.keys['KeyS'] = true, () => this.keys['KeyS'] = false);
setupTouchBtn('touch-atk', () => this.player.performMeleeAttack(this.sound, this.particles, this.enemies, this.walls), () => {});
setupTouchBtn('touch-ranged', () => this.player.performRangedAttack(this.mouseWorldPos, this.sound, this.particles), () => {});
setupTouchBtn('touch-use', () => this.handleInteract(), () => {});
}
startGame() {
this.sound.init();
document.getElementById('title-screen').style.display = 'none';
this.loadZone(1);
this.isRunning = true;
this.lastTime = performance.now();
requestAnimationFrame((t) => this.gameLoop(t));
}
loadZone(zoneId, spawnPos = null) {
this.currentZoneId = zoneId;
const zoneDef = ZONES.find(z => z.id === zoneId);
// Clear world arrays
this.walls = [];
this.enemies = [];
this.sanctuaryStones = [];
this.pressurePlates = [];
this.rangedSwitches = [];
this.doors = [];
this.weaponAltars = [];
this.goldLoots = [];
this.healthContainerLoots = [];
this.worldItems = [];
this.projectiles = [];
this.particles = [];
this.floatingTexts = [];
this.beaconAltar = null;
// Update HUD
document.getElementById('zone-name').innerText = zoneDef.name;
document.getElementById('zone-sub').innerText = zoneDef.sub;
this.showToast(`Entering: ${zoneDef.name}`);
// Build zone level layout
this.buildZoneGeometry(zoneId, zoneDef);
// Position player
if (spawnPos) {
this.player.x = spawnPos.x;
this.player.y = spawnPos.y;
} else {
this.player.x = 120;
this.player.y = zoneDef.height / 2;
}
this.updateHUD();
}
buildZoneGeometry(zoneId, zoneDef) {
const W = zoneDef.width;
const H = zoneDef.height;
// Outer perimeter walls
this.walls.push(new Wall(0, 0, W, 40));
this.walls.push(new Wall(0, H - 40, W, 40));
this.walls.push(new Wall(0, 0, 40, H)); // West outer wall
// Exit portal / gateway to next zone on East wall
if (zoneId < 6) {
this.walls.push(new Wall(W - 40, 0, 40, H / 2 - 80));
this.walls.push(new Wall(W - 40, H / 2 + 80, 40, H / 2 - 80));
// Zone exit trigger is gap at W-40, H/2 - 80 to H/2 + 80
} else {
this.walls.push(new Wall(W - 40, 0, 40, H)); // Zone 6 is sealed climax arena
}
// ==========================================
// ZONE 1: THE ASHEN THRESHOLD (Prologue)
// Sanctuary Stone 1, basic crawler constructs, initial movement & combat training
// ==========================================
if (zoneId === 1) {
// Sanctuary Stone 1 near entrance
this.sanctuaryStones.push(new SanctuaryStone(200, H / 2, 1, 1));
// Pillar obstacles
this.walls.push(new Wall(480, 160, 60, 220));
this.walls.push(new Wall(480, H - 380, 60, 220));
this.walls.push(new Wall(880, 260, 70, 380));
// Initial breakable cache wall
this.walls.push(new Wall(600, 40, 100, 40, true, 2, 'vitalStem'));
// Enemies (Crawlers)
this.enemies.push(new CorruptedConstruct(650, 280, 'crawler'));
this.enemies.push(new CorruptedConstruct(650, H - 280, 'crawler'));
this.enemies.push(new CorruptedConstruct(1100, 340, 'crawler'));
this.enemies.push(new CorruptedConstruct(1250, H - 340, 'crawler'));
this.enemies.push(new CorruptedConstruct(1380, H / 2, 'sentinel'));
}
// ==========================================
// ZONE 2: THE RUSTED ARCHIVES (Early)
// Sanctuary Stone 2, Shop Altar, Warden's Band hidden chest, money farming
// ==========================================
else if (zoneId === 2) {
// Sanctuary Stone 2
this.sanctuaryStones.push(new SanctuaryStone(700, H / 2, 2, 2));
// Internal library shelves and partitions
this.walls.push(new Wall(360, 100, 40, 320));
this.walls.push(new Wall(360, H - 420, 40, 320));
this.walls.push(new Wall(1020, 140, 40, 300));
this.walls.push(new Wall(1020, H - 440, 40, 300));
// Secret breakable wall hiding Warden's Band accessory
this.walls.push(new Wall(360, 420, 40, 160, true, 3, 'wardensBand'));
// Enemies
this.enemies.push(new CorruptedConstruct(520, 220, 'crawler'));
this.enemies.push(new CorruptedConstruct(520, H - 220, 'crawler'));
this.enemies.push(new CorruptedConstruct(880, 260, 'sentinel'));
this.enemies.push(new CorruptedConstruct(880, H - 260, 'sentinel'));
this.enemies.push(new CorruptedConstruct(1350, 320, 'sentinel'));
this.enemies.push(new CorruptedConstruct(1450, H - 320, 'knight'));
// Extra gold caches
this.goldLoots.push(new GoldLoot(280, 140, 15));
this.goldLoots.push(new GoldLoot(280, H - 140, 15));
}
// ==========================================
// ZONE 3: THE COLLAPSED GALLERY (Mid-Early)
// Weapon Upgrade 1 Altar (Ignited Edge), Pressure Plate puzzle, Crack-marked wall hiding Container #4
// ==========================================
else if (zoneId === 3) {
// Pressure plate puzzle and gate
this.pressurePlates.push(new PressurePlate(580, 220, 'doorGallery'));
this.doors.push(new GateDoor('doorGallery', 820, 380, 40, 240, false));
this.walls.push(new Wall(820, 40, 40, 340));
this.walls.push(new Wall(820, 620, 40, H - 660));
// Weapon Upgrade 1 Altar: Ignited Edge
this.weaponAltars.push(new WeaponAltar(
1350,
H / 2,
1,
"Altar of the Ignited Edge",
"The ancient flame awakens! Your Ember Blade is infused with celestial fire—higher attack speed and searing ember damage."
));
// Hidden crack wall hiding Container 4 (4th Health Container)
this.walls.push(new Wall(1550, 40, 120, 40, true, 3, 'container4'));
// Enemies & Mini-boss sentinel gauntlet
this.enemies.push(new CorruptedConstruct(450, 300, 'crawler'));
this.enemies.push(new CorruptedConstruct(450, H - 300, 'sentinel'));
this.enemies.push(new CorruptedConstruct(1050, 280, 'knight'));
this.enemies.push(new CorruptedConstruct(1050, H - 280, 'knight'));
this.enemies.push(new CorruptedConstruct(1650, H / 2, 'knight'));
}
// ==========================================
// ZONE 4: THE SUNKEN VAULT (Mid-Game Turning Point)
// Ranged Weapon Discovery: Aether Shard hidden behind breakable wall,
// Phantom Lantern light puzzle, crystal switches
// ==========================================
else if (zoneId === 4) {
// Internal submerged ruins
this.walls.push(new Wall(420, 120, 40, 400));
this.walls.push(new Wall(420, H - 520, 40, 400));
// Hidden wall hiding the AETHER SHARD
this.walls.push(new Wall(420, 520, 40, 100, true, 3, 'aetherShard'));
// Distant crystal switch requiring Aether Shard to open doorVault
this.rangedSwitches.push(new RangedSwitch(1250, 180, 'doorVault'));
this.doors.push(new GateDoor('doorVault', 1400, 420, 40, 260, false));
this.walls.push(new Wall(1400, 40, 40, 380));
this.walls.push(new Wall(1400, 680, 40, H - 720));
// Light-puzzle reward: Phantom Lantern
this.worldItems.push(new WorldItemLoot(
950,
H / 2,
'phantomLantern',
"Phantom Lantern",
"Accessory: Casts a radiant ethereal halo that exposes secret cracked passages and illuminates the darkest vaults."
));
// Enemies
this.enemies.push(new CorruptedConstruct(650, 260, 'sentinel'));
this.enemies.push(new CorruptedConstruct(650, H - 260, 'sentinel'));
this.enemies.push(new CorruptedConstruct(1100, 320, 'knight'));
this.enemies.push(new CorruptedConstruct(1100, H - 320, 'knight'));
this.enemies.push(new CorruptedConstruct(1680, H / 2, 'knight'));
}
// ==========================================
// ZONE 5: THE TWIN SPIRE (Late Game)
// Sanctuary Stone 3, Weapon Upgrade 2 (Solar Falchion),
// Shadow Cloak accessory, Health Container #5 ranged switch room
// ==========================================
else if (zoneId === 5) {
// Sanctuary Stone 3 at Spire entrance
this.sanctuaryStones.push(new SanctuaryStone(220, H / 2, 3, 5));
// Spire layout & Gauntlet
this.walls.push(new Wall(520, 80, 50, 400));
this.walls.push(new Wall(520, H - 480, 50, 400));
// Weapon Upgrade 2: Solar Falchion Altar
this.weaponAltars.push(new WeaponAltar(
1100,
H / 2,
2,
"Altar of the Solar Falchion",
"The purest solar essence floods your blade! Devastating wide sweep, deep resonance, and radiant starlight power."
));
// Ranged switch for Health Container 5 secret chamber (if not bought from shop)
this.rangedSwitches.push(new RangedSwitch(1450, 160, 'doorSpireSecret'));
this.doors.push(new GateDoor('doorSpireSecret', 1600, 120, 30, 180, false));
this.walls.push(new Wall(1600, 40, 30, 80));
this.walls.push(new Wall(1600, 300, 30, 200));
// Spawn container 5 if player doesn't have it yet
if (this.player.maxContainers < 5) {
this.healthContainerLoots.push(new HealthContainerLoot(1800, 200, 5));
}
// Shadow Cloak reward in south wing
this.worldItems.push(new WorldItemLoot(
1800,
H - 200,
'shadowCloak',
"Shadow Cloak",
"Accessory: Weaves dark celestial fabric into your garment, greatly dampening trap impact and incoming enemy blows."
));
// Tough combat gauntlet
this.enemies.push(new CorruptedConstruct(720, 240, 'knight'));
this.enemies.push(new CorruptedConstruct(720, H - 240, 'knight'));
this.enemies.push(new CorruptedConstruct(1380, 320, 'knight'));
this.enemies.push(new CorruptedConstruct(1380, H - 320, 'knight'));
this.enemies.push(new CorruptedConstruct(1500, H / 2, 'knight'));
}
// ==========================================
// ZONE 6: THE CORE CHAMBER (Climax)
// Corrupted Titan Boss + Core Beacon Reignite Altar
// ==========================================
else if (zoneId === 6) {
// Arena boundary pillars
this.walls.push(new Wall(450, 180, 80, 80));
this.walls.push(new Wall(450, H - 260, 80, 80));
this.walls.push(new Wall(1250, 180, 80, 80));
this.walls.push(new Wall(1250, H - 260, 80, 80));
// Core Beacon in the far center
this.beaconAltar = new BeaconAltar(1500, H / 2);
// Core Guardian Boss: Corrupted Titan Construct
this.enemies.push(new CorruptedConstruct(1050, H / 2, 'boss'));
this.enemies.push(new CorruptedConstruct(800, 250, 'knight'));
this.enemies.push(new CorruptedConstruct(800, H - 250, 'knight'));
}
}
setDoorState(doorId, open) {
for (const d of this.doors) {
if (d.id === doorId) {
d.isOpen = open;
}
}
}
spawnGoldLoot(x, y, amount) {
this.goldLoots.push(new GoldLoot(x, y, amount));
}
spawnHealthContainer(x, y, index) {
this.healthContainerLoots.push(new HealthContainerLoot(x, y, index));
}
spawnWorldItem(x, y, key, name, desc) {
this.worldItems.push(new WorldItemLoot(x, y, key, name, desc));
}
handleInteract() {
// 1. Check Dialogue dismissal
const diag = document.getElementById('dialogue-modal');
if (diag.style.display === 'block') {
this.closeDialogue();
return;
}
// 2. Check Sanctuary Stone interaction
for (const s of this.sanctuaryStones) {
const d = Math.hypot(s.x - this.player.x, s.y - this.player.y);
if (d < s.radius + this.player.radius + 20) {
s.activate(this.player, this.sound, this.particles, this.floatingTexts);
return;
}
}
// 3. Check Weapon Altar interaction
for (const a of this.weaponAltars) {
const d = Math.hypot(a.x - this.player.x, a.y - this.player.y);
if (d < a.radius + this.player.radius + 25) {
a.activate(this.player, this.sound, this.particles, this.floatingTexts);
return;
}
}
// 4. Check Beacon Altar interaction in Zone 6
if (this.beaconAltar && !this.beaconAltar.activated) {
const d = Math.hypot(this.beaconAltar.x - this.player.x, this.beaconAltar.y - this.player.y);
if (d < this.beaconAltar.radius + this.player.radius + 30) {
// Only can activate once boss is defeated
const bossAlive = this.enemies.some(e => e.type === 'boss' && !e.isDead);
if (bossAlive) {
this.showToast("The Corrupted Titan still guards the Beacon!");
} else {
this.beaconAltar.activate(this.player, this.sound, this.particles);
}
return;
}
}
// 5. Check Shop Altar in Zone 2
if (this.currentZoneId === 2) {
const shopDist = Math.hypot(700 - this.player.x, 250 - this.player.y);
if (shopDist < 70) {
this.openShop();
return;
}
}
}
useQuickItem(itemKey) {
if (itemKey === 'vitalStem') {
if (this.player.vitalStems > 0 && this.player.hp < this.player.maxHp) {
this.player.vitalStems -= 1;
this.player.heal(4, this.particles, this.floatingTexts); // 2 containers = 4 HP
this.sound.playItemPickup();
this.showToast("Used Vital Stem (+4 HP)");
} else if (this.player.vitalStems <= 0) {
this.showToast("No Vital Stems remaining!");
}
} else if (itemKey === 'bindingSeal') {
if (this.player.bindingSeals > 0) {
// Freeze closest active enemy within 280px for 3 seconds
let closest = null;
let minDist = 280;
for (const e of this.enemies) {
if (e.isDead) continue;
const d = Math.hypot(e.x - this.player.x, e.y - this.player.y);
if (d < minDist) {
minDist = d;
closest = e;
}
}
if (closest) {
this.player.bindingSeals -= 1;
closest.freezeTimer = 3.0;
this.sound.playFreeze();
this.showToast("Corrupted Construct Bound in Stasis!");
this.floatingTexts.push(new FloatingText(closest.x, closest.y - 25, "FROZEN (3s)", '#38bdf8', 16));
for (let i = 0; i < 20; i++) {
this.particles.push(new Particle(
closest.x,
closest.y,
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100,
'#38bdf8',
3,
0.5,
'ember'
));
}
} else {
this.showToast("No targets in range for Binding Seal.");
}
} else {
this.showToast("No Binding Seals remaining!");
}
}
this.updateHUD();
}
toggleInventory() {
const modal = document.getElementById('inventory-modal');
if (modal.style.display === 'flex') {
modal.style.display = 'none';
this.isPaused = false;
} else {
this.renderInventory();
modal.style.display = 'flex';
this.isPaused = true;
}
}
renderInventory() {
const list = document.getElementById('inventory-list');
list.innerHTML = '';
const itemsData = [
{
key: 'wardensBand',
name: "Warden's Band",
type: "Accessory",
desc: "Polished silver crest ring. Significantly speeds up weapon swing recovery.",
has: this.inventory.includes('wardensBand')
},
{
key: 'phantomLantern',
name: "Phantom Lantern",
type: "Accessory",
desc: "Crystalline light source. Reveals hairline wall cracks and submerged hidden pathways.",
has: this.inventory.includes('phantomLantern')
},
{
key: 'shadowCloak',
name: "Shadow Cloak",
type: "Accessory",
desc: "Starlight-woven shroud. Absorbs kinetic shock, granting armor against traps and blows.",
has: this.inventory.includes('shadowCloak')
},
{
key: 'vitalStem',
name: `Vital Stem (x${this.player.vitalStems})`,
type: "Consumable",
desc: "Gleaming green stardust flora. Instantly regenerates 4 HP (2 full heart containers).",
has: this.player.vitalStems > 0,
isConsumable: true
},
{
key: 'bindingSeal',
name: `Binding Seal (x${this.player.bindingSeals})`,
type: "Consumable",
desc: "Etched cipher rune. Ensnared construct is frozen motionless in stasis for 3 seconds.",
has: this.player.bindingSeals > 0,
isConsumable: true
}
];
itemsData.forEach(item => {
if (!item.has) return;
const card = document.createElement('div');
card.className = `item-card ${this.player.equipped[item.key] ? 'equipped' : ''}`;
let actionBtnText = '';
if (item.isConsumable) {
actionBtnText = `<button class="btn" style="padding:4px 10px; font-size:11px;" onclick="game.useQuickItem('${item.key}'); game.renderInventory();">Use</button>`;
} else {
const isEq = this.player.equipped[item.key];
actionBtnText = `<button class="btn" style="padding:4px 10px; font-size:11px;" onclick="game.toggleEquip('${item.key}')">${isEq ? 'Unequip' : 'Equip'}</button>`;
}
card.innerHTML = `
<div class="item-info">
<div class="item-name">${item.name} <span class="item-badge">${item.type}</span></div>
<div class="item-desc">${item.desc}</div>
</div>
<div>${actionBtnText}</div>
`;
list.appendChild(card);
});
if (list.children.length === 0) {
list.innerHTML = '<div style="color:#64748b; text-align:center; padding:20px;">Your satchel is currently empty. Explore the Observatory to discover relics.</div>';
}
}
toggleEquip(itemKey) {
this.player.equipped[itemKey] = !this.player.equipped[itemKey];
this.sound.playItemPickup();
this.renderInventory();
this.updateHUD();
}
openShop() {
this.isPaused = true;
const modal = document.getElementById('shop-modal');
modal.style.display = 'flex';
this.renderShop();
}
closeShop() {
const modal = document.getElementById('shop-modal');
modal.style.display = 'none';
this.isPaused = false;
}
renderShop() {
document.getElementById('shop-gold-amount').innerText = this.gold;
const list = document.getElementById('shop-list');
list.innerHTML = '';
const shopItems = [
{
id: 'vitalStem',
name: "Vital Stem",
cost: 50,
desc: "Restores 2 full heart containers (4 HP).",
available: this.shopVitalStems > 0,
stock: this.shopVitalStems,
buy: () => {
if (this.gold >= 50 && this.shopVitalStems > 0) {
this.gold -= 50;
this.shopVitalStems -= 1;
this.player.vitalStems += 1;
this.sound.playShopPurchase();
this.renderShop();
this.updateHUD();
}
}
},
{
id: 'bindingSeal',
name: "Binding Seal",
cost: 75,
desc: "Cipher rune that freezes an enemy for 3 seconds.",
available: this.shopBindingSeals > 0,
stock: this.shopBindingSeals,
buy: () => {
if (this.gold >= 75 && this.shopBindingSeals > 0) {
this.gold -= 75;
this.shopBindingSeals -= 1;
this.player.bindingSeals += 1;
this.sound.playShopPurchase();
this.renderShop();
this.updateHUD();
}
}
},
{
id: 'container5',
name: "Heart Container Upgrade #5",
cost: 300,
desc: "Expand your maximum vitality to the 5th and final celestial heart container (10 Max HP).",
available: this.shopContainer5Available && this.player.maxContainers < 5,
stock: this.shopContainer5Available ? 1 : 0,
buy: () => {
if (this.gold >= 300 && this.shopContainer5Available) {
this.gold -= 300;
this.shopContainer5Available = false;
this.player.maxContainers = 5;
this.player.hp = this.player.maxHp;
this.sound.playMajorPickup();
this.triggerScreenFlash('rgba(230, 57, 70, 0.45)');
this.triggerShimmer(2500);
this.showToast("Heart Container #5 Purchased!");
this.renderShop();
this.updateHUD();
}
}
}
];
shopItems.forEach(item => {
const card = document.createElement('div');
card.className = 'item-card';
const canAfford = this.gold >= item.cost && item.available;
card.innerHTML = `
<div class="item-info">
<div class="item-name">${item.name} <span class="item-badge" style="color:var(--gold);">${item.cost} Sigils</span></div>
<div class="item-desc">${item.desc} (In Stock: ${item.stock})</div>
</div>
<div>
<button class="btn" style="padding:6px 14px; font-size:12px;" ${!canAfford ? 'disabled' : ''} onclick="window.shopAction('${item.id}')">Purchase</button>
</div>
`;
list.appendChild(card);
});
window.shopAction = (id) => {
const target = shopItems.find(i => i.id === id);
if (target) target.buy();
};
}
openDialogue(title, text) {
const modal = document.getElementById('dialogue-modal');
document.getElementById('dialogue-title').innerText = title;
document.getElementById('dialogue-body').innerText = text;
modal.style.display = 'block';
}
closeDialogue() {
document.getElementById('dialogue-modal').style.display = 'none';
}
showToast(text) {
const toast = document.getElementById('notification-toast');
toast.innerText = text;
toast.classList.add('show');
if (this.toastTimeout) clearTimeout(this.toastTimeout);
this.toastTimeout = setTimeout(() => {
toast.classList.remove('show');
}, 2800);
}
triggerScreenFlash(color) {
const el = document.getElementById('screen-flash');
el.style.background = color;
setTimeout(() => {
el.style.background = 'transparent';
}, 150);
}
triggerShimmer(duration = 1800) {
const el = document.getElementById('shimmer-overlay');
el.style.boxShadow = 'inset 0 0 100px rgba(255, 224, 102, 0.4), 0 0 40px rgba(244, 197, 66, 0.3)';
setTimeout(() => {
el.style.boxShadow = 'inset 0 0 100px rgba(0,0,0,0.8)';
}, duration);
}
updateHUD() {
// 1. Update Hearts Display
const container = document.getElementById('hearts-container');
container.innerHTML = '';
for (let i = 0; i < this.player.maxContainers; i++) {
const heartBox = document.createElement('div');
heartBox.className = 'heart-box';
// Container has 2 HP
const hpInThisContainer = Math.max(0, Math.min(2, this.player.hp - (i * 2)));
// SVG Heart Container
let fillColor = 'none';
let strokeColor = '#94a3b8';
if (hpInThisContainer === 2) {
fillColor = '#e63946'; // Full heart
strokeColor = '#fca5a5';
} else if (hpInThisContainer === 1) {
fillColor = 'url(#halfHeartGrad)'; // Half heart
strokeColor = '#e63946';
} else {
fillColor = 'rgba(15, 22, 35, 0.8)'; // Empty heart
strokeColor = '#475569';
}
heartBox.innerHTML = `
<svg viewBox="0 0 32 32" width="28" height="28">
<defs>
<linearGradient id="halfHeartGrad" x1="0" x2="1" y1="0" y2="0">
<stop offset="50%" stop-color="#e63946"/>
<stop offset="50%" stop-color="rgba(15, 22, 35, 0.8)"/>
</linearGradient>
</defs>
<path d="M16 28 C8 20 2 15 2 9 A7 7 0 0 1 16 6 A7 7 0 0 1 30 9 C30 15 24 20 16 28 Z"
fill="${fillColor}" stroke="${strokeColor}" stroke-width="2"/>
</svg>
`;
container.appendChild(heartBox);
}
// 2. Gold count
document.getElementById('gold-amount').innerText = this.gold;
// 3. Weapon Name
const weaponLabel = document.getElementById('weapon-name');
if (this.player.weaponLevel === 0) weaponLabel.innerText = "Ember Blade";
if (this.player.weaponLevel === 1) weaponLabel.innerText = "Ignited Edge";
if (this.player.weaponLevel === 2) weaponLabel.innerText = "Solar Falchion";
// 4. Quick Consumable counts
document.getElementById('vital-stem-count').innerText = this.player.vitalStems;
document.getElementById('binding-seal-count').innerText = this.player.bindingSeals;
}
onPlayerDeath() {
this.sound.playDamagePlayer();
document.getElementById('game-over-screen').style.display = 'flex';
}
respawnAtSanctuary() {
document.getElementById('game-over-screen').style.display = 'none';
this.player.isDead = false;
this.player.hp = this.player.maxHp;
this.loadZone(this.lastSanctuaryZone, this.lastSanctuaryPos);
}
onVictory() {
this.isPaused = true;
document.getElementById('victory-screen').style.display = 'flex';
document.getElementById('victory-stats').innerHTML = `
<div>Weapon State: <b>${this.player.weaponLevel === 2 ? 'Solar Falchion (Max)' : 'Ignited Edge'}</b></div>
<div>Health Essence: <b>${this.player.maxContainers} Containers (${this.player.maxHp} Max HP)</b></div>
<div>Sigils Collected: <b>${this.gold} Sigils</b></div>
<div>Relics Acquired: <b>${this.inventory.length} Accessories</b></div>
`;
}
restartGame() {
document.getElementById('victory-screen').style.display = 'none';
this.player = new Player(200, 450);
this.gold = 0;
this.inventory = [];
this.lastSanctuaryZone = 1;
this.lastSanctuaryPos = { x: 200, y: 450 };
this.loadZone(1);
this.isPaused = false;
}
gameLoop(timestamp) {
const dt = Math.min(0.05, (timestamp - this.lastTime) / 1000);
this.lastTime = timestamp;
if (!this.isPaused && this.isRunning) {
this.update(dt);
}
this.render();
requestAnimationFrame((t) => this.gameLoop(t));
}
update(dt) {
const zoneDef = ZONES.find(z => z.id === this.currentZoneId);
// Calculate Mouse in World Position based on Camera
this.mouseWorldPos.x = this.mouseScreenPos.x + this.camX;
this.mouseWorldPos.y = this.mouseScreenPos.y + this.camY;
// 1. Update Player
this.player.update(dt, this.keys, this.mouseWorldPos, this.walls, this.sound, this.particles, this.floatingTexts, this.currentZoneId);
// 2. Check Zone Exit Gateway to next zone
if (this.currentZoneId < 6) {
if (this.player.x > zoneDef.width - 50 && Math.abs(this.player.y - zoneDef.height / 2) < 80) {
this.loadZone(this.currentZoneId + 1);
return;
}
}
// 3. Update Enemies
for (const enemy of this.enemies) {
enemy.update(dt, this.player, this.walls, this.sound, this.particles, this.floatingTexts);
}
// 4. Update Sanctuary Stones
for (const s of this.sanctuaryStones) {
s.update(dt, this.particles);
}
// 5. Update Pressure Plates
for (const p of this.pressurePlates) {
p.update(this.player, this.enemies, this.sound);
}
// 6. Update Projectiles
for (let i = this.projectiles.length - 1; i >= 0; i--) {
const proj = this.projectiles[i];
proj.update(dt, this.particles);
let collided = false;
// Projectile vs Walls
for (const w of this.walls) {
if (w.broken) continue;
if (pointInRect(proj.x, proj.y, w.x, w.y, w.w, w.h)) {
collided = true;
if (w.isCrack) {
w.hit(proj.damage, this.sound, this.particles);
}
break;
}
}
// Projectile vs Ranged Switches
if (!collided && proj.isPlayer) {
for (const sw of this.rangedSwitches) {
const d = Math.hypot(sw.x - proj.x, sw.y - proj.y);
if (d < sw.radius + proj.radius) {
sw.hit(this.sound, this.particles, this.floatingTexts);
collided = true;
break;
}
}
}
// Projectile vs Enemies (if fired by player)
if (!collided && proj.isPlayer) {
for (const enemy of this.enemies) {
if (enemy.isDead) continue;
const d = Math.hypot(enemy.x - proj.x, enemy.y - proj.y);
if (d < enemy.radius + proj.radius) {
enemy.takeDamage(proj.damage, this.sound, this.particles, this.floatingTexts, 'aether');
collided = true;
break;
}
}
}
// Projectile vs Player (if fired by enemy/boss)
if (!collided && !proj.isPlayer) {
const d = Math.hypot(this.player.x - proj.x, this.player.y - proj.y);
if (d < this.player.radius + proj.radius) {
this.player.takeDamage(proj.damage, this.sound, this.particles, this.floatingTexts);
collided = true;
}
}
if (collided || proj.life <= 0) {
this.projectiles.splice(i, 1);
}
}
// 7. Update Collectibles & Loot
for (const loot of this.goldLoots) {
loot.update(dt, this.player, this.sound, this.particles, this.floatingTexts);
}
for (const heart of this.healthContainerLoots) {
heart.update(dt, this.player, this.sound, this.particles, this.floatingTexts);
}
for (const item of this.worldItems) {
item.update(dt, this.player, this.sound, this.particles, this.floatingTexts);
}
// 8. Update Beacon Altar
if (this.beaconAltar) {
this.beaconAltar.update(dt);
}
// 9. Update Particles & Floating Text
for (let i = this.particles.length - 1; i >= 0; i--) {
this.particles[i].update(dt);
if (this.particles[i].life <= 0) this.particles.splice(i, 1);
}
for (let i = this.floatingTexts.length - 1; i >= 0; i--) {
this.floatingTexts[i].update(dt);
if (this.floatingTexts[i].life <= 0) this.floatingTexts.splice(i, 1);
}
// 10. Update Camera to follow player smoothly
const targetCamX = this.player.x - this.canvas.width / 2;
const targetCamY = this.player.y - this.canvas.height / 2;
this.camX += (targetCamX - this.camX) * 0.1;
this.camY += (targetCamY - this.camY) * 0.1;
// Clamp Camera within zone bounds
this.camX = Math.max(0, Math.min(zoneDef.width - this.canvas.width, this.camX));
this.camY = Math.max(0, Math.min(zoneDef.height - this.canvas.height, this.camY));
// 11. Check Interaction Prompts
this.checkInteractionPrompts();
}
checkInteractionPrompts() {
const prompt = document.getElementById('interaction-prompt');
let show = false;
let text = "[E] Interact";
// Sanctuary stone prompt
for (const s of this.sanctuaryStones) {
const d = Math.hypot(s.x - this.player.x, s.y - this.player.y);
if (d < 50) {
show = true;
text = "[E] Rest at Sanctuary Stone";
break;
}
}
// Weapon Altar prompt
if (!show) {
for (const a of this.weaponAltars) {
if (!a.activated) {
const d = Math.hypot(a.x - this.player.x, a.y - this.player.y);
if (d < 55) {
show = true;
text = "[E] Claim Weapon Upgrade";
break;
}
}
}
}
// Shop prompt in Zone 2
if (!show && this.currentZoneId === 2) {
const d = Math.hypot(700 - this.player.x, 250 - this.player.y);
if (d < 70) {
show = true;
text = "[E] Trade with Observatory Altar";
}
}
// Beacon Altar in Zone 6
if (!show && this.beaconAltar && !this.beaconAltar.activated) {
const d = Math.hypot(this.beaconAltar.x - this.player.x, this.beaconAltar.y - this.player.y);
if (d < 75) {
show = true;
text = "[E] Reignite Core Beacon";
}
}
if (show) {
prompt.innerText = text;
prompt.style.display = 'block';
} else {
prompt.style.display = 'none';
}
}
render() {
const zoneDef = ZONES.find(z => z.id === this.currentZoneId);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.save();
this.ctx.translate(-this.camX, -this.camY);
// 1. Draw Floor Tile Grid
this.ctx.fillStyle = zoneDef.bgFloor;
this.ctx.fillRect(0, 0, zoneDef.width, zoneDef.height);
// Floor tile lines
this.ctx.strokeStyle = 'rgba(255,255,255,0.03)';
this.ctx.lineWidth = 1;
for (let x = 0; x < zoneDef.width; x += 60) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, zoneDef.height);
this.ctx.stroke();
}
for (let y = 0; y < zoneDef.height; y += 60) {
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(zoneDef.width, y);
this.ctx.stroke();
}
// Zone 2: Merchant Altar pedestal in world
if (this.currentZoneId === 2) {
this.ctx.fillStyle = '#334155';
this.ctx.strokeStyle = '#f4c542';
this.ctx.lineWidth = 2;
this.ctx.fillRect(660, 210, 80, 80);
this.ctx.strokeRect(660, 210, 80, 80);
// Floating merchant gold orb
this.ctx.fillStyle = '#f59e0b';
this.ctx.shadowBlur = 14;
this.ctx.shadowColor = '#fde047';
this.ctx.beginPath();
this.ctx.arc(700, 250, 16, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.shadowBlur = 0;
}
// 2. Draw Pressure Plates & Ranged Switches
for (const p of this.pressurePlates) p.draw(this.ctx);
for (const s of this.rangedSwitches) s.draw(this.ctx);
// 3. Draw Sanctuary Stones & Altars
for (const s of this.sanctuaryStones) s.draw(this.ctx);
for (const a of this.weaponAltars) a.draw(this.ctx);
if (this.beaconAltar) this.beaconAltar.draw(this.ctx);
// 4. Draw Collectibles & Loot
for (const l of this.goldLoots) l.draw(this.ctx);
for (const h of this.healthContainerLoots) h.draw(this.ctx);
for (const item of this.worldItems) item.draw(this.ctx);
// 5. Draw Walls & Gates
const phantomActive = this.player.equipped.phantomLantern;
for (const w of this.walls) w.draw(this.ctx, phantomActive);
for (const d of this.doors) d.draw(this.ctx);
// 6. Draw Exit Portal to next zone (if not in final zone)
if (this.currentZoneId < 6) {
const exitY = zoneDef.height / 2 - 80;
this.ctx.fillStyle = 'rgba(244, 197, 66, 0.15)';
this.ctx.strokeStyle = '#f4c542';
this.ctx.lineWidth = 2;
this.ctx.fillRect(zoneDef.width - 40, exitY, 40, 160);
this.ctx.strokeRect(zoneDef.width - 40, exitY, 40, 160);
// Gateway chevron indicator
this.ctx.fillStyle = '#fde047';
this.ctx.beginPath();
this.ctx.moveTo(zoneDef.width - 25, zoneDef.height / 2 - 12);
this.ctx.lineTo(zoneDef.width - 10, zoneDef.height / 2);
this.ctx.lineTo(zoneDef.width - 25, zoneDef.height / 2 + 12);
this.ctx.fill();
}
// 7. Draw Enemies
for (const enemy of this.enemies) enemy.draw(this.ctx);
// 8. Draw Player
this.player.draw(this.ctx);
// 9. Draw Projectiles & Particles
for (const p of this.projectiles) p.draw(this.ctx);
for (const p of this.particles) p.draw(this.ctx);
for (const txt of this.floatingTexts) txt.draw(this.ctx);
this.ctx.restore();
}
}
// Instantiate global engine
let game = null;
window.addEventListener('DOMContentLoaded', () => {
game = new GameEngine();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.7-flash","provider":"openrouter","persona":"full","sessionId":"4631c5ae-02c0-4e8b-a563-f0a5bc69f641","tokensIn":670043,"tokensOut":44195,"tokensTotal":714238,"cost":0.30092883749999993,"turns":18,"toolCalls":17,"failedToolCalls":0,"timestamp":"2026-08-17T17:16:24.309Z"} -->