← RPG results

RPG

inkling · full

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 39,844 bytes · SHA-256 0389133070bd
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Shard of Eternity — Action RPG</title>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700&family=Cinzel:wght@600&family=Merriweather:ital,wght@0,300;0,400;1,400&display=swap');

    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      background: #0b0c10;
      color: #e8e6e3;
      font-family: 'Merriweather', Georgia, serif;
      overflow: hidden;
      height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    #gameWrap {
      position: relative;
      width: 1024px;
      height: 720px;
      max-width: 100vw;
      max-height: 100vh;
      background: linear-gradient(135deg, #0f1115 0%, #181a20 60%, #22242a 100%);
      border: 2px solid #333;
      border-radius: 14px;
      overflow: hidden;
      box-shadow: 0 0 60px rgba(0,0,0,0.9), inset 0 0 120px rgba(200,180,120,0.03);
    }

    canvas { display: block; width: 100%; height: 100%; }

    #hud {
      position: absolute;
      top: 0; left: 0; right: 0;
      height: 72px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 22px;
      pointer-events: none;
      z-index: 10;
      background: linear-gradient(to bottom, rgba(10,11,16,0.92) 0%, rgba(10,11,16,0.45) 70%, transparent);
    }

    .hud-group { display: flex; align-items: center; gap: 16px; }

    .hearts {
      display: flex; gap: 4px; align-items: center;
    }
    .heart {
      width: 22px; height: 20px;
      position: relative;
      filter: drop-shadow(0 0 3px rgba(255,60,90,0.4));
    }
    .heart svg { display: block; width: 100%; height: 100%; }
    .heart.full path { fill: #ff3b5a; stroke: #c0203a; stroke-width: 1; }
    .heart.half path { fill: url(#halfGrad); stroke: #ff8a9e; stroke-width: 1; }
    .heart.empty path { fill: #2a2a32; stroke: #444; stroke-width: 1; }

    .money {
      display: flex; align-items: center; gap: 6px;
      font-family: 'Cinzel', serif; font-size: 16px; color: #f0c040;
      letter-spacing: 1px; text-shadow: 0 0 6px rgba(240,192,64,0.3);
    }
    .money-icon {
      width: 18px; height: 18px; border-radius: 50%;
      background: radial-gradient(circle at 35% 30%, #ffe066 0%, #d4a01a 60%, #7a5a10 100%);
      box-shadow: inset -2px -2px 3px rgba(0,0,0,0.4), 0 0 4px rgba(240,192,64,0.3);
    }

    .weapon-badge {
      display: flex; align-items: center; gap: 6px;
      font-family: 'Cinzel', serif; font-size: 13px; color: #c0b0a0;
      background: rgba(255,255,255,0.05); padding: 4px 10px; border-radius: 20px;
      border: 1px solid rgba(255,255,255,0.08);
    }
    .weapon-icon { width: 16px; height: 16px; border-radius: 2px; background: #ccc; box-shadow: 0 0 4px rgba(255,255,255,0.3); }

    #overlay {
      position: absolute; inset: 0; z-index: 20;
      display: flex; align-items: center; justify-content: center;
      pointer-events: none;
    }

    #startScreen, #gameOverScreen, #shopUI {
      pointer-events: auto;
      background: rgba(10,12,18,0.92);
      border: 2px solid rgba(200,180,130,0.2);
      border-radius: 16px;
      padding: 48px 64px;
      max-width: 520px;
      text-align: center;
      box-shadow: 0 0 80px rgba(0,0,0,0.6), inset 0 0 40px rgba(200,180,120,0.05);
    }

    #startScreen h1 {
      font-family: 'Cinzel Decorative', serif; font-size: 42px; color: #e8dcc8;
      text-shadow: 0 0 30px rgba(200,180,120,0.35), 0 2px 8px rgba(0,0,0,0.9);
      margin-bottom: 12px; letter-spacing: 3px;
    }
    #startScreen .subtitle {
      font-family: 'Cinzel', serif; font-size: 16px; color: #b0a898;
      margin-bottom: 28px; letter-spacing: 2px;
    }
    #startScreen p { color: #aaa8a0; line-height: 1.7; margin-bottom: 24px; font-size: 15px; }
    #startScreen .controls { color: #a89f88; font-size: 13px; margin-bottom: 28px; line-height: 1.6; }
    .btn {
      display: inline-block; padding: 14px 36px; border-radius: 8px;
      background: linear-gradient(180deg, #c0a460 0%, #8a6840 100%);
      color: #1a150e; font-family: 'Cinzel', serif; font-weight: 700; font-size: 18px;
      border: none; cursor: pointer; letter-spacing: 2px; box-shadow: 0 4px 20px rgba(200,160,80,0.25), 0 6px 0 #5a4428;
      transition: transform .1s, box-shadow .2s; text-shadow: 0 1px 1px rgba(255,255,255,0.15);
    }
    .btn:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(200,160,80,0.35), 0 9px 0 #5a4428; }
    .btn:active { transform: translateY(2px); box-shadow: 0 2px 8px rgba(200,160,80,0.2), 0 3px 0 #5a4428; }

    #gameOverScreen h2 {
      font-family: 'Cinzel Decorative', serif; font-size: 36px; color: #e8c8c0;
      text-shadow: 0 0 20px rgba(240,120,120,0.3); margin-bottom: 16px;
    }
    #gameOverScreen p { color: #b8a898; font-size: 16px; margin-bottom: 24px; }

    #shopUI { pointer-events: auto; display: none; }
    #shopUI h2 { font-family: 'Cinzel Decorative', serif; font-size: 28px; color: #c0b0a0; margin-bottom: 20px; text-shadow: 0 0 16px rgba(200,180,150,0.2); }
    #shopUI .items { display: flex; flex-direction: column; gap: 10px; width: 100%; }
    .shop-item {
      display: flex; align-items: center; gap: 14px; padding: 12px 16px;
      background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
      border-radius: 10px; text-align: left; transition: background .15s;
    }
    .shop-item:hover { background: rgba(255,255,255,0.08); }
    .shop-item .icon { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 18px; box-shadow: 0 0 8px rgba(0,0,0,0.4); }
    .shop-item .info { flex: 1; }
    .shop-item .name { font-family: 'Cinzel', serif; font-size: 15px; color: #e0dcc8; }
    .shop-item .desc { font-size: 12px; color: #9a9588; }
    .shop-item .price { font-family: 'Cinzel', serif; color: #f0c040; font-weight: 600; min-width: 70px; text-align: right; }
    #shopUI .close-btn { margin-top: 20px; background: linear-gradient(180deg, #6a7a8a 0%, #3a4555 100%); color: #d8d8d8; padding: 10px 28px; font-family: 'Cinzel', serif; border-radius: 6px; border: none; cursor: pointer; box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
    #shopUI .close-btn:hover { transform: translateY(-1px); }

    .damage-float {
      position: absolute; pointer-events: none; z-index: 30;
      font-family: 'Cinzel', serif; font-weight: 700; font-size: 18px;
      color: #ff6b6b; text-shadow: 0 0 4px rgba(255,0,0,0.4), 1px 1px 0 rgba(0,0,0,0.6);
      animation: floatUp .8s ease-out forwards;
    }
    @keyframes floatUp {
      0% { opacity: 1; transform: translateY(0) scale(1); }
      100% { opacity: 0; transform: translateY(-30px) scale(0.8); }
    }

    #saveMsg {
      position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
      background: rgba(200,160,80,0.15); border: 1px solid rgba(200,160,80,0.3);
      padding: 8px 20px; border-radius: 20px; color: #e8c898; font-family: 'Cinzel', serif; font-size: 14px; pointer-events: none; opacity: 0; transition: opacity .3s; z-index: 25;
    }

    #inventoryBar {
      position: absolute; bottom: 0; left: 0; right: 0;
      height: 64px; background: linear-gradient(to top, rgba(10,11,16,0.95) 0%, rgba(10,11,16,0.4) 100%);
      display: flex; align-items: center; justify-content: center; gap: 14px; padding: 0 24px; z-index: 10;
    }
    .inv-slot {
      width: 48px; height: 48px; border-radius: 10px; border: 2px solid rgba(255,255,255,0.1);
      background: rgba(255,255,255,0.03); display: flex; align-items: center; justify-content: center; position: relative;
      transition: border-color .15s, background .15s;
    }
    .inv-slot.active { border-color: rgba(240,192,64,0.5); background: rgba(240,192,64,0.08); box-shadow: 0 0 8px rgba(240,192,64,0.15); }
    .inv-slot .qty { position: absolute; bottom: 2px; right: 4px; font-size: 9px; color: #aaa; font-family: 'Cinzel', serif; }
    .inv-icon { width: 28px; height: 28px; border-radius: 50%; box-shadow: 0 0 6px rgba(0,0,0,0.5); }

    /* Particle canvas overlay for effects */
    #fxCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; }
  </style>
</head>
<body>

<div id="gameWrap">
  <canvas id="world"></canvas>
  <canvas id="fxCanvas"></canvas>

  <div id="hud">
    <div class="hud-group">
      <div class="hearts" id="hearts">
        <!-- Hearts generated by JS -->
      </div>
    </div>
    <div class="hud-group" style="justify-content:center; flex:1; justify-content:center;">
      <div class="weapon-badge" id="weaponBadge">
        <div class="weapon-icon" id="weaponIcon"></div>
        <span id="weaponName">Iron Blade</span>
      </div>
    </div>
    <div class="hud-group">
      <div class="money"><div class="money-icon"></div><span id="moneyText">0 G</span></div>
    </div>
  </div>

  <div id="inventoryBar">
    <!-- Slots generated -->
  </div>

  <div id="overlay">
    <div id="startScreen">
      <h1>Shard of Eternity</h1>
      <div class="subtitle">A Short Action Adventure</div>
      <p>
        Deep within the forgotten ruins, a crystal shard pulses with ancient light.<br>
        Equip your blade, grow stronger, and reclaim the lost treasure.
      </p>
      <div class="controls">
        <b>WASD / Arrow Keys</b> — Move<br>
        <b>Left Click / J</b> — Attack<br>
        <b>Right Click / K</b> — Ranged (mid-game)<br>
        <b>Space / Enter</b> — Interact (shop, save, pickup)<br>
        <b>1-5</b> — Use consumable
      </div>
      <button class="btn" onclick="startGame()">Begin Journey</button>
    </div>

    <div id="shopUI">
      <h2>Merchant of the Ruin</h2>
      <div class="items" id="shopItems"></div>
      <button class="close-btn" onclick="closeShop()">Leave</button>
    </div>

    <div id="gameOverScreen" style="display:none;">
      <h2>Fallen Hero</h2>
      <p>You have fallen in the ruins. Your spirit lingers, but the shard remains.<br>Try again — stronger this time.</p>
      <button class="btn" onclick="startGame()">Respawn</button>
    </div>
  </div>

  <div id="saveMsg">Progress saved to crystal.</div>
</div>

<svg style="position:absolute; width:0; height:0; overflow:hidden;" aria-hidden="true">
  <defs>
    <linearGradient id="halfGrad" x1="0" y1="0" x2="1" y2="0">
      <stop offset="50%" stop-color="#ff3b5a"/>
      <stop offset="50%" stop-color="#2a2a32"/>
    </linearGradient>
  </defs>
</svg>

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

  function playSound(type, options={}) {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    const now = audioCtx.currentTime;
    const freq = options.freq || 400;
    osc.type = options.type || 'triangle';
    osc.frequency.setValueAtTime(freq, now);
    if (options.sweep) osc.frequency.exponentialRampToValueAtTime(options.sweep, now + 0.1);
    gain.gain.setValueAtTime(options.vol || 0.06, now);
    gain.gain.exponentialRampToValueAtTime(0.001, now + options.dur || 0.15);
    osc.start(now);
    osc.stop(now + (options.dur || 0.15));
  }

  function soundAttack() { playSound('attack', { freq: 500, type: 'square', vol: 0.06, dur: 0.12 }); }
  function soundHit() { playSound('hit', { freq: 220, type: 'sine', vol: 0.07, dur: 0.2, sweep: 80 }); }
  function soundPickup() { playSound('pickup', { freq: 700, type: 'triangle', vol: 0.05, dur: 0.25, sweep: 1200 }); playSound('pickup2', { freq: 950, type: 'triangle', vol: 0.03, dur: 0.3, sweep: 1600 }); }
  function soundDeath() { playSound('death', { freq: 300, type: 'sawtooth', vol: 0.08, dur: 0.5, sweep: 50 }); }
  function soundSave() { playSound('save', { freq: 900, type: 'sine', vol: 0.05, dur: 0.3, sweep: 1400 }); }
  function soundStep() { playSound('step', { freq: 150, type: 'sine', vol: 0.015, dur: 0.08 }); }
  function soundShop() { playSound('shop', { freq: 600, type: 'triangle', vol: 0.05, dur: 0.4, sweep: 400 }); }
  function soundUpgrade() { playSound('upgrade', { freq: 550, type: 'triangle', vol: 0.07, dur: 0.35, sweep: 1100 }); playSound('upgrade2', { freq: 880, type: 'triangle', vol: 0.05, dur: 0.35, sweep: 1400 }); }
  function soundBow() { playSound('bow', { freq: 650, type: 'sine', vol: 0.06, dur: 0.2, sweep: 300 }); }

  // ======================= GAME STATE =======================
  const canvas = document.getElementById('world');
  const ctx = canvas.getContext('2d');
  const fxCanvas = document.getElementById('fxCanvas');
  const fxCtx = fxCanvas.getContext('2d');
  let W = 1024, H = 720;

  let gameState = 'title'; // title, playing, dead
  let keys = {};
  let mouse = { x: 0, y: 0, down: false };

  // Player
  const player = {
    x: 420, y: 340,
    vx: 0, vy: 0,
    speed: 2.4,
    health: 3,
    maxHealth: 3,
    healthTarget: 3,
    money: 0,
    weaponLevel: 0, // 0 basic, 1 upgraded (steel), 2 master (void)
    rangedUnlocked: false,
    inventory: [],
    facing: 1,
    invuln: 0,
    stepTimer: 0,
    attackCooldown: 0,
    attackAnim: 0,
  };

  // Weapon definitions
  const weapons = [
    { name: "Iron Blade", dmg: 8, speed: 0.55, range: 40, iconColor: "#aaa", desc: "Basic sword" },
    { name: "Steel Edge", dmg: 16, speed: 0.40, range: 55, iconColor: "#7cbcf0", desc: "Upgraded blade" },
    { name: "Void Scythe", dmg: 28, speed: 0.30, range: 70, iconColor: "#c080f0", desc: "Master weapon" },
  ];

  // Inventory items (>=5 distinct)
  const itemDefs = [
    { id: 'potion', name: 'Health Potion', desc: 'Restores 2 HP', icon: '#ff6b6b', qty: 1, price: 30, use: () => { if (player.health < player.maxHealth) { player.health = Math.min(player.maxHealth, player.health + 2); spawnText('+2 HP', player.x, player.y - 20, '#6bff6b'); soundPickup(); } } },
    { id: 'shield', name: 'Crystal Shield', desc: 'Blocks next hit', icon: '#6bbfff', qty: 1, price: 50, passive: true, effect: () => { /* handled by shield flag */ } },
    { id: 'ring', name: 'Swift Ring', desc: '+15% speed', icon: '#ffe066', qty: 1, price: 40, passive: true },
    { id: 'crystal', name: 'Power Crystal', desc: '+3 damage (temp)', icon: '#c080f0', qty: 2, price: 60, use: () => { player.tempDmg = (player.tempDmg||0) + 3; spawnText('+3 DMG', player.x, player.y - 20, '#c080f0'); soundPickup(); } },
    { id: 'heartshard', name: 'Heart Shard', desc: '+1 Max Health', icon: '#ff3b5a', qty: 1, price: 0, use: () => { if (player.maxHealth < 5) { player.maxHealth = Math.min(5, player.maxHealth + 1); player.health = player.maxHealth; spawnText('Max HP +1!', player.x, player.y - 20, '#ff3b5a'); soundUpgrade(); } } },
    { id: 'orb', name: 'Gold Orb', desc: 'Worth 25 gold', icon: '#e8c050', qty: 3, price: 0, use: () => { player.money += 25; spawnText('+25 G', player.x, player.y - 20, '#ffe066'); soundPickup(); } },
  ];

  // World entities
  let entities = [];
  let particles = [];
  let textFloats = [];

  // Save points
  let savePoints = [
    { x: 200, y: 200, active: true },
    { x: 780, y: 420, active: false },
    { x: 600, y: 620, active: false },
  ];
  let lastSave = { x: 420, y: 340 };

  // Zones
  const zones = [
    { x: 0, y: 0, w: 340, h: 260, type: 'start' },
    { x: 340, y: 0, w: 280, h: 260, type: 'forest' },
    { x: 620, y: 0, w: 404, h: 260, type: 'ruin' },
    { x: 0, y: 260, w: 340, h: 460, type: 'shop_zone' },
    { x: 340, y: 260, w: 280, h: 460, type: 'mid' },
    { x: 620, y: 260, w: 404, h: 460, type: 'cave' },
  ];

  // Shop position
  let shopPos = { x: 450, y: 500 };

  // ======================= SETUP =======================
  function resize() {
    const wrap = document.getElementById('gameWrap');
    const rect = wrap.getBoundingClientRect();
    W = rect.width; H = rect.height;
    canvas.width = W; canvas.height = H;
    fxCanvas.width = W; fxCanvas.height = H;
  }
  window.addEventListener('resize', resize);

  // ======================= DRAWING =======================
  function drawWorld() {
    // Background tiles
    const tileSize = 64;
    const rows = Math.ceil(W / tileSize) + 2;
    const cols = Math.ceil(H / tileSize) + 2;
    const offX = (player.x - W/2) % tileSize;
    const offY = (player.y - H/2) % tileSize;

    for (let r = 0; r < cols; r++) {
      for (let c = 0; c < rows; c++) {
        const x = c * tileSize - offX - tileSize;
        const y = r * tileSize - offY - tileSize;

        // Determine zone color
        let color = '#161820';
        if (y + H/2 > 260) color = '#1a1e2a';
        else if (x + W/2 > 620) color = '#181a22';
        else if (x + W/2 > 340) color = '#1a1d24';

        // Subtle grid
        ctx.fillStyle = color;
        ctx.fillRect(x, y, tileSize, tileSize);
        ctx.strokeStyle = 'rgba(255,255,255,0.015)';
        ctx.strokeRect(x, y, tileSize, tileSize);
      }
    }

    // Save crystals
    savePoints.forEach(s => {
      const sx = s.x - player.x + W/2;
      const sy = s.y - player.y + H/2;
      if (sx < -30 || sx > W+30 || sy < -30 || sy > H+30) return;
      ctx.save();
      ctx.translate(sx, sy);
      const glow = 15 + Math.sin(Date.now()/300)*5;
      ctx.shadowColor = s.active ? '#c0a460' : '#777';
      ctx.shadowBlur = glow;
      ctx.fillStyle = s.active ? '#e8c898' : '#888';
      ctx.beginPath();
      // Crystal shape
      for (let i = 0; i < 6; i++) {
        const a = (i/6)*Math.PI*2;
        const r = 18 + (i%2 ? 6 : 2);
        const px = Math.cos(a) * r, py = Math.sin(a) * r;
        if (i === 0) ctx.moveTo(px, -py);
        else ctx.lineTo(px, -py);
      }
      ctx.closePath();
      ctx.fill();
      ctx.strokeStyle = 'rgba(255,255,255,0.3)';
      ctx.stroke();
      ctx.restore();
      // Label
      ctx.fillStyle = '#aaa';
      ctx.font = '10px Cinzel, serif';
      ctx.textAlign = 'center';
      ctx.fillText('SAVE', sx, sy + 36);
    });

    // Shop stall
    const shopX = shopPos.x - player.x + W/2;
    const shopY = shopPos.y - player.y + H/2;
    if (shopX > -60 && shopX < W+60 && shopY > -60 && shopY < H+60) {
      // Stall base
      ctx.save();
      ctx.translate(shopX, shopY);
      ctx.shadowColor = '#8a6840';
      ctx.shadowBlur = 20;
      // Roof
      ctx.fillStyle = '#5a4428';
      ctx.beginPath();
      ctx.moveTo(-40, -10); ctx.lineTo(40, -10); ctx.lineTo(50, -30); ctx.lineTo(-50, -30); ctx.closePath();
      ctx.fill();
      // Pole
      ctx.fillStyle = '#8a6840';
      ctx.fillRect(-4, -10, 8, 30);
      // Banner
      ctx.fillStyle = '#d4a050';
      ctx.fillRect(-35, -28, 70, 8);
      ctx.fillStyle = '#3a2810';
      ctx.font = '10px Cinzel, serif';
      ctx.textAlign = 'center';
      ctx.fillText('SHOP', 0, -20);
      ctx.restore();
    }

    // Enemies
    entities.forEach(e => {
      if (e.type === 'enemy') {
        const ex = e.x - player.x + W/2;
        const ey = e.y - player.y + H/2;
        if (ex < -40 || ex > W+40 || ey < -40 || ey > H+40) return;
        ctx.save();
        ctx.translate(ex, ey);
        ctx.rotate(Math.PI/8 * Math.sin(Date.now()/200));
        // Body
        ctx.fillStyle = e.hp > 15 ? '#4a2a1a' : '#8a4a3a';
        ctx.beginPath();
        ctx.arc(0, 0, 16 + e.hp/3, 0, Math.PI*2);
        ctx.fill();
        // Eyes (glowing)
        ctx.fillStyle = e.hp > 10 ? '#ff5050' : '#ff0000';
        ctx.shadowColor = '#ff3030';
        ctx.shadowBlur = 8;
        ctx.beginPath(); ctx.arc(-6, -3, 3, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(6, -3, 3, 0, Math.PI*2); ctx.fill();
        ctx.shadowBlur = 0;
        // Health bar
        ctx.fillStyle = '#2a1a0a';
        ctx.fillRect(-18, -22, 36, 4);
        ctx.fillStyle = e.hp > 5 ? '#e06040' : '#ff3030';
        ctx.fillRect(-18, -22, 36 * (e.hp / e.maxHp), 4);
        ctx.restore();
      } else if (e.type === 'drop') {
        const dx = e.x - player.x + W/2;
        const dy = e.y - player.y + H/2;
        if (dx < -20 || dx > W+20 || dy < -20 || dy > H+20) return;
        ctx.save();
        ctx.translate(dx, dy);
        ctx.shadowColor = e.itemColor || '#ffe066';
        ctx.shadowBlur = 10;
        ctx.fillStyle = e.itemColor || '#ffe066';
        ctx.beginPath();
        ctx.arc(0, 0, 10, 0, Math.PI*2);
        ctx.fill();
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 1;
        ctx.stroke();
        ctx.restore();
      }
    });

    // Projectiles
    entities.forEach(e => {
      if (e.type === 'projectile') {
        const px = e.x - player.x + W/2;
        const py = e.y - player.y + H/2;
        ctx.save();
        ctx.translate(px, py);
        ctx.fillStyle = '#a0d0ff';
        ctx.shadowColor = '#80c0ff';
        ctx.shadowBlur = 6;
        ctx.beginPath();
        ctx.arc(0, 0, 5, 0, Math.PI*2);
        ctx.fill();
        ctx.restore();
      }
    });

    // Player
    const px = W/2; const py = H/2;
    ctx.save();
    ctx.translate(px, py);

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

    // Weapon
    const wData = weapons[player.weaponLevel];
    const angle = Math.atan2(mouse.y - py, mouse.x - px);
    const reach = wData.range;
    ctx.save();
    ctx.rotate(angle);
    // Blade
    const bladeGrad = ctx.createLinearGradient(0, 0, reach, -5);
    bladeGrad.addColorStop(0, '#ddd');
    bladeGrad.addColorStop(0.4, wData.iconColor);
    bladeGrad.addColorStop(1, '#fff');
    ctx.fillStyle = bladeGrad;
    ctx.fillRect(0, -4, reach, 8);
    ctx.fillStyle = '#fff';
    ctx.fillRect(reach - 8, -3, 8, 6);
    // Swing indicator on attack
    if (player.attackAnim > 0) {
      ctx.fillStyle = 'rgba(255,255,255,0.15)';
      ctx.beginPath();
      ctx.arc(reach/2, 0, reach*0.6, -0.6, 0.6);
      ctx.fill();
      player.attackAnim -= 0.08;
    }
    ctx.restore();

    // Body
    ctx.fillStyle = '#d0c8b8';
    ctx.beginPath();
    ctx.arc(0, 0, 14, 0, Math.PI*2);
    ctx.fill();
    // Head
    ctx.fillStyle = '#e8dcc8';
    ctx.beginPath();
    ctx.arc(0, -6, 10, 0, Math.PI*2);
    ctx.fill();
    // Eyes
    ctx.fillStyle = '#333';
    const eyeDir = Math.sign(Math.cos(angle)) || 1;
    ctx.beginPath();
    ctx.arc(3 * eyeDir, -7, 2, 0, Math.PI*2);
    ctx.fill();
    // Armor glow if shield
    if (player.inventory.some(i => i.id === 'shield' && i.passive)) {
      ctx.strokeStyle = 'rgba(100,160,255,0.35)';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.arc(0, 0, 19, 0, Math.PI*2);
      ctx.stroke();
    }

    ctx.restore();

    // Particles
    particles.forEach(p => {
      p.x += p.vx; p.y += p.vy;
      p.life -= p.decay;
      if (p.life <= 0) return;
      const px = p.x - player.x + W/2;
      const py = p.y - player.y + H/2;
      ctx.save();
      ctx.translate(px, py);
      ctx.globalAlpha = p.life / p.maxLife;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(0, 0, p.size, 0, Math.PI*2);
      ctx.fill();
      ctx.restore();
    });
    particles = particles.filter(p => p.life > 0);

    // Floating texts
    textFloats.forEach(t => {
      t.y -= 0.6; t.life -= 0.015;
      if (t.life <= 0) return;
      const tx = t.x - player.x + W/2;
      const ty = t.y - player.y + H/2;
      ctx.save();
      ctx.globalAlpha = t.life;
      ctx.fillStyle = t.color;
      ctx.font = 'bold 14px Cinzel, serif';
      ctx.textAlign = 'center';
      ctx.fillText(t.text, tx, ty);
      ctx.restore();
    });
    textFloats = textFloats.filter(t => t.life > 0);
  }

  // ======================= UPDATE =======================
  function spawnText(text, x, y, color) {
    textFloats.push({ text, x, y, color: color || '#fff', life: 1 });
  }

  function spawnParticles(x, y, count, color) {
    for (let i = 0; i < count; i++) {
      const a = Math.random() * Math.PI * 2;
      particles.push({
        x, y,
        vx: Math.cos(a) * (1 + Math.random()*2),
        vy: Math.sin(a) * (1 + Math.random()*2),
        life: 1,
        maxLife: 1,
        decay: 0.02 + Math.random()*0.02,
        size: 2 + Math.random()*3,
        color
      });
    }
  }

  function spawnDrop(x, y) {
    // Random drop
    const roll = Math.random();
    let dropItem = null;
    if (roll < 0.1) dropItem = { id: 'potion', ...itemDefs.find(i => i.id === 'potion') };
    else if (roll < 0.2) dropItem = { id: 'crystal', ...itemDefs.find(i => i.id === 'crystal') };
    else if (roll < 0.35) dropItem = { id: 'shield', ...itemDefs.find(i => i.id === 'shield') };
    else if (roll < 0.45) dropItem = { id: 'ring', ...itemDefs.find(i => i.id === 'ring') };
    else {
      dropItem = { id: 'orb', ...itemDefs.find(i => i.id === 'orb') };
    }
    entities.push({ type: 'drop', x, y, item: dropItem, itemColor: dropItem.icon, life: 300 });
  }

  function spawnEnemy(x, y, hp = 20) {
    entities.push({ type: 'enemy', x, y, hp, maxHp: hp, vx: 0, vy: 0, speed: 0.8, timer: 0 });
  }

  function update() {
    if (gameState !== 'playing') return;

    // Player movement
    let dx = 0, dy = 0;
    if (keys['w'] || keys['arrowup']) dy -= 1;
    if (keys['s'] || keys['arrowdown']) dy += 1;
    if (keys['a'] || keys['arrowleft']) dx -= 1;
    if (keys['d'] || keys['arrowright']) dx += 1;
    const len = Math.hypot(dx, dy);
    if (len > 0) { dx /= len; dy /= len; }
    const boost = player.inventory.some(i => i.passive && i.id === 'ring') ? 1.15 : 1;
    player.vx = dx * player.speed * boost;
    player.vy = dy * player.speed * boost;
    player.x += player.vx;
    player.y += player.vy;
    if (player.x < 20) player.x = 20;
    if (player.x > 960) player.x = 960;
    if (player.y < 20) player.y = 20;
    if (player.y > 680) player.y = 680;
    player.facing = dx !== 0 ? Math.sign(dx) : player.facing;

    // Step sound
    if (len > 0) {
      player.stepTimer--;
      if (player.stepTimer <= 0) {
        player.stepTimer = 15;
        soundStep();
      }
    }

    // Attack cooldown
    if (player.attackCooldown > 0) player.attackCooldown--;

    // Mouse attack
    if (mouse.down && player.attackCooldown <= 0) {
      player.attackCooldown = weapons[player.weaponLevel].speed * 60;
      player.attackAnim = 1;
      soundAttack();
      const ang = Math.atan2(mouse.y - H/2, mouse.x - W/2);
      const reach = weapons[player.weaponLevel].range;
      // Hit check
      entities.forEach(e => {
        if (e.type === 'enemy') {
          const dist = Math.hypot(e.x - player.x, e.y - player.y);
          const dir = Math.atan2(e.y - player.y, e.x - player.x);
          if (dist < reach + 16 && Math.abs(ang - dir) < 0.8) {
            const dmg = weapons[player.weaponLevel].dmg + (player.tempDmg || 0);
            e.hp -= dmg;
            spawnParticles(e.x, e.y, 8, '#f0a040');
            spawnText('-' + dmg, e.x, e.y - 20, '#ff8080');
            soundHit();
          }
        }
      });
    }

    // Ranged bow (right click / space when unlocked)
    if (keys['k'] || (mouse.right && player.rangedUnlocked)) {
      // This is handled by mouse.right event, but let's also allow K key
    }

    // Right click bow
    if (mouse.right && player.rangedUnlocked) {
      mouse.right = false; // consume
      soundBow();
      const ang = Math.atan2(mouse.y - H/2, mouse.x - W/2);
      entities.push({
        type: 'projectile',
        x: player.x, y: player.y,
        vx: Math.cos(ang) * 8,
        vy: Math.sin(ang) * 8,
        life: 120,
        ang: ang,
      });
    }

    // Update entities
    for (let i = entities.length - 1; i >= 0; i--) {
      const e = entities[i];
      if (e.type === 'enemy') {
        // Move toward player slowly
        const dx = player.x - e.x, dy = player.y - e.y;
        const dist = Math.hypot(dx, dy);
        if (dist > 12) {
          e.x += (dx / dist) * e.speed;
          e.y += (dy / dist) * e.speed;
        }
        // Damage player
        if (dist < 14 && player.invuln <= 0 && !player.inventory.some(i => i.passive && i.id === 'shield')) {
          player.health -= 1;
          player.invuln = 30;
          spawnParticles(player.x, player.y, 10, '#ff5050');
          spawnText('-1', player.x, player.y - 20, '#ff6060');
          soundHit();
          if (player.health <= 0) {
            soundDeath();
            gameState = 'dead';
            document.getElementById('gameOverScreen').style.display = 'block';
          }
        }
        // Enemy death
        if (e.hp <= 0) {
          spawnParticles(e.x, e.y, 14, '#ffa060');
          spawnDrop(e.x, e.y);
          entities.splice(i, 1);
        }
      } else if (e.type === 'drop') {
        e.life--;
        // Collect
        const dist = Math.hypot(e.x - player.x, e.y - player.y);
        if (dist < 22) {
          // Add to inventory
          const existing = player.inventory.find(i => i.id === e.item.id);
          if (existing) existing.qty = (existing.qty || 1) + 1;
          else player.inventory.push({ ...e.item, qty: 1 });
          player.money += (e.item.price || 0);
          soundPickup();
          spawnParticles(e.x, e.y, 10, '#ffe066');
          if (e.item.id === 'potion') spawnText('+Potion', e.x, e.y - 20, '#ff6b6b');
          entities.splice(i, 1);
        } else if (e.life <= 0) {
          entities.splice(i, 1);
        }
      } else if (e.type === 'projectile') {
        e.x += e.vx; e.y += e.vy;
        e.life--;
        // Hit enemy
        for (let j = entities.length - 1; j >= 0; j--) {
          const en = entities[j];
          if (en.type === 'enemy') {
            const dist = Math.hypot(e.x - en.x, e.y - en.y);
            if (dist < 14) {
              en.hp -= 10;
              spawnParticles(e.x, e.y, 6, '#a0d0ff');
              entities.splice(i, 1);
              if (en.hp <= 0) {
                spawnParticles(en.x, en.y, 14, '#ffa060');
                spawnDrop(en.x, en.y);
                entities.splice(j, 1);
              }
              break;
            }
          }
        }
        if (e.life <= 0) entities.splice(i, 1);
      }
    }

    // Player invulnerability timer
    if (player.invuln > 0) player.invuln--;

    // Check save points
    savePoints.forEach(s => {
      const d = Math.hypot(s.x - player.x, s.y - player.y);
      if (d < 30 && keys[' '] && s.active) {
        lastSave = { x: s.x, y: s.y };
        showSaveMsg();
        soundSave();
        keys[' '] = false;
      }
    });

    // Check shop interaction
    const dShop = Math.hypot(shopPos.x - player.x, shopPos.y - player.y);
    if (dShop < 40) {
      // Auto-show shop on space
      if (keys[' '] || keys['enter']) {
        openShop();
        keys[' '] = false; keys['enter'] = false;
      }
    } else {
      document.getElementById('shopUI').style.display = 'none';
    }

    // Progression triggers
    // Mid-game: unlock bow when reached ~mid-progression (e.g., 3 kills or reached certain zone)
    const kills = entities.filter(e => false); // not tracking kills directly, let's use zone entry
    // Simplified: unlock bow when player reaches x > 500 once
    if (!player.rangedUnlocked && player.x > 500) {
      player.rangedUnlocked = true;
      spawnText('Bow Unlocked!', player.x, player.y - 30, '#6bbfff');
      soundUpgrade();
    }

    // Weapon upgrade triggers
    // Upgrade 1 at 150 gold, Upgrade 2 at 300 gold + at least one item owned
    if (player.weaponLevel === 0 && player.money >= 120) {
      player.weaponLevel = 1;
      player.money -= 120;
      soundUpgrade();
      spawnText('Upgraded!', player.x, player.y - 30, '#c0b0ff');
      updateUI();
    }
    if (player.weaponLevel === 1 && player.money >= 250 && player.inventory.length > 0) {
      player.weaponLevel = 2;
      player.money -= 250;
      soundUpgrade();
      spawnParticles(player.x, player.y, 20, '#f0a080');
      spawnText('Master Blade!', player.x, player.y - 30, '#e8c0a0');
      updateUI();
    }
  }

  // ======================= SHOP =======================
  const shopInventory = [
    { itemId: 'potion', qty: 1, price: 30 },
    { itemId: 'shield', qty: 1, price: 50 },
    { itemId: 'ring', qty: 1, price: 40 },
    { itemId: 'crystal', qty: 1, price: 60 },
    { itemId: 'orb', qty: 1, price: 20 },
  ];

  function openShop() {
    initAudio();
    document.getElementById('shopUI').style.display = 'block';
    soundShop();
    const container = document.getElementById('shopItems');
    container.innerHTML = '';
    shopInventory.forEach(s => {
      const def = itemDefs.find(i => i.id === s.itemId);
      if (!def) return;
      const btn = document.createElement('button');
      btn.className = 'shop-item';
      btn.innerHTML = `<div class="icon" style="background:${def.icon}; color:#fff;">●</div>
        <div class="info"><div class="name">${def.name}</div><div class="desc">${def.desc}</div></div>
        <div class="price">${s.price} G</div>`;
      btn.onclick = () => {
        if (player.money >= s.price) {
          player.money -= s.price;
          const existing = player.inventory.find(i => i.id === def.id);
          if (existing) existing.qty = (existing.qty || 1) + 1;
          else player.inventory.push({ ...def, qty: 1 });
          soundPickup();
          spawnParticles(player.x, player.y, 10, '#ffe066');
          updateUI();
          btn.style.opacity = '0.5';
          btn.innerHTML = btn.innerHTML.replace(s.price + ' G', 'SOLD');
        }
      };
      container.appendChild(btn);
    });
  }
  function closeShop() { document.getElementById('shopUI').style.display = 'none'; }

  // ======================= UI =======================
  function updateUI() {
    // Hearts
    let html = '';
    const fullHearts = Math.floor(player.health / 2);
    const half = (player.health % 2) === 1;
    for (let i = 0; i < 3; i++) {
      if (i < fullHearts) html += `<div class="heart full"><svg viewBox="0 0 24 20"><path d="M12 19s-6-5.5-6-12a6 6 0 0 1 12 0c0 6.5-6 12-6 12z"/></svg></div>`;
      else if (i === fullHearts && half) html += `<div class="heart half"><svg viewBox="0 0 24 20"><path d="M12 19s-6-5.5-6-12a6 6 0 0 1 12 0c0 6.5-6 12-6 12z"/></svg></div>`;
      else html += `<div class="heart empty"><svg viewBox="0 0 24 20"><path d="M12 19s-6-5.5-6-12a6 6 0 0 1 12 0c0 6.5-6 12-6 12z"/></svg></div>`;
    }
    document.getElementById('hearts').innerHTML = html;

    // Money
    document.getElementById('moneyText').textContent = player.money + ' G';

    // Weapon
    const w = weapons[player.weaponLevel];
    document.getElementById('weaponName').textContent = w.name;
    document.getElementById('weaponIcon').style.background = w.iconColor;

    // Inventory bar
    const invBar = document.getElementById('inventoryBar');
    invBar.innerHTML = '';
    for (let i = 0; i < 5; i++) {
      const slot = document.createElement('div');
      slot.className = 'inv-slot' + (i === 0 ? ' active' : '');
      if (player.inventory[i]) {
        const item = player.inventory[i];
        slot.innerHTML = `<div class="inv-icon" style="background:${item.icon}; border:1px solid rgba(255,255,255,0.2);"></div>`;
        if (item.qty > 1) slot.innerHTML += `<div class="qty">${item.qty}</div>`;
      }
      invBar.appendChild(slot);
    }
  }

  function showSaveMsg() {
    const msg = document.getElementById('saveMsg');
    msg.style.opacity = '1';
    setTimeout(() => msg.style.opacity = '0', 2000);
  }

  // ======================= INPUT =======================
  window.addEventListener('keydown', e => {
    keys[e.key.toLowerCase()] = true;
    if (['1','2','3','4','5'].includes(e.key)) {
      const idx = parseInt(e.key) - 1;
      if (player.inventory[idx]) {
        player.inventory[idx].use();
        if (player.inventory[idx].qty <= 0 || (player.inventory[idx].qty === undefined)) {
          // Remove if no qty or qty depleted
          // Actually keep for consumables, reduce qty
          if (player.inventory[idx].qty > 1) player.inventory[idx].qty--;
          else player.inventory.splice(idx, 1);
          updateUI();
        }
        updateUI();
      }
    }
  });
  window.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
  window.addEventListener('mousemove', e => {
    const rect = canvas.getBoundingClientRect();
    mouse.x = e.clientX - rect.left;
    mouse.y = e.clientY - rect.top;
  });
  window.addEventListener('mousedown', e => {
    initAudio();
    mouse.down = true;
    if (e.button === 2) mouse.right = true;
  });
  window.addEventListener('mouseup', e => {
    mouse.down = false;
    mouse.right = false;
  });
  window.addEventListener('contextmenu', e => e.preventDefault());

  // ======================= INIT & LOOP =======================
  let lastTime = 0;
  function loop(timestamp) {
    const dt = timestamp - lastTime; lastTime = timestamp;
    update();
    drawWorld();
    requestAnimationFrame(loop);
  }

  function startGame() {
    initAudio();
    gameState = 'playing';
    document.getElementById('startScreen').style.display = 'none';
    document.getElementById('gameOverScreen').style.display = 'none';
    document.getElementById('shopUI').style.display = 'none';
    resize();
    // Reset state
    player.x = 420; player.y = 340;
    player.vx = 0; player.vy = 0;
    player.health = 3;
    player.maxHealth = 3; // Progresses to 5 through gameplay
    player.money = 0;
    player.weaponLevel = 0;
    player.rangedUnlocked = false;
    player.inventory = [];
    player.invuln = 0;
    player.attackCooldown = 0;
    player.attackAnim = 0;
    entities = [];
    particles = [];
    textFloats = [];

    // Place initial entities
    entities.push({ type: 'enemy', x: 250, y: 180, hp: 12, maxHp: 12, speed: 0.9, timer: 0 });
    entities.push({ type: 'enemy', x: 800, y: 150, hp: 15, maxHp: 15, speed: 0.7, timer: 0 });
    entities.push({ type: 'enemy', x: 500, y: 500, hp: 18, maxHp: 18, speed: 1.0, timer: 0 });
    entities.push({ type: 'enemy', x: 150, y: 550, hp: 10, maxHp: 10, speed: 1.1, timer: 0 });
    entities.push({ type: 'enemy', x: 850, y: 650, hp: 20, maxHp: 20, speed: 0.6, timer: 0 });

    // Place drops
    entities.push({ type: 'drop', x: 300, y: 120, item: itemDefs.find(i => i.id === 'potion'), itemColor: '#ff6b6b', life: 999 });
    entities.push({ type: 'drop', x: 650, y: 200, item: itemDefs.find(i => i.id === 'shield'), itemColor: '#6bbfff', life: 999 });
    entities.push({ type: 'drop', x: 550, y: 580, item: itemDefs.find(i => i.id === 'ring'), itemColor: '#ffe066', life: 999 });
    // Heart Shard for health progression (earn max health to reach 5)
    entities.push({ type: 'drop', x: 820, y: 300, item: itemDefs.find(i => i.id === 'heartshard'), itemColor: '#ff3b5a', life: 999 });

    updateUI();
    loop(0);
  }

  // Pre-render title
  resize();
  ctx.fillStyle = '#0b0c10';
  ctx.fillRect(0,0,W,H);
  // Title visual
  ctx.save();
  ctx.translate(W/2, H/2 - 40);
  ctx.fillStyle = '#e8dcc8';
  ctx.font = 'bold 52px "Cinzel Decorative", serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.shadowColor = '#c0a060';
  ctx.shadowBlur = 20;
  ctx.fillText('SHARD OF ETERNITY', 0, 0);
  ctx.restore();
  updateUI(); // Initialize empty health
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"f7f224a7-ff08-457b-a13c-dc262ee5c440","tokensIn":365252,"tokensOut":17021,"tokensTotal":382273,"turns":11,"toolCalls":10,"failedToolCalls":0,"timestamp":"2026-07-18T22:36:27.845Z"} -->