← RPG results

RPG

thinkingcap qwen3.6 27b · middle

Initial view of RPG

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 59,522 bytes · SHA-256 5bf2a5fa020d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Realm of Echoes - Action Adventure RPG</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; image-rendering: pixelated; }
#ui-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#hud { position: absolute; top: 12px; left: 12px; display: flex; gap: 8px; align-items: center; }
.heart { width: 24px; height: 24px; filter: drop-shadow(0 0 3px rgba(255,50,50,0.6)); transition: all 0.3s; }
.heart.empty { opacity: 0.25; filter: none; }
#gold-display { color: #ffd700; font-size: 18px; font-weight: bold; text-shadow: 0 0 8px rgba(255,215,0,0.5); margin-left: 12px; }
#weapon-name { color: #aaa; font-size: 13px; margin-left: 8px; }
#inventory-btn, #map-btn { position: absolute; top: 12px; right: 12px; pointer-events: all; background: rgba(0,0,0,0.7); border: 1px solid #555; color: #ccc; padding: 8px 14px; cursor: pointer; font-size: 13px; border-radius: 4px; }
#map-btn { right: 120px; }
#inventory-btn:hover, #map-btn:hover { background: rgba(50,50,80,0.9); border-color: #88f; color: #fff; }
#minimap { position: absolute; bottom: 12px; right: 12px; border: 2px solid #333; border-radius: 4px; opacity: 0.85; }
#dialog-box { position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%); background: rgba(10,10,20,0.92); border: 2px solid #446; color: #ddd; padding: 16px 24px; max-width: 500px; text-align: center; font-size: 15px; line-height: 1.5; display: none; border-radius: 8px; }
#shop-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(10,10,20,0.96); border: 2px solid #c9a030; color: #ddd; padding: 24px; width: 420px; display: none; border-radius: 8px; pointer-events: all; }
#shop-panel h2 { text-align: center; color: #ffd700; margin-bottom: 16px; font-size: 20px; }
.shop-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; margin: 4px 0; background: rgba(255,255,255,0.05); border-radius: 4px; cursor: pointer; transition: background 0.2s; }
.shop-item:hover { background: rgba(255,215,0,0.15); }
.shop-item .name { font-size: 14px; }
.shop-item .cost { color: #ffd700; font-size: 13px; }
#inv-panel { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background: rgba(10,10,20,0.96); border: 2px solid #4488cc; color: #ddd; padding: 20px; width: 380px; display: none; border-radius: 8px; pointer-events: all; }
#inv-panel h2 { text-align: center; margin-bottom: 12px; color: #6af; font-size: 18px; }
.inv-item { padding: 6px 10px; margin: 3px 0; background: rgba(255,255,255,0.04); border-radius: 4px; font-size: 13px; display: flex; justify-content: space-between; }
.inv-item .use-btn { color: #6f6; cursor: pointer; margin-left: 8px; }
#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 { font-size: 48px; color: #c9a030; text-shadow: 0 0 30px rgba(201,160,48,0.5); margin-bottom: 8px; letter-spacing: 4px; }
#title-screen .subtitle { color: #778; font-size: 16px; margin-bottom: 40px; }
#start-btn { background: linear-gradient(135deg, #2a1a0a, #4a3020); border: 2px solid #c9a030; color: #ffd700; padding: 14px 48px; font-size: 18px; cursor: pointer; border-radius: 6px; letter-spacing: 2px; transition: all 0.3s; }
#start-btn:hover { background: linear-gradient(135deg, #4a3020, #6a5040); box-shadow: 0 0 20px rgba(201,160,48,0.4); transform: scale(1.05); }
#controls-hint { color: #556; font-size: 13px; margin-top: 30px; text-align: center; line-height: 1.8; }
#game-over-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; }
#game-over-screen h1 { color: #f44; font-size: 48px; margin-bottom: 20px; }
#restart-btn { background: #333; border: 2px solid #f44; color: #f66; padding: 12px 36px; font-size: 16px; cursor: pointer; border-radius: 6px; margin-top: 20px; }
#victory-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,10,30,0.95); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; }
#victory-screen h1 { color: #ffd700; font-size: 42px; margin-bottom: 16px; text-shadow: 0 0 30px rgba(255,215,0,0.5); }
#notification { position: absolute; top: 60px; left: 50%; transform: translateX(-50%); color: #ffd700; font-size: 14px; text-shadow: 0 0 10px rgba(255,215,0,0.5); opacity: 0; transition: opacity 0.3s; pointer-events: none; }
#xp-bar-container { position: absolute; top: 44px; left: 12px; width: 160px; height: 8px; background: rgba(0,0,0,0.5); border-radius: 4px; overflow: hidden; }
#xp-bar { height: 100%; background: linear-gradient(90deg, #4a2, #6d3); width: 0%; transition: width 0.4s; }
#level-display { color: #8c8; font-size: 12px; margin-left: 8px; line-height: 8px; }
</style>
</head>
<body>

<div id="title-screen">
    <h1>REALM OF ECHOES</h1>
    <div class="subtitle">An Action-Adventure RPG</div>
    <button id="start-btn" onclick="startGame()">BEGIN QUEST</button>
    <div id="controls-hint">
        WASD / Arrow Keys — Move &nbsp;|&nbsp; SPACE — Attack<br>
        E — Interact &nbsp;|&nbsp; I — Inventory &nbsp;|&nbsp; M — Map<br>
        1-3 — Use Items &nbsp;|&nbsp; Ranged: Hold SHIFT + Click
    </div>
</div>

<div id="game-over-screen">
    <h1>YOU HAVE FALLEN</h1>
    <p style="color:#aaa;">The realm claims another soul...</p>
    <button id="restart-btn" onclick="location.reload()">TRY AGAIN</button>
</div>

<div id="victory-screen">
    <h1>VICTORY!</h1>
    <p style="color:#ccc;max-width:400px;text-align:center;line-height:1.6;">The Shadow Lord has been vanquished.<br>The realm is free once more.</p>
    <button id="restart-btn" onclick="location.reload()" style="border-color:#ffd700;color:#ffd700;margin-top:24px;">PLAY AGAIN</button>
</div>

<canvas id="game"></canvas>
<div id="ui-overlay">
    <div id="hud">
        <svg class="heart" viewBox="0 0 24 24"><path fill="#e33" 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>
        <svg class="heart" viewBox="0 0 24 24"><path fill="#e33" 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>
        <svg class="heart" viewBox="0 0 24 24"><path fill="#e33" 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>
        <span id="gold-display">🪙 0</span>
        <span id="weapon-name">Iron Sword</span>
    </div>
    <div id="xp-bar-container"><div id="xp-bar"></div></div>
    <span id="level-display" style="position:absolute;top:46px;left:180px;">Lv 1</span>
    <button id="map-btn" onclick="toggleMap()">🗺 Map</button>
    <button id="inventory-btn" onclick="toggleInventory()">🎒 Inventory</button>
    <canvas id="minimap" width="120" height="90"></canvas>
    <div id="dialog-box"></div>
    <div id="notification"></div>
    <div id="shop-panel">
        <h2>⚒ Merchant's Wares</h2>
        <div id="shop-items"></div>
        <p style="text-align:center;margin-top:12px;color:#888;font-size:12px;">Press ESC to close</p>
    </div>
    <div id="inv-panel">
        <h2>🎒 Inventory</h2>
        <div id="inv-items"></div>
        <p style="text-align:center;margin-top:12px;color:#888;font-size:12px;">Press I or ESC to close</p>
    </div>
</div>

<script>
// ==================== AUDIO ENGINE ====================
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() { if (!audioCtx) audioCtx = new AudioCtx(); }

function playSound(type, vol=0.3) {
    if (!audioCtx) return;
    const now = audioCtx.currentTime;
    const g = audioCtx.createGain();
    g.connect(audioCtx.destination);
    g.gain.setValueAtTime(vol, now);
    
    switch(type) {
        case 'walk': {
            const o = audioCtx.createOscillator();
            o.type = 'sine'; o.frequency.value = 80 + Math.random()*40;
            g.gain.exponentialRampToValueAtTime(0.01, now+0.06);
            o.connect(g); o.start(now); o.stop(now+0.06);
            break;
        }
        case 'attack': {
            const o = audioCtx.createOscillator();
            o.type = 'sawtooth'; o.frequency.setValueAtTime(300, now);
            o.frequency.exponentialRampToValueAtTime(80, now+0.15);
            g.gain.exponentialRampToValueAtTime(0.01, now+0.15);
            o.connect(g); o.start(now); o.stop(now+0.15);
            break;
        }
        case 'hit': {
            const buf = audioCtx.createBuffer(1, audioCtx.sampleRate*0.1, audioCtx.sampleRate);
            const d = buf.getChannelData(0);
            for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.exp(-i/d.length*4);
            const s = audioCtx.createBufferSource(); s.buffer=buf;
            g.gain.exponentialRampToValueAtTime(0.01, now+0.1);
            s.connect(g); s.start(now);
            break;
        }
        case 'pickup': {
            const o = audioCtx.createOscillator();
            o.type='sine'; o.frequency.setValueAtTime(400,now);
            o.frequency.exponentialRampToValueAtTime(800,now+0.15);
            g.gain.exponentialRampToValueAtTime(0.01,now+0.2);
            o.connect(g); o.start(now); o.stop(now+0.2);
            break;
        }
        case 'heal': {
            const o = audioCtx.createOscillator();
            o.type='sine'; o.frequency.setValueAtTime(500,now);
            o.frequency.exponentialRampToValueAtTime(900,now+0.3);
            g.gain.exponentialRampToValueAtTime(0.01,now+0.4);
            o.connect(g); o.start(now); o.stop(now+0.4);
            break;
        }
        case 'shop': {
            const o = audioCtx.createOscillator();
            o.type='triangle'; o.frequency.setValueAtTime(600,now);
            o.frequency.exponentialRampToValueAtTime(300,now+0.12);
            g.gain.exponentialRampToValueAtTime(0.01,now+0.15);
            o.connect(g); o.start(now); o.stop(now+0.15);
            break;
        }
        case 'save': {
            [400,500,600].forEach((f,i)=>{
                const o=audioCtx.createOscillator();o.type='sine';o.frequency.value=f;
                const g2=audioCtx.createGain();g2.gain.setValueAtTime(0.15,now+i*0.1);
                g2.gain.exponentialRampToValueAtTime(0.01,now+i*0.1+0.2);
                o.connect(g2);g2.connect(audioCtx.destination);o.start(now+i*0.1);o.stop(now+i*0.1+0.2);
            });
            break;
        }
        case 'ranged': {
            const o=audioCtx.createOscillator();o.type='sine';o.frequency.setValueAtTime(900,now);
            o.frequency.exponentialRampToValueAtTime(200,now+0.1);
            g.gain.exponentialRampToValueAtTime(0.01,now+0.12);
            o.connect(g);o.start(now);o.stop(now+0.12);
            break;
        }
        case 'levelup': {
            [523,659,784].forEach((f,i)=>{
                const o=audioCtx.createOscillator();o.type='triangle';o.frequency.value=f;
                const g2=audioCtx.createGain();g2.gain.setValueAtTime(0.2,now+i*0.15);
                g2.gain.exponentialRampToValueAtTime(0.01,now+i*0.15+0.3);
                o.connect(g2);g2.connect(audioCtx.destination);o.start(now+i*0.15);o.stop(now+i*0.15+0.3);
            });
            break;
        }
        case 'death': {
            const o=audioCtx.createOscillator();o.type='sawtooth';o.frequency.setValueAtTime(200,now);
            o.frequency.exponentialRampToValueAtTime(40,now+1);
            g.gain.exponentialRampToValueAtTime(0.01,now+1);
            o.connect(g);o.start(now);o.stop(now+1);
            break;
        }
    }
}

// ==================== CANVAS SETUP ====================
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const minimapCanvas = document.getElementById('minimap');
const mctx = minimapCanvas.getContext('2d');

function resize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();

// ==================== CONSTANTS ====================
const TILE = 32;
const ROOM_W = 20, ROOM_H = 15;
const PLAYER_SIZE = 24;
const ENEMY_SIZE = 22;

// Tile types
const T = { EMPTY:0, WALL:1, FLOOR:2, WATER:3, GRASS:4, DOOR:5, SAVEPOINT:6, SHOP:7, CHEST:8, BRIDGE:9 };

// ==================== MAP DATA ====================
// 5 rooms forming a progression path
const rooms = [
    // Room 0: Starting Village (peaceful)
    { name:"Village", bg:'#2a3a1a', floor:'#4a6a3a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:false },
    // Room 1: Dark Forest
    { name:"Dark Forest", bg:'#1a2a0a', floor:'#2a4a1a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:false },
    // Room 2: Crystal Caverns (mid-game - ranged weapon here)
    { name:"Crystal Caverns", bg:'#0a1a2a', floor:'#1a3a5a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:true },
    // Room 3: Merchant's Keep (shop room)
    { name:"Merchant's Keep", bg:'#2a2010', floor:'#4a3a20', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:null, midgame:false },
    // Room 4: Shadow Fortress (final area)
    { name:"Shadow Fortress", bg:'#1a0a1a', floor:'#2a1a2a', walls:[], enemies:[], items:[], npcs:[], exits:{}, boss:true, midgame:false }
];

function buildMaps() {
    // Room 0: Village - open area with some trees/walls
    rooms[0].walls = [];
    for(let x=0;x<ROOM_W;x++){rooms[0].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[0].walls[x][y]=T.FLOOR;}
    // Border walls
    for(let x=0;x<ROOM_W;x++){rooms[0].walls[x][0]=T.WALL;rooms[0].walls[x][ROOM_H-1]=T.WALL;}
    for(let y=0;y<ROOM_H;y++){rooms[0].walls[0][y]=T.WALL;rooms[0].walls[ROOM_W-1][y]=T.WALL;}
    // Interior walls (decorative)
    [[5,3],[6,3],[7,3],[5,4],[5,5],[12,8],[13,8],[14,8],[12,9],[12,10]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[0].walls[x][y]=T.WALL;});
    // Door to room 1 (right side)
    rooms[0].walls[ROOM_W-1][7]=T.DOOR;
    rooms[0].exits = { right:1 };
    // Save point
    rooms[0].items.push({x:3,y:3,type:'savepoint',collected:false});
    // Gold pickup
    rooms[0].items.push({x:8,y:5,type:'gold',amount:15,collected:false});
    // Health potion
    rooms[0].items.push({x:14,y:3,type:'healthPotion',collected:false});

    // Room 1: Dark Forest
    rooms[1].walls = [];
    for(let x=0;x<ROOM_W;x++){rooms[1].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[1].walls[x][y]=T.FLOOR;}
    for(let x=0;x<ROOM_W;x++){rooms[1].walls[x][0]=T.WALL;rooms[1].walls[x][ROOM_H-1]=T.WALL;}
    for(let y=0;y<ROOM_H;y++){rooms[1].walls[0][y]=T.WALL;rooms[1].walls[ROOM_W-1][y]=T.WALL;}
    // Dense trees (walls)
    [[3,2],[4,2],[5,2],[3,3],[8,5],[9,5],[10,5],[8,6],[15,3],[16,3],[17,3],[15,4],[15,5],
     [4,10],[5,10],[6,10],[4,11],[12,10],[13,10],[14,10]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[1].walls[x][y]=T.WALL;});
    // Doors: left to room 0, right to room 2, down to room 3
    rooms[1].walls[0][7]=T.DOOR;
    rooms[1].walls[ROOM_W-1][7]=T.DOOR;
    rooms[1].walls[10][ROOM_H-1]=T.DOOR;
    rooms[1].exits = { left:0, right:2, down:3 };
    // Enemies
    rooms[1].enemies.push({x:6,y:4,type:'slime',hp:8,maxHp:8,dmg:2,speed:0.8,xp:15,gold:5});
    rooms[1].enemies.push({x:13,y:7,type:'slime',hp:8,maxHp:8,dmg:2,speed:0.8,xp:15,gold:5});
    rooms[1].enemies.push({x:9,y:11,type:'wolf',hp:14,maxHp:14,dmg:3,speed:1.2,xp:25,gold:10});
    // Items
    rooms[1].items.push({x:7,y:8,type:'gold',amount:20,collected:false});
    rooms[1].items.push({x:16,y:9,type:'shield',collected:false}); // Accessory 1
    rooms[1].items.push({x:4,y:13,type:'healthPotion',collected:false});

    // Room 2: Crystal Caverns (mid-game)
    rooms[2].walls = [];
    for(let x=0;x<ROOM_W;x++){rooms[2].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[2].walls[x][y]=T.FLOOR;}
    for(let x=0;x<ROOM_W;x++){rooms[2].walls[x][0]=T.WALL;rooms[2].walls[x][ROOM_H-1]=T.WALL;}
    for(let y=0;y<ROOM_H;y++){rooms[2].walls[0][y]=T.WALL;rooms[2].walls[ROOM_W-1][y]=T.WALL;}
    // Crystal formations (walls)
    [[3,4],[3,5],[4,6],[7,3],[8,3],[9,3],[14,5],[15,5],[16,5],[14,6],
     [5,10],[6,10],[7,10],[5,11],[12,8],[13,8],[14,8]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[2].walls[x][y]=T.WALL;});
    // Doors: left to room 1, down to room 4
    rooms[2].walls[0][7]=T.DOOR;
    rooms[2].walls[10][ROOM_H-1]=T.DOOR;
    rooms[2].exits = { left:1, down:4 };
    // Enemies (harder)
    rooms[2].enemies.push({x:5,y:7,type:'crystalGolem',hp:20,maxHp:20,dmg:4,speed:0.6,xp:35,gold:15});
    rooms[2].enemies.push({x:12,y:4,type:'wolf',hp:18,maxHp:18,dmg:4,speed:1.4,xp:30,gold:12});
    rooms[2].enemies.push({x:9,y:12,type:'crystalGolem',hp:25,maxHp:25,dmg:5,speed:0.7,xp:40,gold:18});
    // Ranged weapon discovery!
    rooms[2].items.push({x:8,y:6,type:'rangedWeapon',collected:false});
    rooms[2].items.push({x:3,y:9,type:'gold',amount:30,collected:false});
    rooms[2].items.push({x:15,y:11,type:'manaPotion',collected:false}); // Accessory 2

    // Room 3: Merchant's Keep (shop)
    rooms[3].walls = [];
    for(let x=0;x<ROOM_W;x++){rooms[3].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[3].walls[x][y]=T.FLOOR;}
    for(let x=0;x<ROOM_W;x++){rooms[3].walls[x][0]=T.WALL;rooms[3].walls[x][ROOM_H-1]=T.WALL;}
    for(let y=0;y<ROOM_H;y++){rooms[3].walls[0][y]=T.WALL;rooms[3].walls[ROOM_W-1][y]=T.WALL;}
    // Shop counter
    [[8,4],[9,4],[10,4],[11,4]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[3].walls[x][y]=T.SHOP;});
    rooms[3].walls[10][ROOM_H-1]=T.DOOR; // up to room 1
    rooms[3].exits = { up:1 };
    // NPC merchant at shop counter
    rooms[3].npcs.push({x:9.5,y:5,type:'merchant'});
    // Save point in keep
    rooms[3].items.push({x:4,y:8,type:'savepoint',collected:false});
    rooms[3].items.push({x:15,y:6,type:'gold',amount:25,collected:false});

    // Room 4: Shadow Fortress (final)
    rooms[4].walls = [];
    for(let x=0;x<ROOM_W;x++){rooms[4].walls.push([]);for(let y=0;y<ROOM_H;y++)rooms[4].walls[x][y]=T.FLOOR;}
    for(let x=0;x<ROOM_W;x++){rooms[4].walls[x][0]=T.WALL;rooms[4].walls[x][ROOM_H-1]=T.WALL;}
    for(let y=0;y<ROOM_H;y++){rooms[4].walls[0][y]=T.WALL;rooms[4].walls[ROOM_W-1][y]=T.WALL;}
    // Fortress walls
    [[3,3],[4,3],[5,3],[6,3],[3,4],[3,5],[14,3],[15,3],[16,3],[17,3],[17,4],[17,5],
     [8,8],[9,8],[10,8],[11,8],[12,8],[8,9],[12,9]].forEach(([x,y])=>{if(x<ROOM_W&&y<ROOM_H)rooms[4].walls[x][y]=T.WALL;});
    rooms[4].walls[10][0]=T.DOOR; // up from room 2
    rooms[4].exits = { up:2 };
    // Enemies (hardest)
    rooms[4].enemies.push({x:5,y:6,type:'shadowKnight',hp:30,maxHp:30,dmg:6,speed:1.0,xp:50,gold:25});
    rooms[4].enemies.push({x:15,y:7,type:'shadowKnight',hp:30,maxHp:30,dmg:6,speed:1.0,xp:50,gold:25});
    rooms[4].enemies.push({x:8,y:11,type:'crystalGolem',hp:35,maxHp:35,dmg:7,speed:0.8,xp:55,gold:30});
    // Boss
    rooms[4].bossEnemy = {x:9.5,y:12,type:'shadowLord',hp:60,maxHp:60,dmg:8,speed:0.9,xp:200,gold:100,defeated:false};
    // Items
    rooms[4].items.push({x:3,y:12,type:'healthPotion',collected:false});
    rooms[4].items.push({x:16,y:12,type:'gold',amount:50,collected:false});
}

// ==================== SHOP DATA ====================
const shopItems = [
    { id:'healthPotion', name:'Health Potion', desc:'Restores 4 HP', cost:20, icon:'🧪' },
    { id:'manaPotion', name:'Mana Crystal', desc:'+1 Max Mana (ranged)', cost:35, icon:'💎' },
    { id:'ironArmor', name:'Iron Armor', desc:'+2 Max HP', cost:60, icon:'🛡️' },
    { id:'speedBoots', name:'Swift Boots', desc:'+15% Move Speed', cost:45, icon:'👢' },
    { id:'powerRing', name:'Power Ring', desc:'+2 Attack Damage', cost:50, icon:'💍' },
];

// ==================== GAME STATE ====================
let game = null;

function createGameState() {
    return {
        running: false,
        paused: false,
        player: {
            x: 3 * TILE + TILE/2, y: 7 * TILE + TILE/2,
            room: 0,
            hp: 6, maxHp: 6, // starts at 3 hearts = 6 HP (each heart = 2)
            xp: 0, level: 1, nextLevelXp: 50,
            gold: 10,
            weapon: { name:'Iron Sword', damage:4, speed:0.4, range:36, color:'#aaa' },
            weaponLevel: 0, // 0=iron, 1=steel, 2=fury blade
            hasRangedWeapon: false,
            mana: 5, maxMana: 5,
            accessories: [],
            speed: 2.5,
            attackCooldown: 0,
            invincibleTimer: 0,
            facing: {x:1,y:0},
            walkFrame: 0,
            damageFlash: 0,
        },
        projectiles: [],
        enemies: [],
        particles: [],
        notifications: [],
        dialogText: '',
        dialogTimer: 0,
        shopOpen: false,
        inventoryOpen: false,
        mapOpen: false,
        keysDown: {},
        lastWalkSound: 0,
        gameTime: 0,
        bossDefeated: false,
        weaponUpgradesFound: 0,
    };
}

// ==================== PARTICLE SYSTEM ====================
function spawnParticles(x, y, color, count=8, speed=3, life=30) {
    for(let i=0;i<count;i++){
        const angle = Math.random()*Math.PI*2;
        const spd = speed*(0.5+Math.random());
        game.particles.push({
            x, y, vx:Math.cos(angle)*spd, vy:Math.sin(angle)*spd,
            life, maxLife:life, color, size: 2+Math.random()*3
        });
    }
}

function spawnDamageNumber(x, y, amount) {
    game.particles.push({
        x, y, vx:(Math.random()-0.5)*1, vy:-2,
        life:40, maxLife:40, color:'#ff4', size:0, text:'-'+amount, isText:true
    });
}

function spawnGoldNumber(x, y, amount) {
    game.particles.push({
        x, y, vx:(Math.random()-0.5)*1, vy:-1.5,
        life:45, maxLife:45, color:'#ffd700', size:0, text:'+'+amount+'🪙', isText:true
    });
}

function spawnXpNumber(x, y, amount) {
    game.particles.push({
        x, y, vx:(Math.random()-0.5)*1, vy:-1.2,
        life:40, maxLife:40, color:'#6f6', size:0, text:'+'+amount+' XP', isText:true
    });
}

// ==================== INPUT ====================
document.addEventListener('keydown', e => {
    if (!game || !game.running) return;
    game.keysDown[e.key.toLowerCase()] = true;
    
    if (e.key === 'i' || e.key === 'I') toggleInventory();
    if (e.key === 'm' || e.key === 'M') toggleMap();
    if (e.key === 'Escape') { closeAllPanels(); }
    if (e.key >= '1' && e.key <= '3') useItemSlot(parseInt(e.key)-1);
});

document.addEventListener('keyup', e => {
    if (!game) return;
    game.keysDown[e.key.toLowerCase()] = false;
});

canvas.addEventListener('click', e => {
    if (!game || !game.running || game.shopOpen || game.inventoryOpen) return;
    // Ranged attack on click (if has ranged weapon and shift held or just click)
    if (game.player.hasRangedWeapon && game.keysDown['shift']) {
        fireProjectile(e.clientX, e.clientY);
    }
});

// ==================== UI FUNCTIONS ====================
function updateHUD() {
    const p = game.player;
    const hearts = document.querySelectorAll('.heart');
    const totalHearts = Math.ceil(p.maxHp / 2);
    
    // Adjust heart count display
    while(hearts.length < 5) {
        const svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
        svg.setAttribute('class','heart');
        svg.setAttribute('viewBox','0 0 24 24');
        const path = document.createElementNS('http://www.w3.org/2000/svg','path');
        path.setAttribute('fill','#e33');
        path.setAttribute('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.appendChild(path);
        document.getElementById('hud').insertBefore(svg, document.getElementById('gold-display'));
    }
    
    const allHearts = document.querySelectorAll('.heart');
    for(let i=0;i<allHearts.length;i++){
        if(i < totalHearts) {
            allHearts[i].style.display = '';
            const hpPerHeart = 2;
            const heartHp = Math.min(hpPerHeart, p.hp - i*hpPerHeart);
            allHearts[i].classList.toggle('empty', heartHp <= 0);
        } else {
            allHearts[i].style.display = 'none';
        }
    }
    
    document.getElementById('gold-display').textContent = '🪙 ' + p.gold;
    document.getElementById('weapon-name').textContent = p.weapon.name;
    document.getElementById('level-display').textContent = 'Lv ' + p.level;
    const xpPct = (p.xp / p.nextLevelXp) * 100;
    document.getElementById('xp-bar').style.width = xpPct + '%';
}

function showNotification(text, duration=2000) {
    game.notifications.push({text, timer:duration});
    const el = document.getElementById('notification');
    el.textContent = text;
    el.style.opacity = '1';
    setTimeout(()=>{el.style.opacity='0';}, duration);
}

function showDialog(text, duration=3000) {
    game.dialogText = text;
    game.dialogTimer = duration;
    const el = document.getElementById('dialog-box');
    el.textContent = text;
    el.style.display = 'block';
}

function toggleInventory() {
    if (game.shopOpen) return;
    game.inventoryOpen = !game.inventoryOpen;
    game.mapOpen = false;
    document.getElementById('inv-panel').style.display = game.inventoryOpen ? 'block' : 'none';
    if(game.inventoryOpen) updateInvPanel();
}

function toggleMap() {
    game.mapOpen = !game.mapOpen;
    game.inventoryOpen = false;
    document.getElementById('inv-panel').style.display = 'none';
}

function closeAllPanels() {
    game.shopOpen = false;
    game.inventoryOpen = false;
    game.mapOpen = false;
    document.getElementById('shop-panel').style.display = 'none';
    document.getElementById('inv-panel').style.display = 'none';
}

function updateInvPanel() {
    const p = game.player;
    let html = `<div class="inv-item"><span>⚔️ ${p.weapon.name}</span><span style="color:#aaa">DMG:${p.weapon.damage}</span></div>`;
    if(p.hasRangedWeapon) html += `<div class="inv-item"><span>🏹 Crystal Bow</span><span style="color:#88f">Mana: ${p.mana}/${p.maxMana}</span></div>`;
    
    p.accessories.forEach((acc, i) => {
        const icons = {'ironArmor':'🛡️','speedBoots':'👢','powerRing':'💍','shield':'🔰'};
        html += `<div class="inv-item"><span>${icons[acc.id]||'✦'} ${acc.name}</span></div>`;
    });
    
    // Consumables
    const consumables = p.accessories.filter(a => a.consumable);
    if(consumables.length > 0) {
        html += '<hr style="border-color:#333;margin:8px 0;">';
        consumables.forEach((c,i) => {
            html += `<div class="inv-item"><span>${c.icon||'🧪'} ${c.name}</span><span class="use-btn" onclick="useItemSlot(${i})">Use</span></div>`;
        });
    }
    
    document.getElementById('inv-items').innerHTML = html;
}

function useItemSlot(slot) {
    const p = game.player;
    const consumables = p.accessories.filter(a => a.consumable);
    if(slot < consumables.length) {
        const item = consumables[slot];
        if(item.id === 'healthPotion') {
            p.hp = Math.min(p.maxHp, p.hp + 4);
            playSound('heal');
            spawnParticles(p.x, p.y, '#f66', 12, 2, 25);
            showNotification('+4 HP Restored!');
        } else if(item.id === 'manaPotion') {
            p.mana = Math.min(p.maxMana, p.mana + 3);
            playSound('heal');
            spawnParticles(p.x, p.y, '#68f', 12, 2, 25);
            showNotification('+3 Mana Restored!');
        }
        // Remove consumable
        const idx = p.accessories.indexOf(item);
        if(idx >= 0) p.accessories.splice(idx, 1);
        updateInvPanel();
        updateHUD();
    }
}

function openShop() {
    game.shopOpen = true;
    playSound('shop');
    const panel = document.getElementById('shop-panel');
    let html = '';
    shopItems.forEach(item => {
        const owned = game.player.accessories.some(a => a.id === item.id && !a.consumable);
        html += `<div class="shop-item" onclick="buyItem('${item.id}')" style="${owned?'opacity:0.4;pointer-events:none':''}">
            <span class="name">${item.icon} ${item.name}</span>
            <span style="font-size:11px;color:#999;">${item.desc}</span>
            <span class="cost">🪙 ${item.cost}</span>
        </div>`;
    });
    document.getElementById('shop-items').innerHTML = html;
    panel.style.display = 'block';
}

function buyItem(id) {
    const item = shopItems.find(i => i.id === id);
    if(!item || game.player.gold < item.cost) { showNotification("Not enough gold!"); return; }
    
    game.player.gold -= item.cost;
    playSound('pickup');
    
    const acc = { id: item.id, name: item.name, icon: item.icon, consumable: item.consumable || false };
    if(item.id === 'ironArmor') {
        game.player.maxHp += 2;
        game.player.hp += 2;
        showNotification("Iron Armor equipped! +2 Max HP");
    } else if(item.id === 'speedBoots') {
        game.player.speed *= 1.15;
        showNotification("Swift Boots equipped! Speed increased!");
    } else if(item.id === 'powerRing') {
        game.player.weapon.damage += 2;
        showNotification("Power Ring equipped! +2 Damage");
    } else {
        acc.consumable = true;
        showNotification(`Purchased ${item.name}!`);
    }
    
    game.player.accessories.push(acc);
    spawnParticles(game.player.x, game.player.y, '#ffd700', 15, 3, 20);
    updateHUD();
    openShop(); // Refresh shop display
}

// ==================== GAME LOGIC ====================
function startGame() {
    initAudio();
    document.getElementById('title-screen').style.display = 'none';
    game = createGameState();
    buildMaps();
    
    // Load saved game if exists
    loadGame();
    
    game.running = true;
    spawnRoomEnemies(game.player.room);
    requestAnimationFrame(gameLoop);
}

function spawnRoomEnemies(roomIdx) {
    const room = rooms[roomIdx];
    game.enemies = [];
    room.enemies.forEach(e => {
        if(!e.dead) {
            game.enemies.push({...e, x: e.x*TILE+TILE/2, y: e.y*TILE+TILE/2, 
                attackCooldown:0, hitFlash:0, state:'patrol', patrolAngle:Math.random()*Math.PI*2});
        }
    });
    if(room.bossEnemy && !room.bossEnemy.defeated) {
        game.enemies.push({...room.bossEnemy, x: room.bossEnemy.x*TILE+TILE/2, y: room.bossEnemy.y*TILE+TILE/2,
            attackCooldown:0, hitFlash:0, state:'patrol', patrolAngle:Math.random()*Math.PI*2});
    }
}

function gameLoop(timestamp) {
    if(!game || !game.running) return;
    
    const dt = 1/60;
    game.gameTime += dt;
    
    update(dt);
    render();
    
    requestAnimationFrame(gameLoop);
}

function update(dt) {
    if(game.shopOpen || game.inventoryOpen) return;
    
    const p = game.player;
    
    // Movement
    let dx=0, dy=0;
    if(game.keysDown['w']||game.keysDown['arrowup']) dy=-1;
    if(game.keysDown['s']||game.keysDown['arrowdown']) dy=1;
    if(game.keysDown['a']||game.keysDown['arrowleft']) dx=-1;
    if(game.keysDown['d']||game.keysDown['arrowright']) dx=1;
    
    if(dx!==0 || dy!==0) {
        const len = Math.sqrt(dx*dx+dy*dy);
        dx/=len; dy/=len;
        p.facing.x = dx; p.facing.y = dy;
        
        const newX = p.x + dx * p.speed;
        const newY = p.y + dy * p.speed;
        
        // Collision check
        if(!isWall(newX, p.y)) p.x = newX;
        if(!isWall(p.x, newY)) p.y = newY;
        
        p.walkFrame += 0.15;
        
        // Walk sound
        if(game.gameTime - game.lastWalkSound > 0.3) {
            playSound('walk', 0.08);
            game.lastWalkSound = game.gameTime;
        }
    }
    
    // Room transitions
    checkRoomTransition();
    
    // Attack
    p.attackCooldown -= dt;
    if(game.keysDown[' '] && p.attackCooldown <= 0) {
        performAttack();
        p.attackCooldown = p.weapon.speed;
    }
    
    // Invincibility timer
    if(p.invincibleTimer > 0) p.invincibleTimer -= dt;
    if(p.damageFlash > 0) p.damageFlash -= dt;
    
    // Interact (E key)
    if(game.keysDown['e']) {
        checkInteractions();
        game.keysDown['e'] = false;
    }
    
    // Update enemies
    updateEnemies(dt);
    
    // Update projectiles
    updateProjectiles(dt);
    
    // Update particles
    game.particles = game.particles.filter(pt => {
        pt.x += pt.vx;
        pt.y += pt.vy;
        pt.life--;
        return pt.life > 0;
    });
    
    // Dialog timer
    if(game.dialogTimer > 0) {
        game.dialogTimer -= dt*1000;
        if(game.dialogTimer <= 0) document.getElementById('dialog-box').style.display = 'none';
    }
    
    updateHUD();
}

function isWall(px, py) {
    const room = rooms[game.player.room];
    const tx = Math.floor(px / TILE);
    const ty = Math.floor(py / TILE);
    if(tx<0||tx>=ROOM_W||ty<0||ty>=ROOM_H) return true;
    const tile = room.walls[tx][ty];
    return tile === T.WALL || tile === T.WATER;
}

function checkRoomTransition() {
    const p = game.player;
    const room = rooms[p.room];
    
    // Check if player is at a door position
    const tx = Math.floor(p.x / TILE);
    const ty = Math.floor(p.y / TILE);
    
    for(const [dir, targetRoom] of Object.entries(room.exits)) {
        let edgeX, edgeY;
        switch(dir) {
            case 'right': edgeX=ROOM_W-1; edgeY=ty; break;
            case 'left': edgeX=0; edgeY=ty; break;
            case 'down': edgeX=tx; edgeY=ROOM_H-1; break;
            case 'up': edgeX=tx; edgeY=0; break;
        }
        
        if(tx===edgeX && ty===edgeY) {
            // Transition!
            p.room = targetRoom;
            
            // Set entry position
            switch(dir) {
                case 'right': p.x=TILE*1.5; p.y=ty*TILE+TILE/2; break;
                case 'left': p.x=(ROOM_W-1.5)*TILE; p.y=ty*TILE+TILE/2; break;
                case 'down': p.x=tx*TILE+TILE/2; p.y=TILE*1.5; break;
                case 'up': p.x=tx*TILE+TILE/2; p.y=(ROOM_H-1.5)*TILE; break;
            }
            
            spawnRoomEnemies(targetRoom);
            showNotification(`Entered ${rooms[targetRoom].name}`);
            playSound('pickup');
        }
    }
}

function performAttack() {
    const p = game.player;
    playSound('attack');
    
    // Melee attack hitbox
    const ax = p.x + p.facing.x * 20;
    const ay = p.y + p.facing.y * 20;
    
    spawnParticles(ax, ay, p.weapon.color, 5, 2, 15);
    
    game.enemies.forEach(e => {
        if(e.dead) return;
        const dist = Math.sqrt((e.x-ax)**2 + (e.y-ay)**2);
        if(dist < p.weapon.range + ENEMY_SIZE/2) {
            damageEnemy(e, p.weapon.damage);
        }
    });
}

function fireProjectile(targetX, targetY) {
    const p = game.player;
    if(p.mana <= 0) { showNotification("No mana!"); return; }
    
    p.mana--;
    playSound('ranged');
    
    const dx = targetX - (p.x + getCameraOffset().x);
    const dy = targetY - (p.y + getCameraOffset().y);
    const len = Math.sqrt(dx*dx+dy*dy) || 1;
    
    game.projectiles.push({
        x: p.x, y: p.y,
        vx: (dx/len)*6, vy: (dy/len)*6,
        damage: 3 + p.weaponLevel * 2,
        life: 80, color: '#4af'
    });
    
    spawnParticles(p.x, p.y, '#4af', 5, 2, 10);
}

function updateProjectiles(dt) {
    game.projectiles = game.projectiles.filter(proj => {
        proj.x += proj.vx;
        proj.y += proj.vy;
        proj.life--;
        
        // Hit enemies
        for(const e of game.enemies) {
            if(e.dead) continue;
            const dist = Math.sqrt((e.x-proj.x)**2 + (e.y-proj.y)**2);
            if(dist < ENEMY_SIZE/2 + 6) {
                damageEnemy(e, proj.damage);
                spawnParticles(proj.x, proj.y, '#4af', 8, 3, 15);
                return false;
            }
        }
        
        // Hit wall
        if(isWall(proj.x, proj.y)) {
            spawnParticles(proj.x, proj.y, '#666', 4, 2, 10);
            return false;
        }
        
        return proj.life > 0;
    });
}

function damageEnemy(e, dmg) {
    e.hp -= dmg;
    e.hitFlash = 0.2;
    spawnDamageNumber(e.x, e.y - 15, dmg);
    spawnParticles(e.x, e.y, '#f84', 6, 3, 15);
    playSound('hit');
    
    if(e.hp <= 0) {
        e.dead = true;
        const p = game.player;
        p.gold += e.gold || 0;
        p.xp += e.xp || 0;
        
        spawnGoldNumber(e.x, e.y - 5, e.gold);
        spawnXpNumber(e.x, e.y + 10, e.xp);
        spawnParticles(e.x, e.y, '#f44', 20, 4, 30);
        
        // Check level up
        checkLevelUp();
        
        // Boss defeated?
        if(e.type === 'shadowLord') {
            game.bossDefeated = true;
            rooms[4].bossEnemy.defeated = true;
            setTimeout(()=>{
                document.getElementById('victory-screen').style.display='flex';
                playSound('levelup');
            }, 1500);
        }
        
        // Weapon upgrade drops
        if(e.type === 'wolf' && game.player.weaponLevel < 1) {
            upgradeWeapon(1, 'Steel Sword', 7, 0.35, '#8cf');
        } else if((e.type === 'shadowKnight') && game.player.weaponLevel < 2) {
            upgradeWeapon(2, 'Fury Blade', 12, 0.28, '#f4a');
        }
    }
}

function checkLevelUp() {
    const p = game.player;
    while(p.xp >= p.nextLevelXp) {
        p.xp -= p.nextLevelXp;
        p.level++;
        p.nextLevelXp = Math.floor(p.nextLevelXp * 1.5);
        
        // Level up bonuses
        if(p.level === 2) {
            p.maxHp += 2; p.hp += 2;
            showNotification("Level Up! Max HP increased to " + p.maxHp);
        } else if(p.level >= 3 && p.maxHp < 10) {
            p.maxHp = Math.min(10, p.maxHp + 2);
            p.hp += 2;
            showNotification("Level Up! Max HP increased to " + p.maxHp);
        } else {
            p.weapon.damage += 1;
            showNotification(`Level ${p.level}! Damage increased!`);
        }
        
        playSound('levelup');
        spawnParticles(p.x, p.y, '#ff0', 25, 4, 35);
    }
}

function upgradeWeapon(level, name, damage, speed, color) {
    const p = game.player;
    if(p.weaponLevel >= level) return;
    
    p.weaponLevel = level;
    p.weapon.name = name;
    p.weapon.damage = damage;
    p.weapon.speed = speed;
    p.weapon.color = color;
    
    showNotification(`⚔️ Weapon upgraded to ${name}!`);
    playSound('levelup');
    spawnParticles(p.x, p.y, '#ffd700', 30, 5, 40);
    updateHUD();
}

function updateEnemies(dt) {
    const p = game.player;
    
    game.enemies.forEach(e => {
        if(e.dead) return;
        
        e.hitFlash -= dt;
        e.attackCooldown -= dt;
        
        const distToPlayer = Math.sqrt((e.x-p.x)**2 + (e.y-p.y)**2);
        
        // AI: Chase player when close
        if(distToPlayer < 180) {
            e.state = 'chase';
            const dx = p.x - e.x;
            const dy = p.y - e.y;
            const len = Math.sqrt(dx*dx+dy*dy) || 1;
            
            const nx = e.x + (dx/len)*e.speed;
            const ny = e.y + (dy/len)*e.speed;
            
            if(!isWall(nx, e.y)) e.x = nx;
            if(!isWall(e.x, ny)) e.y = ny;
            
            // Attack player
            if(distToPlayer < 28 && e.attackCooldown <= 0) {
                damagePlayer(e.dmg);
                e.attackCooldown = 1.0;
            }
        } else {
            // Patrol
            e.state = 'patrol';
            e.patrolAngle += (Math.random()-0.5)*0.1;
            const nx = e.x + Math.cos(e.patrolAngle)*e.speed*0.3;
            const ny = e.y + Math.sin(e.patrolAngle)*e.speed*0.3;
            
            if(!isWall(nx, e.y)) e.x = nx; else e.patrolAngle += Math.PI;
            if(!isWall(e.x, ny)) e.y = ny; else e.patrolAngle += Math.PI;
        }
    });
}

function damagePlayer(dmg) {
    const p = game.player;
    if(p.invincibleTimer > 0) return;
    
    // Shield accessory reduces damage
    const shieldBonus = p.accessories.some(a => a.id === 'shield') ? -1 : 0;
    dmg = Math.max(1, dmg + shieldBonus);
    
    p.hp -= dmg;
    p.invincibleTimer = 0.8;
    p.damageFlash = 0.3;
    
    playSound('hit');
    spawnParticles(p.x, p.y, '#f22', 10, 3, 20);
    spawnDamageNumber(p.x, p.y - 20, dmg);
    
    if(p.hp <= 0) {
        p.hp = 0;
        playSound('death');
        document.getElementById('game-over-screen').style.display = 'flex';
        game.running = false;
    }
}

function checkInteractions() {
    const p = game.player;
    const room = rooms[p.room];
    
    // Check items at player position
    const tx = Math.floor(p.x / TILE);
    const ty = Math.floor(p.y / TILE);
    
    room.items.forEach(item => {
        if(item.collected) return;
        if(Math.abs(tx - item.x) <= 1 && Math.abs(ty - item.y) <= 1) {
            collectItem(item);
        }
    });
    
    // Check NPCs
    room.npcs.forEach(npc => {
        const dist = Math.sqrt((p.x - npc.x*TILE)**2 + (p.y - npc.y*TILE)**2);
        if(dist < 48) {
            if(npc.type === 'merchant') openShop();
        }
    });
}

function collectItem(item) {
    item.collected = true;
    const p = game.player;
    
    switch(item.type) {
        case 'gold':
            p.gold += item.amount;
            spawnGoldNumber(p.x, p.y - 10, item.amount);
            showNotification(`+${item.amount} Gold`);
            break;
        case 'healthPotion':
            if(p.accessories.filter(a=>a.consumable).length < 5) {
                p.accessories.push({id:'healthPotion',name:'Health Potion',icon:'🧪',consumable:true});
                showNotification("Found Health Potion!");
            } else {
                p.hp = Math.min(p.maxHp, p.hp + 4);
                showNotification("+4 HP");
            }
            break;
        case 'manaPotion':
            if(p.accessories.filter(a=>a.consumable).length < 5) {
                p.accessories.push({id:'manaPotion',name:'Mana Crystal',icon:'💎',consumable:true});
                showNotification("Found Mana Crystal!");
            } else {
                p.mana = Math.min(p.maxMana, p.mana + 3);
                showNotification("+3 Mana");
            }
            break;
        case 'shield':
            if(!p.accessories.some(a=>a.id==='shield')) {
                p.accessories.push({id:'shield',name:'Wooden Shield',icon:'🔰',consumable:false});
                showNotification("Found Wooden Shield! (-1 damage taken)");
            }
            break;
        case 'rangedWeapon':
            if(!p.hasRangedWeapon) {
                p.hasRangedWeapon = true;
                p.maxMana = 5;
                p.mana = 5;
                showNotification("🏹 Discovered Crystal Bow! Hold SHIFT + Click to fire!");
                spawnParticles(p.x, p.y, '#4af', 30, 5, 40);
            }
            break;
        case 'savepoint':
            saveGame();
            showDialog("💾 Progress Saved!", 2000);
            playSound('save');
            spawnParticles(p.x, p.y, '#6f6', 20, 3, 35);
            return; // Don't play pickup sound for savepoints
    }
    
    playSound('pickup');
    spawnParticles(p.x, p.y, '#ffd700', 10, 3, 20);
}

// ==================== SAVE SYSTEM ====================
function saveGame() {
    const data = {
        player: game.player,
        rooms: rooms.map(r => ({
            items: r.items.map(i => ({...i})),
            enemies: r.enemies.map(e => ({type:e.type, dead:e.dead||false})),
            bossEnemy: r.bossEnemy ? {...r.bossEnemy} : null
        })),
        gameTime: game.gameTime,
        bossDefeated: game.bossDefeated
    };
    localStorage.setItem('realmOfEchoes', JSON.stringify(data));
}

function loadGame() {
    try {
        const raw = localStorage.getItem('realmOfEchoes');
        if(!raw) return;
        const data = JSON.parse(raw);
        
        Object.assign(game.player, data.player);
        
        // Restore room states
        data.rooms.forEach((rd, i) => {
            if(rooms[i]) {
                rooms[i].items = rd.items || rooms[i].items;
                if(rd.enemies && rooms[i].enemies) {
                    rd.enemies.forEach((re, j) => {
                        if(re.dead && rooms[i].enemies[j]) rooms[i].enemies[j].dead = true;
                    });
                }
                if(rd.bossEnemy) rooms[i].bossEnemy = rd.bossEnemy;
            }
        });
        
        game.gameTime = data.gameTime || 0;
        game.bossDefeated = data.bossDefeated || false;
    } catch(e) { /* ignore save errors */ }
}

// ==================== RENDERING ====================
function getCameraOffset() {
    const p = game.player;
    return {
        x: canvas.width/2 - p.x,
        y: canvas.height/2 - p.y
    };
}

function render() {
    ctx.fillStyle = '#0a0a0f';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    const cam = getCameraOffset();
    const room = rooms[game.player.room];
    
    // Draw floor
    for(let x=0;x<ROOM_W;x++) {
        for(let y=0;y<ROOM_H;y++) {
            const tile = room.walls[x][y];
            const sx = x*TILE + cam.x;
            const sy = y*TILE + cam.y;
            
            if(tile === T.WALL) {
                ctx.fillStyle = room.bg;
                ctx.fillRect(sx, sy, TILE, TILE);
                // Wall detail
                ctx.fillStyle = 'rgba(0,0,0,0.3)';
                ctx.fillRect(sx+2, sy+2, TILE-4, TILE-4);
                ctx.strokeStyle = 'rgba(255,255,255,0.05)';
                ctx.strokeRect(sx, sy, TILE, TILE);
            } else if(tile === T.DOOR) {
                ctx.fillStyle = room.floor;
                ctx.fillRect(sx, sy, TILE, TILE);
                // Door frame
                ctx.fillStyle = '#654';
                ctx.fillRect(sx+4, sy+2, TILE-8, TILE-4);
                ctx.fillStyle = '#a86';
                ctx.fillRect(sx+6, sy+4, TILE-12, TILE-8);
            } else if(tile === T.SHOP) {
                ctx.fillStyle = room.floor;
                ctx.fillRect(sx, sy, TILE, TILE);
                ctx.fillStyle = '#5a3a10';
                ctx.fillRect(sx+2, sy+4, TILE-4, TILE-6);
            } else {
                ctx.fillStyle = room.floor;
                ctx.fillRect(sx, sy, TILE, TILE);
                // Floor detail
                if((x+y)%3===0) {
                    ctx.fillStyle = 'rgba(255,255,255,0.03)';
                    ctx.fillRect(sx+8, sy+8, 4, 4);
                }
            }
        }
    }
    
    // Draw items on ground
    room.items.forEach(item => {
        if(item.collected) return;
        const sx = item.x*TILE + TILE/2 + cam.x;
        const sy = item.y*TILE + TILE/2 + cam.y;
        
        const bob = Math.sin(game.gameTime*3)*3;
        
        switch(item.type) {
            case 'gold':
                ctx.fillStyle = '#ffd700';
                ctx.beginPath();
                ctx.arc(sx, sy+bob, 6, 0, Math.PI*2);
                ctx.fill();
                ctx.fillStyle = '#ff8';
                ctx.font='10px sans-serif';
                ctx.textAlign='center';
                ctx.fillText('$',sx,sy+bob+4);
                break;
            case 'healthPotion':
                ctx.fillStyle='#f44';
                ctx.beginPath();ctx.arc(sx,sy+bob,7,0,Math.PI*2);ctx.fill();
                ctx.fillStyle='#fff';
                ctx.fillRect(sx-1.5,sy-3+bob,3,6);
                ctx.fillRect(sx-3,sy-1+bob,6,3);
                break;
            case 'manaPotion':
                ctx.fillStyle='#48f';
                ctx.beginPath();ctx.arc(sx,sy+bob,7,0,Math.PI*2);ctx.fill();
                ctx.fillStyle='#aef';
                ctx.fillRect(sx-1.5,sy-3+bob,3,6);
                ctx.fillRect(sx-3,sy-1+bob,6,3);
                break;
            case 'shield':
                ctx.fillStyle='#8B4513';
                ctx.beginPath();
                ctx.moveTo(sx-7,sy-8+bob);ctx.lineTo(sx+7,sy-8+bob);
                ctx.lineTo(sx+7,sy+2+bob);ctx.lineTo(sx,sy+9+bob);
                ctx.lineTo(sx-7,sy+2+bob);ctx.closePath();ctx.fill();
                break;
            case 'rangedWeapon':
                // Glowing crystal bow
                const glow = 0.5 + Math.sin(game.gameTime*4)*0.3;
                ctx.fillStyle=`rgba(68,170,255,${glow})`;
                ctx.beginPath();ctx.arc(sx,sy+bob,10,0,Math.PI*2);ctx.fill();
                ctx.strokeStyle='#4af';ctx.lineWidth=2;
                ctx.beginPath();ctx.arc(sx,sy+bob,6,0.5,Math.PI-0.5);ctx.stroke();
                break;
            case 'savepoint':
                // Glowing crystal
                const sg = 0.4+Math.sin(game.gameTime*2)*0.3;
                ctx.fillStyle=`rgba(100,255,100,${sg})`;
                ctx.beginPath();ctx.arc(sx,sy+bob,8,0,Math.PI*2);ctx.fill();
                ctx.fillStyle='#6f6';
                ctx.font='bold 14px sans-serif';ctx.textAlign='center';
                ctx.fillText('💾',sx,sy+5+bob);
                break;
        }
    });
    
    // Draw NPCs
    room.npcs.forEach(npc => {
        const sx = npc.x*TILE + cam.x;
        const sy = npc.y*TILE + cam.y;
        
        if(npc.type === 'merchant') {
            // Merchant body
            ctx.fillStyle='#8B4513';
            ctx.fillRect(sx-8,sy-2,16,18);
            // Head
            ctx.fillStyle='#daa070';
            ctx.beginPath();ctx.arc(sx,sy-8,8,0,Math.PI*2);ctx.fill();
            // Hat
            ctx.fillStyle='#532';
            ctx.fillRect(sx-9,sy-16,18,6);
            ctx.fillRect(sx-6,sy-20,12,6);
            // Sign
            ctx.fillStyle='#ffd700';ctx.font='bold 10px sans-serif';ctx.textAlign='center';
            ctx.fillText('SHOP',sx,sy+24);
        }
    });
    
    // Draw enemies
    game.enemies.forEach(e => {
        if(e.dead) return;
        const sx = e.x + cam.x;
        const sy = e.y + cam.y;
        
        let bodyColor, eyeColor;
        switch(e.type) {
            case 'slime': bodyColor='#4a2'; eyeColor='#0f0'; break;
            case 'wolf': bodyColor='#654'; eyeColor='#f80'; break;
            case 'crystalGolem': bodyColor='#369'; eyeColor='#aef'; break;
            case 'shadowKnight': bodyColor='#212'; eyeColor='#f0f'; break;
            case 'shadowLord': bodyColor='#100'; eyeColor='#f44'; break;
        }
        
        // Hit flash
        if(e.hitFlash > 0) bodyColor = '#fff';
        
        const size = e.type==='shadowLord' ? ENEMY_SIZE*1.5 : ENEMY_SIZE;
        
        // Shadow
        ctx.fillStyle='rgba(0,0,0,0.3)';
        ctx.beginPath();ctx.ellipse(sx,sy+size/2+2,size/2,4,0,0,Math.PI*2);ctx.fill();
        
        // Body
        ctx.fillStyle=bodyColor;
        if(e.type==='slime') {
            const squish = 1 + Math.sin(game.gameTime*3)*0.1;
            ctx.beginPath();
            ctx.ellipse(sx,sy,size/2,size/2*squish,0,0,Math.PI*2);
            ctx.fill();
        } else if(e.type==='shadowLord') {
            // Boss: larger, more menacing
            ctx.fillRect(sx-size/2,sy-size/2,size,size);
            // Cape
            ctx.fillStyle='#300';
            ctx.beginPath();
            ctx.moveTo(sx-size/2-4,sy+size/2);
            ctx.lineTo(sx,sy+size/2+15);
            ctx.lineTo(sx+size/2+4,sy+size/2);
            ctx.fill();
        } else {
            ctx.fillRect(sx-size/2,sy-size/2,size,size);
        }
        
        // Eyes
        ctx.fillStyle=eyeColor;
        const eyeOff = size*0.15;
        ctx.fillRect(sx-eyeOff-2,sy-3,4,4);
        ctx.fillRect(sx+eyeOff-2,sy-3,4,4);
        
        // HP bar
        if(e.hp < e.maxHp) {
            const bw = size + 10;
            ctx.fillStyle='rgba(0,0,0,0.6)';
            ctx.fillRect(sx-bw/2,sy-size/2-8,bw,4);
            ctx.fillStyle=e.type==='shadowLord'?'#f44':'#c33';
            ctx.fillRect(sx-bw/2,sy-size/2-8,bw*(e.hp/e.maxHp),4);
        }
    });
    
    // Draw player
    drawPlayer(cam.x, cam.y);
    
    // Draw projectiles
    game.projectiles.forEach(proj => {
        const sx = proj.x + cam.x;
        const sy = proj.y + cam.y;
        ctx.fillStyle=proj.color;
        ctx.beginPath();ctx.arc(sx,sy,4,0,Math.PI*2);ctx.fill();
        // Trail
        ctx.fillStyle='rgba(68,170,255,0.3)';
        ctx.beginPath();ctx.arc(sx-proj.vx,sy-proj.vy,3,0,Math.PI*2);ctx.fill();
    });
    
    // Draw particles
    game.particles.forEach(pt => {
        const sx = pt.x + cam.x;
        const sy = pt.y + cam.y;
        const alpha = pt.life / pt.maxLife;
        
        if(pt.isText) {
            ctx.globalAlpha=alpha;
            ctx.fillStyle=pt.color;
            ctx.font='bold 13px sans-serif';
            ctx.textAlign='center';
            ctx.fillText(pt.text,sx,sy);
            ctx.globalAlpha=1;
        } else {
            ctx.globalAlpha=alpha;
            ctx.fillStyle=pt.color;
            ctx.beginPath();ctx.arc(sx,sy,pt.size*alpha,0,Math.PI*2);ctx.fill();
            ctx.globalAlpha=1;
        }
    });
    
    // Room name display
    ctx.fillStyle='rgba(255,255,255,0.15)';
    ctx.font='bold 16px sans-serif';
    ctx.textAlign='center';
    ctx.fillText(room.name, canvas.width/2, canvas.height-30);
    
    // Minimap
    drawMinimap();
}

function drawPlayer(cx, cy) {
    const p = game.player;
    const sx = p.x + cx;
    const sy = p.y + cy;
    
    // Damage flash
    if(p.damageFlash > 0 && Math.floor(game.gameTime*20)%2===0) return;
    
    // Invincibility blink
    if(p.invincibleTimer > 0 && Math.floor(game.gameTime*15)%2===0) {
        ctx.globalAlpha = 0.4;
    }
    
    const bobY = Math.sin(p.walkFrame)*2;
    
    // Shadow
    ctx.fillStyle='rgba(0,0,0,0.3)';
    ctx.beginPath();ctx.ellipse(sx,sy+14,10,4,0,0,Math.PI*2);ctx.fill();
    
    // Body color based on armor
    let bodyColor = '#4a8';
    if(p.accessories.some(a=>a.id==='ironArmor')) bodyColor = '#667';
    
    // Legs (animated)
    const legSwing = Math.sin(p.walkFrame*2)*3;
    ctx.fillStyle='#356';
    ctx.fillRect(sx-4,sy+4+bobY,4,10);
    ctx.fillRect(sx+1,sy+4+bobY,4,10);
    
    // Body
    ctx.fillStyle=bodyColor;
    ctx.fillRect(sx-7,sy-6+bobY,14,14);
    
    // Head
    ctx.fillStyle='#daa070';
    ctx.beginPath();ctx.arc(sx,sy-10+bobY,7,0,Math.PI*2);ctx.fill();
    
    // Eyes (facing direction)
    const eyeX = p.facing.x * 2;
    ctx.fillStyle='#333';
    ctx.fillRect(sx+eyeX-3,sy-11+bobY,2,2);
    ctx.fillRect(sx+eyeX+1,sy-11+bobY,2,2);
    
    // Weapon in hand
    const weaponAngle = Math.atan2(p.facing.y, p.facing.x);
    const attackSwing = (p.attackCooldown > 0) ? Math.sin(p.attackCooldown/p.weapon.speed * Math.PI)*30 : 0;
    
    ctx.save();
    ctx.translate(sx + p.facing.x*8, sy + p.facing.y*8 + bobY);
    ctx.rotate(weaponAngle + attackSwing*Math.PI/180);
    
    // Weapon visual based on level
    if(p.weaponLevel === 0) {
        // Iron Sword - simple blade
        ctx.fillStyle=p.weapon.color;
        ctx.fillRect(0,-2,16,4);
        ctx.fillStyle='#532';
        ctx.fillRect(-2,-3,4,6);
    } else if(p.weaponLevel === 1) {
        // Steel Sword - longer, bluer
        ctx.fillStyle=p.weapon.color;
        ctx.fillRect(0,-2.5,20,5);
        ctx.fillStyle='#8cf';
        ctx.fillRect(14,-3,6,6);
        ctx.fillStyle='#532';
        ctx.fillRect(-2,-4,4,8);
    } else {
        // Fury Blade - glowing, ornate
        const glow = 0.7+Math.sin(game.gameTime*5)*0.3;
        ctx.shadowColor=p.weapon.color;ctx.shadowBlur=10;
        ctx.fillStyle=p.weapon.color;
        ctx.fillRect(0,-3,24,6);
        ctx.fillStyle=`rgba(255,100,150,${glow})`;
        ctx.fillRect(18,-4,8,8);
        ctx.shadowBlur=0;
        ctx.fillStyle='#532';
        ctx.fillRect(-2,-5,4,10);
    }
    
    ctx.restore();
    
    // Speed boots visual (trail effect)
    if(p.accessories.some(a=>a.id==='speedBoots')) {
        ctx.globalAlpha=0.2;
        ctx.fillStyle='#6af';
        ctx.beginPath();ctx.arc(sx-3,sy+12+bobY,5,0,Math.PI*2);ctx.fill();
    }
    
    // Shield visual
    if(p.accessories.some(a=>a.id==='shield')) {
        ctx.fillStyle='rgba(139,69,19,0.7)';
        const shieldSide = p.facing.x >= 0 ? -1 : 1;
        ctx.beginPath();
        ctx.moveTo(sx+shieldSide*8-4,sy-2+bobY);
        ctx.lineTo(sx+shieldSide*8+4,sy-2+bobY);
        ctx.lineTo(sx+shieldSide*8+4,sy+6+bobY);
        ctx.lineTo(sx+shieldSide*8,sy+10+bobY);
        ctx.lineTo(sx+shieldSide*8-4,sy+6+bobY);
        ctx.closePath();ctx.fill();
    }
    
    // Power ring glow
    if(p.accessories.some(a=>a.id==='powerRing')) {
        const rg = 0.3+Math.sin(game.gameTime*4)*0.2;
        ctx.fillStyle=`rgba(255,215,0,${rg})`;
        ctx.beginPath();ctx.arc(sx,sy+bobY,16,0,Math.PI*2);ctx.fill();
    }
    
    ctx.globalAlpha=1;
}

function drawMinimap() {
    mctx.fillStyle='#111';
    mctx.fillRect(0,0,120,90);
    
    const roomW = 120/5;
    const roomH = 90/3;
    
    // Draw room grid (simplified layout)
    const roomPositions = [
        {x:0,y:1}, // Village
        {x:1,y:1}, // Dark Forest
        {x:2,y:1}, // Crystal Caverns
        {x:1,y:2}, // Merchant's Keep
        {x:2,y:2}, // Shadow Fortress
    ];
    
    roomPositions.forEach((rp,i) => {
        const visited = game.player.room === i || rooms[i].items.some(it=>it.collected);
        mctx.fillStyle = visited ? (i===game.player.room?'#4a6':'#354') : '#222';
        mctx.fillRect(rp.x*roomW+2, rp.y*roomH+2, roomW-4, roomH-4);
        
        if(visited) {
            mctx.fillStyle='#889';mctx.font='7px sans-serif';mctx.textAlign='center';
            mctx.fillText(rooms[i].name.substring(0,6), rp.x*roomW+roomW/2, rp.y*roomH+roomH/2+3);
        }
    });
    
    // Draw connections
    mctx.strokeStyle='#445';mctx.lineWidth=1;
    [[0,1],[1,2],[1,3],[2,4]].forEach(([a,b])=>{
        const ra=roomPositions[a],rb=roomPositions[b];
        mctx.beginPath();
        mctx.moveTo(ra.x*roomW+roomW/2,ra.y*roomH+roomH/2);
        mctx.lineTo(rb.x*roomW+roomW/2,rb.y*roomH+roomH/2);
        mctx.stroke();
    });
    
    // Player dot
    const pr = roomPositions[game.player.room];
    mctx.fillStyle='#4f4';
    mctx.beginPath();
    mctx.arc(pr.x*roomW+roomW/2,pr.y*roomH+roomH/2,3,0,Math.PI*2);
    mctx.fill();
}

// ==================== MANA REGEN ====================
setInterval(()=>{
    if(!game || !game.running) return;
    if(game.player.mana < game.player.maxMana && game.player.hasRangedWeapon) {
        game.player.mana++;
    }
}, 3000);

// Auto-save every 60 seconds
setInterval(()=>{
    if(!game || !game.running) return;
    saveGame();
}, 60000);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"98886337-6a6c-450d-9d89-3a46c8edb437","tokensIn":74897,"tokensOut":21914,"tokensTotal":96811,"turns":4,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-07-18T23:53:05.267Z"} -->