RPG
thinkingcap qwen3.6 27b · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 49,183 bytes · SHA-256 0ad296ee30b8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dungeon of Echoes</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a0f; overflow: hidden; font-family: 'Segoe UI', sans-serif; user-select: none; }
canvas { display: block; cursor: crosshair; }
#ui-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud { position: absolute; top: 15px; left: 15px; display: flex; align-items: center; gap: 12px; }
.hearts { display: flex; gap: 3px; }
.heart { width: 24px; height: 24px; position: relative; }
.heart svg { width: 100%; height: 100%; }
#money-display { color: #ffd700; font-size: 18px; font-weight: bold; text-shadow: 0 0 8px rgba(255,215,0,0.5); display: flex; align-items: center; gap: 6px; }
#weapon-display { color: #aaa; font-size: 14px; margin-left: 10px; }
#stamina-bar-container { width: 120px; height: 8px; background: rgba(0,0,0,0.6); border-radius: 4px; overflow: hidden; margin-top: 5px; }
#stamina-bar { height: 100%; background: linear-gradient(90deg, #4fc3f7, #29b6f6); width: 100%; transition: width 0.1s; border-radius: 4px; }
#xp-bar-container { width: 150px; height: 6px; background: rgba(0,0,0,0.6); border-radius: 3px; overflow: hidden; margin-top: 3px; }
#xp-bar { height: 100%; background: linear-gradient(90deg, #ab47bc, #7b1fa2); width: 0%; transition: width 0.3s; border-radius: 3px; }
#level-display { color: #ce93d8; font-size: 13px; margin-left: 5px; }
#minimap { position: absolute; top: 15px; right: 15px; width: 140px; height: 140px; border: 2px solid rgba(255,255,255,0.2); border-radius: 8px; overflow: hidden; background: rgba(0,0,0,0.7); }
#minimap canvas { width: 100%; height: 100%; }
#inventory-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(15,15,25,0.95); border: 2px solid #4a4a6a; border-radius: 12px; padding: 20px; display: none; pointer-events: auto; min-width: 420px; max-height: 80vh; overflow-y: auto; }
#inventory-panel h2 { color: #e0e0ff; text-align: center; margin-bottom: 15px; font-size: 20px; letter-spacing: 2px; }
.inv-grid { display: grid; grid-template-columns: repeat(6, 56px); gap: 8px; justify-content: center; }
.inv-slot { width: 56px; height: 56px; background: rgba(30,30,50,0.8); border: 2px solid #3a3a5a; border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; transition: all 0.15s; }
.inv-slot:hover { border-color: #7c4dff; background: rgba(60,60,90,0.8); transform: scale(1.05); }
.inv-slot .item-icon { font-size: 24px; }
.inv-slot .item-count { position: absolute; bottom: 2px; right: 4px; color: #fff; font-size: 11px; font-weight: bold; text-shadow: 0 0 3px #000; }
#shop-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(15,15,25,0.97); border: 2px solid #ffd700; border-radius: 12px; padding: 25px; display: none; pointer-events: auto; min-width: 400px; }
#shop-panel h2 { color: #ffd700; text-align: center; margin-bottom: 15px; font-size: 22px; letter-spacing: 2px; }
.shop-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; margin: 6px 0; background: rgba(30,30,50,0.7); border-radius: 8px; cursor: pointer; transition: all 0.15s; }
.shop-item:hover { background: rgba(60,60,90,0.8); transform: translateX(5px); }
.shop-item .name { color: #e0e0ff; font-size: 14px; }
.shop-item .desc { color: #888; font-size: 11px; }
.shop-item .price { color: #ffd700; font-weight: bold; font-size: 14px; }
#save-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(15,15,25,0.97); border: 2px solid #4fc3f7; border-radius: 12px; padding: 25px; display: none; pointer-events: auto; text-align: center; }
#save-panel h2 { color: #4fc3f7; margin-bottom: 10px; }
#save-panel p { color: #aaa; margin-bottom: 15px; font-size: 14px; }
.save-btn { padding: 10px 30px; background: linear-gradient(135deg, #4fc3f7, #29b6f6); border: none; border-radius: 8px; color: #fff; font-size: 16px; cursor: pointer; margin: 5px; transition: all 0.2s; }
.save-btn:hover { transform: scale(1.05); box-shadow: 0 0 15px rgba(79,195,247,0.5); }
#message-log { position: absolute; bottom: 15px; left: 15px; max-width: 350px; pointer-events: none; }
.msg { color: #ccc; font-size: 13px; padding: 4px 10px; margin: 2px 0; background: rgba(0,0,0,0.6); border-radius: 4px; animation: msgFade 4s forwards; white-space: nowrap; }
@keyframes msgFade { 0%,70%{opacity:1} 100%{opacity:0} }
#controls-hint { position: absolute; bottom: 15px; right: 15px; color: #555; font-size: 11px; text-align: right; line-height: 1.6; }
#title-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a0f 70%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#title-screen h1 { color: #e0d0ff; font-size: 48px; letter-spacing: 6px; text-shadow: 0 0 30px rgba(124,77,255,0.5); margin-bottom: 10px; }
#title-screen .subtitle { color: #7c4dff; font-size: 18px; letter-spacing: 3px; margin-bottom: 40px; }
.start-btn { padding: 15px 50px; background: linear-gradient(135deg, #7c4dff, #6200ea); border: none; border-radius: 10px; color: #fff; font-size: 20px; cursor: pointer; letter-spacing: 2px; transition: all 0.3s; }
.start-btn:hover { transform: scale(1.08); box-shadow: 0 0 25px rgba(124,77,255,0.6); }
#death-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(20,0,0,0.9); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#death-screen h1 { color: #ff4444; font-size: 48px; margin-bottom: 20px; }
#death-screen p { color: #aaa; margin-bottom: 30px; }
</style>
</head>
<body>
<div id="title-screen">
<h1>DUNGEON OF ECHOES</h1>
<div class="subtitle">An Action-Adventure RPG</div>
<button class="start-btn" onclick="startGame()">BEGIN QUEST</button>
</div>
<div id="death-screen">
<h1>YOU HAVE FALLEN</h1>
<p>Your journey ends here...</p>
<button class="start-btn" onclick="restartGame()">TRY AGAIN</button>
</div>
<canvas id="gameCanvas"></canvas>
<div id="ui-overlay">
<div id="hud">
<div class="hearts" id="hearts-container"></div>
<div id="money-display"><span>🪙</span><span id="money-val">0</span></div>
<div id="weapon-display">⚔️ Rusty Sword</div>
<div id="level-display">Lv.1</div>
<div id="xp-bar-container"><div id="xp-bar"></div></div>
</div>
<div id="stamina-bar-container"><div id="stamina-bar"></div></div>
<div id="minimap"><canvas id="minimapCanvas" width="140" height="140"></canvas></div>
<div id="inventory-panel">
<h2>🎒 INVENTORY</h2>
<div class="inv-grid" id="inv-grid"></div>
</div>
<div id="shop-panel">
<h2>🏪 MERCHANT'S SHOP</h2>
<div id="shop-items"></div>
<p style="text-align:center;color:#888;margin-top:10px;font-size:12px;">Press ESC to close</p>
</div>
<div id="save-panel">
<h2>💾 SAVE POINT</h2>
<p id="save-info">Your progress will be saved here.</p>
<button class="save-btn" onclick="saveGame()">SAVE GAME</button>
<button class="save-btn" style="background:linear-gradient(135deg,#66bb6a,#43a047)" onclick="loadGame()" id="load-btn">LOAD GAME</button>
</div>
<div id="message-log"></div>
<div id="controls-hint">WASD - Move | CLICK - Attack<br>I - Inventory | E - Interact<br>SHIFT - Sprint</div>
</div>
<script>
// ==================== AUDIO ENGINE ====================
const AudioEngine = {
ctx: null,
init() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
},
play(type, vol = 0.3) {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const g = this.ctx.createGain();
g.connect(this.ctx.destination);
g.gain.setValueAtTime(vol, t);
switch(type) {
case 'walk': {
const o = this.ctx.createOscillator();
o.type = 'sine'; o.frequency.setValueAtTime(80 + Math.random()*40, t);
g.gain.exponentialRampToValueAtTime(0.001, t+0.06);
o.connect(g); o.start(t); o.stop(t+0.06);
break;
}
case 'attack': {
const o = this.ctx.createOscillator();
o.type = 'sawtooth'; o.frequency.setValueAtTime(300, t);
o.frequency.exponentialRampToValueAtTime(80, t+0.15);
g.gain.exponentialRampToValueAtTime(0.001, t+0.15);
o.connect(g); o.start(t); o.stop(t+0.15);
break;
}
case 'attack2': {
const o = this.ctx.createOscillator();
o.type = 'square'; o.frequency.setValueAtTime(400, t);
o.frequency.exponentialRampToValueAtTime(60, t+0.2);
g.gain.exponentialRampToValueAtTime(0.001, t+0.2);
o.connect(g); o.start(t); o.stop(t+0.2);
break;
}
case 'attack3': {
const buf = this.ctx.createBufferSource();
const b = this.ctx.createBuffer(1, this.ctx.sampleRate*0.3, this.ctx.sampleRate);
const d = b.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*4);
buf.buffer=b; g.gain.exponentialRampToValueAtTime(0.001,t+0.3);
buf.connect(g); buf.start(t);
break;
}
case 'ranged': {
const o = this.ctx.createOscillator();
o.type='sine'; o.frequency.setValueAtTime(800,t);
o.frequency.exponentialRampToValueAtTime(200,t+0.12);
g.gain.exponentialRampToValueAtTime(0.001,t+0.12);
o.connect(g); o.start(t); o.stop(t+0.12);
break;
}
case 'hit': {
const buf = this.ctx.createBufferSource();
const b = this.ctx.createBuffer(1, this.ctx.sampleRate*0.15, this.ctx.sampleRate);
const d = b.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*6);
buf.buffer=b; g.gain.setValueAtTime(vol*1.5,t);
g.gain.exponentialRampToValueAtTime(0.001,t+0.15);
buf.connect(g); buf.start(t);
break;
}
case 'pickup': {
const o = this.ctx.createOscillator();
o.type='sine'; o.frequency.setValueAtTime(400,t);
o.frequency.linearRampToValueAtTime(800,t+0.1);
g.gain.exponentialRampToValueAtTime(0.001,t+0.2);
o.connect(g); o.start(t); o.stop(t+0.2);
break;
}
case 'coin': {
const o = this.ctx.createOscillator();
o.type='sine'; o.frequency.setValueAtTime(1200,t);
g.gain.exponentialRampToValueAtTime(0.001,t+0.15);
o.connect(g); o.start(t); o.stop(t+0.15);
const o2=this.ctx.createOscillator();
const g2=this.ctx.createGain(); g2.connect(this.ctx.destination);
o2.type='sine'; o2.frequency.setValueAtTime(1600,t+0.08);
g2.gain.setValueAtTime(vol*0.5,t+0.08);
g2.gain.exponentialRampToValueAtTime(0.001,t+0.25);
o2.connect(g2); o2.start(t+0.08); o2.stop(t+0.25);
break;
}
case 'shop': {
const o = this.ctx.createOscillator();
o.type='triangle'; o.frequency.setValueAtTime(600,t);
o.frequency.linearRampToValueAtTime(900,t+0.15);
g.gain.exponentialRampToValueAtTime(0.001,t+0.3);
o.connect(g); o.start(t); o.stop(t+0.3);
break;
}
case 'save': {
[400,500,600].forEach((f,i)=>{
const o=this.ctx.createOscillator();const g2=this.ctx.createGain();g2.connect(this.ctx.destination);
o.type='sine';o.frequency.setValueAtTime(f,t+i*0.12);
g2.gain.setValueAtTime(vol*0.4,t+i*0.12);
g2.gain.exponentialRampToValueAtTime(0.001,t+i*0.12+0.2);
o.connect(g2);o.start(t+i*0.12);o.stop(t+i*0.12+0.2);
});
break;
}
case 'levelup': {
[523,659,784,1047].forEach((f,i)=>{
const o=this.ctx.createOscillator();const g2=this.ctx.createGain();g2.connect(this.ctx.destination);
o.type='sine';o.frequency.setValueAtTime(f,t+i*0.15);
g2.gain.setValueAtTime(vol*0.4,t+i*0.15);
g2.gain.exponentialRampToValueAtTime(0.001,t+i*0.15+0.3);
o.connect(g2);o.start(t+i*0.15);o.stop(t+i*0.15+0.3);
});
break;
}
case 'death': {
const o=this.ctx.createOscillator();o.type='sawtooth';
o.frequency.setValueAtTime(400,t);o.frequency.exponentialRampToValueAtTime(50,t+1);
g.gain.exponentialRampToValueAtTime(0.001,t+1);
o.connect(g);o.start(t);o.stop(t+1);
break;
}
}
}
};
// ==================== PARTICLE SYSTEM ====================
class Particle {
constructor(x,y,vx,vy,life,color,size,type='circle') {
this.x=x;this.y=y;this.vx=vx;this.vy=vy;this.life=life;this.maxLife=life;
this.color=color;this.size=size;this.type=type;this.rotation=Math.random()*Math.PI*2;
}
update(dt) {
this.x+=this.vx*dt;this.y+=this.vy*dt;
this.life-=dt;this.rotation+=3*dt;
if(this.type==='spark') this.vy+=200*dt;
return this.life>0;
}
draw(ctx,camX,camY) {
const a=Math.max(0,this.life/this.maxLife);
ctx.globalAlpha=a;
ctx.fillStyle=this.color;
if(this.type==='circle'||this.type==='spark') {
ctx.beginPath();ctx.arc(this.x-camX,this.y-camY,this.size*a,0,Math.PI*2);ctx.fill();
} else if(this.type==='square') {
ctx.save();ctx.translate(this.x-camX,this.y-camY);ctx.rotate(this.rotation);
ctx.fillRect(-this.size/2,-this.size/2,this.size*a,this.size*a);ctx.restore();
}
ctx.globalAlpha=1;
}
}
const particles = [];
function emitParticles(x,y,count,color,speed=100,life=0.5,size=4,type='circle') {
for(let i=0;i<count;i++) {
const angle=Math.random()*Math.PI*2;
const spd=speed*(0.3+Math.random()*0.7);
particles.push(new Particle(x,y,Math.cos(angle)*spd,Math.sin(angle)*spd,life*(0.5+Math.random()*0.5),color,size*(0.5+Math.random()),type));
}
}
// ==================== GAME CONSTANTS ====================
const TILE = 32;
const MAP_W = 80, MAP_H = 60;
const VIEW_TILES_X = Math.ceil(window.innerWidth/TILE)+2;
const VIEW_TILES_Y = Math.ceil(window.innerHeight/TILE)+2;
// Tile types
const T = { VOID:0, FLOOR:1, WALL:2, DOOR:3, SAVE_POINT:4, SHOP:5, CHEST:6, STAIRS:7, PILLAR:8 };
// ==================== MAP GENERATION ====================
let map = [];
let rooms = [];
let exploredMap = [];
function generateMap() {
map = Array.from({length:MAP_H}, ()=>Array(MAP_W).fill(T.VOID));
exploredMap = Array.from({length:MAP_H}, ()=>new Uint8Array(MAP_W));
rooms = [];
// Generate rooms using BSP-like approach
const roomCount = 18 + Math.floor(Math.random()*6);
for(let attempt=0;attempt<200&&rooms.length<roomCount;attempt++) {
const w=5+Math.floor(Math.random()*8);
const h=4+Math.floor(Math.random()*6);
const x=3+Math.floor(Math.random()*(MAP_W-w-6));
const y=3+Math.floor(Math.random()*(MAP_H-h-6));
let overlap=false;
for(const r of rooms) {
if(x<r.x+r.w+2&&x+w+2>r.y&&y<r.y+r.h+2&&y+h+2>r.y) { overlap=true;break; }
}
if(overlap) continue;
const room={x,y,w,h,cx:Math.floor(x+w/2),cy:Math.floor(y+h/2)};
rooms.push(room);
for(let ry=y;ry<y+h;ry++) for(let rx=x;rx<x+w;rx++) map[ry][rx]=T.FLOOR;
}
// Connect rooms with corridors
for(let i=1;i<rooms.length;i++) {
let cx=rooms[i-1].cx, cy=rooms[i-1].cy;
const tx=rooms[i].cx, ty=rooms[i].cy;
while(cx!==tx) { map[cy][cx]=T.FLOOR; cx+=cx<tx?1:-1; }
while(cy!==ty) { map[cy][cx]=T.FLOOR; cy+=cy<ty?1:-1; }
}
// Place walls around floor tiles
for(let y=0;y<MAP_H;y++) for(let x=0;x<MAP_W;x++) {
if(map[y][x]===T.VOID) {
let adj=false;
for(let dy=-1;dy<=1&&!adj;dy++) for(let dx=-1;dx<=1&&!adj;dx++) {
const ny=y+dy,nx=x+dx;
if(ny>=0&&ny<MAP_H&&nx>=0&&nx<MAP_W&&map[ny][nx]===T.FLOOR) adj=true;
}
if(adj) map[y][x]=T.WALL;
}
}
// Place special tiles in rooms
const shuffled = [...rooms].sort(()=>Math.random()-0.5);
// Save points (3 of them)
for(let i=0;i<3&&i<shuffled.length;i++) {
const r=shuffled[i]; map[r.cy][r.cx]=T.SAVE_POINT;
}
// Shop
if(shuffled.length>3) { map[shuffled[3].cy][shuffled[3].cx]=T.SHOP; }
// Chests (5+)
for(let i=4;i<Math.min(12,shuffled.length);i++) {
const r=shuffled[i]; map[r.cy][r.cx]=T.CHEST;
}
}
// ==================== ITEMS & INVENTORY ====================
const ITEM_DEFS = [
{id:'health_potion',name:'Health Potion',icon:'🧪',desc:'Restores 2 HP',type:'consumable',price:15,stack:true},
{id:'stamina_elixir',name:'Stamina Elixir',icon:'⚡',desc:'Full stamina restore',type:'consumable',price:10,stack:true},
{id:'iron_shield',name:'Iron Shield',icon:'🛡️',desc:'+1 max HP permanently',type:'accessory',price:50,stack:false,effect:{hp:2}},
{id:'speed_boots',name:'Speed Boots',icon:'👢',desc:'+20% move speed',type:'accessory',price:40,stack:false,effect:{speed:0.2}},
{id:'lucky_charm',name:'Lucky Charm',icon:'🍀',desc:'+50% gold find',type:'accessory',price:35,stack:false,effect:{goldBonus:0.5}},
{id:'fire_gem',name:'Fire Gem',icon:'💎',desc:'+2 weapon damage',type:'accessory',price:45,stack:false,effect:{damage:2}},
{id:'mana_crystal',name:'Mana Crystal',icon:'🔮',desc:'Unlocks ranged power',type:'key',price:0,stack:false},
];
let inventory = [];
function addItem(itemId, count=1) {
const def = ITEM_DEFS.find(i=>i.id===itemId);
if(!def) return;
if(def.stack) {
const existing = inventory.find(it=>it.id===itemId);
if(existing) { existing.count+=count; } else { inventory.push({...def,count}); }
} else {
if(inventory.find(it=>it.id===itemId)) return; // already have it
inventory.push({...def,count:1});
}
}
function useItem(index) {
const item = inventory[index];
if(!item || !item.type.startsWith('consumable')) return false;
switch(item.id) {
case 'health_potion': player.heal(2); emitParticles(player.x,player.y,15,'#ff4444',80,0.6,5,'spark'); break;
case 'stamina_elixir': player.stamina=player.maxStamina; emitParticles(player.x,player.y,15,'#4fc3f7',80,0.6,5,'spark'); break;
}
item.count--;
if(item.count<=0) inventory.splice(index,1);
AudioEngine.play('pickup');
return true;
}
// ==================== PLAYER ====================
const player = {
x:0,y:0,vx:0,vy:0,
hp:3,maxHp:3,
stamina:100,maxStamina:100,
speed:120,baseSpeed:120,
damage:5,baseDamage:5,
weaponLevel:0, // 0=base, 1=upgrade1, 2=upgrade2
hasRanged:false,
money:0,
xp:0,xpToNext:30,level:1,
attackCooldown:0,attackSpeed:0.4,
facing:{x:1,y:0},
invincible:0,
animFrame:0,animTimer:0,
walkSoundTimer:0,
equipment:{weapon:'Rusty Sword',shield:null,boots:null,charm:null,gem:null},
heal(amount) { this.hp=Math.min(this.maxHp,this.hp+amount); },
takeDamage(amount) {
if(this.invincible>0) return;
this.hp-=amount;
this.invincible=0.5;
emitParticles(this.x,this.y,12,'#ff0000',120,0.4,4,'spark');
AudioEngine.play('hit',0.4);
if(this.hp<=0) { this.hp=0; onPlayerDeath(); }
},
gainXP(amount) {
this.xp+=amount;
while(this.xp>=this.xpToNext) {
this.xp-=this.xpToNext;
this.level++;
this.xpToNext=Math.floor(this.xpToNext*1.5);
this.maxHp+=2;this.hp=this.maxHp;
this.baseDamage+=1;this.damage=this.baseDamage+this.getAccessoryBonus('damage');
AudioEngine.play('levelup',0.3);
addMessage(`Level Up! Now level ${this.level}`,'#ce93d8');
emitParticles(this.x,this.y,30,'#ce93d8',150,1,6,'spark');
}
},
getAccessoryBonus(stat) {
let bonus=0;
for(const it of inventory) { if(it.effect&&it.effect[stat]) bonus+=it.effect[stat]; }
return bonus;
},
getGoldMultiplier() { return 1+this.getAccessoryBonus('goldBonus'); },
getSpeedMultiplier() { return 1+this.getAccessoryBonus('speed'); },
update(dt) {
// Movement
let mx=0,my=0;
if(keys['w']||keys['arrowup']) my=-1;
if(keys['s']||keys['arrowdown']) my=1;
if(keys['a']||keys['arrowleft']) mx=-1;
if(keys['d']||keys['arrowright']) mx=1;
const sprinting = keys['shift']&&this.stamina>0&&(mx!==0||my!==0);
const spd = this.speed * this.getSpeedMultiplier() * (sprinting?1.6:1);
if(sprinting) this.stamina=Math.max(0,this.stamina-30*dt);
else this.stamina=Math.min(this.maxStamina,this.stamina+20*dt);
const len=Math.sqrt(mx*mx+my*my)||1;
mx/=len; my/=len;
if(mx!==0||my!==0) {
this.facing={x:mx,y:my};
this.animTimer+=dt*(sprinting?12:8);
if(this.animTimer>1) { this.animFrame=(this.animFrame+1)%4; this.animTimer=0; }
} else {
this.animFrame=0;
}
const nx=this.x+mx*spd*dt;
const ny=this.y+my*spd*dt;
if(!isWall(nx,this.y)) this.x=nx;
if(!isWall(this.x,ny)) this.y=ny;
// Walk sound
if(mx!==0||my!==0) {
this.walkSoundTimer+=dt;
const interval=sprinting?0.2:0.35;
if(this.walkSoundTimer>interval) { AudioEngine.play('walk',0.1); this.walkSoundTimer=0; }
}
// Attack cooldown
this.attackCooldown=Math.max(0,this.attackCooldown-dt);
this.invincible=Math.max(0,this.invincible-dt);
// Explore map
const px=Math.floor(this.x/TILE), py=Math.floor(this.y/TILE);
for(let dy=-5;dy<=5;dy++) for(let dx=-5;dx<=5;dx++) {
const ty=py+dy,tx=px+dx;
if(ty>=0&&ty<MAP_H&&tx>=0&&tx<MAP_W) exploredMap[ty][tx]=1;
}
// Check tile interactions
this.checkTileInteractions();
},
checkTileInteractions() {
const tx=Math.floor(this.x/TILE), ty=Math.floor(this.y/TILE);
if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) return;
const tile=map[ty][tx];
if(tile===T.CHEST) {
map[ty][tx]=T.FLOOR;
this.openChest();
} else if(tile===T.SAVE_POINT) {
showSavePanel();
} else if(tile===T.SHOP) {
openShop();
}
},
openChest() {
AudioEngine.play('pickup',0.4);
emitParticles(this.x,this.y,20,'#ffd700',100,0.8,5,'spark');
const gold = Math.floor((10+Math.random()*30)*this.getGoldMultiplier());
this.money+=gold;
addMessage(`Found ${gold} gold!`,'#ffd700');
// Random item drop
if(Math.random()<0.5) {
const possibleItems = ['health_potion','stamina_elixir'];
const itemId = possibleItems[Math.floor(Math.random()*possibleItems.length)];
addItem(itemId);
addMessage(`Found ${ITEM_DEFS.find(i=>i.id===itemId).name}!`,'#4fc3f7');
}
// Weapon upgrade drops at certain points
if(this.weaponLevel<1 && this.level>=2) {
this.weaponLevel=1;
this.equipment.weapon='Iron Blade';
this.baseDamage+=3;this.damage=this.baseDamage+this.getAccessoryBonus('damage');
this.attackSpeed=0.35;
addMessage('Weapon upgraded to Iron Blade!','#ff9800');
emitParticles(this.x,this.y,25,'#ff9800',150,1,6,'spark');
} else if(this.weaponLevel<2 && this.level>=4) {
this.weaponLevel=2;
this.equipment.weapon='Flame Sword';
this.baseDamage+=4;this.damage=this.baseDamage+this.getAccessoryBonus('damage');
this.attackSpeed=0.3;
addMessage('Weapon upgraded to Flame Sword!','#ff5722');
emitParticles(this.x,this.y,30,'#ff5722',180,1.2,7,'spark');
}
// Ranged weapon discovery mid-game (level 3+)
if(!this.hasRanged && this.level>=3) {
this.hasRanged=true;
addMessage('Discovered ranged attack! Hold SHIFT+CLICK','#9c27b0');
emitParticles(this.x,this.y,25,'#9c27b0',120,1,5,'spark');
}
// Health upgrade
if(this.maxHp<5 && Math.random()<0.3) {
this.maxHp=Math.min(5,this.maxHp+2);
this.hp=this.maxHp;
addMessage('Max HP increased!','#ff4444');
emitParticles(this.x,this.y,15,'#ff4444',80,0.6,5,'spark');
}
},
attack() {
if(this.attackCooldown>0) return;
this.attackCooldown=this.attackSpeed;
const isRanged = keys['shift'] && this.hasRanged;
if(isRanged) {
AudioEngine.play('ranged',0.3);
projectiles.push({
x:this.x,y:this.y,
vx:this.facing.x*400,vy:this.facing.y*400,
damage:this.damage,life:1.5,color:'#9c27b0'
});
emitParticles(this.x+this.facing.x*16,this.y+this.facing.y*16,5,'#9c27b0',60,0.3,3,'spark');
} else {
const soundType = this.weaponLevel===0?'attack':this.weaponLevel===1?'attack2':'attack3';
AudioEngine.play(soundType,0.3);
const range = 40 + this.weaponLevel*15;
const arc = Math.PI/2;
const fx=this.facing.x, fy=this.facing.y;
// Melee hitbox check
for(const e of enemies) {
if(e.hp<=0) continue;
const dx=e.x-this.x, dy=e.y-this.y;
const dist=Math.sqrt(dx*dx+dy*dy);
if(dist>range) continue;
const dot=(dx*fx+dy*fy)/dist;
if(dot<Math.cos(arc/2)) continue;
e.takeDamage(this.damage);
emitParticles(e.x,e.y,8,this.weaponLevel===2?'#ff5722':'#fff',100,0.4,4,'spark');
}
// Swing visual
emitParticles(
this.x+fx*30,this.y+fy*30,
6,'#ffffff',80,0.2,3,'circle'
);
}
},
draw(ctx,camX,camY) {
const sx=this.x-camX, sy=this.y-camY;
const bob = Math.sin(this.animTimer*Math.PI*2)*2*(this.animFrame!==0?1:0);
ctx.save();
ctx.translate(sx,sy+bob);
// Shadow
ctx.fillStyle='rgba(0,0,0,0.3)';
ctx.beginPath();ctx.ellipse(0,14,12,5,0,0,Math.PI*2);ctx.fill();
// Body color based on equipment
let bodyColor='#6a8caf';
if(this.equipment.shield) bodyColor='#7c9ab5';
if(this.equipment.boots) bodyColor='#8aaecf';
// Legs
const legSwing = Math.sin(this.animTimer*Math.PI*2)*4*(this.animFrame!==0?1:0);
ctx.fillStyle='#3a4a5a';
ctx.fillRect(-6,6,5,10+legSwing/2);
ctx.fillRect(1,6,5,10-legSwing/2);
// Boots visual
if(this.equipment.boots) {
ctx.fillStyle='#8d6e63';
ctx.fillRect(-7,14+Math.max(0,legSwing/2),7,4);
ctx.fillRect(0,14-Math.max(0,-legSwing/2),7,4);
}
// Body
ctx.fillStyle=bodyColor;
ctx.fillRect(-8,-6,16,14);
// Shield visual
if(this.equipment.shield) {
ctx.fillStyle='#9e9e9e';
ctx.fillRect(8,-4,5,10);
ctx.strokeStyle='#757575';ctx.lineWidth=1;
ctx.strokeRect(8,-4,5,10);
}
// Head
ctx.fillStyle='#f5d6b8';
ctx.fillRect(-5,-16,10,12);
// Eyes
const eyeOffX = this.facing.x*2;
const eyeOffY = this.facing.y*1;
ctx.fillStyle='#333';
ctx.fillRect(-2+eyeOffX,-12+eyeOffY,2,2);
ctx.fillRect(2+eyeOffX,-12+eyeOffY,2,2);
// Weapon
const weaponAngle = Math.atan2(this.facing.y,this.facing.x);
ctx.save();
ctx.translate(this.facing.x*8,this.facing.y*6);
ctx.rotate(weaponAngle + (this.attackCooldown>0?-0.5:0));
if(this.weaponLevel===0) {
// Rusty sword - brownish
ctx.fillStyle='#a1887f';ctx.fillRect(-2,-14,4,16);
ctx.fillStyle='#795548';ctx.fillRect(-4,-2,8,3);
} else if(this.weaponLevel===1) {
// Iron blade - silver
ctx.fillStyle='#cfd8dc';ctx.fillRect(-2.5,-16,5,18);
ctx.fillStyle='#90a4ae';ctx.fillRect(-5,-2,10,3);
ctx.fillStyle='#b0bec5';ctx.fillRect(-1,-17,3,3);
} else {
// Flame sword - red/orange with glow
ctx.shadowColor='#ff5722';ctx.shadowBlur=8;
ctx.fillStyle='#ff7043';ctx.fillRect(-2.5,-18,5,20);
ctx.fillStyle='#ffab91';ctx.fillRect(-1,-19,3,4);
ctx.shadowBlur=0;
}
ctx.restore();
// Invincibility flash
if(this.invincible>0 && Math.floor(this.invincible*10)%2===0) {
ctx.globalAlpha=0.5;
ctx.fillStyle='#fff';ctx.fillRect(-8,-16,16,30);
ctx.globalAlpha=1;
}
// Lucky charm visual
if(this.equipment.charm) {
ctx.fillStyle='#4caf50';
ctx.beginPath();ctx.arc(0,-10,2,0,Math.PI*2);ctx.fill();
}
ctx.restore();
}
};
// ==================== ENEMIES ====================
let enemies = [];
const ENEMY_TYPES = {
slime: {hp:8,speed:35,damage:2,xp:10,color:'#4caf50',size:10,attackSpeed:1.2},
skeleton: {hp:15,speed:50,damage:4,xp:20,color:'#e0e0e0',size:12,attackSpeed:0.9},
bat: {hp:6,speed:70,damage:3,xp:12,color:'#7b1fa2',size:8,attackSpeed:0.7},
ghost: {hp:12,speed:45,damage:5,xp:25,color:'#90caf9',size:11,attackSpeed:1.0},
golem: {hp:30,speed:25,damage:6,xp:40,color:'#8d6e63',size:16,attackSpeed:1.5},
};
function spawnEnemies() {
enemies = [];
for(const room of rooms) {
if(room===rooms[0]) continue; // Don't spawn in starting room
const count = 1+Math.floor(Math.random()*3);
for(let i=0;i<count;i++) {
const types = Object.keys(ENEMY_TYPES);
let typeIdx = Math.min(types.length-1, Math.floor(Math.random()*(1+rooms.indexOf(room)/rooms.length*2)));
const type = ENEMY_TYPES[types[typeIdx]];
enemies.push({
x: room.x*TILE + (Math.random()*room.w)*TILE,
y: room.y*TILE + (Math.random()*room.h)*TILE,
hp:type.hp, maxHp:type.hp, speed:type.speed, damage:type.damage,
xp:type.xp, color:type.color, size:type.size, attackSpeed:type.attackSpeed,
type:types[typeIdx],
attackCooldown:0, aggroRange:120+Math.random()*60,
state:'idle', alertTimer:0, hitFlash:0,
vx:0,vy:0, animTimer:Math.random()*Math.PI*2
});
}
}
}
function updateEnemies(dt) {
for(const e of enemies) {
if(e.hp<=0) continue;
const dx=player.x-e.x, dy=player.y-e.y;
const dist=Math.sqrt(dx*dx+dy*dy);
e.animTimer+=dt*3;
e.hitFlash=Math.max(0,e.hitFlash-dt);
e.attackCooldown=Math.max(0,e.attackCooldown-dt);
if(dist<e.aggroRange) {
e.state='chase';
const len=dist||1;
const nx=dx/len, ny=dy/len;
const spd=e.speed*(player.stamina>50?1:1.2); // Slightly faster if player is sprinting
const ex=e.x+nx*spd*dt, ey=e.y+ny*spd*dt;
if(!isWall(ex,e.y)) e.x=ex;
if(!isWall(e.x,ey)) e.y=ey;
// Attack player
if(dist<25 && e.attackCooldown<=0) {
e.attackCooldown=e.attackSpeed;
player.takeDamage(e.damage);
emitParticles(player.x,player.y,6,'#ff4444',80,0.3,3,'spark');
}
} else {
e.state='idle';
// Wander
if(Math.random()<0.02) {
const angle=Math.random()*Math.PI*2;
e.vx=Math.cos(angle)*e.speed*0.3;
e.vy=Math.sin(angle)*e.speed*0.3;
}
const wx=e.x+e.vx*dt, wy=e.y+e.vy*dt;
if(!isWall(wx,e.y)) e.x=wx;
if(!isWall(e.x,wy)) e.y=wy;
}
}
}
function drawEnemies(ctx,camX,camY) {
for(const e of enemies) {
if(e.hp<=0) continue;
const sx=e.x-camX, sy=e.y-camY;
if(sx<-50||sx>window.innerWidth+50||sy<-50||sy>window.innerHeight+50) continue;
ctx.save();
ctx.translate(sx,sy);
// Shadow
ctx.fillStyle='rgba(0,0,0,0.3)';
ctx.beginPath();ctx.ellipse(0,e.size*0.8,e.size*0.7,e.size*0.25,0,0,Math.PI*2);ctx.fill();
const bob=Math.sin(e.animTimer)*2;
if(e.type==='slime') {
// Slime - squishy blob
ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
ctx.beginPath();
ctx.ellipse(0,bob,e.size, e.size*0.7+Math.sin(e.animTimer*2)*2, 0, 0, Math.PI*2);
ctx.fill();
// Eyes
ctx.fillStyle='#333';
ctx.fillRect(-4,-4+bob,3,3);ctx.fillRect(2,-4+bob,3,3);
} else if(e.type==='skeleton') {
// Skeleton - bony figure
ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
ctx.fillRect(-6,-12+bob,12,18);
ctx.fillRect(-4,-16+bob,8,6);
ctx.fillStyle='#333';
ctx.fillRect(-3,-15+bob,2,2);ctx.fillRect(1,-15+bob,2,2);
// Arms
ctx.fillRect(-10,-8+bob,4,10);ctx.fillRect(6,-8+bob,4,10);
} else if(e.type==='bat') {
// Bat - small with wings
const wingFlap=Math.sin(e.animTimer*5)*8;
ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
ctx.beginPath();ctx.ellipse(0,bob,6,4,0,0,Math.PI*2);ctx.fill();
// Wings
ctx.fillRect(-14,-2+bob+wingFlap,8,3);
ctx.fillRect(6,-2+bob-wingFlap,8,3);
} else if(e.type==='ghost') {
// Ghost - translucent
ctx.globalAlpha=0.7;
ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
ctx.beginPath();ctx.arc(0,bob-4,e.size,Math.PI,0);
ctx.lineTo(e.size,e.size+bob);
for(let i=0;i<4;i++) {
const wx=e.size-i*(e.size*2/3);
ctx.quadraticCurveTo(wx+e.size/6,e.size-4+bob,wx,e.size+bob);
}
ctx.fill();
ctx.globalAlpha=1;
ctx.fillStyle='#333';
ctx.fillRect(-4,-6+bob,3,3);ctx.fillRect(2,-6+bob,3,3);
} else if(e.type==='golem') {
// Golem - big rock creature
ctx.fillStyle=e.hitFlash>0?'#fff':e.color;
ctx.fillRect(-10,-14+bob,20,24);
ctx.fillRect(-8,-18+bob,16,6);
ctx.fillStyle='#5d4037';
ctx.fillRect(-5,-17+bob,3,3);ctx.fillRect(2,-17+bob,3,3);
// Cracks
ctx.strokeStyle='#4e342e';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(-3,-8+bob);ctx.lineTo(2,0+bob);ctx.stroke();
}
// Health bar
if(e.hp<e.maxHp) {
const bw=e.size*2;
ctx.fillStyle='rgba(0,0,0,0.5)';
ctx.fillRect(-bw/2,-e.size-10+bob,bw,4);
ctx.fillStyle='#f44336';
ctx.fillRect(-bw/2,-e.size-10+bob,bw*(e.hp/e.maxHp),4);
}
ctx.restore();
}
}
// ==================== PROJECTILES ====================
let projectiles = [];
function updateProjectiles(dt) {
for(let i=projectiles.length-1;i>=0;i--) {
const p=projectiles[i];
p.x+=p.vx*dt; p.y+=p.vy*dt;
p.life-=dt;
if(isWall(p.x,p.y)||p.life<=0) {
emitParticles(p.x,p.y,5,p.color,60,0.3,3,'spark');
projectiles.splice(i,1); continue;
}
for(const e of enemies) {
if(e.hp<=0) continue;
const dx=e.x-p.x, dy=e.y-p.y;
if(Math.sqrt(dx*dx+dy*dy)<e.size+5) {
e.takeDamage(p.damage);
emitParticles(e.x,e.y,8,p.color,100,0.4,4,'spark');
projectiles.splice(i,1); break;
}
}
}
}
function drawProjectiles(ctx,camX,camY) {
for(const p of projectiles) {
ctx.fillStyle=p.color;
ctx.shadowColor=p.color;ctx.shadowBlur=6;
ctx.beginPath();ctx.arc(p.x-camX,p.y-camY,4,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
}
}
// ==================== ENEMY DAMAGE ====================
function EnemyTakeDamage(amount) {
// Already handled in enemy object
}
enemies.prototype.takeDamage = function(amount) {};
// Fix: add takeDamage to enemy objects after creation
Object.defineProperty(Object.getPrototypeOf({}), 'takeDamage', {value:function(){} });
// We'll handle it inline instead - enemies are plain objects
function damageEnemy(e, amount) {
e.hp-=amount;
e.hitFlash=0.15;
if(e.hp<=0) {
player.gainXP(e.xp);
const gold=Math.floor((3+Math.random()*8)*player.getGoldMultiplier());
player.money+=gold;
emitParticles(e.x,e.y,20,e.color,120,0.6,5,'spark');
// Drop coins visual
for(let i=0;i<3;i++) {
setTimeout(()=>emitParticles(e.x+Math.random()*20-10,e.y,'#ffd700',40,0.4,3,'square'),i*50);
}
}
}
// Patch the enemy objects
function patchEnemies() {
for(const e of enemies) {
e.takeDamage = function(amount) { damageEnemy(this, amount); };
}
}
// ==================== MAP HELPERS ====================
function isWall(x,y) {
const tx=Math.floor(x/TILE), ty=Math.floor(y/TILE);
if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) return true;
const t=map[ty][tx];
return t===T.WALL || t===T.VOID || t===T.PILLAR;
}
// ==================== CAMERA ====================
let camX=0, camY=0;
function updateCamera() {
const targetX = player.x - window.innerWidth/2;
const targetY = player.y - window.innerHeight/2;
camX += (targetX-camX)*0.1;
camY += (targetY-camY)*0.1;
}
// ==================== RENDERING ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const mmCanvas = document.getElementById('minimapCanvas');
const mmCtx = mmCanvas.getContext('2d');
function resize() {
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
const TILE_COLORS = {
[T.VOID]: '#0a0a0f',
[T.FLOOR]: '#2a2a3e',
[T.WALL]: '#1a1a2e',
[T.DOOR]: '#4a3a2a',
[T.SAVE_POINT]: '#1a3a4a',
[T.SHOP]: '#3a2a1a',
[T.CHEST]: '#2a2a3e',
[T.PILLAR]: '#2a2a3e',
};
function drawMap() {
const startX=Math.floor(camX/TILE)-1;
const startY=Math.floor(camY/TILE)-1;
const endX=startX+VIEW_TILES_X+2;
const endY=startY+VIEW_TILES_Y+2;
for(let ty=startY;ty<endY;ty++) {
for(let tx=startX;tx<endX;tx++) {
if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) continue;
const sx=tx*TILE-camX, sy=ty*TILE-camY;
const tile=map[ty][tx];
// Fog of war
if(!exploredMap[ty][tx]) { ctx.fillStyle='#0a0a0f'; ctx.fillRect(sx,sy,TILE,TILE); continue; }
ctx.fillStyle=TILE_COLORS[tile]||'#2a2a3e';
ctx.fillRect(sx,sy,TILE,TILE);
// Floor detail
if(tile===T.FLOOR) {
ctx.fillStyle='rgba(255,255,255,0.02)';
if((tx+ty)%2===0) ctx.fillRect(sx,sy,TILE,TILE);
}
// Wall top highlight
if(tile===T.WALL) {
ctx.fillStyle='rgba(255,255,255,0.05)';
ctx.fillRect(sx,sy,TILE,3);
ctx.fillStyle='rgba(0,0,0,0.3)';
ctx.fillRect(sx,sy+TILE-3,TILE,3);
}
// Special tiles
if(tile===T.SAVE_POINT) {
const pulse=Math.sin(Date.now()/500)*0.3+0.7;
ctx.fillStyle=`rgba(79,195,247,${pulse*0.3})`;
ctx.fillRect(sx,sy,TILE,TILE);
// Crystal shape
ctx.fillStyle='#4fc3f7';
ctx.beginPath();
ctx.moveTo(sx+TILE/2,sy+6);ctx.lineTo(sx+TILE-8,sy+TILE/2);
ctx.lineTo(sx+TILE/2,sy+TILE-6);ctx.lineTo(sx+8,sy+TILE/2);
ctx.fill();
} else if(tile===T.SHOP) {
ctx.fillStyle='rgba(255,215,0,0.15)';
ctx.fillRect(sx,sy,TILE,TILE);
// Shop sign
ctx.fillStyle='#ffd700';
ctx.font='bold 16px sans-serif';ctx.textAlign='center';
ctx.fillText('🏪',sx+TILE/2,sy+TILE/2+5);
} else if(tile===T.CHEST) {
const bob=Math.sin(Date.now()/400)*2;
ctx.fillStyle='#8d6e63';
ctx.fillRect(sx+8,sy+10+bob,16,14);
ctx.fillStyle='#ffd700';
ctx.fillRect(sx+12,sy+12+bob,8,4);
// Glow
ctx.fillStyle='rgba(255,215,0,0.1)';
ctx.beginPath();ctx.arc(sx+TILE/2,sy+TILE/2,18,0,Math.PI*2);ctx.fill();
}
}
}
}
function drawMinimap() {
mmCtx.fillStyle='#0a0a0f';
mmCtx.fillRect(0,0,140,140);
const scale=1.5;
const px=Math.floor(player.x/TILE), py=Math.floor(player.y/TILE);
const offX=70-px*scale, offY=70-py*scale;
for(let ty=py-40;ty<py+40;ty++) {
for(let tx=px-40;tx<px+40;tx++) {
if(tx<0||tx>=MAP_W||ty<0||ty>=MAP_H) continue;
if(!exploredMap[ty][tx]) continue;
const tile=map[ty][tx];
let color='#333';
if(tile===T.FLOOR) color='#4a4a5e';
else if(tile===T.WALL) color='#2a2a3e';
else if(tile===T.SAVE_POINT) color='#4fc3f7';
else if(tile===T.SHOP) color='#ffd700';
else if(tile===T.CHEST) color='#8d6e63';
mmCtx.fillStyle=color;
mmCtx.fillRect(tx*scale+offX,ty*scale+offY,scale,scale);
}
}
// Player dot
mmCtx.fillStyle='#ff4444';
mmCtx.beginPath();mmCtx.arc(70,70,2.5,0,Math.PI*2);mmCtx.fill();
// Enemy dots
for(const e of enemies) {
if(e.hp<=0) continue;
const ex=Math.floor(e.x/TILE)*scale+offX;
const ey=Math.floor(e.y/TILE)*scale+offY;
mmCtx.fillStyle='#ff6666';
mmCtx.fillRect(ex-1,ey-1,2,2);
}
}
// ==================== UI UPDATES ====================
function updateHUD() {
// Hearts
const hc=document.getElementById('hearts-container');
hc.innerHTML='';
for(let i=0;i<player.maxHp/2;i++) {
const div=document.createElement('div');
div.className='heart';
const filled = player.hp > i*2;
const half = !filled && player.hp > i*2-1;
if(filled) {
div.innerHTML='<svg viewBox="0 0 24 24"><path fill="#f44336" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
} else if(half) {
div.innerHTML='<svg viewBox="0 0 24 24"><defs><linearGradient id="hg"><stop offset="50%" stop-color="#f44336"/><stop offset="50%" stop-color="#555"/></linearGradient></defs><path fill="url(#hg)" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
} else {
div.innerHTML='<svg viewBox="0 0 24 24"><path fill="#444" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
}
hc.appendChild(div);
}
document.getElementById('money-val').textContent=player.money;
const weaponNames=['Rusty Sword','Iron Blade','Flame Sword'];
document.getElementById('weapon-display').innerHTML=`⚔️ ${weaponNames[player.weaponLevel]}`;
document.getElementById('level-display').textContent=`Lv.${player.level}`;
document.getElementById('xp-bar').style.width=`${(player.xp/player.xpToNext)*100}%`;
document.getElementById('stamina-bar').style.width=`${(player.stamina/player.maxStamina)*100}%`;
}
// ==================== MESSAGES ====================
function addMessage(text, color='#ccc') {
const log=document.getElementById('message-log');
const msg=document.createElement('div');
msg.className='msg';msg.style.color=color;msg.textContent=text;
log.appendChild(msg);
setTimeout(()=>msg.remove(),4000);
}
// ==================== INVENTORY UI ====================
function updateInventoryUI() {
const grid=document.getElementById('inv-grid');
grid.innerHTML='';
for(let i=0;i<18;i++) {
const slot=document.createElement('div');
slot.className='inv-slot';
if(inventory[i]) {
const it=inventory[i];
slot.innerHTML=`<span class="item-icon">${it.icon}</span>${it.count>1?`<span class="item-count">${it.count}</span>`:''}`;
slot.title=`${it.name}\n${it.desc}`;
if(it.type==='consumable') {
slot.onclick=()=>{ useItem(i); updateInventoryUI(); };
}
}
grid.appendChild(slot);
}
}
// ==================== SHOP ====================
let shopOpen=false;
function openShop() {
shopOpen=true;
AudioEngine.play('shop',0.3);
const panel=document.getElementById('shop-panel');
panel.style.display='block';
renderShop();
}
function closeShop() {
shopOpen=false;
document.getElementById('shop-panel').style.display='none';
}
function renderShop() {
const container=document.getElementById('shop-items');
container.innerHTML='';
for(const item of ITEM_DEFS) {
if(item.id==='mana_crystal') continue; // Not sold
const div=document.createElement('div');
div.className='shop-item';
const owned=inventory.find(i=>i.id===item.id);
div.innerHTML=`<div><span class="name">${item.icon} ${item.name}</span><br><span class="desc">${item.desc}</span></div><span class="price">🪙${item.price}${owned?' ✓':''}</span>`;
if(!owned) {
div.onclick=()=>{
if(player.money>=item.price) {
player.money-=item.price;
addItem(item.id);
AudioEngine.play('coin',0.3);
addMessage(`Bought ${item.name}!`,'#ffd700');
// Apply accessory effects
if(item.type==='accessory') {
if(item.effect.hp) { player.maxHp+=item.effect.hp; player.hp=player.maxHp; }
if(item.effect.speed) {} // Applied dynamically
if(item.effect.damage) { player.baseDamage+=item.effect.damage; player.damage=player.baseDamage+player.getAccessoryBonus('damage'); }
player.equipment[item.id.replace('_',' ').split(' ')[0].toLowerCase()] = item.name;
}
renderShop(); updateInventoryUI();
} else {
addMessage('Not enough gold!','#f44336');
}
};
}
container.appendChild(div);
}
}
// ==================== SAVE SYSTEM ====================
let savePanelOpen=false;
function showSavePanel() {
if(savePanelOpen) return;
savePanelOpen=true;
AudioEngine.play('save',0.3);
document.getElementById('save-panel').style.display='block';
const hasSave=localStorage.getItem('dungeonOfEchoes_save');
document.getElementById('load-btn').style.display=hasSave?'inline-block':'none';
}
function hideSavePanel() { savePanelOpen=false; document.getElementById('save-panel').style.display='none'; }
function saveGame() {
const data = {
player: {
x:player.x,y:player.y,hp:player.hp,maxHp:player.maxHp,
money:player.money,xp:player.xp,xpToNext:player.xpToNext,level:player.level,
weaponLevel:player.weaponLevel,hasRanged:player.hasRanged,
baseDamage:player.baseDamage,damage:player.damage,attackSpeed:player.attackSpeed,
},
inventory: JSON.parse(JSON.stringify(inventory)),
mapState: rooms.map(r=>({x:r.x,y:r.y,w:r.w,h:r.h})),
};
localStorage.setItem('dungeonOfEchoes_save',JSON.stringify(data));
addMessage('Game saved!','#4fc3f7');
emitParticles(player.x,player.y,25,'#4fc3f7',100,0.8,5,'spark');
hideSavePanel();
}
function loadGame() {
const raw=localStorage.getItem('dungeonOfEchoes_save');
if(!raw) return;
const data=JSON.parse(raw);
Object.assign(player,data.player);
inventory=data.inventory||[];
addMessage('Game loaded!','#4fc3f7');
hideSavePanel();
}
// ==================== INPUT ====================
const keys={};
let mouseDown=false, mouseX=0, mouseY=0;
document.addEventListener('keydown',e=>{
keys[e.key.toLowerCase()]=true;
if(e.key==='i'||e.key==='I') {
const panel=document.getElementById('inventory-panel');
if(panel.style.display==='block') { panel.style.display='none'; }
else { panel.style.display='block'; updateInventoryUI(); }
}
if(e.key==='Escape') { closeShop(); hideSavePanel(); document.getElementById('inventory-panel').style.display='none'; }
});
document.addEventListener('keyup',e=>{ keys[e.key.toLowerCase()]=false; });
canvas.addEventListener('mousedown',e=>{ mouseDown=true; mouseX=e.clientX; mouseY=e.clientY; });
canvas.addEventListener('mouseup',()=>{ mouseDown=false; });
canvas.addEventListener('mousemove',e=>{ mouseX=e.clientX; mouseY=e.clientY; });
// ==================== GAME STATE ====================
let gameState='title'; // title, playing, dead
let gameTime=0;
function startGame() {
document.getElementById('title-screen').style.display='none';
AudioEngine.init();
generateMap();
player.x=rooms[0].cx*TILE+TILE/2;
player.y=rooms[0].cy*TILE+TILE/2;
spawnEnemies();
patchEnemies();
gameState='playing';
addMessage('Welcome to the Dungeon of Echoes!','#ce93d8');
addMessage('Explore, fight, and grow stronger!','#aaa');
}
function restartGame() {
document.getElementById('death-screen').style.display='none';
player.hp=player.maxHp;
player.money=Math.floor(player.money*0.5);
player.xp=0;player.level=1;player.xpToNext=30;
player.weaponLevel=0;player.hasRanged=false;
player.baseDamage=5;player.damage=5;player.attackSpeed=0.4;
player.equipment={weapon:'Rusty Sword',shield:null,boots:null,charm:null,gem:null};
inventory=[];
generateMap();
player.x=rooms[0].cx*TILE+TILE/2;
player.y=rooms[0].cy*TILE+TILE/2;
spawnEnemies();patchEnemies();
gameState='playing';
}
function onPlayerDeath() {
AudioEngine.play('death',0.5);
emitParticles(player.x,player.y,40,'#ff0000',150,1.5,6,'spark');
setTimeout(()=>{
gameState='dead';
document.getElementById('death-screen').style.display='flex';
},800);
}
// ==================== MAIN LOOP ====================
let lastTime=performance.now();
function gameLoop(now) {
requestAnimationFrame(gameLoop);
const dt=Math.min(0.05,(now-lastTime)/1000);
lastTime=now;
if(gameState!=='playing') return;
gameTime+=dt;
// Update
player.update(dt);
updateEnemies(dt);
updateProjectiles(dt);
updateCamera();
// Attack input
if(mouseDown && !shopOpen && !savePanelOpen) {
player.attack();
}
// Update particles
for(let i=particles.length-1;i>=0;i--) {
if(!particles[i].update(dt)) particles.splice(i,1);
}
// Render
ctx.fillStyle='#0a0a0f';
ctx.fillRect(0,0,canvas.width,canvas.height);
drawMap();
drawEnemies(ctx,camX,camY);
player.draw(ctx,camX,camY);
drawProjectiles(ctx,camX,camY);
// Particles on top
for(const p of particles) p.draw(ctx,camX,camY);
// Ambient lighting effect around player
const grad=ctx.createRadialGradient(player.x-camX,player.y-camY,50,player.x-camX,player.y-camY,250);
grad.addColorStop(0,'rgba(0,0,0,0)');
grad.addColorStop(1,'rgba(0,0,0,0.4)');
ctx.fillStyle=grad;
ctx.fillRect(0,0,canvas.width,canvas.height);
// UI updates (throttled)
if(Math.floor(now/200)!==Math.floor((now-dt*1000)/200)) {
updateHUD();
drawMinimap();
}
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"a0e63d4e-372d-4fbb-98d9-28253e348a67","tokensIn":733,"tokensOut":17843,"tokensTotal":18576,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:40:32.253Z"} -->