← RPG results

RPG

thinkingcap qwen3.6 27b · full

Initial view of RPG

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 54,671 bytes · SHA-256 f51b7404d18c
<!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; }
#stats-bar { position: absolute; top: 12px; right: 12px; display: flex; gap: 16px; align-items: center; color: #e8d5b7; font-size: 14px; text-shadow: 0 0 8px rgba(0,0,0,0.8); }
.stat-item { display: flex; align-items: center; gap: 6px; background: rgba(0,0,0,0.5); padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(232,213,183,0.2); }
#weapon-display { position: absolute; bottom: 12px; left: 12px; color: #e8d5b7; font-size: 13px; background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 6px; border: 1px solid rgba(232,213,183,0.2); }
#message-log { position: absolute; bottom: 50px; left: 12px; width: 320px; pointer-events: none; }
.msg { color: #e8d5b7; font-size: 13px; padding: 3px 0; text-shadow: 0 0 6px rgba(0,0,0,0.9); opacity: 1; transition: opacity 0.5s; }
#minimap { position: absolute; top: 12px; right: 12px; border: 2px solid rgba(232,213,183,0.4); border-radius: 4px; background: rgba(0,0,0,0.7); }
#inventory-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 520px; max-height: 80vh; background: linear-gradient(135deg, #1a1420, #0f0b14); border: 2px solid rgba(232,213,183,0.3); border-radius: 12px; padding: 20px; display: none; pointer-events: auto; z-index: 100; overflow-y: auto; }
#inventory-panel h2 { color: #e8d5b7; margin-bottom: 16px; font-size: 20px; text-align: center; border-bottom: 1px solid rgba(232,213,183,0.2); padding-bottom: 10px; }
.inv-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-bottom: 16px; }
.inv-slot { aspect-ratio: 1; background: rgba(255,255,255,0.05); border: 1px solid rgba(232,213,183,0.15); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; transition: all 0.2s; position: relative; }
.inv-slot:hover { background: rgba(255,255,255,0.1); border-color: rgba(232,213,183,0.4); }
.inv-slot .count { position: absolute; bottom: 2px; right: 4px; font-size: 10px; color: #e8d5b7; }
#shop-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 480px; background: linear-gradient(135deg, #1a1420, #0f0b14); border: 2px solid rgba(232,213,183,0.3); border-radius: 12px; padding: 20px; display: none; pointer-events: auto; z-index: 100; }
#shop-panel h2 { color: #ffd700; margin-bottom: 16px; font-size: 20px; text-align: center; border-bottom: 1px solid rgba(255,215,0,0.3); padding-bottom: 10px; }
.shop-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; margin: 4px 0; background: rgba(255,255,255,0.03); border-radius: 6px; cursor: pointer; transition: all 0.2s; }
.shop-item:hover { background: rgba(255,215,0,0.1); }
.shop-item .name { color: #e8d5b7; font-size: 14px; }
.shop-item .price { color: #ffd700; font-size: 13px; }
#game-over-screen, #title-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 200; pointer-events: auto; }
#title-screen { background: radial-gradient(ellipse at center, #1a1430 0%, #050510 100%); }
#game-over-screen { background: rgba(10,5,5,0.92); display: none; }
.title-text { font-size: 48px; color: #e8d5b7; text-shadow: 0 0 30px rgba(232,213,183,0.4), 0 0 60px rgba(100,80,160,0.3); margin-bottom: 8px; letter-spacing: 4px; }
.subtitle { color: #a895c5; font-size: 16px; margin-bottom: 40px; letter-spacing: 2px; }
.btn { padding: 12px 36px; background: linear-gradient(135deg, #3d2b5e, #2a1f45); color: #e8d5b7; border: 1px solid rgba(232,213,183,0.3); border-radius: 8px; font-size: 16px; cursor: pointer; transition: all 0.3s; margin: 4px; letter-spacing: 1px; }
.btn:hover { background: linear-gradient(135deg, #5a3d7e, #3d2b5e); border-color: rgba(232,213,183,0.6); transform: scale(1.05); }
.controls-info { color: #7a6b94; font-size: 12px; margin-top: 30px; text-align: center; line-height: 1.8; }
#damage-flash { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, transparent 40%, rgba(255,0,0,0.3)); pointer-events: none; opacity: 0; transition: opacity 0.1s; z-index: 50; }
#save-notification { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #7df9ff; font-size: 24px; text-shadow: 0 0 20px rgba(125,249,255,0.6); pointer-events: none; opacity: 0; z-index: 50; transition: opacity 0.5s; }
</style>
</head>
<body>

<canvas id="game"></canvas>
<div id="ui-overlay">
  <div id="hud"></div>
  <div id="stats-bar">
    <div class="stat-item" id="gold-display">🪙 0</div>
    <div class="stat-item" id="zone-display">📍 Village</div>
  </div>
  <div id="weapon-display">⚔️ Rusty Sword (1)</div>
  <div id="message-log"></div>
</div>

<div id="inventory-panel">
  <h2>🎒 Inventory</h2>
  <div class="inv-grid" id="inv-grid"></div>
  <div style="color:#7a6b94;font-size:12px;text-align:center;">Press I to close • Click item to use</div>
</div>

<div id="shop-panel">
  <h2>🏪 Merchant Shop</h2>
  <div id="shop-items"></div>
  <div style="color:#7a6b94;font-size:12px;text-align:center;margin-top:12px;">Press E to close • Click item to buy</div>
</div>

<div id="damage-flash"></div>
<div id="save-notification">💾 Game Saved</div>

<div id="title-screen">
  <div class="title-text">Realm of Echoes</div>
  <div class="subtitle">An Action-Adventure RPG</div>
  <button class="btn" onclick="startGame()">Begin Adventure</button>
  <button class="btn" id="load-btn" style="display:none;" onclick="loadGame()">Continue Journey</button>
  <div class="controls-info">
    WASD / Arrow Keys — Move<br>
    Space — Attack &nbsp;|&nbsp; E — Interact &nbsp;|&nbsp; I — Inventory<br>
    R — Use Ranged Weapon (when unlocked)
  </div>
</div>

<div id="game-over-screen">
  <div class="title-text" style="font-size:36px;color:#ff4444;">You Have Fallen</div>
  <div class="subtitle">The echoes fade...</div>
  <button class="btn" onclick="restartGame()">Try Again</button>
  <button class="btn" id="load-btn2" style="display:none;" onclick="loadGame()">Load Save</button>
</div>

<script>
// ============================================================
// REALM OF ECHOES - Complete Game Engine
// ============================================================

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

// --- Constants ---
const TILE = 32;
const VIEW_W = Math.ceil(window.innerWidth / TILE) + 2;
const VIEW_H = Math.ceil(window.innerHeight / TILE) + 2;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// --- Audio Engine (Web Audio API) ---
let audioCtx = null;
function initAudio() {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}

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);

    if (type === 'walk') {
        const o = audioCtx.createOscillator();
        o.type = 'sine'; o.frequency.setValueAtTime(80 + Math.random()*30, now);
        g.gain.exponentialRampToValueAtTime(0.001, now + 0.06);
        o.connect(g); o.start(now); o.stop(now + 0.06);
    } else if (type === 'attack') {
        const o = audioCtx.createOscillator();
        o.type = 'sawtooth'; o.frequency.setValueAtTime(200, now);
        o.frequency.exponentialRampToValueAtTime(80, now + 0.15);
        g.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
        o.connect(g); o.start(now); o.stop(now + 0.15);
    } else if (type === 'hit') {
        const buf = audioCtx.createBufferSource();
        const b = audioCtx.createBuffer(1, audioCtx.sampleRate * 0.1, audioCtx.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, now + 0.1);
        buf.connect(g); buf.start(now);
    } else if (type === 'pickup') {
        const o = audioCtx.createOscillator();
        o.type = 'sine'; o.frequency.setValueAtTime(400, now);
        o.frequency.exponentialRampToValueAtTime(800, now + 0.12);
        g.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
        o.connect(g); o.start(now); o.stop(now + 0.15);
    } else if (type === 'shop') {
        const o = audioCtx.createOscillator();
        o.type = 'triangle'; o.frequency.setValueAtTime(300, now);
        o.frequency.exponentialRampToValueAtTime(600, now + 0.2);
        g.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
        o.connect(g); o.start(now); o.stop(now + 0.25);
    } else if (type === 'save') {
        [400,500,600].forEach((f,i) => {
            const o = audioCtx.createOscillator();
            const gg = audioCtx.createGain();
            o.type='sine'; o.frequency.setValueAtTime(f, now+i*0.12);
            gg.gain.setValueAtTime(0.15, now+i*0.12);
            gg.gain.exponentialRampToValueAtTime(0.001, now+i*0.12+0.2);
            o.connect(gg); gg.connect(audioCtx.destination);
            o.start(now+i*0.12); o.stop(now+i*0.12+0.2);
        });
    } else if (type === 'ranged') {
        const o = audioCtx.createOscillator();
        o.type='sine'; o.frequency.setValueAtTime(600,now);
        o.frequency.exponentialRampToValueAtTime(150,now+0.2);
        g.gain.exponentialRampToValueAtTime(0.001,now+0.2);
        o.connect(g);o.start(now);o.stop(now+0.2);
    } else if (type === 'upgrade') {
        [500,650,800,950].forEach((f,i)=>{
            const o=audioCtx.createOscillator();const gg=audioCtx.createGain();
            o.type='sine';o.frequency.setValueAtTime(f,now+i*0.1);
            gg.gain.setValueAtTime(0.2,now+i*0.1);
            gg.gain.exponentialRampToValueAtTime(0.001,now+i*0.1+0.3);
            o.connect(gg);gg.connect(audioCtx.destination);
            o.start(now+i*0.1);o.stop(now+i*0.1+0.3);
        });
    } else if (type === 'death') {
        const o=audioCtx.createOscillator();o.type='sawtooth';
        o.frequency.setValueAtTime(200,now);o.frequency.exponentialRampToValueAtTime(40,now+1);
        g.gain.setValueAtTime(0.3,now);g.gain.exponentialRampToValueAtTime(0.001,now+1);
        o.connect(g);o.start(now);o.stop(now+1);
    } else if (type === 'heal') {
        const o=audioCtx.createOscillator();o.type='sine';
        o.frequency.setValueAtTime(350,now);o.frequency.exponentialRampToValueAtTime(700,now+0.3);
        g.gain.exponentialRampToValueAtTime(0.001,now+0.35);
        o.connect(g);o.start(now);o.stop(now+0.35);
    }
}

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

function updateParticles() {
    for (let i = particles.length-1; i >= 0; i--) {
        const p = particles[i];
        p.x += p.vx; p.y += p.vy;
        p.vy += 0.05; // gravity
        p.life--;
        if (p.life <= 0) particles.splice(i, 1);
    }
}

function drawParticles(camX, camY) {
    for (const p of particles) {
        const alpha = p.life / p.maxLife;
        ctx.globalAlpha = alpha;
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x - camX, p.y - camY, p.size * alpha, 0, Math.PI*2);
        ctx.fill();
    }
    ctx.globalAlpha = 1;
}

// --- Tile Types ---
const T = {
    EMPTY: 0, GRASS: 1, DIRT: 2, WALL: 3, WATER: 4, BRIDGE: 5,
    FLOOR: 6, DOOR: 7, SAVE_POINT: 8, SHOP: 9, CHEST: 10,
    LAVA: 11, STONE: 12, SAND: 13, SNOW: 14, ICE: 15,
    BOSS_DOOR: 16, TRAP: 17, SHRINE: 18
};

const TILE_COLORS = {
    [T.EMPTY]: null,
    [T.GRASS]: '#2d5a1e',
    [T.DIRT]: '#8b6914',
    [T.WALL]: '#3a3a4a',
    [T.WATER]: '#1a4a7a',
    [T.BRIDGE]: '#6b4c2a',
    [T.FLOOR]: '#5a5060',
    [T.DOOR]: '#8b4513',
    [T.SAVE_POINT]: '#4a3a5a',
    [T.SHOP]: '#5a5060',
    [T.CHEST]: '#5a5060',
    [T.LAVA]: '#aa2200',
    [T.STONE]: '#4a4a5a',
    [T.SAND]: '#c2a848',
    [T.SNOW]: '#d8e8f0',
    [T.ICE]: '#8ac4e0',
    [T.BOSS_DOOR]: '#6a1a1a',
    [T.TRAP]: '#5a5060',
    [T.SHRINE]: '#3a2a5a'
};

const TILE_SOLID = new Set([T.WALL, T.WATER, T.LAVA]);

// --- World Map Generation ---
function createWorld() {
    const W = 80, H = 60;
    const map = Array.from({length: H}, () => Array(W).fill(T.GRASS));

    // Helper to fill rectangle
    function rect(x1,y1,x2,y2,tile) {
        for (let y=y1;y<=y2;y++) for(let x=x1;x<=x2;x++) if(y>=0&&y<H&&x>=0&&x<W) map[y][x]=tile;
    }

    // Zone 1: Village (top-left area, 0-35, 0-28)
    rect(0,0,W,H,T.GRASS);
    // Village buildings
    rect(4,4,9,9,T.WALL); rect(5,5,8,8,T.FLOOR); map[7][6]=T.SHOP;
    rect(12,3,17,8,T.WALL); rect(13,4,16,7,T.FLOOR);
    rect(20,5,25,10,T.WALL); rect(21,6,24,9,T.FLOOR); map[8][22]=T.SAVE_POINT;

    // Paths from village
    rect(10,5,11,25,T.DIRT); rect(13,8,13,25,T.DIRT);
    rect(15,10,40,11,T.DIRT); rect(30,11,31,25,T.DIRT);

    // Zone 2: Forest (middle area)
    rect(0,30,W,35,T.GRASS);
    // Trees as walls scattered
    for(let i=0;i<40;i++){const tx=Math.floor(Math.random()*W),ty=30+Math.floor(Math.random()*18);map[ty][tx]=T.WALL;}
    rect(2,32,5,36,T.GRASS); // clear path
    map[34][3]=T.CHEST;

    // Zone 3: Cave (right side)
    rect(40,15,W-1,38,T.STONE);
    rect(41,16,W-2,37,T.FLOOR);
    // Cave walls pattern
    for(let y=16;y<38;y++)for(let x=41;x<W-1;x++){
        if(Math.random()<0.15)map[y][x]=T.WALL;
    }
    rect(42,17,45,20,T.FLOOR); // clear entrance area
    map[18][43]=T.SAVE_POINT;

    // Zone 4: Underground (bottom)
    rect(0,40,W,H,T.STONE);
    for(let y=41;y<H-1;y++)for(let x=1;x<W-1;x++){
        map[y][x]=T.FLOOR;
        if(Math.random()<0.12)map[y][x]=T.WALL;
    }
    rect(5,42,10,46,T.FLOOR); // clear area
    map[44][7]=T.CHEST;
    map[20][48]=T.CHEST;

    // Zone 5: Lava Cavern (bottom-right)
    rect(35,40,W,H,T.STONE);
    for(let y=41;y<H-1;y++)for(let x=36;x<W-1;x++){
        map[y][x]=T.FLOOR;
        if(Math.random()<0.2)map[y][x]=T.LAVA;
        if(Math.random()<0.08)map[y][x]=T.WALL;
    }
    // Bridges over lava
    rect(45,43,47,43,T.BRIDGE);
    rect(55,46,57,46,T.BRIDGE);
    map[44][50]=T.BOSS_DOOR;

    // Zone 6: Crystal Peak (top-right) - final area
    rect(40,0,W-1,12,T.SNOW);
    for(let y=1;y<11;y++)for(let x=41;x<W-1;x++){
        if(Math.random()<0.15)map[y][x]=T.WALL;
        if(Math.random()<0.03)map[y][x]=T.ICE;
    }
    map[6][55]=T.SHRINE;

    // Connect zones with passages
    rect(28,24,32,26,T.DIRT); // forest to cave connector
    rect(10,37,14,39,T.FLOOR); // forest to underground
    rect(35,37,39,39,T.FLOOR); // cave to lava cavern

    return {map, W, H};
}

// --- Game State ---
let game = null;

function createGameState() {
    const world = createWorld();
    return {
        player: {
            x: 7*TILE, y: 7*TILE,
            hp: 3, maxHp: 6, // 3 hearts * 2 = 6 HP (start with 1.5 hearts visible)
            displayHearts: 3, // starts at 3 heart shapes
            gold: 0,
            weapon: { name: 'Rusty Sword', level: 0, damage: 3, range: TILE*1.2, speed: 400, color: '#aaa' },
            hasRangedWeapon: false,
            ammo: 0,
            inventory: [],
            facing: 'down',
            attacking: false,
            attackTimer: 0,
            invincible: 0,
            walkFrame: 0,
            walkTimer: 0,
            speed: 2.5
        },
        world,
        enemies: [],
        items: [],
        chests: [],
        savePoints: [],
        shops: [],
        zones: ['Village', 'Forest', 'Cave', 'Underground', 'Lava Cavern', 'Crystal Peak'],
        currentZone: 0,
        gameProgress: 0, // 0-100
        midGameReached: false,
        bossDefeated: false,
        gameOver: false,
        paused: false,
        inventoryOpen: false,
        shopOpen: false,
        messages: [],
        time: 0,
        keys: {},
        camera: { x: 0, y: 0 }
    };
}

// --- Enemy Types ---
const ENEMY_TYPES = {
    slime: { hp: 4, damage: 1, speed: 0.8, color: '#4a2', size: 12, gold: 3, xp: 5 },
    bat: { hp: 3, damage: 1, speed: 1.5, color: '#639', size: 10, gold: 2, xp: 3 },
    skeleton: { hp: 8, damage: 2, speed: 1, color: '#ccc', size: 14, gold: 5, xp: 10 },
    ghost: { hp: 6, damage: 2, speed: 1.2, color: 'rgba(150,200,255,0.7)', size: 13, gold: 4, xp: 8 },
    demon: { hp: 12, damage: 3, speed: 1.3, color: '#a22', size: 16, gold: 8, xp: 15 },
    boss: { hp: 40, damage: 4, speed: 0.9, color: '#ff0044', size: 24, gold: 50, xp: 100 }
};

function spawnEnemies() {
    const g = game;
    const enemies = [];

    // Village area - weak enemies
    for (let i = 0; i < 3; i++) {
        enemies.push(createEnemy('slime', 25 + Math.random()*10, 15 + Math.random()*8));
    }

    // Forest
    for (let i = 0; i < 6; i++) {
        const type = Math.random() > 0.5 ? 'bat' : 'slime';
        enemies.push(createEnemy(type, 2 + Math.random()*35, 31 + Math.random()*14));
    }

    // Cave
    for (let i = 0; i < 8; i++) {
        const type = ['skeleton', 'ghost', 'bat'][Math.floor(Math.random()*3)];
        enemies.push(createEnemy(type, 42 + Math.random()*(g.world.W-45), 17 + Math.random()*18));
    }

    // Underground
    for (let i = 0; i < 6; i++) {
        const type = ['skeleton', 'ghost'][Math.floor(Math.random()*2)];
        enemies.push(createEnemy(type, 2 + Math.random()*30, 41 + Math.random()*15));
    }

    // Lava Cavern - tough enemies
    for (let i = 0; i < 8; i++) {
        const type = ['demon', 'skeleton'][Math.floor(Math.random()*2)];
        enemies.push(createEnemy(type, 37 + Math.random()*(g.world.W-42), 41 + Math.random()*15));
    }

    // Boss in lava cavern
    enemies.push(createEnemy('boss', 50*TILE, 44*TILE));

    g.enemies = enemies;
}

function createEnemy(type, x, y) {
    const t = ENEMY_TYPES[type];
    return { type, x, y, hp: t.hp, maxHp: t.hp, damage: t.damage, speed: t.speed, color: t.color, size: t.size, gold: t.gold, alive: true, hitTimer: 0, attackCooldown: 0, aggroRange: TILE * 5, angle: Math.random()*Math.PI*2 };
}

// --- Items ---
const ITEM_DEFS = {
    health_potion: { name: 'Health Potion', icon: '🧪', desc: 'Restores 4 HP', type: 'consumable' },
    greater_health_potion: { name: 'Greater Health Potion', icon: '✨', desc: 'Restores 6 HP', type: 'consumable' },
    iron_shield: { name: 'Iron Shield', icon: '🛡️', desc: '+1 Max Hearts', type: 'accessory' },
    speed_boots: { name: 'Swift Boots', icon: '👢', desc: '+20% Speed', type: 'accessory' },
    lucky_charm: { name: 'Lucky Charm', icon: '🍀', desc: '+50% Gold Find', type: 'accessory' },
    fire_ring: { name: 'Fire Ring', icon: '💍', desc: '+2 Weapon Damage', type: 'accessory' },
    arrow_pack: { name: 'Arrow Pack (10)', icon: '🏹', desc: 'Ranged ammo x10', type: 'ammo' }
};

function spawnItems() {
    const g = game;
    const items = [];

    // Scattered items throughout the world
    items.push({ ...ITEM_DEFS.health_potion, x: 6*TILE, y: 24*TILE });
    items.push({ ...ITEM_DEFS.health_potion, x: 30*TILE, y: 11*TILE });
    items.push({ ...ITEM_DEFS.gold_pickup, x: 8*TILE, y: 5*TILE }, { gold: 5 });

    // Forest items
    items.push({ ...ITEM_DEFS.health_potion, x: 15*TILE, y: 34*TILE });
    items.push({ ...ITEM_DEFS.gold_pickup, x: 20*TILE, y: 36*TILE }, { gold: 8 });

    // Cave items
    items.push({ ...ITEM_DEFS.health_potion, x: 50*TILE, y: 20*TILE });
    items.push({ ...ITEM_DEFS.gold_pickup, x: 45*TILE, y: 30*TILE }, { gold: 10 });

    // Underground
    items.push({ ...ITEM_DEFS.greater_health_potion, x: 8*TILE, y: 44*TILE });
    items.push({ ...ITEM_DEFS.gold_pickup, x: 25*TILE, y: 46*TILE }, { gold: 12 });

    // Lava cavern
    items.push({ ...ITEM_DEFS.greater_health_potion, x: 40*TILE, y: 43*TILE });
    items.push({ ...ITEM_DEFS.gold_pickup, x: 55*TILE, y: 48*TILE }, { gold: 15 });

    // Crystal Peak items
    items.push({ ...ITEM_DEFS.health_potion, x: 60*TILE, y: 5*TILE });
    items.push({ ...ITEM_DEFS.gold_pickup, x: 70*TILE, y: 8*TILE }, { gold: 20 });

    g.items = items;
}

// --- Shop Items ---
const SHOP_ITEMS = [
    { id: 'health_potion', price: 10, maxBuy: 99 },
    { id: 'greater_health_potion', price: 25, maxBuy: 99 },
    { id: 'iron_shield', price: 80, maxBuy: 1 },
    { id: 'speed_boots', price: 60, maxBuy: 1 },
    { id: 'lucky_charm', price: 50, maxBuy: 1 },
    { id: 'fire_ring', price: 70, maxBuy: 1 },
    { id: 'arrow_pack', price: 15, maxBuy: 99 }
];

// --- Weapon Upgrades ---
const WEAPON_UPGRADES = [
    { name: 'Rusty Sword', damage: 3, range: TILE*1.2, speed: 400, color: '#aaa' },
    { name: 'Steel Blade', damage: 5, range: TILE*1.5, speed: 350, color: '#8cf' },
    { name: 'Flame Edge', damage: 8, range: TILE*2, speed: 300, color: '#f84' }
];

// --- Core Game Functions ---
function startGame() {
    initAudio();
    game = createGameState();
    spawnEnemies();
    spawnItems();
    document.getElementById('title-screen').style.display = 'none';
    document.getElementById('game-over-screen').style.display = 'none';
    addMessage("Welcome to the Realm of Echoes!");
    addMessage("Explore, fight, and grow stronger.");
}

function restartGame() {
    startGame();
}

function loadGame() {
    const data = localStorage.getItem('realmOfEchoes');
    if (data) {
        initAudio();
        game = JSON.parse(data);
        document.getElementById('title-screen').style.display = 'none';
        document.getElementById('game-over-screen').style.display = 'none';
        addMessage("Save loaded. Continue your journey!");
    }
}

function saveGame() {
    if (!game) return;
    localStorage.setItem('realmOfEchoes', JSON.stringify(game));
    const notif = document.getElementById('save-notification');
    notif.style.opacity = '1';
    setTimeout(() => notif.style.opacity = '0', 2000);
}

function addMessage(text) {
    if (!game) return;
    game.messages.push({ text, time: Date.now() });
    const log = document.getElementById('message-log');
    const div = document.createElement('div');
    div.className = 'msg';
    div.textContent = text;
    log.appendChild(div);
    setTimeout(() => { div.style.opacity = '0'; }, 3000);
    setTimeout(() => { if (div.parentNode) div.remove(); }, 3500);
}

// --- Input Handling ---
document.addEventListener('keydown', e => {
    if (!game || game.gameOver) return;
    const key = e.key.toLowerCase();
    game.keys[key] = true;

    if (key === 'i') {
        game.inventoryOpen = !game.inventoryOpen;
        document.getElementById('inventory-panel').style.display = game.inventoryOpen ? 'block' : 'none';
        if (game.inventoryOpen) renderInventory();
    }
    if (key === 'e' && game.shopOpen) {
        game.shopOpen = false;
        document.getElementById('shop-panel').style.display = 'none';
    }

    // Prevent scrolling
    if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(e.key.toLowerCase()) || e.key === ' ') {
        e.preventDefault();
    }
});

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

// --- Collision Detection ---
function isSolid(px, py) {
    const g = game;
    const tx = Math.floor(px / TILE);
    const ty = Math.floor(py / TILE);
    if (tx < 0 || ty < 0 || tx >= g.world.W || ty >= g.world.H) return true;
    return TILE_SOLID.has(g.world.map[ty][tx]);
}

function canMoveTo(x, y, size=10) {
    const corners = [
        [x-size+2, y-size+2], [x+size-2, y-size+2],
        [x-size+2, y+size-2], [x+size-2, y+size-2]
    ];
    return !corners.some(([cx,cy]) => isSolid(cx, cy));
}

// --- Player Update ---
function updatePlayer(dt) {
    const p = game.player;
    if (p.invincible > 0) p.invincible -= dt;

    // Movement
    let dx = 0, dy = 0;
    if (game.keys['w'] || game.keys['arrowup']) { dy = -1; p.facing = 'up'; }
    if (game.keys['s'] || game.keys['arrowdown']) { dy = 1; p.facing = 'down'; }
    if (game.keys['a'] || game.keys['arrowleft']) { dx = -1; p.facing = 'left'; }
    if (game.keys['d'] || game.keys['arrowright']) { dx = 1; p.facing = 'right'; }

    const moving = dx !== 0 || dy !== 0;
    if (moving) {
        const len = Math.sqrt(dx*dx + dy*dy);
        dx /= len; dy /= len;
        const nx = p.x + dx * p.speed;
        const ny = p.y + dy * p.speed;
        if (canMoveTo(nx, p.y)) p.x = nx;
        if (canMoveTo(p.x, ny)) p.y = ny;

        // Walk animation & sound
        p.walkTimer += dt;
        if (p.walkTimer > 200) {
            p.walkFrame = (p.walkFrame + 1) % 4;
            playSound('walk', 0.08);
            p.walkTimer = 0;
        }
    } else {
        p.walkFrame = 0;
    }

    // Attack
    if ((game.keys[' '] || game.keys['space']) && !p.attacking) {
        performAttack();
    }

    // Ranged attack
    if (game.keys['r'] && p.hasRangedWeapon && p.ammo > 0 && !p.attacking) {
        performRangedAttack();
    }

    if (p.attacking) {
        p.attackTimer -= dt;
        if (p.attackTimer <= 0) p.attacking = false;
    }

    // Zone detection
    updateZone(p.x, p.y);

    // Interact with nearby objects
    if (game.keys['e']) {
        game.keys['e'] = false;
        interact();
    }
}

function performAttack() {
    const p = game.player;
    p.attacking = true;
    p.attackTimer = p.weapon.speed;
    playSound('attack');

    // Attack hitbox based on facing direction
    let ax = p.x, ay = p.y;
    if (p.facing === 'up') ay -= TILE;
    if (p.facing === 'down') ay += TILE;
    if (p.facing === 'left') ax -= TILE;
    if (p.facing === 'right') ax += TILE;

    // Check enemy hits
    for (const e of game.enemies) {
        if (!e.alive) continue;
        const dist = Math.sqrt((e.x-ax)**2 + (e.y-ay)**2);
        if (dist < p.weapon.range) {
            damageEnemy(e, p.weapon.damage);
        }
    }

    // Attack particles
    spawnParticles(ax, ay, p.weapon.color, 6, 3, 15);
}

function performRangedAttack() {
    const p = game.player;
    if (p.ammo <= 0) return;
    p.ammo--;
    p.attacking = true;
    p.attackTimer = 400;
    playSound('ranged');

    let ax = p.x, ay = p.y;
    const range = TILE * 5;
    if (p.facing === 'up') ay -= range/2;
    if (p.facing === 'down') ay += range/2;
    if (p.facing === 'left') ax -= range/2;
    if (p.facing === 'right') ax += range/2;

    for (const e of game.enemies) {
        if (!e.alive) continue;
        const dist = Math.sqrt((e.x-ax)**2 + (e.y-ay)**2);
        if (dist < TILE * 1.5) {
            damageEnemy(e, p.weapon.damage + 2);
        }
    }

    spawnParticles(ax, ay, '#ff0', 8, 4, 20);
}

function damageEnemy(enemy, dmg) {
    enemy.hp -= dmg;
    enemy.hitTimer = 15;
    playSound('hit');
    spawnParticles(enemy.x, enemy.y, '#ff4', 10, 3, 20);

    if (enemy.hp <= 0) {
        enemy.alive = false;
        const goldMult = game.player.inventory.some(i => i.id === 'lucky_charm') ? 1.5 : 1;
        const goldDrop = Math.floor(enemy.gold * goldMult);
        game.player.gold += goldDrop;
        addMessage(`Defeated ${enemy.type}! +${goldDrop} gold`);
        spawnParticles(enemy.x, enemy.y, '#ff0', 15, 4, 30);

        // Progress tracking
        if (enemy.type === 'boss') {
            game.bossDefeated = true;
            addMessage("🏆 BOSS DEFEATED! The realm is saved!");
            playSound('upgrade');
        }

        updateProgress();
    }
}

function damagePlayer(dmg) {
    const p = game.player;
    if (p.invincible > 0) return;
    p.hp -= dmg;
    p.invincible = 800;
    playSound('hit');

    // Flash screen red
    const flash = document.getElementById('damage-flash');
    flash.style.opacity = '1';
    setTimeout(() => flash.style.opacity = '0', 200);

    spawnParticles(p.x, p.y, '#f44', 8, 3, 20);

    if (p.hp <= 0) {
        p.hp = 0;
        game.gameOver = true;
        playSound('death');
        document.getElementById('game-over-screen').style.display = 'flex';
        const saved = localStorage.getItem('realmOfEchoes');
        if (saved) {
            document.getElementById('load-btn2').style.display = '';
        }
    }

    updateHUD();
}

function interact() {
    const p = game.player;
    // Check nearby tiles for interactive objects
    const tx = Math.floor(p.x / TILE);
    const ty = Math.floor(p.y / TILE);

    for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -1; dx <= 1; dx++) {
            const nx = tx + dx, ny = ty + dy;
            if (nx < 0 || ny < 0 || nx >= game.world.W || ny >= game.world.H) continue;
            const tile = game.world.map[ny][nx];

            if (tile === T.SAVE_POINT) {
                saveGame();
                playSound('save');
                addMessage("💾 Game saved!");
                spawnParticles(p.x, p.y, '#7df9ff', 20, 3, 40);
                game.world.map[ny][nx] = T.FLOOR; // consume save point
            } else if (tile === T.SHOP) {
                openShop();
            } else if (tile === T.CHEST) {
                openChest(nx, ny);
            } else if (tile === T.SHRINE) {
                activateShrine();
            } else if (tile === T.BOSS_DOOR && game.gameProgress >= 80) {
                addMessage("The boss door is sealed. You need more power!");
            }
        }
    }
}

function openChest(tx, ty) {
    const loot = Math.random();
    if (loot < 0.3) {
        // Weapon upgrade
        if (game.player.weapon.level < WEAPON_UPGRADES.length - 1) {
            game.player.weapon.level++;
            const wu = WEAPON_UPGRADES[game.player.weapon.level];
            Object.assign(game.player.weapon, wu);
            addMessage(`⚔️ Weapon upgraded to ${wu.name}!`);
            playSound('upgrade');
            spawnParticles(tx*TILE+TILE/2, ty*TILE+TILE/2, '#ff0', 25, 4, 40);
        } else {
            game.player.gold += 30;
            addMessage("Found 30 gold!");
            playSound('pickup');
        }
    } else if (loot < 0.6) {
        // Health upgrade
        if (game.player.displayHearts < 5) {
            game.player.displayHearts++;
            game.player.maxHp = game.player.displayHearts * 2;
            game.player.hp = Math.min(game.player.hp + 2, game.player.maxHp);
            addMessage("❤️ Max health increased!");
            playSound('upgrade');
        } else {
            game.player.gold += 20;
            addMessage("Found 20 gold!");
        }
    } else if (loot < 0.85) {
        // Gold
        const amt = 15 + Math.floor(Math.random()*20);
        game.player.gold += amt;
        addMessage(`Found ${amt} gold!`);
        playSound('pickup');
    } else {
        // Ranged weapon (mid-game)
        if (!game.player.hasRangedWeapon && game.gameProgress >= 45) {
            game.player.hasRangedWeapon = true;
            game.player.ammo += 10;
            addMessage("🏹 Discovered a Bow! Press R to shoot.");
            playSound('upgrade');
        } else {
            game.player.gold += 10;
            addMessage("Found 10 gold!");
        }
    }

    spawnParticles(tx*TILE+TILE/2, ty*TILE+TILE/2, '#ffd700', 15, 3, 30);
    game.world.map[ty][tx] = T.FLOOR;
}

function activateShrine() {
    if (game.bossDefeated) {
        addMessage("🌟 The shrine glows with ancient power. You are victorious!");
        playSound('upgrade');
        spawnParticles(game.player.x, game.player.y, '#fff', 30, 5, 50);
        game.world.map[Math.floor(game.player.y/TILE)][Math.floor(game.player.x/TILE)] = T.FLOOR;
    } else {
        addMessage("The shrine is dormant. Defeat the boss to awaken it.");
    }
}

function openShop() {
    playSound('shop');
    game.shopOpen = true;
    const panel = document.getElementById('shop-panel');
    panel.style.display = 'block';
    renderShop();
}

function renderShop() {
    const container = document.getElementById('shop-items');
    container.innerHTML = '';
    for (const si of SHOP_ITEMS) {
        const def = ITEM_DEFS[si.id];
        if (!def) continue;
        const div = document.createElement('div');
        div.className = 'shop-item';
        const owned = game.player.inventory.some(i => i.id === si.id && def.type !== 'consumable' && def.type !== 'ammo');
        div.innerHTML = `<span class="name">${def.icon} ${def.name}</span><span class="price">🪙 ${si.price}${owned ? ' (Owned)' : ''}</span>`;
        if (!owned) {
            div.onclick = () => buyItem(si);
        } else {
            div.style.opacity = '0.4';
            div.style.cursor = 'default';
        }
        container.appendChild(div);
    }
}

function buyItem(si) {
    const def = ITEM_DEFS[si.id];
    if (game.player.gold < si.price) {
        addMessage("Not enough gold!");
        return;
    }
    game.player.gold -= si.price;
    playSound('pickup');

    if (def.type === 'consumable') {
        const existing = game.player.inventory.find(i => i.id === si.id);
        if (existing) existing.count++;
        else game.player.inventory.push({ ...def, id: si.id, count: 1 });
    } else if (def.type === 'accessory') {
        game.player.inventory.push({ ...def, id: si.id, count: 1 });
        applyAccessory(si.id);
    } else if (def.type === 'ammo') {
        game.player.ammo += 10;
    }

    addMessage(`Bought ${def.name}!`);
    spawnParticles(game.player.x, game.player.y, '#ffd700', 12, 3, 25);
    updateHUD();
    renderShop();
}

function applyAccessory(id) {
    const p = game.player;
    if (id === 'iron_shield' && p.displayHearts < 5) {
        p.displayHearts++;
        p.maxHp = p.displayHearts * 2;
        addMessage("Iron Shield equipped! +1 Max Heart");
    } else if (id === 'speed_boots') {
        p.speed += 0.5;
        addMessage("Swift Boots equipped! Speed increased!");
    } else if (id === 'fire_ring') {
        p.weapon.damage += 2;
        addMessage("Fire Ring equipped! +2 Damage");
    }
}

function renderInventory() {
    const grid = document.getElementById('inv-grid');
    grid.innerHTML = '';
    for (let i = 0; i < 15; i++) {
        const slot = document.createElement('div');
        slot.className = 'inv-slot';
        if (game.player.inventory[i]) {
            const item = game.player.inventory[i];
            slot.textContent = item.icon || '?';
            if (item.count > 1) {
                const cnt = document.createElement('span');
                cnt.className = 'count';
                cnt.textContent = item.count;
                slot.appendChild(cnt);
            }
            slot.title = `${item.name}\n${item.desc}`;
            if (item.type === 'consumable') {
                slot.onclick = () => useItem(i);
            }
        }
        grid.appendChild(slot);
    }
}

function useItem(index) {
    const item = game.player.inventory[index];
    if (!item || item.type !== 'consumable') return;
    playSound('heal');

    if (item.id === 'health_potion') {
        game.player.hp = Math.min(game.player.hp + 4, game.player.maxHp);
        addMessage("Used Health Potion (+4 HP)");
    } else if (item.id === 'greater_health_potion') {
        game.player.hp = Math.min(game.player.hp + 6, game.player.maxHp);
        addMessage("Used Greater Health Potion (+6 HP)");
    }

    spawnParticles(game.player.x, game.player.y, '#4f4', 10, 2, 25);
    item.count--;
    if (item.count <= 0) game.player.inventory.splice(index, 1);
    renderInventory();
    updateHUD();
}

function updateZone(x, y) {
    const g = game;
    let zone = 0;
    if (y > 38 && x < 35) zone = 3; // Underground
    else if (y > 38) zone = 4; // Lava Cavern
    else if (x >= 40 && y <= 12) zone = 5; // Crystal Peak
    else if (x >= 40) zone = 2; // Cave
    else if (y > 29) zone = 1; // Forest

    if (zone !== g.currentZone) {
        g.currentZone = zone;
        addMessage(`Entered ${g.zones[zone]}`);
    }
}

function updateProgress() {
    const aliveCount = game.enemies.filter(e => e.alive).length;
    const total = game.enemies.length;
    game.gameProgress = Math.min(100, ((total - aliveCount) / total) * 100);

    // Mid-game check for ranged weapon hint
    if (game.gameProgress >= 45 && !game.midGameReached) {
        game.midGameReached = true;
        addMessage("You sense a new power awakening...");
    }
}

// --- Enemy AI ---
function updateEnemies(dt) {
    const p = game.player;
    for (const e of game.enemies) {
        if (!e.alive) continue;
        if (e.hitTimer > 0) e.hitTimer--;
        if (e.attackCooldown > 0) e.attackCooldown -= dt;

        const dx = p.x - e.x;
        const dy = p.y - e.y;
        const dist = Math.sqrt(dx*dx + dy*dy);

        if (dist < e.aggroRange) {
            // Chase player
            const nx = e.x + (dx/dist) * e.speed;
            const ny = e.y + (dy/dist) * e.speed;
            if (canMoveTo(nx, ny, e.size)) {
                e.x = nx; e.y = ny;
            }

            // Attack player on contact
            if (dist < TILE && e.attackCooldown <= 0) {
                damagePlayer(e.damage);
                e.attackCooldown = 1200;
            }
        } else {
            // Wander
            e.angle += (Math.random()-0.5)*0.3;
            const wx = e.x + Math.cos(e.angle)*e.speed*0.3;
            const wy = e.y + Math.sin(e.angle)*e.speed*0.3;
            if (canMoveTo(wx, wy, e.size)) {
                e.x = wx; e.y = wy;
            } else {
                e.angle += Math.PI;
            }
        }
    }
}

// --- Item Pickup ---
function updateItems() {
    const p = game.player;
    for (let i = game.items.length-1; i >= 0; i--) {
        const item = game.items[i];
        const dist = Math.sqrt((p.x-item.x)**2 + (p.y-item.y)**2);
        if (dist < TILE) {
            if (item.gold) {
                p.gold += item.gold;
                addMessage(`+${item.gold} gold`);
            } else {
                const existing = p.inventory.find(it => it.id === item.id);
                if (existing) existing.count++;
                else p.inventory.push({ ...item, count: 1 });
                addMessage(`Picked up ${item.name}`);
            }
            playSound('pickup');
            spawnParticles(item.x, item.y, '#ffd700', 8, 2, 20);
            game.items.splice(i, 1);
        }
    }
}

// --- HUD Update ---
function updateHUD() {
    const p = game.player;
    // Hearts
    const hud = document.getElementById('hud');
    hud.innerHTML = '';
    for (let i = 0; i < p.displayHearts; i++) {
        const heart = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        heart.setAttribute('class', 'heart' + (p.hp <= i*2 ? ' empty' : ''));
        heart.setAttribute('viewBox', '0 0 24 24');
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        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');
        path.setAttribute('fill', '#ff3344');
        heart.appendChild(path);
        hud.appendChild(heart);
    }

    // Gold
    document.getElementById('gold-display').textContent = `🪙 ${p.gold}`;

    // Zone
    document.getElementById('zone-display').textContent = `📍 ${game.zones[game.currentZone]}`;

    // Weapon
    const wd = document.getElementById('weapon-display');
    let wtext = `⚔️ ${p.weapon.name} (${p.weapon.damage})`;
    if (p.hasRangedWeapon) wtext += ` | 🏹 ${p.ammo}`;
    wd.textContent = wtext;
}

// --- Rendering ---
function drawTile(x, y, tileType) {
    const px = x * TILE - game.camera.x;
    const py = y * TILE - game.camera.y;

    if (px < -TILE || py < -TILE || px > canvas.width + TILE || py > canvas.height + TILE) return;

    const color = TILE_COLORS[tileType];
    if (!color) return;

    ctx.fillStyle = color;
    ctx.fillRect(px, py, TILE, TILE);

    // Tile details
    if (tileType === T.GRASS) {
        ctx.fillStyle = '#3a7028';
        for (let i = 0; i < 3; i++) {
            const gx = px + ((x*7+y*13+i*17)%24)+4;
            const gy = py + ((x*11+y*5+i*23)%24)+4;
            ctx.fillRect(gx, gy, 2, 3);
        }
    } else if (tileType === T.WATER) {
        const wave = Math.sin(game.time * 0.003 + x + y) * 3;
        ctx.fillStyle = '#2a6aaa';
        ctx.fillRect(px+4, py+12+wave, TILE-8, 4);
    } else if (tileType === T.LAVA) {
        const glow = Math.sin(game.time*0.005+x+y)*30;
        ctx.fillStyle = `rgb(${170+glow},${40+glow/2},0)`;
        ctx.fillRect(px, py, TILE, TILE);
    } else if (tileType === T.WALL) {
        // Wall texture
        ctx.fillStyle = 'rgba(0,0,0,0.15)';
        ctx.fillRect(px+TILE/2-1, py, 2, TILE);
        ctx.fillRect(px, py+TILE/2-1, TILE, 2);
    } else if (tileType === T.SAVE_POINT) {
        // Glowing crystal
        const pulse = Math.sin(game.time*0.005)*0.3+0.7;
        ctx.fillStyle = `rgba(125,249,255,${pulse})`;
        ctx.beginPath();
        ctx.moveTo(px+TILE/2, py+4);
        ctx.lineTo(px+TILE-6, py+TILE/2);
        ctx.lineTo(px+TILE/2, py+TILE-4);
        ctx.lineTo(px+6, py+TILE/2);
        ctx.closePath();
        ctx.fill();
    } else if (tileType === T.SHOP) {
        // Shop sign
        ctx.fillStyle = '#ffd700';
        ctx.font = '16px serif';
        ctx.textAlign = 'center';
        ctx.fillText('🏪', px+TILE/2, py+TILE-8);
    } else if (tileType === T.CHEST) {
        // Chest
        ctx.fillStyle = '#b87300';
        ctx.fillRect(px+6, py+10, TILE-12, TILE-14);
        ctx.fillStyle = '#ffd700';
        ctx.fillRect(px+TILE/2-3, py+14, 6, 4);
    } else if (tileType === T.SHRINE) {
        const pulse = Math.sin(game.time*0.004)*0.4+0.6;
        ctx.fillStyle = `rgba(255,255,255,${pulse})`;
        ctx.beginPath();
        ctx.arc(px+TILE/2, py+TILE/2, 10, 0, Math.PI*2);
        ctx.fill();
    } else if (tileType === T.BOSS_DOOR) {
        ctx.fillStyle = '#8b0000';
        ctx.fillRect(px+4, py+2, TILE-8, TILE-4);
        ctx.fillStyle = '#ff0000';
        ctx.font = '14px serif';
        ctx.textAlign = 'center';
        ctx.fillText('👹', px+TILE/2, py+TILE-6);
    } else if (tileType === T.STONE) {
        ctx.fillStyle = 'rgba(0,0,0,0.1)';
        const sx = ((x*7+y*3)%5)+2;
        ctx.fillRect(px+sx, py+((x*3+y*7)%5)+2, 4, 3);
    } else if (tileType === T.SNOW) {
        ctx.fillStyle = 'rgba(255,255,255,0.3)';
        for(let i=0;i<3;i++){
            const sx=px+((x*13+y*7+i*19)%28)+2;
            const sy=py+((x*11+y*3+i*17)%28)+2;
            ctx.fillRect(sx,sy,2,2);
        }
    } else if (tileType === T.ICE) {
        ctx.fillStyle = 'rgba(200,240,255,0.4)';
        ctx.fillRect(px+2,py+2,TILE-4,TILE-4);
    }

    // Grid lines for walls
    if (tileType === T.WALL || tileType === T.STONE) {
        ctx.strokeStyle = 'rgba(0,0,0,0.3)';
        ctx.strokeRect(px, py, TILE, TILE);
    }
}

function drawPlayer() {
    const p = game.player;
    const px = p.x - game.camera.x;
    const py = p.y - game.camera.y;

    // Invincibility flash
    if (p.invincible > 0 && Math.floor(p.invincible/100)%2) return;

    ctx.save();
    ctx.translate(px, py);

    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.beginPath();
    ctx.ellipse(0, 14, 10, 4, 0, 0, Math.PI*2);
    ctx.fill();

    // Body bob animation
    const bob = p.walkFrame % 2 === 1 ? -2 : 0;

    // Character body
    ctx.fillStyle = '#5b8';
    ctx.fillRect(-6, -4+bob, 12, 16);

    // Head
    ctx.fillStyle = '#fdb';
    ctx.beginPath();
    ctx.arc(0, -8+bob, 7, 0, Math.PI*2);
    ctx.fill();

    // Eyes based on facing direction
    let eyeOffX = 0, eyeOffY = 0;
    if (p.facing === 'left') eyeOffX = -3;
    if (p.facing === 'right') eyeOffX = 3;
    if (p.facing === 'up') eyeOffY = -2;
    ctx.fillStyle = '#333';
    ctx.fillRect(-3+eyeOffX, -9+bob+eyeOffY, 2, 2);
    ctx.fillRect(1+eyeOffX, -9+bob+eyeOffY, 2, 2);

    // Weapon visual
    const wColor = p.weapon.color;
    if (p.attacking) {
        // Attack swing animation
        let ax=0, ay=0;
        if(p.facing==='right')ax=14;if(p.facing==='left')ax=-14;
        if(p.facing==='down')ay=14;if(p.facing==='up')ay=-14;
        ctx.strokeStyle = wColor;
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(0, -2+bob);
        ctx.lineTo(ax, ay-2+bob);
        ctx.stroke();

        // Attack arc effect
        ctx.strokeStyle = `${wColor}66`;
        ctx.lineWidth = 8;
        const startAngle = p.facing==='right'?-0.5:p.facing==='left'?Math.PI-0.5:p.facing==='down'?Math.PI/2:-Math.PI/2;
        ctx.beginPath();
        ctx.arc(0, bob, TILE*0.6, startAngle-0.8, startAngle+0.8);
        ctx.stroke();
    } else {
        // Idle weapon
        let wx=0, wy=0;
        if(p.facing==='right')wx=10;if(p.facing==='left')wx=-10;
        if(p.facing==='down')wy=10;if(p.facing==='up')wy=-10;
        ctx.strokeStyle = wColor;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(4, 2+bob);
        ctx.lineTo(wx+4, wy+2+bob);
        ctx.stroke();
    }

    // Equipment glow effects
    if (p.inventory.some(i => i.id === 'iron_shield')) {
        ctx.strokeStyle = '#8cf';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(0, bob, 14, 0, Math.PI*2);
        ctx.stroke();
    }

    ctx.restore();
}

function drawEnemy(e) {
    if (!e.alive) return;
    const px = e.x - game.camera.x;
    const py = e.y - game.camera.y;
    if (px < -TILE*2 || py < -TILE*2 || px > canvas.width+TILE*2 || py > canvas.height+TILE*2) return;

    ctx.save();
    ctx.translate(px, py);

    // Shadow
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.beginPath();
    ctx.ellipse(0, e.size, e.size*0.7, 3, 0, 0, Math.PI*2);
    ctx.fill();

    // Hit flash
    const color = e.hitTimer > 0 ? '#fff' : e.color;
    ctx.fillStyle = color;

    if (e.type === 'boss') {
        // Boss - larger, menacing shape
        ctx.beginPath();
        ctx.arc(0, 0, e.size, 0, Math.PI*2);
        ctx.fill();
        // Eyes
        ctx.fillStyle = '#ff0';
        ctx.fillRect(-8,-4,5,5);
        ctx.fillRect(3,-4,5,5);
        // Horns
        ctx.strokeStyle = color;
        ctx.lineWidth = 3;
        ctx.beginPath();ctx.moveTo(-12,-e.size+5);ctx.lineTo(-16,-e.size-8);ctx.stroke();
        ctx.beginPath();ctx.moveTo(12,-e.size+5);ctx.lineTo(16,-e.size-8);ctx.stroke();
    } else if (e.type === 'slime') {
        // Slime - blob shape
        const squish = Math.sin(game.time*0.005)*2;
        ctx.beginPath();
        ctx.ellipse(0, 0, e.size+1, e.size-squish, 0, 0, Math.PI*2);
        ctx.fill();
        // Eyes
        ctx.fillStyle = '#fff';
        ctx.fillRect(-4,-3,3,3);ctx.fillRect(1,-3,3,3);
    } else if (e.type === 'bat') {
        // Bat - wings
        const wingFlap = Math.sin(game.time*0.01)*5;
        ctx.beginPath();
        ctx.arc(0, 0, e.size*0.6, 0, Math.PI*2);
        ctx.fill();
        ctx.fillStyle = color;
        ctx.fillRect(-e.size-3+wingFlap, -2, 8, 4);
        ctx.fillRect(e.size-5-wingFlap, -2, 8, 4);
    } else if (e.type === 'skeleton') {
        // Skeleton - bony shape
        ctx.fillRect(-6,-10,12,20);
        ctx.beginPath();ctx.arc(0,-12,7,0,Math.PI*2);ctx.fill();
        ctx.fillStyle = '#333';
        ctx.fillRect(-3,-13,2,2);ctx.fillRect(1,-13,2,2);
    } else if (e.type === 'ghost') {
        // Ghost - translucent
        ctx.globalAlpha = 0.7;
        ctx.beginPath();
        ctx.arc(0, -4, e.size, Math.PI, 0);
        ctx.lineTo(e.size, 8);
        for(let i=0;i<5;i++){
            const gx=e.size-i*(e.size*2/4);
            ctx.quadraticCurveTo(gx-e.size/10, 2+Math.sin(game.time*0.003+i)*3, gx-e.size/5, 8);
        }
        ctx.fill();
        ctx.fillStyle = '#fff';
        ctx.fillRect(-4,-6,3,3);ctx.fillRect(1,-6,3,3);
    } else if (e.type === 'demon') {
        // Demon - red with spikes
        ctx.beginPath();ctx.arc(0,0,e.size,0,Math.PI*2);ctx.fill();
        for(let i=0;i<8;i++){
            const a=i*Math.PI/4;
            ctx.beginPath();
            ctx.moveTo(Math.cos(a)*e.size, Math.sin(a)*e.size);
            ctx.lineTo(Math.cos(a)*(e.size+6), Math.sin(a)*(e.size+6));
            ctx.strokeStyle=color;ctx.lineWidth=2;ctx.stroke();
        }
        ctx.fillStyle='#ff0';
        ctx.fillRect(-5,-3,4,4);ctx.fillRect(1,-3,4,4);
    }

    // HP bar for enemies with significant damage taken
    if (e.hp < e.maxHp) {
        const bw = 24;
        ctx.fillStyle = 'rgba(0,0,0,0.5)';
        ctx.fillRect(-bw/2, -e.size-8, bw, 4);
        ctx.fillStyle = '#f44';
        ctx.fillRect(-bw/2, -e.size-8, bw*(e.hp/e.maxHp), 4);
    }

    ctx.restore();
}

function drawItem(item) {
    const px = item.x - game.camera.x;
    const py = item.y - game.camera.y;
    if (px < -TILE || py < -TILE || px > canvas.width+TILE || py > canvas.height+TILE) return;

    const bob = Math.sin(game.time*0.004 + item.x)*3;

    ctx.save();
    ctx.translate(px, py+bob);

    // Glow
    ctx.fillStyle = 'rgba(255,215,0,0.2)';
    ctx.beginPath();
    ctx.arc(0, 0, 14, 0, Math.PI*2);
    ctx.fill();

    if (item.gold) {
        // Gold coin
        ctx.fillStyle = '#ffd700';
        ctx.beginPath();
        ctx.arc(0, 0, 6, 0, Math.PI*2);
        ctx.fill();
        ctx.fillStyle = '#aa8800';
        ctx.font = '10px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('$', 0, 4);
    } else {
        // Item icon
        ctx.font = '20px serif';
        ctx.textAlign = 'center';
        ctx.fillText(item.icon || '?', 0, 7);
    }

    ctx.restore();
}

// --- Camera ---
function updateCamera() {
    const p = game.player;
    const targetX = p.x - canvas.width/2;
    const targetY = p.y - canvas.height/2;
    game.camera.x += (targetX - game.camera.x) * 0.1;
    game.camera.y += (targetY - game.camera.y) * 0.1;

    // Clamp to world bounds
    game.camera.x = Math.max(0, Math.min(game.world.W*TILE - canvas.width, game.camera.x));
    game.camera.y = Math.max(0, Math.min(game.world.H*TILE - canvas.height, game.camera.y));
}

// --- Main Game Loop ---
let lastTime = 0;
function gameLoop(timestamp) {
    if (!game || game.gameOver) {
        requestAnimationFrame(gameLoop);
        return;
    }

    const dt = Math.min(timestamp - lastTime, 50);
    lastTime = timestamp;
    game.time = timestamp;

    // Update
    updatePlayer(dt);
    updateEnemies(dt);
    updateItems();
    updateParticles();
    updateCamera();

    // Render
    ctx.fillStyle = '#1a1420';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const camX = game.camera.x;
    const camY = game.camera.y;

    // Draw visible tiles
    const startTX = Math.max(0, Math.floor(camX/TILE)-1);
    const endTX = Math.min(game.world.W, Math.ceil((camX+canvas.width)/TILE)+1);
    const startTY = Math.max(0, Math.floor(camY/TILE)-1);
    const endTY = Math.min(game.world.H, Math.ceil((camY+canvas.height)/TILE)+1);

    for (let y = startTY; y < endTY; y++) {
        for (let x = startTX; x < endTX; x++) {
            drawTile(x, y, game.world.map[y][x]);
        }
    }

    // Draw items
    for (const item of game.items) drawItem(item);

    // Draw enemies
    for (const e of game.enemies) drawEnemy(e);

    // Draw player
    drawPlayer();

    // Draw particles
    drawParticles(camX, camY);

    // Vignette effect
    const gradient = ctx.createRadialGradient(canvas.width/2, canvas.height/2, canvas.width*0.3, canvas.width/2, canvas.height/2, canvas.width*0.7);
    gradient.addColorStop(0, 'transparent');
    gradient.addColorStop(1, 'rgba(0,0,0,0.4)');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Minimap
    const mmW = 160, mmH = 120;
    const mmX = canvas.width - mmW - 16;
    const mmY = canvas.height - mmH - 36;
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.fillRect(mmX-2, mmY-2, mmW+4, mmH+4);
    ctx.strokeStyle = 'rgba(232,213,183,0.3)';
    ctx.strokeRect(mmX-2, mmY-2, mmW+4, mmH+4);

    const scaleX = mmW / (game.world.W * TILE);
    const scaleY = mmH / (game.world.H * TILE);

    // Draw explored area dots
    for (const e of game.enemies) {
        if (!e.alive) continue;
        ctx.fillStyle = '#f44';
        ctx.fillRect(mmX + e.x*scaleX, mmY + e.y*scaleY, 2, 2);
    }

    // Player dot
    ctx.fillStyle = '#4f4';
    ctx.beginPath();
    ctx.arc(mmX + game.player.x*scaleX, mmY + game.player.y*scaleY, 3, 0, Math.PI*2);
    ctx.fill();

    // Progress bar (subtle)
    const pbW = 120;
    const pbX = canvas.width/2 - pbW/2;
    const pbY = canvas.height - 24;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(pbX-2, pbY-2, pbW+4, 10);
    ctx.fillStyle = '#3a6';
    ctx.fillRect(pbX, pbY, pbW*(game.gameProgress/100), 6);
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.font = '8px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(`Progress: ${Math.floor(game.gameProgress)}%`, canvas.width/2, pbY+5);

    requestAnimationFrame(gameLoop);
}

// --- Check for existing save on load ---
window.addEventListener('load', () => {
    if (localStorage.getItem('realmOfEchoes')) {
        document.getElementById('load-btn').style.display = '';
    }
});

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

requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"0082b24b-5e61-45ec-b50c-ce6563287d67","tokensIn":501832,"tokensOut":23096,"tokensTotal":524928,"turns":16,"toolCalls":15,"failedToolCalls":1,"timestamp":"2026-07-18T22:10:46.488Z"} -->